1. 程式人生 > >七、排序 (2)

七、排序 (2)

基於分治思想的排序演算法:歸併排序、快速排序

一、歸併排序(Merge Sort)

1、基本思想

基於分治(分而治之)思想,歸併排序演算法不斷地將原陣列分成大小相等的兩個子陣列(可能相差1),最終當劃分的子陣列大小為1時;然後對前後兩部分分別排序,再將劃分的有序子陣列合併成一個更大的有序陣列。

  • 分治,顧名思義,就是分而治之,將一個大問題分解成小的子問題來解決。小的子問題解決了,大問題也就解決了。

在這裡插入圖片描述

2、合併相鄰有序子序列(治)

例如:求解陣列 a[p…r] 等價於 求解有序的a[p…q]和a[q+1…r]的合併,其中 q= (p+r)/2。

過程:

  1. 申請一個臨時陣列tmp,其大小與a[p…r]相同;
  2. 設定遊標 i 和 j,分別指向a[p…q]和a[q+1…r]的第一個元素;2.設定遊標 i 和 j,分別指向a[p…q]和a[q+1…r]的第一個元素;
  3. 比較 a[i] 和 a[j],若 a[i]<=a[j],將a[i]放入臨時陣列tmp,並 i 後移一位,否則將 a[j] 放入臨時陣列tmp,並 j 後移一位;
  4. 直到一個子陣列中的所有資料都放入到臨時陣列中,再把另一個數組中的資料依次加入到臨時陣列的末尾。所獲得的臨時陣列就是兩個陣列的合併結果
  5. 把臨時陣列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)。