舉一隅不以三隅反,則不復也 ------孔子
前言
冒泡排序是常用排序算法之一,基於冒泡排序的掃描交換思想在其它程序設計中是一種很常用的設計思想。本篇文章從什么是有序序列說起,進而講解掃描交換的思想,以實例帶領讀者理解冒泡排序的設計機理,使讀者能夠見微知著,體會到不一樣的思路,更能夠做到舉一反三。
從有序序列說起
DS是數據項的結構化集合,其結構性體現在數據項之間的相互性和作用上。具體說就是數據項之間的邏輯次序。而序列作為一種最基本的線性結構,其實現機理需要我們掌握好。排序作為處理數據項之間大小關系的算法,有着廣泛應用。在序列中,怎樣評判一個序列是有序的呢?比如序列A{1 2 3 4 5},我們一目了然其實有序排序的(sorted sequence),而序列B{3 2 4 1 5}是無序序列(unsorted sequence)。認真觀察不難發現,序列A中任意一對相鄰的元素對都是順序的(有序的)如相鄰元素對1 2 、2 3、 3 4 、4 5都是順序的;序列B中存在相鄰的元素對是逆序對,如3 2 、4 1。也就是說,在序列中,如果任意一對相鄰元素對都是順序的,那么該序列是有序序列;反之,如果總有一對相鄰元素對是逆序的,那么該序列就是逆序的。這個結論淺顯易懂,但是給我們接下來分析提供了思路。
掃描交換思想
掃描就是順序比較每一對相鄰元素對,如果它們是逆序對,那就交換兩個元素之間的順序使其成為順序對。而冒泡排序Bubblesort就是基於掃描交換實現對序列的排序。在長度為n的序列A中,初始時假設sorted=true;即認為該序列無序,然后在第1趟掃描過程中,如果發現相鄰逆序對(A[i]>A[i+1]),就交換swap(A[i],A[i+1])使其成為相鄰順序對,這樣一趟掃描序列之后,最大元素肯定處在A[n-1],即一趟掃描之后,無序序列長度變為n-1。在第k趟掃描之后,第k大元素已經就緒在A[n-k-1],無序序列長度變為n-k。通過一趟掃描交換使原問題規模由n變為n-1,這就是減治策略。
代碼實現
實現算法1
-
void bubblesort(int A[],int n) //冒泡排序A[0 n)
-
{
-
for(bool sorted=false; sorted=!sorted; n--) //反復掃描交換,每一趟之后原問題規模就減1
-
{
-
for(int j=1;j<n;j++)//從左向右,逐對檢查A[0,n)內各個相鄰元素對
-
{
-
if(A[j-1]>A[j]) //如果發現相鄰逆序對
-
{
-
swap(A[j-1],A[j]); //交換
-
sorted=false; //同時清除全局有序標志
-
}
-
}
-
}
-
}
分析:根據減治法,外層循環不斷縮減待排序列長度,同時置全局有序標志為true;在第k趟掃描中,內層循環處理n-k個元素,進行n-k-1次比較;若在一趟掃描中沒有發現逆序對,則全局標志沒有被賦值false,此時外層循環就跳出結束。
算法的基本步驟就是相鄰元素對的比較以及交換。時間復雜度為O(n2),在最好情況下,序列已經有序,則完成一趟掃描交換即結束O(n),最壞情況下,序列完全逆序排序,第k趟掃描交換需要比較n-k-1次,進行n-k-1次交換,時間復雜度O(n2)
基於代碼實現1我們可以將掃描算法和冒泡排序算法分離,形成如下代碼
實現算法2
-
void bubblesort(int A[],int lo,int hi) //冒泡排序 A[lo,hi)左閉右開區間
-
{
-
while(!(bubble(A,lo,hi--))); //進行掃描交換,直至全序
-
}
-
bool bubble(int A[],int lo,int hi)// 掃描交換 A[lo,hi)
-
{
-
bool sorted=true;
-
while(++lo<hi) //從左向右,逐一檢查各對相鄰元素
-
{
-
if(A[lo-1]>A[lo])
-
{
-
swap(A[lo-1],A[lo]);
-
sorted=false;
-
}
-
}
-
return sorted;
-
}
運行結果
優化策略
對於上述兩個實現方式,如果對於A[0,n],亂序僅限於A[ 0, ),則其前綴和后綴長度相差懸殊,基於此,首先它會作第一趟掃描,並且確認在最后這個位置有一個元素已經就位(這個元素本來就是就位的),在后綴中,存在着大量就位元素,但因為前綴中存在着交換,bubble會返回false,即算法接下來還會繼續掃描下去,直到前綴已經完全有序。而在這些掃描中,本來后綴應該不必要掃描了,因為其已經有序就位了,但是仍然需要掃描交換r次(r為前綴長度= )。這個算法整體消耗時間O(n*r)=O(),應該改進它使其為O(n)
如舉例序列A={4,3,2,1,5,6,7,8,9,10}
運行結果如下
代碼實現3(改進版)
-
void bubblesort(int A[],int lo,int hi)// 冒泡排序A[lo,hi)
-
{
-
while(lo<(hi=bubble(A,lo,hi)));//逐趟掃描交換,直至全序
-
}
-
int bubble(int A[],int lo,int hi)//掃描交換 A[lo,hi) 返回最后一個逆序對的下標
-
{
-
int last=lo; //最右側逆序對下標
-
while(++lo<hi) //自左向右,逐一檢查各對相鄰元素
-
{
-
if(A[lo-1]>A[lo]) //如果逆序
-
{
-
last=lo; //更新最右側逆序對位置記錄
-
swap(A[lo-1],A[lo]); //並交換
-
}
-
}
-
return last; //返回最右側逆序對位置
-
}
更改之后的算法遇到亂序僅限於A[ 0, ),只需要O( n + O( 2 ) )=O(2n)=O(n)時間,而且第一趟掃描之后,序列區間就縮小為亂序的前綴,不會再依次比較后綴中已然有序的元素了,看來有很大改進了。
運行效果對比(可以看到改進版本對於亂序和有序長度差別極大的序列有很大的算法優化,比較次數變得很少)
總結
冒泡排序是根據減治法策略,依托掃描算法來實現序列的排序,其最壞時間復雜度和平均時間復雜度都為O(n2),而最好時間復雜度為O(n)。根據其實現算法,冒泡排序是穩定排序,因為bubblesort中對元素位置唯一調整的可能就是某元素A[i-1]嚴格大於其直接后繼元素A[i],在這種亦步亦趨地交換過程中,重復元素雖然可以相互靠攏,但是絕對不會相互跨越,因此bubblesort屬於穩定排序算法。
小計:
穩定排序算法有:bubblesort(冒泡排序)、insertionsort(插入排序)、mergesort(歸並排序)、radixsort(基數排序)
不穩定排序算法有:shellsort(希爾排序)selectionsort(選擇排序)quicksort(快速排序)heapsort(堆排序)