歸並排序的遞歸實現 merge sort
歸並排序又稱合並排序,遞歸的實現一般用到分治法的思想。本文詳細介紹歸並排序的遞歸實現。
- 直接或間接地調用自身的算法稱為遞歸算法。
- 分治法的設計思想是:將一個難以直接解決的大問題,分割成一些規模較小的相同問題,以便各個擊破,分而治之。
- 分治和遞歸像一對孿生的兄弟,經常同時應用在算法設計中。
分治法的基本思想
分治法的基本思想可以簡單概述為三步:
-
將一個規模為n的問題分解為k個規模較小的子問題。
這些子問題互相獨立,且與原問題相同。
-
遞歸地解這些子問題。
-
將各個子問題的解合並。
下面的偽代碼描述了分治法的一般設計模式:
divide-and-conquer(P){ if (| P | <= nO) adhoc(P); divide P into smaller subinstances Pl, P2, ..,Pk; for (i = l;i <= k; i++ ) yi = divide-and-conquer(Pi); return merge(y1, y2, ..,yk); }
為什么分治法適用
快速排序也可用使用分治法解決,而歸並排序之所以叫歸並,體現在子問題的解決上:
因為解決子問題用到的思想是:合並兩個有序的數組 merge sorted array
如下圖所示:歸並排序之所以可以用分治法解決的是因為:
-
問題規模縮小到一定程度就可以容易地解決。
如果縮小到數組的長度只有2,那么我們可以利用前面介紹的合並兩個有序的數組 merge sorted array算法:
可以看作”將兩個長度為1(有序)的數組的合並“的問題,問題很容易得到了解決。
-
問題都可以分解為若干規模較小的相同問題。
假設需要排序的數組長度為n,那么:n/2長度子序列的排序,n/4、n/8長度的子序列,直至2個元素的子序列的排序,都是相同的問題。
-
分解出的子問題,是相互獨立的。
-
分解出的子問題可以合並成原問題的解。
從最小的子問題入手:將兩個長度為1(有序)的數組的合並,得到長度為2的有序的數組。
中間不斷處理子問題:將兩個有序的數組合並成一個更大的有序數組,以此類推...
到最后:兩個最長的有序子序列(從原數組分解而來)合並成一個有序的數組,得到原數組的排序。
歸並排序分治法的"分"
下圖介紹了二路歸並的子問題“分”法
怎么分,以及分到什么程度是需要考慮的
怎么分
如果按一分為二,二分為四,四分為八的規則來分,就叫做二路歸並(也是歸並排序默認)
如果按一分為三,三分為九,九分為二十七的規則來分,就叫做三路歸並。。。以此類推
當然怎么分就要考慮怎么合,因為我們反復提到了合並兩個有序的數組 merge sorted array,顯然分兩路是最簡單的,可以利用現成的算法去解決合並。
分到什么程度
分到什么程度,首先要明確最小的子問題是什么。
最小的子問題:解決2個數組長度時的排序問題,即將兩個長度為1的數組進行有序合並。
數組長度為2時還要分一次,分成兩個長度為1的子序列,轉而開始做“合”(也就是治)的操作。
圖中''8, 5",''9, 11",''4, 1",''7, 2"分別分成"8", "5","9", "11"之類時就要轉而開始做合的操作了。
數組長度為1的子序列本身已經是有序的,所以不需要做任何處理。
歸並排序分治法的"分和治"
圖中描述了歸並排序遞歸實現程序運行的過程:每對黑色的箭頭代表“分”操作,每對墨藍色的箭頭代表“合”操作(也就是治)
分治法的運行過程可以看作是對稱的,每“分”一次,就需要“治”一次。
雖然上圖像看似不對稱,實際數下箭頭就會發現對稱之處:
- 圖中有10對黑色箭頭:代表進行了10次分,即每次分都將問題分解成了2個子問題。
- 圖中有10對墨藍箭頭:代表進行了10次治,即每次治都將2個子問題進行解決(合並兩個有序的數組 merge sorted array)。
歸並排序的遞歸實現完整代碼
遞歸程序的代碼很少,難在理解原理。
如果想用非遞歸的方式實現歸並排序,請看我的上一篇文章:歸並排序的非遞歸實現 merge sort
1 /** 2 * 合並排序的遞歸實現(分治法) 3 * @param A 亂序的數組A 4 * @param low 數組的起始下標 5 * @param high 數組的末尾下標 6 */ 7 void merge_sort(int A[], int low, int high) { 8 if (low < high) { // 說明至少還存在兩個元素:需要進行分 9 int i = (low + high) / 2; // 獲得中間位置的下標(偏左) 10 merge_sort(A, low, i); // 分操作:對左半部分的子序列遞歸調用 11 merge_sort(A, i+1, high); // 分操作:對右半部分的子序列遞歸調用 12 merge(A, low, i, high, high-low+1); // 治操作:解決有序兩個子序列的合並 13 } 14 }
其中merge算法的實現,請查看我的上一篇文章介紹:合並兩個有序的數組 merge sorted array。下面給出了實現:
運行測試:
1 int main() { 2 int a[] = {8, 5, 3, 9, 11, 6, 4, 1, 10, 7, 2, 11}; 3 merge_sort(a, 0, 10); // 歸並排序的非遞歸實現 4 for (int i=0;i < 11; i++) { 5 printf("%d ",a[i]); 6 } 7 // 1 2 3 4 5 6 7 8 9 10 11 8 }
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 }