已知的排序算法有很多种,如果不考虑堆排序这种依赖于特定数据结构的算法,大致上可以分成两类——一类属于分解问题型的,也就是将问题拆分成更小的规模再解决,快排算法和归并排序都是分解问题的算法,大类上都属于分治算法。另一种不分解类型的,基本思路是每轮操作会向有序的结果走一小步,直到最终有序。冒泡排序,选择排序和希尔排序都属于这种类型。
下面以非降序为例介绍下冒泡算法:
算法步骤:
for i <-- 1 to length-1
for j <-- 1 to length-i
if a[j]>a[j+1]
swap(a[j],a[j+1]);
这是算法课上介绍的第一个排序算法,但是刚开始学习的时候总觉得好像不能一眼看出这个算法是对的(不想选择排序和插入排序那么直观),于是想着证明一下:
(1)fact1:在外层循环里,我们在第i轮循环里将第i大的数字放到了length-i的位置上了。
可以用归纳法来证明
a.首先当i=1时,假设最大值是a[x],如果a[x]=a[x+1],j=x时不做改变,否则a[x]的值赋值给a[x+1],所以a[x+1]变成了最大值。
b.此时继续遍历数组,从这个过程可以看出,最大值不断会往后移动,直到移动到a[length]的位置。
c.现在最大值处于正确的位置了,我们将问题减小到下标为1~length-1的数组的排序,这也是内层循环不断减小的原因。
(2)我们的外层循环执行了length-1次,也就是length-1个数的位置是正确的,那么最终的排序结果就是正确的。
代码:
public void bupple2(int [] array){
for(int i = 0;i<array.length;i++) {
for(int j = 0;j<array.length-i-1;j++) {
if(array[j]>array[j+1]) {
int temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
}
}
}
}
时间复杂度分析:
显然两层循环,如果判断和swap操作算作基本操作,操作次数为
n
−
1
+
n
−
2
+
n
−
3
+
.
.
.
+
1
=
n
(
n
−
1
)
/
2
n-1+n-2+n-3+...+1=n(n-1)/2
n−1+n−2+n−3+...+1=n(n−1)/2
是
O
(
n
2
)
O(n^2)
O(n2)的时间复杂度
冒泡排序属于稳定排序,也就是大小相等的两个数的相对顺序,在排序结束后没有发生变化
对于冒泡排序,还有很多可以改进的地方,比如说某次外层循环如果没有交换,那么算法应该结束。而不是继续下一轮循环。如果这样判断程序是否停止,仔细观察到话,我们可以再改进一点。看下面这两个例子:
1 , 2 , 3 , 4 , 0 , 5 1,2,3,4,0,5 1,2,3,4,0,5
如果按照冒泡排序的算法,排序过程中数组状态如下:
(1)
1
,
2
,
3
,
0
,
4
,
5
1,2,3,0,4,5
1,2,3,0,4,5
(2)
1
,
2
,
0
,
3
,
4
,
5
1,2,0,3,4,5
1,2,0,3,4,5
(3)
1
,
0
,
2
,
3
,
4
,
5
1,0,2,3,4,5
1,0,2,3,4,5
(4)
0
,
1
,
2
,
3
,
4
,
5
0,1,2,3,4,5
0,1,2,3,4,5
结束
在这个过程中,因为最小值0比骄靠后,但是每次只往前挪一点点,所以挪了4次才结束。加入我们从后往前执行冒泡排序,也就是将较小的数字往前放,过程如下:
(1)
1
,
2
,
3
,
0
,
4
,
5
1,2,3,0,4,5
1,2,3,0,4,5
(2)
0
,
1
,
2
,
3
,
4
,
5
0,1,2,3,4,5
0,1,2,3,4,5 最小值0在第一轮外层循环中就到了正确的位置
结束
鸡尾酒排序
基于冒泡排序的改进算法
(1)假如有一个较小的数字放在靠后的位置,正向冒泡排序在外层循环中,每次仅仅将其移动一个位置,效率较低(这里说效率低并不是指移动次数增加了,而是因为遍历中存在较多的无效遍历,即已经排好的数据因为整体数据排序没有完成还要不断地被遍历)。采用逆向冒泡的方式可以提高效率。
(2)类似的,假如有一个较大的数字处于靠前的位置,逆向冒泡排序效率反而比较低了,原因相同
(3)鸡尾酒排序是使用往返排序的策略,逆向正向交替进行
再有一点,鸡尾酒排序符合计算机的设计,因为它的局部性更高。在第一轮排序中装在进内存中的数据,在第二轮排序中马上可以再次利用。