快速排序法详解
快速排序法(QuickSort)是一種非常快的對比排序方法。它也Divide-And-Conquer思想的實現之一。自從其產生以來,快速排序理論得到了極大的改進,然而在實際中卻十分難以編程出正確健壯的代碼。本文將對快速排序算法的基本理論和編程實踐方面做作一個全面的講解。在本文講解中,將忽略很多細枝末節,試圖給讀者形成一個非常具體的快速排序形象。
1.快速排序---基本理論
因為該算法是Divide-And-Conquer思想的一個實現,所以本文將以Divide-And-Conquer思想對其進行分析。首先,假設所要排序的數字存儲在數組S中,則該算法的操作可以拆分為兩部分:
- 在S中選出一個元素v;
- 將S數組分為三個子數組。其中v這個元素單獨形成子數組1,比v小的元素形成子數組2,比v大的元素形成自數組3.
- 分別對子數組2和子數組3進行前兩步操作,實現遞歸排序;
- 返回時,依次返回S1,V,S2;
該程序具有平均運行時間T(n) = O(nlgn), 最差運行時間T(n) = O(n^2);
下面給出一個簡單的排序實例對以上算法進行簡單說明:
初始數組為--------------> S: 6,10,13,5,8,3,2,11
將第一個元素賦值給v----->v = 6;
以v為標準將S進行拆分--->[2,5,3],[6],[8,13,10,11] <----------將得到的數組命名為S1, S2;
同樣對子數組S1進行拆分->[ ], [2], [ 5, 3] <--------------------拆分之后,第一個子數組為空。將得到的數組命名為S12;
對子數組S2進行拆分----->[ ], [8], [13, 10, 11]<---------------將得到的數組命名為S22;
此時的數組S為---------->2,5,3,6,8,13,10,11
對子數組S12進行拆分---->[3], [5],[ ];
對自數組S22進行拆分---->[10,11],[13],[]<--------------------將得到的數組命名為S221
此時的數組S為----------->2,3,5,6,8,10,11,13
對子數組S221進行拆分--->[ ], [11], [13]
對后得到的數組為-------->2,3,5,6,8,10,11,13;
根據以上分析,編寫快速排序算法程序,得到的程序如下:
1 #include <string> 2 #include <iostream> 3 4 ?using namespace::std; 5 6 ?int Partition( int A[], int p, int q ) 7 { 8 int key = A[p]; 9 int i = p; 10 for(int j = p + 1 ;j < q; j++ ) 11 { 12 if( A[j] <= key ) 13 { 14 i++; 15 swap<int>(A[i], A[j]); 16 } 17 } 18 swap<int>(A[p], A[i]); 19 return i; 20 } 21 22 ?void QuickSort( int A[], int p, int q ) 23 { 24 if( p < q ) 25 { 26 int r = Partition(A, p, q); 27 QuickSort(A,p,r-1); 28 QuickSort(A,r+1,q); 29 } 30 } 31 32 ?int main() 33 { 34 int A[10] = {8,1,4,9,0,3,5,2,7,6}; 35 QuickSort(A,0,9); 36 for( int k = 0; k < 10; k++ ) 37 cout << A[k] << " "; 38 cout << endl; 39 }計算結果如圖:
看似結果很好,但是很遺憾,在實際中,我們卻并不采用這樣的程序。為什么呢?因為該程序還有幾點需要進行改進:
- 當我們輸入的數組S是已經排序好的一列數,那么這個程序的運行時間將是O(n^2),這個效率是插入排序的效率,所以是很低很低的。(可以利用遞歸樹進行具體分析)
- 為了提高效率,可以使得i和j分別從左邊和右邊進行搜索,將值分別與v進行對比,當S[i]>v而S[j]<v的時候,再進行交換,這樣可以提高交換的效率(也即降低交換的次數)
- 快速排序算法在數組很小的時候的效率是十分低下的,其速度并沒有插入排序算法的速度快,因而在數組的大小小于一定的值之后,應該采用插入排序完成排序。
為了解決第一個問題,很多專家學者進行了如下嘗試:
下面給出以上分析之后的快速排序算法程序:
1 #include <string> 2 #include <iostream> 3 #include <algorithm> 4 ?using namespace::std; 5 6 ?int Median3(int A[], int p, int q ) 7 { 8 int c = ( p + q ) / 2; 9 if( A[p] > A[c] ) 10 swap<int>(A[p], A[c]); 11 if( A[p] > A[q] ) 12 swap<int>(A[p], A[q]); 13 if( A[c] > A[q] ) 14 swap<int>(A[c], A[q]); 15 swap<int>(A[c],A[q-1]); 16 return A[q-1]; 17 } 18 19 ?int Partition( int A[], int p, int q ) 20 { 21 int key = Median3( A, p, q ); 22 int i = p; 23 int j = q-1; 24 while(1) 25 { 26 while( A[++i] < key ){} 27 while( A[--j] > key ){} 28 if( i < j ) 29 swap<int>( A[i], A[j] ); 30 else 31 break; 32 } 33 swap( A[i], A[q-1] ); 34 return i; 35 } 36 37 void InsertionSort(int A[], int N) 38 { 39 int tmp; 40 int j; 41 int p; 42 43 for( p = 1; p < N; p++ ) 44 { 45 tmp = A[p]; 46 for( j = p; j > 0 && A[j -1] > tmp; j -- ) 47 A[j] = A[j-1]; 48 A[j] = tmp; 49 } 50 } 51 52 #define cutoff 5 53 void QuickSort(int A[], int p, int q) 54 { 55 if( p + cutoff <= q ) 56 { 57 int r = Partition(A, p, q); 58 QuickSort( A, p, r - 1 ); 59 QuickSort( A, r + 1, q ); 60 } 61 else 62 InsertionSort(A + p, q - p + 1 ); 63 } 64 65 int main() 66 { 67 int A[8] = {6,10,13,5,8,3,2,11}; 68 QuickSort(A,0,7); 69 for( int k = 0; k < 8; k++ ) 70 cout << A[k] << " "; 71 cout << endl; 72 }排序結果如圖所示:
該程序中,cutoff的值必須大于等與2!
因為若是cutoff = 1;也就是說,插入法排序的數字只有一個;那么遞歸的最內一層是兩個數字。在這個時候就會出現問題,具體分析如下:
以上例中的數組A為例,在遞歸樹的右側,會出現對13,11的排序;此時,p = 6, q = 7;
設C, L, R分別代表了中間,左邊和右邊三個值,那么根據Median3函數算法的計算,最終得到L = 13, C = 13, R = 11; 于是:
L < C? => L和C不交換;
L > R => L和R交換,此時 C = 11, L = 11, R = 13;
C < R => C和R不交換;
所以最后得到的Key = 11, 經過Median3排序之后的順序是11,13;
于是對其進行排序,完成時i = 7, 因此在執行33句時會交換A[7]和A[6],交換之后得到的順序是 13, 11;
這個順序就是最終的排序結果,因此在排序的最后導致了程序的排序結果錯誤;
產生這個錯誤的主要原因是:剩余了兩個數,而在求meidian值的時候,對三個數進行了對比。
同時,若cutoff的值小于2還將產生一個錯誤,那就數:
--j的崗哨依賴于數組的元素A[P] < key,這樣才使得,--j不會越過p值;而在上述情況中,A[p] = key值,為了提高程序的效率, 該程序在編寫時設定,當A[j] = A[p]時,j會繼續搜索,所以導致--j越過了A[p];
所以在設定cutoff的時候,cutoff的值至少為2,也就說InsertionSort至少要對兩個數進行排序或者更多。
總結
- 上一篇: 可忽略的小概率
- 下一篇: ucOS_II移植:Stm32启动代码分