冒泡排序及其算法优化分析
1.基本冒泡排序
冒泡排序的基本思想:假设被排序的记录数组d[1...N]垂直竖立,将每个记录d[i]看作是一个气泡,那么重的气泡就会向下下沉,轻的气泡就会向上升。每次都是相邻的两个气泡d[i]和d[i+1]进行比较。如果d[i]>d[i+1],那么就交换两个气泡,然后在比较d[i+1]和d[i+2],以此类推,知道所有的气泡都有序排列。假设排序20,37,11,42,29。
第1次冒泡:20。37,11,42,29 d[0]和d[1]比较 第2次冒泡:20,11,37,42,29 d[1]和d[2]比较 第3次冒泡:20,11,37,42,29 d[2]和d[3]比较 第4次冒泡:20,11,37,29,42 d[3]和d[4]比较
那么就找到了最重的气泡42,接下来按同样的方法找出第二重、第三重……的气泡,直到完成排序。根据以上分析可知需要双层循环。第一层循环控制次数,第二层循环控制要排序的数据范围。如下所示
第0次比较第0个到第n-1个,下标控制0~n-2 第1次比较第0个到第n-2个,下标控制0~n-3 . . 第i次比较第0到第n-i+1个,下标控制0到n-i-2 . . 第n-2次比较第0到第1个,下标控制0到0
冒泡排序算法的代码如下
int main(void) { int i, j, tmp; int N = 5; int a[N] = {20,37,11,42,29}; for( i=0; i<N; i++ ){ for( j=0; j<N-i-1; j++ ){ if( a[j]>a[j+1] ){ tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; } } } for(i=0; i<N; i++) printf("%d\n", a[i]); return 0; }
冒泡排序的最好、最坏、平均情况下的时间复杂度都是O(n^2)。但是若在某趟排序中未发现气泡位置的交换,则说明排序的无序区中所有的气泡均满足轻者在上,重者在下的原则,即为正序,则冒泡排序过程可在此次扫描后就终止。基于这种考虑,提出第一种改进算法
2.冒泡排序算法优化一:不做每次扫描都判断是否已经排序完成
如果在某趟循环中没有任何数据交换发生,则表明数据已经排序完成。那么剩余的循环就不需要再执行了。改进后的算法代码如下
int main(void) { int i, j, tmp; int N = 5; int isSorted = 0; int a[5] = {20,37,11,42,29}; for( i=0; i<N&&(!isSorted); i++ ){ //只有在没有排序的情况下(!isSorted)才进行继续循环 isSorted = 1; //设定排序标志 for( j=0; j<N-i-1; j++ ){ if( a[j]>a[j+1] ){ isSorted = 0; //如果没有排序,就重新设定标志 tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; } } } for(i=0; i<N; i++) printf("%d\n", a[i]); return 0; }
这种排序方法,如果数据初始状态就是正序,那么扫描一趟就可以完成。所需要的比较和数据移动的次数分别时最小值n-1和0,也就是算法最好的时间复杂度是O(n);若初始数据反序,则需要进行n-1趟排序,每趟排序进行n-i次关键字的比较,且每次比较都必须移动数据三次来达到交换数据位置,这种情况下比较次数达到最大值n(n-1)/2,移动次数也达到最大值3n(n-1)/2,所以最坏的时间复杂度还是O(n^2)。平均的时间复杂度仍为O(n^2)
2.冒泡排序改进二:记录犯罪现场
在冒泡排序的每一趟扫描中,记住最后一次交换发生的位置lastexchange也是有所帮助的。因为该位置之前的相邻的记录已经有序,故下一趟排序开始的时候,0到lastexchange已经是有序的了,lastexchange到n-1是无序区,所以一趟排序可能使当前有序区扩展多个记录,即较大缩小无序区范围,而非递减1,以此减少排序趟数。
void BubbleSort(int array[], int len) { int m = len – 1; int k, j; int tmp; while(m>0){ for(k=j=0; j<m; j++){ if(array[j] > array[j+1]){ tmp = array[j]; array[j] = array[j+1]; array[j+1] = tmp; k=j;//记录每次交换的位置 } } m=k;//记录最后一个交换的位置 } }
这种算法可以看出,若记录的初始状态是正序(从小到大),则一趟扫描即可完成排序,所需的关键比较和记录移动的次数分别达到最小值n-1和0。即算法最好的时间复杂度是O(n);若初始记录是反序,则需要进行n-1趟排序,每趟排序要进行n-i次关键字的比较,且每次比较都必须移动记录三次来达到交换记录位置。这时比较次数达到最大值n(n-1)/2,移动次数也达到最大值3n(n-1)/2
因此.这种办法的最坏时间复杂度也为O(n^2)。在平均情况下.算法较大地改变了无序区的范围,从而减少了比较的次数,但总的比较次数仍为O(n^2).所以算法的平均时间复杂度为O(n^2)。因此.算法2最好的时间复杂度为O(n)。平均,最坏时刻复杂度为O(n^2)。
4.冒泡排序改进三:双向扫描
若记录的初始状态为:只有最轻的气泡位于d[n]的位置(或者最重的气泡位于d[0]的位置),其余的气泡均已经排好序,在上述的三种算法中都要进行n-1趟扫描,实际只需要一趟扫描就可以完成排序。所以对于这种不对称的情况,可以对冒泡排序又做一次改进。在排序的过程中交替改变扫描方向,即先从下向上扫描,再从上向下扫描,来回扫描,这样就得到了双向冒泡排序
void BubbleSort(int array[], int len) { int low, up, index, i; low = 0; up = len – 1; index = low; int tmp; while(up>low){ for(i=low; i<up; i++){//从上向下扫描 if(array[i]>array[i=1]){ tmp = array[i]; array[i] = array[i+1]; array[i+1] = tmp; index = i; } } up = index; //记录最后一个交换的位置 for(i=up; i>low; i--){//从最后一个交换位置处从下到上扫描 if(array[i]<array[i-1]){ tmp = array[i]; array[i] = array[i+1]; array[i+1] = tmp; index = i; } } low = index; //记录最后一个交换的位置 } }
从这种算法可以看出.若记录的初始状态是正序(从小到大)的.则一趟扫描即可完成排序.所需的关键比较和记录移动的次数分别达到最小值n-1和0。即算法最好的时间复杂度为O(n);若初始记录是反序(从大到小)的.则需要进行[n/2]趟排序。如果只有最重的气泡在最上面(或者最轻的气泡在最下面),其余的有序,这时候就只需要比较1趟。但是在最坏的情况下,算法的复杂度也为O(n^2)。因此.算法最好的时间复杂度为O(n),最坏时刻复杂度为O(n^2)。
5.实际测试
分别用随机数生成器生成500,5000,20000个随机的整数,然后排序。每种排序算法跑10次,取平均时间,可以在下表中看到运行的时间。单位为毫秒。
500随机整数 | 5000随机整数 | 20000随机整数 | |
基本排序方法 | 1.5625 |
145.3125 |
2428.125 |
改进方法一 | 1.5625 | 139.0625 | 2279.6875 |
改进方法二 | 1.5625 |
134.375 |
2153.125 |
改进方法三 | 1.5625 |
104.6875 |
1651.5625 |
6.结论
从上面的表格可以看出,在数据量比较小的时候,这几种算法基本没有任何区别。当数据量比较大的时候,双向扫描冒泡排序会有更好的效率。但是效率并没有根本的提升。因此冒泡排序确实不是我们排序的首选。在数据量比较大的时候,快速排序等会有非常明显的优势。但是在数据量很小的时候,各种排序算法的效率差别并不是很大。那么冒泡排序也会有自己的用武之地。因此,最重要的是熟悉各种算法的性能表现并且根据数据的数量,以及当前运行的环境,开发的进度选择最合适的算法。
借鉴 http://blog.chinaunix.net/uid-22744029-id-1770037.html