歸併排序的非遞迴實現 merge sort
歸併排序也稱為合併排序,本文詳細介紹歸併非遞迴的實現。
問題描述
有一串亂序的數字,將它們(利用合併排序的思想)排列成有序的。
通常使用一個陣列來儲存這個串無序的序列,輸出也用一個陣列來表示
輸入:亂序的陣列A,陣列的長度n
輸出:有序的陣列A
特殊情形(元素個數為2i)
基本思路:看作是一顆倒過來的滿二叉樹,兩兩成對
這張圖敘述了合併排序演算法的基本思路,類似一棵倒放的二叉樹。
圖中標記的解釋:state0代表初始的狀態,經過第一趟合併(merge1)之後得到的狀態為State1,以此類推。
歸併排序的基本思路
-
在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)
-
按步驟1的思想以此類推,經過多次合併最終得到有序的陣列,也就是State3。
可以看出經過一共3趟合併,最終得到有序的陣列。
可以看出每趟要執行的合併次數不同,第一趟合併執行4次,第二趟合併執行2次,第三趟合併行1次。
歸併排序的迴圈體設計思路
看了上述的演算法思想,可以知道演算法可以設計成兩層迴圈
- 外層迴圈遍歷趟數
- 內層迴圈遍歷合併次數
下面的偽碼描述了兩層迴圈體的實現:
1 merge_sort(A[], n) { 2 while (趟數條件) { // 外層迴圈:合併的大趟數 3 while (執行合併的條件) { // 內層迴圈:每趟合併的次數 4 // 進行兩兩合併 5 } 6 } 7 }
一般情形(陣列的元素個數不一定是2i)
如圖可知,一般陣列元素的個數不一定是2i個。右邊多出的"10, 7, 2"子樹可以視作一般情況的情形。
雖然元素個數不一定是2i個,但是任意元素個數的n,必然可以拆分成2j + m個元素的情形(數學條件不去深究)
由圖可知,特殊情形思想中的兩兩合併的規則不能滿足一般情況的合併需求
- 圖中灰色的箭頭代表無需合併,因為找不到配對的元素。
- 圖中墨藍色的箭頭代表兩個長度不相等的元素也需要進行合
將上述的合併需求稱為長短合併,容易知道長短合併僅存在1次或0次。
下面討論的合併前/後的序列長度特指兩兩合併後得到的序列長度。
合併趟數的迴圈設計
雖然一般情形與特殊情形的合併規則有差別(一般情形複雜),但是可以設計一個通用的合併趟數條件。
設定變數t:記錄每趟合併後序列的長度(t=2k),其中k是趟次(如圖中的merge1、2、3、4)
- 通過觀察發現:每次合併後序列的大小有規律,第一趟後合併的序列大小都是2,第二趟合併後的序列大小都是4,以此類推..
- "10, 7, 2"這三個元素組合而成的序列長度雖然不滿足上述的規律,但是並不影響趟數的計算。24 = 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 }