歸併排序的非遞迴實現

凱廸bob發表於2021-02-05

歸併排序的非遞迴實現 merge sort

歸併排序也稱為合併排序,本文詳細介紹歸併非遞迴的實現。

問題描述

有一串亂序的數字,將它們(利用合併排序的思想)排列成有序的。

通常使用一個陣列來儲存這個串無序的序列,輸出也用一個陣列來表示

輸入:亂序的陣列A,陣列的長度n

輸出:有序的陣列A

 

特殊情形(元素個數為2i)

基本思路:看作是一顆倒過來的滿二叉樹,兩兩成對

這張圖敘述了合併排序演算法的基本思路,類似一棵倒放的二叉樹。

圖中標記的解釋:state0代表初始的狀態,經過第一趟合併(merge1)之後得到的狀態為State1,以此類推。

歸併排序的基本思路

  1. 在State0初始狀態時,兩兩合併,合併用到的演算法是“合併有序的陣列 merge sorted array”。即每次合併得到的都是有序的陣列。

    兩兩合併的規則是:將兩個相同序列長度的序列進行合併,合併後的序列長度double。

    第一趟合併(merge 1)呼叫了4次merge sorted array,得到了4個有序的陣列:"5, 8","3, 9","6, 4","1, 4"(每個合併後的序列長度為2)

    第二趟合併(merge 2)呼叫了2次merge sorted array,得到了2個有序的陣列:"3, 5, 8, 9","1, 4, 6, 11''(每個合併後的序列長度為4)

  2. 按步驟1的思想以此類推,經過多次合併最終得到有序的陣列,也就是State3。

    可以看出經過一共3趟合併,最終得到有序的陣列。

    可以看出每趟要執行的合併次數不同,第一趟合併執行4次,第二趟合併執行2次,第三趟合併行1次。

歸併排序的迴圈體設計思路

看了上述的演算法思想,可以知道演算法可以設計成兩層迴圈

  1. 外層迴圈遍歷趟數
  2. 內層迴圈遍歷合併次數

下面的偽碼描述了兩層迴圈體的實現: 

1 merge_sort(A[], n) {
2     while (趟數條件) {    // 外層迴圈:合併的大趟數 
3         while (執行合併的條件) {    // 內層迴圈:每趟合併的次數
4         // 進行兩兩合併
5         }
6     }
7 }

 

一般情形(陣列的元素個數不一定是2i

如圖可知,一般陣列元素的個數不一定是2i個。右邊多出的"10, 7, 2"子樹可以視作一般情況的情形。

雖然元素個數不一定是2i個,但是任意元素個數的n,必然可以拆分成2+ m個元素的情形(數學條件不去深究)

由圖可知,特殊情形思想中的兩兩合併的規則不能滿足一般情況的合併需求

  • 圖中灰色的箭頭代表無需合併,因為找不到配對的元素。
  • 圖中墨藍色的箭頭代表兩個長度不相等的元素也需要進行合

將上述的合併需求稱為長短合併,容易知道長短合併僅存在1次或0次。

下面討論的合併前/後的序列長度特指兩兩合併後得到的序列長度。

 

合併趟數的迴圈設計

雖然一般情形特殊情形的合併規則有差別(一般情形複雜),但是可以設計一個通用的合併趟數條件。

設定變數t:記錄每趟合併序列的長度(t=2k),其中k是趟次(如圖中的merge1、2、3、4)

  • 通過觀察發現:每次合併後序列的大小有規律,第一趟後合併的序列大小都是2,第二趟合併後的序列大小都是4,以此類推..
  • "10, 7, 2"這三個元素組合而成的序列長度雖然不滿足上述的規律,但是並不影響趟數的計算。2= 16 ≥ 11,4趟後迴圈結束。
  • 可以設計成:if 最後合併後的序列長度≥實際元素的個數n,這時可以說明迴圈結束

下面的虛擬碼給出了合併趟數的迴圈設計,以及迴圈結束的條件。

1 merge_sort(A[], n) {
2     int t = 1;  // t:每趟合併後序列的長度
3     while (t<n) { // 合併趟數的結束條件是:最後合併後的序列長度t≥陣列元素的個數n
4         t *= 2;
5         // TODO:每趟進行兩兩合併
6     }
7 }

 

每趟合併次數的迴圈設計

從上圖可以看出:每趟的合併次數和元素的總數n有關,且和合併前/後的序列長度有關。

陣列的元素總數n越大,自然需要合併的次數更多。

每趟合併前序列長度越長,這趟需要合併的次數更少。

設定變數s:記錄合併前序列的長度

下面列出了一般情形下的合併數學規律

記m為兩兩合併的次數(圖中每對黑色箭頭代表一次兩兩合併)

記j為長短合併的次數(圖中每對墨藍箭頭代表一次長短合併)

第一趟合併 :n=11,s=1,t=2,m=5,j=0

第二趟合併 :n=11,s=2,t=4,m=2,j=1

第三趟合併 :n=11,s=4,t=8,m=1,j=0

第四趟合併 :n=11,s=8,t=16, m=0,j=1

存在公式:m = n / t,j =(n % t > s ) ? 1: 0 可以設計如下的內層迴圈

其中第二個公式不太好理解,可以以merge2的合併過程作為參考:

兩兩合併得到了"3, 5, 8, 9","1, 4, 6, 11"兩個序列後。還剩餘3個元素,因為3>2=s,所以還要進行長短合併。

 1 merge_sort(A[], n) {
 2     int t = 1;
 3     int i;    // 每趟合併時第一個序列的起始位置
 4     int s;     // 合併前序列的長度
 5     while (t < n) {
 6         t *= 2;
 7         i = 0;
 8         s = t;
 9         while(i + t < n) {// 每趟進行兩兩合併,結束條件:
10             // TODO:兩兩合併操作(對兩個長度相等的陣列進行合併)
11             i = i + t
12         }
13         if (i + s < n) {    // 判斷:還有兩兩長度不相同的陣列需要合併
14             // TODO:長短合併操作(對於長度不相同的陣列進行合併)
15         }
16     }
17 }

 

歸併排序的完整程式碼

 1 /**
 2  * 歸併排序演算法
 3  * @param A 亂序陣列A 
 4  * @param n 陣列A的元素個數
 5  */
 6 void merge_sort(int A[], int n) {
 7     int i,s;  
 8     int t = 1;
 9     while (t < n) {        // 外層迴圈:合併的大趟數  
10         s = t;
11         t *=2;
12         i = 0;
13         while (i + t < n) {        // 內層迴圈:每趟需要合併的次數
14             merge(A, i, i+s-1, i+s*2-1, t); 
15             i = i + t;
16         }
17         if (i + s < n) {  // 判斷還有剩下的元素待處理。
18             merge(A, i, i+s-1, n-1, n-i);
19         }
20     }
21 }

其中merge演算法,請檢視我的上一篇文章介紹:合併兩個有序的陣列。下面給出了實現:

 1 /**
 2  * 合併兩個有序的子陣列( A[p]~A[q]及A[q+l]~A[r]已按遞增順序排序 )
 3  * @param A 整數陣列
 4  * @param p 第一個子陣列的起始下標
 5  * @param q 第一個子陣列的末尾下標
 6  * @param r 第二個字陣列的末尾下標
 7  * @param n A陣列的元素個數
 8  */
 9 void merge(int A[], int p, int q, int r, int n) {
10     int *B = new int[n];        // 建立緩衝區
11     int k = 0;                  // 指向B的遊標,主要用於插入資料進B中
12     int i = p, j = q + 1;
13     while (i <= q && j <= r) {                  // while迴圈的跳出條件是:i和j只要有一個超過各種陣列的界限
14         if (A[i] >= A[j]) {
15             B[k++] = A[j++];
16         } else {
17             B[k++] = A[i++];
18         }
19     }
20     if (i == q+1) {    // 說明是前半段先遍歷完,把後半段的拼到陣列後面
21         while (j <= r) {
22             B[k++] = A[j++];
23         }
24     } else {
25         while (i <= q) {
26             B[k++] = A[i++];
27         }
28     }
29     // 將選定的部分替換為B的陣列
30     k = 0;
31     for (i = p; i <= r; i++) {
32         A[i] = B[k++];
33     }
34     delete[] B;
35 }

 

相關文章