1. 程式人生 > 其它 >歸併排序演算法理解

歸併排序演算法理解

歸併排序演算法簡介

歸併排序就是利用歸併的思想實現的排序方法

假設初始序列含有n個記錄,看成是n個有序的子序列,每個子序列的長度為1,然後兩兩歸併,得到 |n/2||x|表示不小於x的最小整數)個長度為2或1的有序子序列;再兩兩歸併,如此重複,直至得到一個長度為n的有序序列為止,這種排序方法稱為2路歸併排序


歸併排序演算法的遞迴實現

MSort()函式
//對順序表L作歸併排序(此函式的作用就是進行一次封裝,達到統一介面的效果)
void MergeSort(SqList *L){
    MSort(L->r, L->r, 1, L->length);
}

//將SR[s..t]歸併排序為TR1[s..t]
void MSort(int SR[], int TR1[], int s, int t){
    int m;
    int TR2[MAXSIZE + 1];

    //遞迴結束條件
    if (s == t)
        TR1[s] = SR[s];
    else{
        //將SR[s..t]平分為SR[s..m]和SR[m+1..t]
        m = (s + t) / 2;
        //遞迴將SR[s..m]歸併為有序的TR2[s..m]
        MSort(SR, TR2, s, m);
        //遞迴將SR[m+1..t]歸併為有序TR2[m+1..t]
        MSort(SR, TR2, m + 1, t);
        //將TR2[s..m]和TR2[m+1..t] 歸併到TR1[s..t]
        Merge(TR2,TR1, s, m, t);
    }
}

假設對陣列{50,10,90,30,70,40,80,60,20}進行排序,L.length=9

則第一次呼叫MSort()函式的時候(還未進行遞迴),我們將原陣列分成了兩個部分

  • “MSort(SR,TR2,1,5):是將陣列SR中的第1~5的關鍵字歸併到有序的TR2(呼叫前TR2為空陣列)

  • “MSort(SR,TR2,6,9):是將陣列SR中的第6~9的關鍵字歸併到有序的TR2

如下圖所示:

接下來,將進行不斷的遞迴深入,直到滿足s==t的遞迴結束條件,也就是劃分的每個部分只剩下一個元素

然後一層一層退出迭代,每次退出一層迭代,就會呼叫一次Merge()函式,將TR2陣列的兩個部分歸併為一個TR1陣列

整個過程如下圖所示:

在理解了整體的演算法流程之後,我們再看一下Merge()函式的實現

Merge()函式
//將有序的SR[i..m]和SR[m+1..n]歸併為有序的TR[i..n]
void Merge(int SR[], int TR[], int i, int m, int n){
    int j, k, l;
    //將SR中記錄由小到大歸併入TR
    for (j = m + 1, k = i; i <= m && j <= n; k++){
        if (SR[i] < SR[j])
            TR[k] = SR[i++];
        else
            TR[k] = SR[j++];
    }
    if (i <= m){ //將剩餘的SR[i..m]複製到TR
        for (l = 0; l <= m - i; l++)
            TR[k + l]=SR[i + l];
    }
    if (j<=n){ //將剩餘的SR[j..n]複製到TR
        for (l = 0; l <= n - j; l++)
            TR[k + l] = SR[j + l];
    }
}

注意在該函式中:

  • i為SR陣列前一部分的索引

  • j為SR陣列後一部分的索引

  • k為TR陣列(歸併目標陣列)的索引

前面已經提到,每次退出一層遞迴,都會呼叫一個歸併函式Merge(),將兩個部分的陣列歸併為一個數組

這裡我們只解釋最後一次歸併,也就是最複雜的一次歸併

最後一次遞迴呼叫的Merge是將{10,30,50,70,90}與{20,40,60,80}歸併為最終有序的序列

因此陣列SR為{10,30,50,70,90,20,40,60,80},i=1,m=5,n=9

如圖所示:

首先,該函式的第一個for迴圈負責將SR陣列的前後兩個部分的元素,按照順序(逐個比較)進行歸併

例如第一次歸併的操作:

  • SR[i]=SR[1]=10,SR[j]=SR[6]=20,TR[k]=TR[1]=10,並且i++

如此迴圈,直至i或j越界( i>m 或 j>n )

一旦這兩個部分有一個部分越界之後,說明這個部分的元素已經完成了歸併

則接下來我們需要將另一個部分的未歸併的剩餘元素直接複製到TR[]中

如下圖展示了後半部分先歸併完成的情況:

最後,將歸併剩下的陣列資料,移動到TR的後面:

當前k=9,i=m=5,for迴圈l=0,TR[k+l]=SR[i+l]=90,完成歸併排序,如下圖所示:


遞迴實現歸併排序演算法總結

由於歸併排序在歸併過程中需要與原始記錄序列同樣數量的儲存空間存放歸併結果以及遞迴時深度為log2n(2為底)的棧空間

因此空間複雜度為O(n+logn)

Merge函式中if(SR[i] < SR[j])語句說明需要兩兩比較,不存在跳躍,因此歸併排序是一種穩定的排序演算法

歸併排序是一種比較佔用記憶體,但卻效率高且穩定的演算法


歸併排序演算法的非遞迴實現

《大話資料結構》中的實現方式:

MergeSort2()函式
//對順序表L作歸併非遞迴排序
void MergeSort2(SqList *L){
    //申請額外空間
    int * TR = (int *)malloc(L->length * sizeof(int));
    int k = 1;
    while (k < L->length){
        MergePass(L->r, TR, k, L->length);
        //子序列長度加倍
        k = 2 * k;
        MergePass(TR, L->r, k, L->length);
        //子序列長度加倍
        k = 2 * k;
    }
}

以上是非遞迴實現的歸併排序演算法的主體函式,其實現方式是使用L->r陣列和TR陣列互相進行兩兩歸併演算法

如下圖所示,並以此類推:

-

每次歸併完成之後,將歸併的子序列長度x2,直至結束

其核心函式為MergePass()函式,接下來看它的實現方式

MergePass()函式
//將SR[]中相鄰長度為s的子序列兩兩歸併到TR[]
void MergePass(int SR[], int TR[], int s, int n){
    int i = 1;
    int j;
    while (i <= n - 2 * s + 1){
        //兩兩歸併
        Merge(SR, TR, i, i + s - 1, i + 2 * s - 1);
        i = i + 2 * s;
    }
    //歸併最後兩個序列
    if (i < n - s + 1)
        Merge(SR, TR, i, i + s - 1, n);
    //若最後只剩下單個子序列
    else
        for (j = i; j <= n; j++)
            TR[j] = SR[j];
}

首先,第一次呼叫MergePass(L.r,TR,k,L.length)函式的時候:

  • L.r是初始無序狀態,TR為新申請的空陣列,k=1,L.length=9

問:為什麼while的條件是i<=n-2s+1

答:因為下面呼叫Merge()函式時,將兩個部分進行歸併,而其中的後一部分的結尾下標為:i+2s-1,因此若i>n-2s+1則有i+2s-1>n,會導致陣列越界

這樣第一次呼叫這個函式時,兩兩歸併的範圍限於1-8,9號元素會被剩下來

對於這種多餘的元素,我們再判斷,i+s-1(這是用來歸併的前一部分的終點)是否小於n:

  • i+s-1<n,說明歸併的前一部分的終點並未到達n,則可以繼續歸併,移相得:i<n-s+1,則進入if分支,繼續呼叫Merge()函式進行歸併

  • i+s-1>=n,說明若繼續歸併,則歸併的前一部分的終點已經達到n,則無法歸併,進入else分支,可以直接將剩餘部分的元素複製到TR陣列

注意:Merge(int SR[], int TR[], int i, int m, int n)

  • i值指前一部分的開始下標

  • m值指前一部分的結束下標(後一部分從m+1開始)

  • n值為後一部分的結束下標

非遞迴實現歸併排序演算法總結

非遞迴的迭代方法,避免了遞迴時深度為log2n(2為底)的棧空間,空間只是用到申請歸併臨時用的TR陣列,空間複雜度為O(n),避免遞迴在時間效能上也有一定的提升

應該說,使用歸併排序演算法時,儘量考慮使用非遞迴方法

另一種非遞迴實現方式(來自小甲魚資料結構視訊教程)

註釋十分詳細,不再講解

#include<stdio.h>
#include<stdlib.h>

#define MAXSIZE 10

void MergeSort(int k[],int n){
    //next是用來標誌temp陣列下標的
    int i,next;

    //每次歸併都是對兩段資料進行對比排序
    //left\right分別代表左面和右面(前面和後面)的兩段資料
    //min和max分別代表各段資料的最前和最後下標
    int left_min,left_max,right_min,right_max;

    //申請一段記憶體用於存放排序的臨時變數
    int *temp = (int *)malloc(n * sizeof(int));

    //步長:i;從步長=1開始逐級遞增
    for(i=1; i<n; i*=2){
        //每次步長遞增,都從頭開始歸併處理
        for(left_min=0; left_min<n-i; left_min = right_max){
            //兩段資料和步長之間關係
            right_min = left_max = left_min + i;
            right_max = left_max + i;

            //最後的下標不能超過n,否則無意義
            if(right_max>n)
                right_max = n;

            //每次的內層迴圈都會將排列好的資料返回到K陣列,因此next指標需每次清零
            next = 0;

            //兩端資料均未排完
            while(left_min<left_max&&right_min<right_max){
                if(k[left_min] < k[right_min])
                    temp[next++] = k[left_min++];
                else
                    temp[next++] = k[right_min++];
            }

            //上面的歸併排序迴圈結束後,可能有一段資料尚未完全被排列帶temp陣列中
            //剩下未排列到temp中的資料一定是按照升序排列的最大的一部分資料
            //此時有兩種情況:left未排列完成,right未排列完成
            //若是left未排列完成(left_min<left_max),則對於這一段資料省去temp陣列的中轉,直接賦值到k陣列,即從right_max開始倒著賦值
            //若是right未排列完成,則可以想到,那一段資料本就在應該放置的位置,則無需處理
            while(left_min < left_max)
            //上面分析應該從right_max開始倒著賦值,但是實際因為右邊的資料段已經全部排列
            //故此時right_min=right_max
            //且這裡將right_min移動到需要的位置,方便下面賦值時使用
                k[--right_min] = k[--left_max];

            while(next>0)
            //把排列好的資料段賦值給k陣列
            //這裡可以直接用上面經過--right_min倒數過來的right_min值
            //經過上面倒數的處理,right_min恰好在需要賦值和不需要賦值的資料段的分界處
                k[--right_min] = temp[--next];
        }
    }
}

//測試
int main(){
    int i,a[10] = {5,2,6,0,3,9,1,7,4,8};
    MergeSort(a,10);
    printf("排序後的結果是:");
    for(i=0; i<10; i++)
        printf("%d",a[i]);
    printf("\n\n");
    return 0;
}