图解排序算法之「冒泡排序」(详细解析)

1. 基本思想

冒泡排序(Bubble Sort)是最基础的排序算法之一,它的核心思想是:多次遍历要排序的序列,在遍历的过程中,当发现两个相邻的元素逆序,就交换这两个元素的位置,直到某次遍历不需要交换元素为止。此时整个序列都不存在两个元素逆序的情况,即满足了顺序要求。

为了让大家对冒泡排序有更加清晰的认识,我们以下面这组数据作为例子来演示冒泡排序:

现在,我们需要对包含 8 个元素的序列 [1, 9, 2, 6, 0, 8, 1, 7] 进行升序(从小到大)排序。

按照冒泡排序的思想,我们需要遍历这个序列,如果遍历的过程发现相邻元素中,左边的元素大于右边的元素时,就交换这两个元素的位置。如果是左边的元素小于所等于右边的元素时,满足从小到大的顺序要求,元素位置维持不变。下面就是一趟遍历的过程:
图解排序算法之「冒泡排序」(详细解析)_第1张图片
第一趟遍历下来,9 作为未排序区间中的最大元素,如同冒泡般上浮到了最右侧,形成了已排序区间的第一个元素。

我们下面接着第二趟:
图解排序算法之「冒泡排序」(详细解析)_第2张图片
走完第二趟之后,我们可以看到 8 作为未排序区间中的最大元素“上浮”到了未排序区间的最右侧,形成了已排序区间的第二个元素。

其实看到这里,大家都应该明白了,我们只要多再进行 7 − 2 7 - 2 72 趟遍历,就能将已排序区间扩大到整个序列,完成冒泡排序的过程,使得序列中所有的元素都是有序的。这便是冒泡排序的过程,如果你还不懂的话,推荐你自己在纸上手动模拟一遍

听说还有人分不清楚选择排序和冒泡排序,这里简单说一下:选择排序的一次遍历仅是为了在未排序的区间中选出最值元素,然后放到已排序区间中;而冒泡排序的一次遍历是为了让较大的元素向未排序区间末端方向“上浮”,造成的结果不仅是最值元素被移到末端,还有过程中较大元素往“末端方向”移动,上面例子的第二趟遍历就很好地体现了这一过程。

2. 代码实现

冒泡排序的代码非常简单,直接上去就是两层 for 循环,给人一种暴力的直视感。外层循环控制遍历的趟数,内层循环控制控制每趟遍历访问区间的范围。if 条件句用来判断相邻元素是否逆序,里面的三行则是经典的元素交换语句。冒泡排序,一气呵成~


public void bubbleSort(int[] arr, int n) {
    //控制遍历的趟数
	for (int i = 0; i < n - 1; i++) {
        //控制遍历区间的范围
		for (int j = 0; j < n - i - 1; j++) {
            //判断是否逆序
			if (arr[j] > arr[j + 1]) {
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
			}
		}
	}
}

我们可以从上面代码得出,冒泡排序的时间复杂度为 O ( n 2 ) O(n^2) O(n2)。由于在两个元素相等的时候,并不会执行交换位置的操作,所以相等元素在排序前后相对顺序是不变的,即冒泡排序是一种稳定排序。

3. 优化

虽然说冒泡排序的最坏时间复杂度是 O ( n 2 ) O(n^2) O(n2),但是它的最佳时间复杂度应该是 O ( n ) O(n) O(n)。换句话说,上面提供的代码其实还有很大的优化空间。因为冒泡排序是一种基于比较的排序,我们在思考优化的时候,可以往减少比较次数的方向思考。

优化点1:其实这个优化点已经写在了开头的“基本思想”里面,我们算法只需要执行到某趟遍历不需要交换元素位置即可,因为此时所有相邻元素都满足顺序条件,不需要再继续遍历。

在代码层面,我们只需要加上一个标志变量,用于记录在这一趟遍历中是否发了元素交换,如果有则继续下一趟遍历,否则停止循环,完成排序。对应的优化版代码如下:

public void bubbleSort(int[] arr, int n) {
	for (int i = 0; i < n - 1; i++) {
		boolean isSorted = true; // #1 添加标记
		for (int j = 0; j < n - i - 1; j++) {
			if (arr[j] > arr[j + 1]) {
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
				isSorted = false; // #2 发生了交换
			}
		}
		if (isSorted) {
			break;  // #3 如果没有发生交换,则完成了排序
		}
	}
}

优化点2:这个优化点单纯通过思考有点难想到,需要对排序过程进行观察和模拟才会感受到。其实,每趟遍历的区间右边界都是由上一趟遍历最后一次发生交换的位置决定的。因为如果这个位置之后都没有发生交换,就说明之后的元素都是非逆序的,我们可以将有序区间的左边界直接扩展到最后一次发生交换的下一个位置。

在代码层面,我们可以将这两个优化点合并在一起,详情代码如下:

public void bubbleSort(int[] arr, int n) {
	int lastIndex = n - 1;  // #1 有序区间的左边界
	for (int i = 0; i < n - 1; i++) {
		int tempIndex = lastIndex; // #2 标记
        // #3 遍历的右边界变成了lastIndex
		for (int j = 0; j < lastIndex; j++) {
			if (arr[j] > arr[j + 1]) {
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
				tempIndex = j;  // #4 记录发生交换的位置
			}
		}
		// #5 如果标记的值不变,则说明没有发生交换
		if (tempIndex == lastIndex) {
			break;  
		} else {
			lastIndex = tempIndex;
		}
	}
}

至此,冒泡排序的优化就搞完了。因为在大多数时候我们排序的数据并不都是在极端情况,加了优化的冒泡排序能在很多时候比同样是 O ( n 2 ) O(n^2) O(n2) 的普通选择排序有更加优异的表现。


顺手更新一篇水文,希望你也可以顺手点赞呀~

你可能感兴趣的:(图解排序算法之「冒泡排序」(详细解析))