分治算法: 歸併排序(詳解)

分治算法:

分治算法和冒泡排序一樣有着好聽的名字。工作方案如下:
1. 將一個問題劃分爲同一類型的若干子問題,子問題最好規模相同。
2. 對這些子問題求解(一般用遞歸)
3. 有必要的話,合併這些子問題,得到原始問題的答案。


歸併排序:

根據分治思想,我們可以實現歸併排序:要將一個數組排序,可以先(遞歸地)將它分成兩半分別排序,然後將結果歸併起來。這個算法的基本操作是合併兩個已排序的表,因爲這兩個表是有序的,所以可以通過一次遍歷來完成(花費線性時間)。

實現分析:
實現歸併的一種直接的方法是將兩個不同的有序數組歸併到第三個數組中,這會導致歸併排序的空間複雜度爲O(N),這也是歸併排序的主要缺點。我們可以修改代碼,使得它可以在原地歸併,但已有的實現都過於複雜,沒有實際意義。
如果在每次遞歸時都生成一個臨時數組,那麼在任一時刻就可能用logN個臨時數組處在活動期。但實際上,因爲merge方法在mergeSort的最後一行,我們可以只使用一個公有的數組。我在實現中把該數組放在類的下面,當然也可以將它放在public型的mergeSort方法中,通過參數傳遞。

以下是歸併排序的自頂向下的實現代碼:

    private static Integer[] aux;

    public static void mergeSort(Integer[] r){
        aux = new Integer[r.length];
        mergeSort(r, 0, r.length - 1);
    }

    private static void mergeSort(Integer[] r, int left, int right){
        if(right<=left)//如果數組大小不大於1,不再遞歸
            return;
        int mid = (left+right)/2;
        mergeSort(r, left, mid);
        mergeSort(r, mid+1, right);
        merge(r, left, mid, right);
    }

    private static void merge(Integer[] r, int left, int mid, int right){
        int i=left, j=mid+1;

        for(int k=left; k<=right; k++)
            aux[k] = r[k];

        for(int k=left; k<=right; k++)
            if(i>mid)//mid是左側數列的最後一個位置。這裏判斷i是否溢出
                r[k] = aux[j++];
            else if(j>right)
                r[k] = aux[i++];
            else if(aux[i]<aux[j])//比較元素的大小
                r[k] = aux[i++];
            else
                r[k] = aux[j++];
    }

算法分析:
假設數組的長度N是2的k次方(N=2^k),這樣的話我們總可以把它分成兩個等長的數組。對於N=1,歸併排序所用時間爲1;否則,歸併排序所有時間爲排序兩個子數組的時間加上合併的時間
計算過程如下:
歸併排序時間複雜度分析

對於N不是2的冪次的數組,答案幾乎是一樣的。所以歸併排序的時間複雜度爲O(NlogN),空間複雜度爲O(N)

與其他的O(NlogN)排序算法比較,歸併排序的運行時間嚴重依賴於比較元素和在數組中移動元素的相對開銷,這和語言有關。如,在Java中比較操作是昂貴的,但移動元素的開銷的較小的;而C++通常相反。


總結:

歸併排序的時間複雜度爲O(NlogN),空間複雜度爲O(N)。可以自頂向下或自底向上地實現。對於長度很小的數組,遞歸地開銷還是比較昂貴的,我們可以對小數組使用插入排序這樣的算法,以此改進歸併排序的性能。總之,歸併排序是一種漸進最優的基於比較排序的算法。

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