歸並排序的非遞歸實現 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 }
