七、排序 (2)
基於分治思想的排序演算法:歸併排序、快速排序
一、歸併排序(Merge Sort)
1、基本思想
基於分治(分而治之)思想,歸併排序演算法不斷地將原陣列分成大小相等的兩個子陣列(可能相差1),最終當劃分的子陣列大小為1時;然後對前後兩部分分別排序,再將劃分的有序子陣列合併成一個更大的有序陣列。
- 分治,顧名思義,就是分而治之,將一個大問題分解成小的子問題來解決。小的子問題解決了,大問題也就解決了。
2、合併相鄰有序子序列(治)
例如:求解陣列 a[p…r] 等價於 求解有序的a[p…q]和a[q+1…r]的合併,其中 q= (p+r)/2。
過程:
- 申請一個臨時陣列tmp,其大小與a[p…r]相同;
- 設定遊標 i 和 j,分別指向a[p…q]和a[q+1…r]的第一個元素;2.設定遊標 i 和 j,分別指向a[p…q]和a[q+1…r]的第一個元素;
- 比較 a[i] 和 a[j],若 a[i]<=a[j],將a[i]放入臨時陣列tmp,並 i 後移一位,否則將 a[j] 放入臨時陣列tmp,並 j 後移一位;
- 直到一個子陣列中的所有資料都放入到臨時陣列中,再把另一個數組中的資料依次加入到臨時陣列的末尾。所獲得的臨時陣列就是兩個陣列的合併結果
- 把臨時陣列tmp中的資料拷貝到原陣列 a[p…r]
3、實現
void merge_sort(int *data, int start, int end, int *result)
{
if(1 == end - start)//如果區間中只有兩個元素,則對這兩個元素進行排序
{
if(data[start] > data[end])
{
int temp = data[start];
data[start] = data[end];
data[end] = temp;
}
return;
}
else if(0 == end - start)//如果只有一個元素,則不用排序
return;
else
{
//繼續劃分子區間,分別對左右子區間進行排序
merge_sort(data,start,(end-start+1)/2+start,result);
merge_sort(data,(end-start+1)/2+start+1,end,result);
//開始歸併已經排好序的start到end之間的資料
merge(data,start,end,result);
//把排序後的區間資料複製到原始資料中去
for(int i = start;i <= end;++i)
data[i] = result[i];
}
}
void merge(int *data,int start,int end,int *result)
{
int left_length = (end - start + 1) / 2 + 1;//左部分割槽間的資料元素的個數
int left_index = start;
int right_index = start + left_length;
int result_index = start;
while(left_index < start + left_length && right_index < end+1)
{
//對分別已經排好序的左區間和右區間進行合併
if(data[left_index] <= data[right_index])
result[result_index++] = data[left_index++];
else
result[result_index++] = data[right_index++];
}
while(left_index < start + left_length)
result[result_index++] = data[left_index++];
while(right_index < end+1)
result[result_index++] = data[right_index++];
}
4、分析
(1)穩定排序演算法
- 關鍵在於合併相鄰有序子序列的部分。
- 在合併的過程中,若a[p…q]和a[q+1…r]之間有值相同的元素,那麼將a[p…q]中的元素放入tmp陣列中,就使得值相同的元素,在合併前後的先後順序不變。
(2)時間複雜度
a、遞迴方法的時間複雜度
遞迴方法:一個問題A可以分解為多個子問題B、C,那麼求解問題A可分解為求解B、C。問題B、C解決之後,我們將B、C的結果合併成問題A的結果。
定義求解問題A的時間是T(a),求解問題B、C的時間分別是 T(b) 和 T(c) ,則可得遞迴公式:
T(a) = T(b) + T(c) + K
其中,K 等於將兩個子問題 B、C 的結果合併成問題A的結果所消耗的時間
==》遞迴程式碼的時間複雜度也可以寫成遞推公式
b、歸併排序的時間複雜度
- 假設對 n 個元素進行歸併排序需要 T(n),那分解成兩個子陣列排序的時間都是 T(n/2);
- 合併相鄰有序子序列的時間複雜度是 O(n) 。
可得 ==》
T(1) = C; //n=1 時,只需要常量級的執行時間,所以表示為 C。
T(n) = 2 * T(n/2) + n; // n > 1
分解可得 ==》
T(n) = 2 * T(n/2) + n;
= 2 * (2 * T(n/4) + n/2) + n = 4 * T(n/4) + 2 * n
= 4*(2*T(n/8) + n/4) + 2*n = 8*T(n/8) + 3*n
= 8*(2*T(n/16) + n/8) + 3*n = 16*T(n/16) + 4*n
……
= 2^k * T(n/2^k) + k * n
……
==》當 T(n/2^k) = T(1) 求得
==》k=log2n
==》將 k 值帶入,可得 T(n) = Cn + nlog2n
==》時間複雜度:T(n) = O(nlogn)
(3)空間複雜度——O(n)
每次合併操作都需申請額外的記憶體空間,但在完成之後,臨時記憶體空間就被釋放掉。
==》 在任意時刻,CPU只會有一個函式在執行,也就是隻有一個臨時記憶體空間在使用 ==》最大不會超過 n 個數據的大小:O(n)
二、快速排序 (QuickSort)
1、基本思想
基於分治思想,快速排序演算法:選擇一個基準數,通過一趟排序將要排序的資料分割成獨立的兩部分;其中一部分的所有資料都比另外一部分的所有資料都要小。然後,再按此方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序序列。
快速排序的流程:
(1) 從數列中挑出一個基準值。
(2) 將所有比基準值小的擺放在基準前面,所有比基準值大的擺在基準的後面(相同的數可以到任一邊);在這個分割槽退出之後,該基準就處於數列的中間位置。
(3) 遞迴地把"基準值前面的子數列"和"基準值後面的子數列"進行排序。
2、虛擬碼
partition(A, p, r) {
key := A[r];
i := p;
for j := p to r-1 do {
if A[j] < key {
swap A[i] with A[j]
i := i + 1
}
}
swap A[i] with A[r]
return i
}
通過遊標 i 把 A[p…r-1] 分成兩部分。A[p…i-1] 的元素都是小於 key 的,我們暫且叫它“已處理區間”,A[i…r-1] 是“未處理區間”。我們每次都從未處理的區間 A[i…r-1] 中取一個元素 A[j],與 key 對比,如果小於 key,則將其加入到已處理區的尾部,也就是 A[i] 的位置。
3、效能分析
- 不穩定
- 原地排序——空間複雜度為O(1)
- 時間複雜度:大部分情況下的時間複雜度都可以做到 O(nlogn),只有在極端情況下,才會退化到 O(n2)。