自己動手寫快速排序快速排序演算法不算複雜的演算法,但是實際寫代碼的時候卻是最容易出錯的代碼,寫的不對就容易死迴圈或者劃分錯誤。所以還是自己動手多寫會印象深刻點。
一、初探—樸素的快速排序演算法
void quick_sort(int a[], int l, int u){ if (l >= u) return; int m = l; for (int i=l+1; i<=u; i++) { if (a[i] < a[l]) swap(a, i, ++m); } swap(a, l, m); quick_sort(a, l, m-1); quick_sort(a, m+1, u);}
這個樸素的快速排序有個缺陷就是在一些極端情況如所有元素都相等時(或者元素本身有序,如1,2,3,4,5等),樸素演算法時間複雜度為O(N^2)。
二、改進—雙向劃分快速排序演算法
一種改進方法就是採用雙向劃分,使用兩個變數i和j,i從左往右掃描,移過小元素,遇到大元素停止;j從右往左掃描,移過大元素,遇到小元素停止。然後測試i和j是否交叉,如果交叉則停止,否則交換i與j對應的元素值。注意,如果數組中有相同的元素,則遇到相同的元素時,我們停止掃描,並交換i和j的元素值。雖然這樣交換次數增加了,但是卻將所有元素相同的最壞情況由O(N^2)變成了差不多O(NlgN)的情況。比如數組A={2,2,2,2,2}, 則使用樸素快速排序方法,每次都是劃分n個元素為1組1個和和1組n-1個,時間複雜度為O(N^2),而使用雙向劃分後,第一次劃分的位置是2,基本可以平衡劃分兩部分。代碼如下:
void quick_sort_2(int a[], int l, int u){ if (l >= u) return; int i = l; int j = u+1; int t = a[l];//選擇最左邊元素作為樞紐元 while (1) { do { i++; } while (a[i] < t && i <= u); //注意i<=u這個判斷條件,不能越界。 do { j--; } while (a[j] > t); if (i > j) break; swap(a, i, j); } swap(a, l, j); //注意這裡是交換l和j,而不是l和i,因為i與j交叉後,a[i...u]都大於等於樞紐元t,而樞紐元又在最左邊,所以不能與i交換。只能與j交換。 quick_sort_2(a, l, j-1); quick_sort_2(a, j+1, u);}
雖然雙向劃分解決了所有元素相同的問題,但是對於一個已經排好序的數組如1,2,3,4,5,該方法還是會達到O(N^2)的複雜度。此外,雙向劃分還要注意的一點是代碼中迴圈的寫法,如果寫成while(a[i]<t) {i++;}等形式,因為當左右劃分的兩個值都等於樞紐元時,會導致死迴圈。
三、繼續改進—隨機化樞紐元
為瞭解決上述問題,可以進一步改進,通過隨機選取樞紐元的方式可以在一定程度改進效能。當然,常用的方法還有三數取中等策略。此外,在資料基本有序的情況下,使用插入排序可以得到很好的效能,而且在排序很小的子數組時,插入排序比快速排序更快。通過使用隨機化樞紐元和插入排序來綜合改進快速排序,代碼如下:
/*快速排序修改版*/void quick_sort_3(int a[], int l, int u){ if (u-l < 7) return insert_sort(a, l, u); //元素數目小於7調用插入排序 swap(a, l, randint(l, u)); //隨機選取樞紐元 int i = l; int j = u+1; int t = a[l]; while (1) { do { i++; } while (a[i] < t && i <= u); do { j--; } while (a[j] > t); if (i > j) break; swap(a, i, j); } swap(a, l, j); quick_sort_3(a, l, j-1); quick_sort_3(a, j+1, u);}/*返回[l,u]範圍的隨機數*/int randint(int l, int u){ srand(time(NULL)); return rand()%(u-l+1) + l;}/*插入排序實現*/void insert_sort(int a[], int l, int u) { cout << "insert_sort" << endl; for (int i=l; i<u; i++) { for (int j=i+1; j>l && a[j-1]>a[j]; j--) swap(a, j, j-1); }}
四、再探—採用三數取中方法
除了隨機化樞紐元,還可以採用三數取中的方法,三數取中指的就是從數組A[left... right]中選擇左中右三個值進行排序,並使用中值作為樞紐元。如數組A={1, 3, 5, 2, 4},則我們對A[0]、A[2]、A[4]進行排序,選擇中值A[4]作為樞紐元,數組在三數取中排序後變成A={1,3,4 ,2,5}。我們選擇排序後的A[2]=4作為樞紐元,並將其交換到right-1的位置,也就是位置3,從而最終數組變成A={1, 3, 2, 4, 5},然後就可以從i=1和j=2開始雙向劃分進行快速排序了。
/*三數取中演算法*/int median3(int a[], int left, int right){ int center = (left + right) / 2; /*三數排序*/ if( a[left] > a[center] ) swap(a, left, center); if( a[left] > a[right] ) swap(a, left, right); if( a[center] > a[right] ) swap(a, center, right); /* assert: a[left] <= a[center] <= a[right] */ swap(a, center, right-1); //交換樞紐元到位置right-1 return a[right-1]; //返回樞紐元}
使用三數取中的快速排序如下:
void quick_sort_4(int a[], int l, int u){ if (u - l < 7) return insert_sort(a, l, u); int pivot = median3(a, l, u); //得到樞紐元 int i = l, j = u - 1; while (1) { while (a[++i] < pivot) ; while (a[--j] > pivot) ; if (i > j) break; swap(a, i, j); } swap(a, i, u-1); //注意,這裡是i與樞紐元位置u-1交換,因為這裡是選擇u-1作為樞紐元,與前面選擇l作為樞紐元不同 quick_sort_4(a, l, i-1); quick_sort_4(a, i+1, u);}
五、號外—非遞迴寫快速排序
非遞迴寫快速排序著實比較少見,不過練練手總是好的。需要用到棧,注意壓棧的順序。這裡把劃分函數提取出來,便於多次調用,代碼如下:
void quick_sort_5(int a[], int l, int u){ if (l >= u) return ; stack<int> s; //棧儲存劃分位置 int left = l, right = u; int p = partition(a, left, right); if (p-1 > left) { //左半部分兩個邊界值入棧 s.push(p-1); s.push(left); } if (p+1 < right) { //右半部分兩個邊界值入棧 s.push(right); s.push(p+1); } while (!s.empty()) { //棧不為空白,則迴圈劃分過程 left = s.top(); s.pop(); right = s.top(); s.pop(); p = partition(a, left, right); if (p-1 > left) { s.push(p-1); s.push(left); } if (p+1 < right) { s.push(right); s.push(p+1); } }}/*劃分函數,返回樞紐元的位置*/int partition(int a[], int l, int u){ int m = l; for (int i=l+1; i<=u; i++) { if (a[i] < a[l]) swap(a, i, ++m); } swap(a, l, m); return m;}