前言
学过的内容还是需要总结的,这里就亡羊补牢了。
冒泡排序作为初级算法中最常见的算法,我们在熟悉不过了,它有很多的演变形式,不要只盯着最初版本的双层for循环了。
原版
我们现在要对一个无序数组进行从小到大的排序,写一个冒泡排序的话,最常见的代码如下:
public static void test1(int[] arrays) {
int size = arrays.length;
//最外层for循环控制次数,2个数需要比较1次,n个数需要比较n-1次
for (int i = 0; i < size - 1; i++) {
//里面的for循环控制每一轮循环中每个数比较的次数;-i是去除每次放在最后面排好序的数字
for (int j = 0; j < size - 1 - i; j++) {
if (arrays[j + 1] < arrays[j]) {
int temp = 0;
temp = arrays[j + 1];
arrays[j + 1] = arrays[j];
arrays[j] = temp;
}
}
}
System.out.println(Arrays.toString(arrays));
}
这个不做过多的解释了,下面来看第二版
第一版
[1,3,4,5,6,7,8]
比如上面这个数组本来就是有序,或者经过几轮排序达到了有序,再之后根本就没有比较的意义了。为了优化这里可以声明一个标志位,当没有发生元素交换的时候,说明该数组已经是有序的了,就不在进行比较,直接退出for循环,所以需要在第一轮for循环中修改一下。
private static void test2(int[] arrays) {
int size = arrays.length;
for (int i = 0; i < size - 1; i++) {
boolean flag = false;
for (int j = 0; j < size - i - 1; j++) {
if (arrays[j] > arrays[j + 1]) {
int temp = 0;
temp = arrays[j + 1];
arrays[j + 1] = arrays[j];
arrays[j] = temp;
//如果发生了元素交换,说明不是有序的
flag = true;
}
}
//这一轮没有发生交换,则说明数据已经是有序的了
if (!flag) {
break;
}
}
System.out.println(Arrays.toString(arrays));
}
第二版
[4,3,1,2,5,6,7,8]
经过第一次的优化,我们发现还是有优化的空间,如上这个数组,虽然该轮排序的时候有元素的交换,可以说明是无序的,但我们发现后面四位数已经给是有序的了,也没有比较的意义。我们可以找到最后一次交换元素的位置,从这个位置往后都是排好序的,是不需要比较的,所以需要在第二轮for循环中修改一下。
private static void test3(int[] arrays) {
int size = arrays.length;
int n = arrays.length - 1;
//记录上一次元素交换的位置
int lastExchange = 0;
for (int i = 0; i < size - 1; i++) {
boolean flag = false;
for (int j = 0; j < n; j++) {
if (arrays[j] > arrays[j + 1]) {
int temp = 0;
temp = arrays[j + 1];
arrays[j + 1] = arrays[j];
arrays[j] = temp;
//如果发生了元素交换,说明不是有序的
flag = true;
//记录本轮最后一次交换元素的位置,它之后的不用在比较了
lastExchange = j;
}
}
n = lastExchange;
if (!flag) {
break;
}
}
System.out.println(Arrays.toString(arrays));
}
第三版
[2,3,4,5,6,7,1,8]
如上这个数组,我们发现除了倒数第2个数“1”之外,其他数字都已经是有序的了,如果每次还是要按照上面的流程,还是需要进行7轮比较,很不值当,所以还是有改进的空间。
冒泡排序是从左向右排序,就无法解决上面这个数组的问题,所以根据冒泡排序,可以演化出一个新的排序算法,叫做鸡尾酒排序,它的思想还是冒泡排序的思想,第一轮从左向右比较,第二轮从右向左比较,第三轮再从左向右,以此类推,就像钟摆一样。
private static void test4(int[] arrays) {
int size = arrays.length;
//最外层控制轮数,里面有两个for循环,所以是n/2轮(4个数、5个数都是2轮,以此类推n个数,是n/2轮)
for (int i = 0; i < size / 2; i++) {
int temp = 0;
boolean flag = false;
//第一轮(奇数轮)从左到右
for (int j = 0; j < size - 1 - i; j++) {
if (arrays[j] > arrays[j + 1]) {
temp = arrays[j + 1];
arrays[j + 1] = arrays[j];
arrays[j] = temp;
flag = true;
}
}
if (!flag) {
break;
}
//第二轮(偶数轮)从右到左;有可能奇数轮拍完之后不是有序,偶数轮变成有序了,所以需要重置标志位。
flag = false;
//前i个已经是有序的了,所以只许比较后i个
for (int k = size - i - 1; k > i; k--) {
if (arrays[k] < arrays[k - 1]) {
temp = arrays[k - 1];
arrays[k - 1] = arrays[k];
arrays[k] = temp;
flag = true;
}
}
if (!flag) {
break;
}
}
System.out.println(Arrays.toString(arrays));
}
总结
面对这几版优化,最一开始的时候我也想了很久,最后意识到算法这东西一定要把最基础的概念搞的特别清晰才能进行下一步。
第一版的优化是宏观的,第一层for循环控制的是轮数,为了减少轮数,所以可以在第一层for循环内做一下改变,当没有发生元素交换,往后几轮就不用再比较了。
第二版的优化相对来说是微观的,第二层for循环控制的是每一轮比较的次数,为了减少比较的次数,可以在第二层for循环中做手脚,只需要记录本轮中上一次比较元素交换的位置即可。
第三版的写法就比较冗余了,但是针对某些情况也可以降低时间复杂度。