歸併排序【MergeSort】

歸併排序

歸併排序(MERGE-SORT)是利用歸併的思想實現的排序方法,該算法採用經典的分治(divide-and-conquer)策略(分治法將問題分(divide)成一些小的問題然後遞歸求解,而治(conquer)的階段則將分的階段得到的各答案”修補”在一起,即分而治之)。

首先我們可以思考:如何將兩個有序的序列合併,並且合併後依然有序呢?
這個比較簡單,我們只需要用兩個指針,一開始都指向兩個序列的頭部,然後開始比較兩個指針所指數的大小,將小的那個放入一個新建的空間中,然後將它從相應的隊列中刪除,同時相應隊列的指針後移一位;如此往復這個操作,那麼兩個有序的序列就合併了。

public int[] merge(int arrA[], int arrB[]) {
    // 創建合併後的數組
    int[] result = new int[arrA.length + arrB.length];
    int p1 = 0; // arrA的指針
    int p2 = 0; // arrB的指針
    int i = 0;  // 當前要放入result的位置
    // 比較兩個指針所指數的大小,將小的那個放入新的數組中,同時相應隊列的指針後移
    while(p1 < arrA.length && p2 < arrB.length) {
        result[i++] = arrA[p1] < arrB[p2] ? arrA[p1++] : arrB[p2++];
    }

    // 現在必有一個序列已經過完了,下面的循環只會進入一個
    while(p1 < arrA.length) {
        // 當前arrB走完了,將arrA中剩下的直接放入結果數組中
        result[i++] = arrA[p1++];
    }

    while(p2 < arrB.length) {
        // 當前arrA走完了,將arrB中剩下的直接放入結果數組中
        result[i++] = arrB[p2++];
    }
    return result;
}

歸併排序就用到了這樣的思想:先把大的數組拆分成兩個序列,將這兩個序列分別進行排序,然後再合併起來。
可能有人會想,拆分成的兩個序列又怎麼排序呢?很簡單,我們繼續將它拆分,當其不能拆分了,就認爲這個不能拆分的個體已經排好序了,然後就可以進行合併了。

假設初始的數組是[5,6,4,7,3,2],我們可以先將其分成兩個小的部分進行排序,進而再合併起來。
1. 首先拆分成兩個部分:[5,6,4][7,3,2],將這兩個部分分別排序後,再用外排的方式將兩者合併排序起來。但是這兩個部分怎麼分別排序呢?我們可以繼續拆分
2. 將[5,6,4][7,3,2]兩個部分再進行拆分,得到:[5,6],[4][7,3],[2];現在又可以繼續將它們拆分
3. 繼續拆分得到{[5],[6][4]} {[7][3],[2]}; 現在已經無法繼續拆分了,就可以進行合併了
4. 第一次歸併後:[5,6][4][3,7][2]
5. 第二次歸併後:[4,5,6][2,3,7]
6. 第三次歸併後:[2,3,4,5,6,7]

算法實現

public class MergeSort {

    public void mergeSort(int[] arr) {
        if(arr == null || arr.length < 2) {
            return ;
        }
        mergeSort(arr, 0, arr.length - 1);
    }

    /**
        * 將數組在指定範圍內進行拆分合並
        */
    public void mergeSort(int[] arr, int l, int r) {
        // 已經無法再拆分了
        if(l == r) {
            return ;
        }
        // 從中間拆分
        int mid = l + ((r-l) >> 1);
        mergeSort(arr, l, mid);
        mergeSort(arr, mid + 1, r);
        // 進行合併
        merge(arr, l, mid, r);
    }

    /**
    * 合併,l到m,m+1到r 兩段分別有序
    */
    public void merge(int[] arr, int l, int m, int r) {
        int[] help = new int[r - l + 1];
        int i = 0;
        int p1 = l;
        int p2 = m + 1;
        while (p1 <= m && p2 <= r) {
            help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
        }
        while (p1 <= m) {
            help[i++] = arr[p1++];
        }
        while (p2 <= r) {
            help[i++] = arr[p2++];
        }
        for (i = 0; i < help.length; i++) {
            arr[l + i] = help[i];
        }
    }
}

時間複雜度

這裏到了遞歸的算法,每一個遞歸都可以轉換成非遞歸的方式。
遞歸分治法有一個Master公式,可以用來求解時間複雜度
簡單的提一下Master公式:
T(N) = a * T(N/b) + O(N^d), N表示樣本量,N/b表示子過程的樣本量,a表示子過程執行次數,O(N^d)表示除去子過程之外,剩下的時間複雜度

我們這裏是將數組從中間進行拆分,分成兩段分別排序,所以子過程樣本量是N/2;左右兩個子過程,所以執行兩次,a爲2;剩下了一個合併的排序,時間複雜度爲O(N)
所以最終的Master公式是:T(n)=2T(n/2)+O(N),

得到公式後,就可以去套答案了:
1. log(b,a) > d: 時間複雜度O(N^log(b,a))
2. log(b,a) = d: 時間複雜度O(N^d * logN)
3. log(b,a) < d: 時間複雜度O(N^d)

所以最終的時間複雜度是:O(N*logN)

空間複雜度

O(N)

穩定性

歸併排序是穩定的排序

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章