冒泡排序
在我们刚开始接触计算机高级语言的时候,肯定会学习排序算法,而冒泡排序就是其中最基础的算法。
想当年我也是对这些一窍不通,靠死记硬背才在考试中勉强过关。而背程序代码在学习中实在是非常不可取的一种方式。这里我就将我所学的分享给大家,希望大家少走一些弯路。如果发现错误,请告知我,我将不胜感谢。
一、设计思想
在计算机领域中,冒泡排序是一种最基础且最简单的排序算法。它重复地走访要排序的元素列,并依次比较两个相邻的元素,如果它们的顺序(这里的顺序因具体情况而定)错误就把他们交换过来。而重复走访元素的目的是重复地进行这项操作直到没有相邻元素需要交换为止。
二、命名
为何会有冒泡排序这样奇怪的名称?
从设计思想来看,越大(越小)的元素会经由交换慢慢 “浮” 到数列的顶端(升序或者降序排列),就如同在碳酸饮料中二氧化碳气泡最终会上浮到水面一样,由此得出了 “冒泡排序” 一名。
三、图示
第一趟排序:
第一次排序:10和1比较,10大于1,交换位置 [1,10,35,61,89,36,55]
第二次排序:10和35比较,10小于35,不交换位置 [1,10,35,61,89,36,55]
第三次排序:35和61比较,35小于61,不交换位置 [1,10,35,61,89,36,55]
第四次排序:61和89比较,61小于89,不交换位置 [1,10,35,61,89,36,55]
第五次排序:89和36比较,89大于36,交换位置 [1,10,35,61,36,89,55]
第六次排序:89和55比较,89大于55,交换位置 [1,10,35,61,36,55,89]
第一趟总共进行了六次比较,排序结果:[1,10,35,61,36,55,89]
第二趟排序:
第一次排序:1和10比较,1小于10,不交换位置 1,10,35,61,36,55,89
第二次排序:10和35比较,10小于35,不交换位置 1,10,35,61,36,55,89
第三次排序:35和61比较,35小于61,不交换位置 1,10,35,61,36,55,89
第四次排序:61和36比较,61大于36,交换位置 1,10,35,36,61,55,89
第五次排序:61和55比较,61大于55,交换位置 1,10,35,36,55,61,89
第二趟总共进行了5次比较,排序结果:1,10,35,36,55,61,89
第三趟排序:
1和10比较,1小于10,不交换位置 1,10,35,36,55,61,89
第二次排序:10和35比较,10小于35,不交换位置 1,10,35,36,55,61,89
第三次排序:35和36比较,35小于36,不交换位置 1,10,35,36,55,61,89
第四次排序:36和61比较,36小于61,不交换位置 1,10,35,36,55,61,89
第三趟总共进行了4次比较,排序结果:1,10,35,36,55,61,89
到目前位置已经为有序的情形了。
四、语言实现
在这里拿java举例,其他语言编写方式与此大致相同。
public static void bubbleSort(int arr[]){
for(int i=0;i<arr.length-1;i++){
for(int j=0;j<arr.length-1-i;j++){
if(arr[j]>arr[j+1]){
int temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
}
外层循环:(0,arr.length-1)
两边都是开区间。其实右边也可以是闭区间,但是即使 i 取到了arr.lenght-1,内层循环也不会执行,因此外层循环进行arr.length-1次即可完成排序。
内层循环:[0,arr.length-1-i)
为了要保证数组下标不越界,j+1必须要小于length-1。在内层循环中,因为每排序一趟就会有一个数字确定,因此下一次的排序就会比上一次排序的数字少一个,于是就有了arr.length-1-i。
常见搭配:
- 外层 i: (0,length-1)
内层 j: [0,length-1-i) - 外层 i: (1,length-1)
内层 j: [0,length-i) - 外层 i: (length-1,0)
内层 j: (length-1-i,0] - 外层 i: (length-1,1)
内层 j: (length-i,0]
五、优化设计
对于冒泡排序来说,也可以在不同情况下进行优化来使效率达到最大。
1.在外层循环没有达到length-1时,数组中的数据已经到达有序状态,但冒泡算法仍然继续执行下一次循环,知道完成length-1次循环,只是之后的循环没有任何意义。
解决方案:
设置一个标志位flag,如果发生了数据交换则赋值为1;如果没有交换则赋值为0。在这样的情况下一轮比较结束后如果flag仍未0,就表示这一轮没有发生交换,说明数组中数据已经达到有序状态,就没有必要继续进行下去。
代码实现:
public static void bubbleSort(int arr[]){
int flag=1; //标志位,用于表示是否要进行交换
for(int i=0;i<arr.length-1;i++){ //外层循环,一共执行length-1次
flag=0;
for(int j=0;j<arr.length-1-i;j++){
if(arr[j]>arr[j+1]){
//选出最大值放在数组的末尾
int temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
flag=1; //当数据发生交换时,标志位为1
}
}
if(flag!=1) //当没有发生交换时,表示数组已经达到有序状态,之后直接return,不继续循环
break;
}
}
2.当数组中数据部分有序时,使用鸡尾酒排序会有更高的效率。
鸡尾酒排序又被称为双向冒泡排序、搅拌排序、涟漪排序等,是冒泡排序的一种变形。但是与冒泡排序算法中仅从低到高去比较序列中每个元素不同的是,该算法在排序时是以双向在序列中进行的,先从低到高再从高到低。
解决方案:
数组中数据本是无规律排放的,先找到最小的数字,把它放到第一位,然后找到最大的数字放到最后一位。之后再找到第二小的数字放到第二位,再找到第二大的数字放到倒数第二位。以此类推,直到排序完成。
以序列(2,3,4,5,1)为例,鸡尾酒排序只需要访问两次(升序降序各一次 )次序列就可以完成排序,但如果使用冒泡排序则需要四次。
代码实现:
public static void cocktailSort(int arr[]){
int left=0,right=arr.length-1;
int temp;
while(left<right){
//将大的元素移动到数组的尾部
for(int j=left;j<right-1;j++){
if(arr[j]>arr[j+1]){
temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
right--; //一趟排序完成后最后一个数据已经确定,向前移动一位
//将小的元素移动到数组的头部
for(j=right;j>left;j--){
if(arr[j-1]>arr[j]){
temp=arr[j-1];
arr[j-1]=arr[j];
arr[j]=temp;
}
}
left++; //一趟排序完成后开始的数据已经确定,向后移动一位
}
}
注意:当数组中的数据为乱序时,鸡尾酒排序与冒泡排序的时间复杂度都非常糟糕。
六、算法分析
(1)时间复杂度
当数组的初始状态本为正序,一趟扫描即可完成排序。所需的数据比较次数与交换次数都到达了最小值,此时冒泡排序的时间复杂度到达最小,为O(n)。
当数组的初始状态为倒序时,需要进行n-1趟排序。每趟排序要进行n-i次数据比较(1≤i≤n-1),且每次比较都必须交换三次位置。在这种情况下,比较和交换次数都达到最大值,为O(n²)。
综上所述:冒泡排序的平均时间复杂度为O(n²)。
(2)空间复杂度
冒泡排序再排序过程中,需要一个临时的变量(文中的temp)进行两两交换,所需要的额外空间为1,因此空间复杂度为O(1)。
(3)算法稳定性
首先来了解一下什么时算法的稳定性:
算法稳定性指的是原本键值一样的元素在排序后的位置不发生改变。
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,是不会再交换的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
七、总结
排序算法远不止以上所述,还有其他更加优秀的排序算法,我在以后的博客会依次完成。
我们对于算法的学习在看懂之后一定要进行相应的练习,让排序的思想深深印刻在脑海中。并且对于一些细节的地方可以根据调试缩小复杂度,让我们的代码更加的完美。