这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战
欢迎关注公众号OpenCoder,来和我做朋友吧~❤😘😁🐱🐉👀
一.什么是冒泡排序
冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。
它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。
这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
-
时间复杂度:O(n²)
-
算法稳定性:稳定排序算法
-
实 质:把小(大)的元素往前(后)调
-
思想: 交换思想
-
原理:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
-
图解:(参考《Java数据结构和算法》书籍上的图片)
-
未排序的一行人:
-
从第一个人开始与第二个人进行比较,矮的站前面,高的继续和后面的人比,直到最后一人成为最高的,那么第一轮比较结束
-
然后第二轮继续比较,从第一个人开始依次比较,注意本轮不需要和最后一人(最高的)进行比较了,将第二高的人排在了倒数第二的位置,本轮结束。后续按照此规则继续比较,直到整个队列从小到大排序。如下:
-
二. 逻辑推演
现有一个数组,里面的数据为: [11,17,28,34,67,48,47,66],我们以此数据来分析:
第一轮比较:
原始数据排列情况如下:
我们冒泡排序比较是从第一个元素开始进行,然后和第二个元素进行比较,由于11和17比较,11比17小,不需要交换;因此!紧接着应该由17和28进行比较,这时发现17比28小,位置继续保持不变!
接下来继续由28和34进行比较:
发现依然不变,继续让34和67进行比较:
34比67小,依然不变。继续:
当67和48比较的时候,由于67比48大!因此两者交互了!
接下来:67和47进行比较,67比47大,继续交换:
最后,67和66继续比较,发现67更大,那么继续完成交换:
小结:
通过第一轮 7 次比较,最终将最大值交换到了最右边!
第二轮比较:
依然从第一个数进行比较,将第二大的数给排序到右边去:
第三轮:
排序后的结果:
第四轮:
第五轮:
第六轮:
第七轮:
小结:
第一轮比较7次
第二轮比较6次
第三轮比较5次
...
第七轮比较1次
三.冒泡排序第一版(入门)
代码:
public static void main(String[] args) {
int[] arr = {11,17,28,34,67,48,47,66};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int[] array){
//循环次数计数
int count = 0;
for (int i = 0; i <array.length-1 ; i++) {
System.out.println("第"+(++count)+"次循环");
for (int j = 0; j < array.length-i-1; j++) {
int tmp = 0;
if(array[j]>array[j+1]){
tmp = array[j];
array[j] = array[j+1];
array[j+1]=tmp;
}
}
}
}
结果输出:
第1次循环
第2次循环
第3次循环
第4次循环
第5次循环
第6次循环
第7次循环
[11, 17, 28, 34, 47, 48, 66, 67]
总结:
代码非常简单,使用双循环进行排序。外部循环控制所有的回合,内部循环实现每一轮的冒泡处理,先进行元素比较,在进行元素交换。
四.冒泡排序第二版(优化)
问题分析:
原始的冒泡排序有哪些可以优化的点呢?
回顾一下刚才图上描述的细节,仍然以[11, 17, 28, 34, 47, 48, 66, 67]数据为例,当排序算法分别执行第5轮、第6轮和第7轮时数列状态如下:
第5轮:
第6轮:
第7轮:
优化思路:
很显然可以看出,经过第5轮排序后,整个数列已然是有序的了。可是排序算法仍然在继续执行第6轮、第7轮排序。
在这种情况下,如果能判断出数列已经有序,并作出标记,那么剩下的几轮排序就不必执行了,可以提前结束排序工作,下面我们来进行优化。
冒泡第二版优化代码:
public static void main(String[] args) {
int[] arr ={11,17,28,34,67,48,47,66};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int[] array){
//循环次数计数
int count = 0;
for (int i = 0; i <array.length-1 ; i++) {
System.out.println("第"+(++count)+"次循环");
//有序标记,每一轮的初始值都是true
boolean isSorted = true;
for (int j = 0; j < array.length-i-1; j++) {
int tmp = 0;
if(array[j]>array[j+1]){
tmp = array[j];
array[j] = array[j+1];
array[j+1]=tmp;
//因为发生了元素交换所以当前不是有序的,标记为false
isSorted = false;
}
}
//如果isSorted为true则说明,一轮循环都没有发现有元素交换,那当前已经是一个有序数列了,就不需要再排序了
if(isSorted){
break;
}
}
}
结果输出:
第1次循环
第2次循环
第3次循环
[11, 17, 28, 34, 47, 48, 66, 67]
总结:
与第1版代码比较,优化代码做了小小改动,利用了布尔变量isSorted作为标记。如果在当前排序中有发生元素交换就说明数列无序,如果没有发生元素交换就说明数列已经有序,然后直接跳出大循环。本数列为例,发现只循环了3次就完成了排序工作,提高了算法性能。
五.冒泡排序第三版(无敌)
问题分析:
为了明说问题,我们使用上面的冒泡优化算法来排序一个新数列:[3,4,2,1,5,6,7,8],这个数列的特点是前部分元素[3,4,2,1]是无序的,后半部分的元素[5,6,7,8] 是有序的。
第二版代码推演:
public static void main(String[] args) {
int[] arr ={3,4,2,1,5,6,7,8};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int[] array){
//循环次数计数
int count = 0;
for (int i = 0; i <array.length-1 ; i++) {
System.out.println("外部大循环第"+(++count)+"次循环");
//有序标记,每一轮的初始值都是true
boolean isSorted = true;
for (int j = 0; j < array.length-i-1; j++) {
System.out.println(" 内部小循环第"+(j+1)+"次循环");
int tmp = 0;
if(array[j]>array[j+1]){
tmp = array[j];
array[j] = array[j+1];
array[j+1]=tmp;
//因为发生了元素交换所以当前不是有序的,标记为false
isSorted = false;
}
}
//如果isSorted为true则说明,一轮循环都没有发现有元素交换,那当前已经是一个有序数列了,就不需要再排序了
if(isSorted){
break;
}
}
}
打印结果:
外部大循环第1次循环
内部小循环第1次循环
内部小循环第2次循环
内部小循环第3次循环
内部小循环第4次循环
内部小循环第5次循环
内部小循环第6次循环
内部小循环第7次循环
外部大循环第2次循环
内部小循环第1次循环
内部小循环第2次循环
内部小循环第3次循环
内部小循环第4次循环
内部小循环第5次循环
内部小循环第6次循环
外部大循环第3次循环
内部小循环第1次循环
内部小循环第2次循环
内部小循环第3次循环
内部小循环第4次循环
内部小循环第5次循环
外部大循环第4次循环
内部小循环第1次循环
内部小循环第2次循环
内部小循环第3次循环
内部小循环第4次循环
[1, 2, 3, 4, 5, 6, 7, 8]
发现问题:
虽然我们使用了优化算法,减少了外部大循环的循环次数,但是对于内部小循环的次数仍然是7次、6次、5次、4次。然而内部小循环的5,6,7,8几个数字的判断比较是无意义的,右边的许多元素已经是有序的,可是每轮还是白白的比较了许多次。这正是接下来需要优化的另一个点。
优化思路:
这个问题关键在于对数列的有序区的界定,刚才代码中,第1轮排序过后结果为[2,3,1,4,5,6,7,8],发现最后5个元素已经属于有序区了。
因此后面的多次元素比较是没有意义的。
如何避免?我们可以在每一轮排序后,记录最后一次元素的交换位置,该位置即为无序数列边界,再往后就是有序区了。能区分有序和无序就可以只需要比较无序区即可。
冒泡第三版优化代码:
public static void main(String[] args) {
int[] arr ={3,4,2,1,5,6,7,8};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int[] array){
//循环次数计数
int count = 0;
//**(新增代码)记录最后一次交换的位置
int lastExchangeIndex = 0;
//**(新增代码)无序边界,每一轮比较只需要比较到这里即可,首次比较要比完所有数字
int sortBorder = array.length-1;
for (int i = 0; i <array.length-1 ; i++) {
System.out.println("外部大循环第"+(++count)+"次循环");
//有序标记,每一轮的初始值都是true
boolean isSorted = true;
//**(新增代码)
for (int j = 0; j < sortBorder; j++) {
System.out.println(" 内部小循环第"+(j+1)+"次循环");
int tmp = 0;
if(array[j]>array[j+1]){
tmp = array[j];
array[j] = array[j+1];
array[j+1]=tmp;
//因为发生了元素交换所以当前不是有序的,标记为false
isSorted = false;
//**(新增代码)更新最近一次交换元素的位置
lastExchangeIndex = j;
}
}
System.out.println();
//**(新增代码)如果isSorted为true则说明,一轮循环都没有发现有元素交换,那当前已经是一个有序数列了,就不需要再排序了
sortBorder = lastExchangeIndex;
if(isSorted){
break;
}
}
}
输出结果:
外部大循环第1次循环
内部小循环第1次循环
内部小循环第2次循环
内部小循环第3次循环
内部小循环第4次循环
内部小循环第5次循环
内部小循环第6次循环
内部小循环第7次循环
外部大循环第2次循环
内部小循环第1次循环
内部小循环第2次循环
外部大循环第3次循环
内部小循环第1次循环
外部大循环第4次循环
[1, 2, 3, 4, 5, 6, 7, 8]
总结:
在本次代码中,sortBorder就是无序数列的边界。再每一轮排序过程中,处于sortBorder之后的元素就不需要在进行比较了,肯定是有序的。所以我们会看到只有第一轮循环,内部小循环循环了7次,并且找到无序边界。后面的N轮循环,内部的小循环只会循环到边界处,则大大降低了内部循环的循环次数。
六.总结
冒泡排序到这里其实还不是最优的,冒泡排序还有一个王者版本,被称为鸡尾酒排序。它是基于冒泡排序的一种升级排序法,鸡尾酒下回我们不见不散。
欢迎关注公众号OpenCoder,来和我做朋友吧~❤😘😁🐱🐉👀