《大话数据结构》第9章 排序 9.9 快速排序(上)
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}進行排序。我們通過代碼的講解來學習快速排序的精妙。
??????? 我們來看代碼。
又是一句代碼,和歸并排序一樣,由于需要遞歸調用,因此我們外封裝了一個函數。現在我們來看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函數實現是怎么樣的。
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,正好是待排序的序列的中間值,因此遞歸樹是平衡的,此時性能也比較好。
?
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)+(log2n)×n= O(nlog2n)
?
??????? 也就是說,在最優的情況下,快速排序算法的時間復雜度為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
總結
以上是生活随笔為你收集整理的《大话数据结构》第9章 排序 9.9 快速排序(上)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《大话数据结构》第9章 排序 9.8 归
- 下一篇: 《大话数据结构》第9章 排序 9.9 快