歸並排序的遞歸實現


歸並排序的遞歸實現 merge sort

歸並排序又稱合並排序,遞歸的實現一般用到分治法的思想。本文詳細介紹歸並排序的遞歸實現。

  • 直接或間接地調用自身的算法稱為遞歸算法。
  • 分治法的設計思想是:將一個難以直接解決的大問題,分割成一些規模較小的相同問題,以便各個擊破,分而治之。
  • 分治和遞歸像一對孿生的兄弟,經常同時應用在算法設計中。

分治法的基本思想

分治法的基本思想可以簡單概述為三步:

  1. 將一個規模為n的問題分解為k個規模較小的子問題。

    這些子問題互相獨立,且與原問題相同。

  2. 遞歸地解這些子問題。

  3. 將各個子問題的解合並。

下面的偽代碼描述了分治法的一般設計模式:

 
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

如下圖所示:歸並排序之所以可以用分治法解決的是因為:

  1. 問題規模縮小到一定程度就可以容易地解決。

    如果縮小到數組的長度只有2,那么我們可以利用前面介紹的合並兩個有序的數組 merge sorted array算法:

    可以看作”將兩個長度為1(有序)的數組的合並“的問題,問題很容易得到了解決。

  2. 問題都可以分解為若干規模較小的相同問題。

    假設需要排序的數組長度為n,那么:n/2長度子序列的排序,n/4、n/8長度的子序列,直至2個元素的子序列的排序,都是相同的問題。

  3. 分解出的子問題,是相互獨立的。

  4. 分解出的子問題可以合並成原問題的解。

    從最小的子問題入手:將兩個長度為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 }

 

 

 
 
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM