Date: 2021/03/07
前言
冒泡排序 Bubble Sort
是大家非常熟悉的算法,本文将简单回顾冒泡排序的基本思想,重点是讨论冒泡排序的优化。
最简单的冒泡排序
冒泡排序的基本思想是依次比较相邻的两个元素,如果第一个比第二个大,则交换这两个元素;否则继续比较。经过一轮比较后,就可以将最大的元素放在最后面,再对剩余的元素依次比较,最多经过 n-1
轮比较,得到最终的有序序列。
本文以升序排序为例。
例如对于一组元素 values=[11,10,4,28,24,12,13,16]
,从前往后依次比较相邻元素 的过程如下图:
经过一轮比较后,确定了最大元素 28
的最终位置,接下来只要对前7
个数重复相同的操作即可。
这是最简单的冒泡排序算法,也是理解冒泡排序思想的核心,实现代码如下:
/**
* 最简单的冒泡排序 O(n*n)
*
* @param values
*/
public static void sort(int[] values) {
int i = 0, j = 0;
for (i = values.length - 2; i >= 0; --i) {
for (j = 0; j <= i; j++) {
if(values[j] > values[j+1]){
int temp = values[j];
values[j] = values[j+1];
values[j+1] = temp;
}
}
}
}
有了上面对冒泡的理解,我们开始考虑冒泡排序的优化。
冒泡排序的优化
优化1:及时止损
冒泡排序有一个很大的缺陷:在排序的过程中,对于一个已经是升序的序列,也要依次进行比较,这显然是多余的操作,这种情况下应该及时终止算法,减少冒泡的次数。
如何判断是否应该终止呢?我们只需要判断冒泡的过程中是否进行了交换操作(设置一个 boolean
变量记录是否交换),如果没有任何交换,说明已经是升序的序列,此时可以终止算法。
还是上面的例子,经过两轮交换后,就可以终止算法了。
优化2:减少比较次数
优化1中是判断 所有还未排好序的元素 是否是有序的,在此基础上,我们可以进一步优化。
对于所有未排好序的元素来说,可能不是有序的,但是尾部的一部分元素如果构成了有序序列,也可以跳过对尾部这些元素的比较。也就是说,我们只需要对冒泡过程中 最后一次交换的位置 之前的元素进行比较。
这种思路其实也包含了优化1的情况。优化1中,最后一次交换的位置为 0
。
例如对于 values=[10,11,4,13,24,12,14,16]
,第一轮比较结束后,24
排在最后。第二轮比较过程中,记录的最后一次交换的位置是 3
,也就是图中黄色元素 12
的位置,第三轮比较只需要比较 12
之前的元素即可。
如果是按照优化1的方法,只判断是否发生了交换,第三轮比较依然会比较到
16
之前元素。
AC代码
public static void bubbleSort(int[] values) {
int i = 0, j = 0;
for (i = values.length - 2; i >= 0; --i) {
/** 1. tailSortedFlag 是用来记录每一趟最后一次交换时j的位置 ,如果尾部是局部有序的,那么只需要排序最后一次交换 * 位置之前的元素即可。
* tailSortedFlag的初始值用来处理本就排过序的情况,对于冒泡排序不同的写法,初始值取不同值,这里取0
*/
int tailSortedFlag = 0;
for (j = 0; j <= i; ++j) {
/** 2. 比较时不取等号是稳定的排序,取等号是不稳定的排序
* >是升序 <是降序 */
if (values[j] > values[j + 1]) {
SortUtils.swap(values, j, j + 1);
/** 3. 每交换一次就记录以下当前位置 */
tailSortedFlag = j;
}
}
i = tailSortedFlag;
}
}
总结
冒泡排序最好的时间复杂度为 O(n)
(在序列本身是有序的情况下),最坏时间复杂度为 O(n*n)
(序列和最终的目标是逆序的情况下),平均时间复杂度为 O(n*n)
。
空间复杂度为 O(1)
。是稳定的排序算法。
冒泡排序的效率并不高,但是对任何一种算法来说,重要的是其中的思想,优化的过程也能提高我们对算法设计和分析的能力。
本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情