排序—時間復雜度為O(nlogn)的兩種排序算法


上一個排序隨筆中分析了三種時間復雜度為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 快速排序

快速排序的缺點就是不是穩定的並且對分區值的選擇依賴大,但是分區值的選取問題可以由方法改善。所以快速排序比歸並排序更適合大規模數據的單一關鍵值排序,實際應用場景中快速排序也比歸並排序應用的多。

 

該篇博客是自己的學習博客,水平有限,如果有哪里理解不對的地方,希望大家可以指正!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM