上一個排序隨筆中分析了三種時間復雜度為O(n2)的排序算法,它們適合小規模數據的排序;這次我們試着分析時間復雜為O(nlogn)的排序算法,它們比較適合大規模的數據排序。
1 歸並排序
1.1 原理
將待排序列划分為前后兩部分,直到子序列的區間長度為1;對前后兩部分分別進行排序,再將排好序的兩部分合並在一起。
1.2 實現
1 static void Merge(ElemType* pElem, int p, int q, int r) //特別注意這個數組的細節問題 2 { 3 ElemType* temp = new ElemType[r - p + 1]; //臨時數組 4 int i = p, j = q + 1, k = 0; 5 6 while (i <= q && j <= r) //合並有序子序列 7 { 8 if (pElem[i] <= pElem[j]) 9 temp[k++] = pElem[i++]; 10 else 11 temp[k++] = pElem[j++]; 12 } 13 while (i <= q) 14 temp[k++] = pElem[i++]; 15 while (j <= r) 16 temp[k++] = pElem[j++]; 17 18 for (k = 0, i = p; k < r - p + 1; ) //將數據元素重新放回 19 pElem[i++] = temp[k++]; 20 } 21 22 static void MergeSortInternal(ElemType* pElem, int p, int r) 23 { 24 if (p >= r) return; //序列區間為1 25 26 int m = (p + r) / 2; //區間中間點 27 MergeSortInternal(pElem, p, m); 28 MergeSortInternal(pElem, m + 1, r); 29 Merge(pElem, p, m, r); //合並兩子序列 30 } 31 32 void MergeSort(ElemType* pElem, int n) //歸並排序 33 { 34 MergeSortInternal(pElem, 0, n -1); 35 }
測試結果:

1.3 算法分析
1.3.1 時間復雜度
歸並排序算法由遞歸實現,所以進行時間復雜度分析時也可以通過遞歸公式分析。因為每次划分區域都選擇中間點進行划分,所以遞歸公式可以寫成:
T(n) = T(n/2) + T(n/2) + n, T(1) = C(常數) //每次合並都要調用Merge()函數,它的時間復雜度為O(n)
等價於:T(n) = 2kT(n/2k) + k * n, 遞歸的最終狀態為T(1)即n/2k = 1,所以k = log2n。
T(n) = nT(1) + n * logn。
所以,遞歸排序算法的時間復雜度為O(nlogn),不管待排序列的逆序度如何,時間復雜度不變。
1.3.2 空間復雜度
這里的空間復雜度其實是有分歧的,我就理解成王爭老師的說法吧,因為歸並排序在運行期間,同一時間段內只有一個Merge()在運行,Merge()函數的最大空間復雜度為O(n),所以歸並排序的空間復雜度為O(n)。
1.3.3 穩定性
歸並排序中會改變數據元素的相對位置的只有兩子序列合並時,只要我們將前面的子序列中相等的數據元素放在臨時數組的前面,那么相等數據元素的位置不會改變,所以歸並排序是穩定的排序算法。
1.3.4 比較操作和交換操作的執行次數
比較操作和交換操作的執行次數可以由時間復雜度分析過程得出,Merge()中總的交換次數為n * logn,因為不管兩個子序列的大小,子序列中的各個元素都會先放入臨時數組temp中,再重新放回原序列;比較操作的次數小於等於交換操作次數,最大交換次數為n * logn。
2 快速排序
2.1 原理
快速排序和歸並排序一樣運用了分治的思想。選取分區值,將待排序列分為兩個前后兩部分,前部分數據元素的值小於等於分區值,后部分的數據元素的值大於等於分區值;繼續對前后兩部分分別進行分區,直到分區大小為1。
2.2 實現
1 void swap(ElemType* elem1, ElemType* elem2) //交換數據元素 2 { 3 ElemType temp; 4 temp = *elem1; 5 *elem1 = *elem2; 6 *elem2 = temp; 7 } 8 9 static int Partition(ElemType* pElem, int p, int r) 10 { 11 //將首、中、尾三個位置的數據元素依序排列 12 int mid, pivot; //分區值 13 mid = p + ((r - p) >> 1); //取中間點 14 if (pElem[p] > pElem[mid]) swap(pElem + p, pElem + mid); 15 if (pElem[mid] > pElem[r]) swap(pElem + mid, pElem + r); 16 if (pElem[p] > pElem[mid]) swap(pElem + p, pElem + mid); 17 18 //分區 19 int i = p, j = r; 20 pivot = pElem[mid]; 21 while (i <= j) 22 { 23 while (pElem[i] < pivot) 24 ++i; 25 while (pElem[j] > pivot) 26 --j; 27 if (i <= j) 28 { 29 swap(pElem + i, pElem + j); 30 ++i; 31 --j; 32 } 33 } 34 35 return i; 36 } 37 38 static void QuickSortInternal(ElemType* pElem, int p, int r) 39 { 40 if (r <= p) return; //子序列長度為1 41 42 int q = Partition(pElem, p, r); 43 QuickSortInternal(pElem, p, q - 1); 44 QuickSortInternal(pElem, q, r); 45 VisitArray(pElem, p, r); 46 } 47 48 void QuickSort(ElemType* pElem, int n) //快速排序 49 { 50 QuickSortInternal(pElem, 0, n - 1); //為了統一操作 51 }
測試結果

2.3 算法分析
2.3.1 最壞情況時間復雜度、最好情況時間復雜度和平均情況時間復雜度
1)最壞情況時間復雜度:如果每次分區值選取的恰好是最大或最小值,那么就有n次分區操作,分區操作的時間復雜度為O(n),所以最壞情況時間復雜度為O(n2);
2)最好情況時間復雜度:如果每次分區值都為序列中數據元素的中位數,那么根據遞歸公式就可以寫成T(n) = T(n/2) + T(n/2) + n;那么時間復雜度為O(nlogn);
3)平均情況時間復雜度:可以將區間划分為n / 10、9 * n / 10代入遞推公式,可以得到時間復雜度為O(nlogn)。
2.3.2 空間復雜度
快速排序過程中,只用到了有限個數的臨時變量,所以空間復雜度為O(1)。
2.3.3 穩定性
快速排序改變相對位置的操作為交換,這里的交換操作是在和分區值比較的基礎上進行的,這樣會導致相等數據元素的相對位置發生改變,所以快速排序算法是不穩定的。
2.3.4 比較操作和交換操作執行的次數
最壞情況下,待排序列為正序或逆序,需要執行n-1次遞歸調用,第i次需要執行n-i次比較操作,所以比較操作執行次數為n * (n - 1) / 2,交換操作的執行次數為0(對應正序情況)或n * (n - 1) / 2(對應逆序情況);最好情況下,遞歸調用執行logn次,第i次遞歸的比較次數為n / 2k,所以比較次數為n-1,最大交換次數也為n-1。
3 歸並排序和快速排序的的適用場景
3.1 歸並排序
歸並排序在所有情況下的時間復雜度為O(nlogn)而且是穩定的,但是缺點也非常明顯,它的空間復雜度為O(n)。所以,歸並排序適合數據規模大、對多個關鍵字排序、內存限制小的場景。
3.2 快速排序
快速排序的缺點就是不是穩定的並且對分區值的選擇依賴大,但是分區值的選取問題可以由方法改善。所以快速排序比歸並排序更適合大規模數據的單一關鍵值排序,實際應用場景中快速排序也比歸並排序應用的多。
該篇博客是自己的學習博客,水平有限,如果有哪里理解不對的地方,希望大家可以指正!
