快速排序法(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的時候,再進行交換,這樣可以提高交換的效率(也即降低交換的次數)
- 快速排序算法在數組很小的時候的效率是十分低下的,其速度並沒有插入排序算法的速度快,因而在數組的大小小於一定的值之後,應該採用插入排序完成排序。
爲了解決第一個問題,很多專家學者進行了如下嘗試:
- 選取最前面的兩個不同的元素,取其中較大的一個賦值給v;但是這種做法和第一種做法有相同的弊端,讀者可自行進行分析,在此不作贅述。
- 在諸多元素之中選取一個隨機的元素作爲v。這種做法可以避免O(n^2)的弊端,但是隨機數的產生需要花費很多的時間,所以這種做法是正確的,但是卻並不是高效的。
- 選取最左邊,中間和最右邊三個數中的中間值。比如左中右三個值分別是0、8、6,那麼我們就選取6作爲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至少要對兩個數進行排序或者更多。