《大話資料結構》第9章 排序 9.9 快速排序(上)_大話資料結構

來源:互聯網
上載者:User
9.9.1 快速排序介紹

        終於我們的高手要登場了,如果將來你工作後,你的老闆要讓你寫個排序演算法,而你會的演算法中竟然沒有快速排序,我想你還是不要聲張,偷偷去把快速排序演算法找來敲進電腦,這樣至少你不至於被大伙兒取笑。
        事實上,不論是C++ STL、Java SDK或者.NET FrameWork SDK等開發套件中的原始碼裡都能找到它的某種實現版本。 
        快速排序演算法最早由圖靈獎獲得者Tony Hoare設計出來的,他在形式化方法理論,以及ALGOL60 程式設計語言的發明都有卓越的貢獻,是上世紀最偉大的電腦科學家之一。而這快速排序演算法只是他眾多貢獻中的一個小發明而已。
        更牛的是,我們現在要學習的這個快速排序演算法,被列為20世紀10大演算法之一。我們這些玩編程的人還有什麼理由不去學習它呢。
        希爾排序相當於直接插入排序的升級,它們同屬於插入排序類,堆排序相當於簡單選擇排序的升級,它們同屬於選擇排序類。而快速排序其實就是我們前面認為最慢的冒泡排序的升級,它們都屬於交換排序類。即它也是通過不斷的比較和移動交換來實現排序的,只不過它的實現,增大了記錄的比較和移動的距離,將關鍵字較大的記錄從前面直接移動到後面,關鍵字較小的記錄從後面直接移動到前面,從而減少了總的比較次數和移動交換次數。


9.9.2 快速排序演算法
        快速排序(Quick Sort)的基本思想是:通過一趟排序將待排記錄分割成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分記錄的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序的目的。
        從字面上感覺不出它的好處來。假設現在要對數組{50,10,90,30,70,40,80,60,20}進行排序。我們通過代碼的講解來學習快速排序的精妙。
        我們來看代碼。

/* 對順序表L作快速排序 */void QuickSort(SqList *L){  QSort(L,1,L->length);}

又是一句代碼,和歸併排序一樣,由於需要遞迴調用,因此我們外封裝了一個函數。現在我們來看QSort的實現。

/* 對順序表L中的子序列L->r[low..high]作快速排序 */void QSort(SqList *L,int low,int high){ int pivot; if(low<high) {  pivot=Partition(L,low,high);  /* 將L->r[low..high]一分為二,算出樞軸值pivot */  QSort(L,low,pivot-1);   /*  對低子表遞迴排序 */  QSort(L,pivot+1,high);   /*  對高子表遞迴排序 */ }}

從這裡,你應該能理解前面代碼“QSort(L,1,L->length);”中1和L->length代碼的意思了,它就是當前待排序的序列最小下標值low和最大下標值high。
        這一段代碼的核心是“pivot=Partition(L,low,high);”在執行它之前,L.r的數組值為{50,10,90,30,70,40,80,60,20}。Partition函數要做的,就是先選取當中的一個關鍵字,比如選擇第一個關鍵字50,然後想盡辦法將它放到一個位置,使得它左邊的值都比它小,右邊的值比它大。我們將這樣的關鍵字稱為樞軸(pivot)。
        在經過Partition(L,1,9)的執行之後,數組變成{20,10,40,30,50,70,80,60,90},並傳回值5給pivot,數字5表明50放置在數組下標為5的位置。此時,電腦把原來的數組變成了兩個位於50左和右小數組{20,10,40,30}和{70,80,60,90},而後的遞迴調用“QSort(L,1,5-1);”和“QSort(L,5+1,9);”語句,其實就是在對{20,10,40,30}和{70,80,60,90}分別進行同樣的Partition操作,直到順序全部正確為止。
        到了這裡,應該說理解起來還不算困難。下面我們就來看看快速排序最關鍵的Partition函數實現是怎麼樣的。

/* 交換順序表L中子表的記錄,使樞軸記錄到位,並返回其所在位置 *//* 此時在它之前(後)的記錄均不大(小)於它。 */int Partition(SqList *L,int low,int high){  int pivotkey; pivotkey=L->r[low];  /* 用子表的第一個記錄作樞軸記錄 */ while(low<high)      /* 從表的兩端交替地向中間掃描 */ {    while(low<high&&L->r[high]>=pivotkey)   high--;   swap(L,low,high); /* 將比樞軸記錄小的記錄交換到低端 */   while(low<high&&L->r[low]<=pivotkey)   low++;   swap(L,low,high); /* 將比樞軸記錄大的記錄交換到高端 */ } return low;     /* 返回樞軸所在位置 */}

1) 程式開始執行,此時low=1,high=L.length=9。第4行,我們將L.r[low]=L.r[1]=50賦值給樞軸變數pivotkey,如圖9-9-1所示。
 

2) 第5~13行為while迴圈,目前low=1<high=9,執行內部語句。
3) 第7行,L.r[high]= L.r[9]=20≯pivotkey=50,因此不執行第8行。
4) 第9行,交換L.r[low]與L.r[high]的值,使得L.r[1]=20,L.r[9]=50。為什麼要交換,就是因為第7行的比較知道,L.r[high]是一個比pivotkey=50(即L.r[low])還要小的值,因此它應該交換到50的左側,如圖9-9-2所示。
 
5) 第10行,當L.r[low]= L.r[1]=20,pivotkey=50,L.r[low]<pivotkey,因此第11行,low++,此時low=2。繼續迴圈,L.r[2]=10<50,low++,此時low=3。L.r[3]=90>50,退出迴圈。
6) 第12行,交換L.r[low]=L.r[3]與L.r[high]=L.r[9]的值,使得L.r[3]=50,L.r[9]=90。此時相當於將一個比50大的值90交換到了50的右邊。注意此時low已經指向了3,如圖9-9-3所示。
 
7) 繼續第5行,因為low=3<high=9,執行迴圈體。
8) 第7行,當L.r[high]= L.r[9]=90,pivotkey=50,L.r[high]>pivotkey,因此第8行,high--,此時high=8。繼續迴圈,L.r[8]=60>50,high--,此時high=7。L.r[7]=80>50,high--,此時high=6。L.r[6]=40<50,退出迴圈。
9) 第9行,交換L.r[low]=L.r[3]=50與L.r[high]=L.r[6]=40的值,使得L.r[3]=40,L.r[6]=50,如圖9-9-4所示。
 

10) 第10行,當L.r[low]= L.r[3]=40,pivotkey=50,L.r[low]<pivotkey,因此第11行,low++,此時low=4。繼續迴圈L.r[4]=30<50,low++,此時low=5。L.r[5]=70>50,退出迴圈。
11) 第12行,交換L.r[low]=L.r[5]=70與L.r[high]=L.r[6]=50的值,使得L.r[5]=50,L.r[6]=70,如圖9-9-5所示。
 

12) 再次迴圈。因low=5<high=6,執行迴圈體後,low=high=5,退出迴圈,如圖9-9-6所示。
 

13) 最後第14行,返回low的值5。函數執行完成。接下來就是遞迴調用“QSort(L,1,5-1);”和“QSort(L,5+1,9);”語句,對{20,10,40,30}和{70,80,60,90}分別進行同樣的Partition操作,直到順序全部正確為止。我們就不再示範了。
        通過這段代碼的類比,大家應該能夠明白,Partition函數,其實就是將選取的pivotkey不斷交換,將比它小的換到它的左邊,比它大的換到它的右邊,它也在交換中不斷的更改自己的位置,直到完全滿足這個要求為止。

 


9.9.3 快速排序複雜度分析
        我們來分析一下快速排序法的效能。快速排序的時間效能取決於快速排序遞迴的深度,可以用遞迴樹來描述遞迴演算法的執行情況。如圖9-9-7,它是{50,10,90,30,70,40,80,60,20}在快速排序過程中的遞迴過程。由於我們的第一個關鍵字是50,正好是待排序的序列的中間值,因此遞迴樹是平衡的,此時效能也比較好。
 
        在最優情況下,Partition每次都劃分得很均勻,如果排序n個關鍵字,其遞迴樹的深度就為⌊log 2n⌋+1(⌊x⌋表示不大於x的最大整數),即僅需遞迴log 2n次,需要時間為T(n)的話,第一次Partiation應該是需要對整個數組掃描一遍,做n次比較。然後,獲得的樞軸將數組一分為二,那麼各自還需要T(n/2)的時間(注意是最好情況,所以是平分兩半)。於是不斷的劃分下去,我們就有了下面的不等式推斷:
T(n)≤2T(n/2) +n,T(1)=0
T(n)≤2(2T(n/4)+n/2) +n=4T(n/4)+2n
T(n)≤4(2T(n/8)+n/4) +2n=8T(n/8)+3n
……
T(n)≤nT(1)+(log 2n)×n= O(nlog 2n)

 

        也就是說,在最優的情況下,快速排序演算法的時間複雜度為O(nlogn)。
        在最壞的情況下,待排序的序列為正序或者逆序,每次劃分只得到一個比上一次劃分少一個記錄的子序列,注意另一個為空白。如果遞迴樹畫出來,它就是一棵斜樹。此時需要執行n-1次遞迴調用,且第i次劃分需要經過n-i次關鍵字的比較才能找到第i個記錄,也就是樞軸的位置,因比較次數為 ,最終其時間複雜度為O(n2)。
        平均的情況,設樞軸的關鍵字應該在第k的位置(1≤k≤n),那麼

         
        由數學歸納法可證明,其數量級為O(nlogn)。
        就空間複雜度來說,主要是遞迴造成的棧空間的使用,最好情況,遞迴樹的深度為log2n,其空間複雜度也就為O(logn),最壞情況,需要進行n-1遞迴調用,其空間複雜度為O(n),平均情況,空間複雜度也為O(logn)。
        可惜的是,由於關鍵字的比較和交換是跳躍進行的,因此,快速排序是一種不穩定的排序方法。

(下篇將講解快速排序的各種最佳化)

出處:http://www.cnblogs.com/cj723/archive/2011/04/27/2029993.html

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.