1. 程式人生 > >分而治之-歸併排序

分而治之-歸併排序

如果有1個數組,陣列的左半部分和右半部分都已經排好序,如何將該數組合成1個有序的陣列?

開闢1個同樣大小的臨時空間輔助我們完成歸併過程,如下圖

k:表示歸併過程中,當前需要替換的原陣列位置

i,j:要替換k位置的資料,當前需要考慮的元素,也就是從i和j位置中的元素中取1個最小的,替換到原陣列k的位置。

m:中間位置,也就是陣列左半部分最大的索引位置。

第一次:i和j位置的元素比較,1比2小,則將1替換k的位置,然後i++,k++;

第二次:i和j位置的元素比較,3比2大, 則將2替換k的位置,然後j++,k++;

依次類推,最後原陣列就變成了1個有序的陣列,這就是歸併的過程。

程式碼實現:

    /**
     * 合併函式
     * @param arr   原始陣列
     * @param left  要合併陣列的最左側索引位置
     * @param mid   左側有序陣列和右側有序陣列的分界線
     * @param right 要合併陣列的最右側索引位置
     */
    void __merge(int[] arr, int left, int mid, int right) {
        //將arr陣列 left~right之間的元素copy到arrCopy中
        int[] arrCopy = new int[right - left + 1];
        for (int i = left; i <= right; i++)
            arrCopy[i - left] = arr[i];//賦值

        int i = left, j = mid + 1;
        //給arr陣列left ~ right之間的陣列賦值
        for (int k = left; k <= right; k++) {
            if (i > mid) {//如果i大於mid,說明左側已沒有可以賦值的元素,則選取右側的元素
                arr[k] = arrCopy[j - left];
                j++;
            } else if (j > right) {//說明右側已沒有賦值的元素,則選取左側的元素
                arr[k] = arrCopy[i - left];
                i++;
            } else if (arrCopy[i - left] < arrCopy[j - left]) {//左側元素小於右側元素,選取左側元素
                arr[k] = arrCopy[i - left];
                i++;
            } else {//否則選取右側元素
                arr[k] = arrCopy[j - left];
                j++;
            }
        }
    }

測試:

    @Test
    public void test__merge(){
        int[] arr = new int[]{1,3,5,8,2,4,6,7};
        int left = 0;
        int right = arr.length - 1;
        int mid = (left + right)/2;
        __merge(arr,left,mid,right);
        Arrays.stream(arr).forEach(item->{
            System.out.print(item + " ");
        });
    }

測試結果如下,從測試結果可以看出,陣列已經變成一個有序的陣列。

如果我們要對陣列 5,1,3,8,7,4,6,2 進行排序,可以將其分為兩個大小各為4的子陣列,對兩個子陣列進行排序,然後合併它們,生成有序陣列。同樣,可以將每個子陣列,再次劃分成兩個子陣列,然後對子陣列進行排序和合並。依次劃分,直到子陣列大小變為1。 這樣就可以將一個無序的陣列變成有序的陣列。

程式碼實現:

    /**
     *遞迴使用歸併排序,對arr[left....right]範圍進行排序
     * @param arr
     * @param left
     * @param right
     */
    public void mergeSort(int[] arr,int left,int right){
        if (left >= right)//如果只有1個元素,返回
            return;
        int mid = (left + right)/2;
        mergeSort(arr,left,mid);//整個函式執行完,arr[left....mid]變成有序
        mergeSort(arr,mid + 1,right);//arr[mid + 1....right]變成有序
        __merge(arr,left,mid,right);//合併後,arr[left....right] 變成有序。
    }

測試

    @Test
    public void test__mergeSort(){
        int[] arr = new int[]{5,1,3,8,7,4,6,2};
        int left = 0;
        int right = arr.length-1;
        mergeSort(arr,left,right);
        Arrays.stream(arr).forEach(item->{
            System.out.print(item + " ");
        });
    }

從執行結果可以看出陣列已經排好序。

時間複雜度分析

我們假設對n個元素進行歸併排序需要的時間為T(n),那麼分解成兩個子陣列排序的時間都是T(n/2),__merge合併兩個子陣列的時間複雜度為O(n),則歸併排序的時間複雜度公式如下:

T(1) = C;   n=1 時,只需要常量級的執行時間,所以表示為 C。
T(n) = 2*T(n/2) + n; n>1

通過這個公式,進行分解:

T(n) = 2*T(n/2) + n
     = 2*(2*T(n/4) + n/2) + n = 4*T(n/4) + 2*n
     = 4*(2*T(n/8) + n/4) + 2*n = 8*T(n/8) + 3*n
     = 8*(2*T(n/16) + n/8) + 3*n = 16*T(n/16) + 4*n
     ......
     = 2^k * T(n/2^k) + k * n
     ......

當 T(n/2^k)=T(1) 時,也就是 n/2^k=1,可以得到k=log2n,所以T(n) = nlog2n+Cn。用大O表示法的話,其時間複雜度為O(nlogn).

歸併排序的執行效率與原始陣列的有序程度無關,所以是非常穩定的排序演算法,最好、最壞、平均 時間複雜度都是O(nlogn)。