整理:數據結構與算法之歸併排序(遞歸的虛擬機棧幀的演示)

目錄

1、基本思想

2、歸併的實例:A-B兩個有序數組歸併

3、遞歸:分解一個無需數組,然後歸併

3.1 遞歸分析

3.2 虛擬機棧:演示遞歸的執行過程(重點)

4、算法分析


1、基本思想

分析歸併排序之前,我們先來了解一下分治算法

分治算法的基本思想是將一個規模爲N的問題分解爲K個規模較小的子問題,這些子問題相互獨立且與原問題性質相同。求出子問題的解,就可得到原問題的解。

分治算法的一般步驟:

  • 分解,將要解決的問題劃分成若干規模較小的同類問題;
  • 求解,當子問題劃分得足夠小時,用較簡單的方法解決;
  • 合併,按原問題的要求,將子問題的解逐層合併構成原問題的解。

歸併排序是分治算法的典型應用。

歸併排序先將一個無序的N長數組切成N個有序子序列(只有一個數據的序列認爲是有序序列),然後兩兩合併,再將合併後的N/2(或者N/2 + 1)個子序列繼續進行兩兩合併,以此類推得到一個完整的有序數組。過程如下圖所示:

 

2、歸併的實例:A-B兩個有序數組歸併

歸併排序的核心思想是將兩個有序的數組歸併到另一個數組中,所以需要開闢額外的空間。

第一步要理清歸併的思路。假設現在有兩個有序數組A和B,要將兩者有序地歸併到數組C中。我們用一個實例來推演:

 

上圖中,

A數組中有四個元素,B數組中有六個元素,

首先比較A、B中的第一個元素,將較小的那個放到C數組的第一位,因爲該元素就是A、B所有元素中最小的。

上例中,7小於23,所以將7放到了C中。

然後,用23與B中的其他元素比較,如果小於23,繼續按順序放到C中;如果大於23,則將23放入C中。

23放入C中之後,用23之後的47作爲基準元素,與B中的其他元素繼續比較,重複上面的步驟。

如果有一個數組的元素已經全部複製到C中了,那麼將另一個數組中的剩餘元素依次插入C中即可。

至此結束。

按照上面的思路,用java實現:

 

    /**
     * 
     * - 歸併arrayA與arrayB到arrayC中
     * 
     * - @param arrayA 待歸併的數組A
     * 
     * - @param sizeA 數組A的長度
     * 
     * - @param arrayB 待歸併的數組B
     * 
     * - @param sizeB 數組B的長度
     * 
     * - @param arrayC 輔助歸併排序的數組
     */
    public static void merge(int[] arrayA, int sizeA, int[] arrayB, int sizeB, int[] arrayC) {

        int i = 0, j = 0, k = 0; // 分別當作arrayA、arrayB、arrayC的下標指針

        while (i < sizeA && j < sizeB) { // 兩個數組都不爲空
            if (arrayA[i] < arrayB[j]) { // 將兩者較小的那個放到arrayC中
                arrayC[k++] = arrayA[i++];
            } else {
                arrayC[k++] = arrayB[j++];
            }
        } // 該循環結束後,一個數組已經完全複製到arrayC中了,另一個數組中還有元素

        // 後面的兩個while循環用於處理另一個不爲空的數組
        while (i < sizeA) {
            arrayC[k++] = arrayA[i++];
        }

        while (j < sizeB) {
            arrayC[k++] = arrayA[j++];
        }

        for (int l = 0; l < arrayC.length; l++) { // 打印新數組中的元素
            System.out.print(arrayC[l] + "\t");
        }
    }

3、遞歸:分解一個無序數組,然後歸併

再歸併之前,還有一步工作需要提前做好,就是數組的分解,可以通過遞歸的方法來實現。遞歸(Recursive)是算法設計中常用的思想。

這樣通過先遞歸的分解數組再合併數組就完成了歸併排序。完整的java代碼如下:

 

public class Sort {

    private int[] array; // 待排序的數組

    public Sort(int[] array) {
        this.array = array;
    }

    // 按順序打印數組中的元素
    public void display() {
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + "\t");
        }
        System.out.println();
    }

    // 歸併排序

    public void mergeSort() {

        int[] workSpace = new int[array.length]; // 用於輔助排序的數組
        recursiveMergeSort(workSpace, 0, workSpace.length - 1);
    }

    /**
     * 
     * - 遞歸的歸併排序
     * 
     * - @param workSpace 輔助排序的數組
     * 
     * - @param lowerBound 欲歸併數組段的最小下標
     * 
     * - @param upperBound 欲歸併數組段的最大下標
     */
    private void recursiveMergeSort(int[] workSpace, int lowerBound, int upperBound) {

        if (lowerBound == upperBound) { // 該段只有一個元素,不用排序
            return;
        } else {
            int mid = (lowerBound + upperBound) / 2;
            recursiveMergeSort(workSpace, lowerBound, mid); // 對低位段歸併排序
            recursiveMergeSort(workSpace, mid + 1, upperBound); // 對高位段歸併排序
            merge(workSpace, lowerBound, mid, upperBound);
            display();
        }
    }

    /**
     * 
     * - 對數組array中的兩段進行合併,lowerBound~mid爲低位段,mid+1~upperBound爲高位段
     * 
     * - @param workSpace 輔助歸併的數組,容納歸併後的元素
     * 
     * - @param lowerBound 合併段的起始下標
     * 
     * - @param mid 合併段的中點下標
     * 
     * - @param upperBound 合併段的結束下標
     */
    private void merge(int[] workSpace, int lowerBound, int mid, int upperBound) {

        int lowBegin = lowerBound; // 低位段的起始下標
        int lowEnd = mid; // 低位段的結束下標
        int highBegin = mid + 1; // 高位段的起始下標
        int highEnd = upperBound; // 高位段的結束下標
        int j = 0; // workSpace的下標指針
        int n = upperBound - lowerBound + 1; // 歸併的元素總數

        while (lowBegin <= lowEnd && highBegin <= highEnd) {
            if (array[lowBegin] < array[highBegin]) { // 將兩者較小的那個放到workSpace中
                workSpace[j++] = array[lowBegin++];
            } else {
                workSpace[j++] = array[highBegin++];
            }
        }

        while (lowBegin <= lowEnd) {
            workSpace[j++] = array[lowBegin++];
        }

        while (highBegin <= highEnd) {
            workSpace[j++] = array[highBegin++];
        }

        for (j = 0; j < n; j++) { // 將歸併好的元素複製到array中
            array[lowerBound++] = workSpace[j];
        }

    }
}

用以下代碼測試:

 

int [] a = {6,2,7,4,8,1,5,3};
Sort sort = new Sort(a);
sort.mergeSort();

打印結果如下:

 

3.1 遞歸分析

歸併的順序是這樣的:先將初始數組分爲兩部分,先歸併低位段,再歸併高位段。對低位段與高位段繼續分解,低位段分解爲更細分的一對低位段與高位段,高位段同樣分解爲更細分的一對低位段與高位段,依次類推。

上例中,

第一步,歸併的是 [6與2]

第二步歸併的是 [7和4]

第三部歸併的是前兩步歸併好的子段 [2,6]與[4,7]

至此,數組的左半部分(低位段)歸併完畢,得到[2,4,6,7] ,然後歸併右半部分(高位段)。

所以第四步歸併的是 [8與1]

第五部歸併的是 [5與3]

第六步歸併的是前兩步歸併好的字段 [1,8]與[3,5],

至此,數組的右半部分歸併完畢,右邊得到 [1,3,5,8]

最後一步就是歸併數組的 左半部分[2,4,6,7] 右半部分[1,3,5,8]

歸併排序結束。

 

在本文開始對歸併排序的描述中,第一躺歸併是對所有相鄰的兩個元素歸併結束之後,才進行下一輪歸併,並不是先歸併左半部分,再歸併右半部分,但是程序的執行順序與我們對歸併排序的分析邏輯不一致,所以理解起來有些困難。

 

3.2 虛擬機棧:演示遞歸的執行過程(重點)

先參考:算法設計方法:遞歸的內涵與經典應用

下面結合代碼與圖例來詳細分析一下歸併排序的過程。

虛擬機棧(VM Stack)是描述Java方法執行的內存模型,每一次方法的調用都伴隨着一次壓棧、出棧操作。

我們要排序的數組爲:

int [] a = {6,2,7,4,8,1,5,3}

main()方法調用mergeSort()方法時,被調用的方法被壓入棧中,然後程序進入mergeSort()方法:

 

    public void mergeSort() {
        int[] workSpace = new int[array.length]; // 用於輔助排序的數組
        recursiveMergeSort(workSpace, 0, workSpace.length - 1);
    }

此時,mergeSort()又調用了recursiveMergeSort(workSpace,0,7)方法,recursiveMergeSort(workSpace,0,7)方法也被壓入棧中,在mergeSort()之上。

然後,程序進入到 遞去①:recursiveMergeSort(workSpace,0,7方法:

 

if (lowerBound == upperBound) { // 該段只有一個元素,不用排序
    return;
} else {
    int mid = (lowerBound + upperBound) / 2;
    recursiveMergeSort(workSpace, lowerBound, mid); // 對低位段歸併排序
    recursiveMergeSort(workSpace, mid + 1, upperBound); // 對高位段歸併排序
    merge(workSpace, lowerBound, mid, upperBound);
    display();
}

lowerBound參數值爲0,upperBound參數值爲7,不滿足lowerBound == upperBound的條件,所以方法進入else分支,

然後調用方法 recursiveMergeSort(workSpace,0,3)

遞去②:recursiveMergeSort(workSpace,0,3被壓入棧中,此時棧的狀態如下:

 

然而,recursiveMergeSort(workSpace,0,3)不能立即返回,

它在內部又會調用 遞去③:recursiveMergeSort(workSpace,0,1)

recursiveMergeSort(workSpace,0,1)又調用了 遞去④:recursiveMergeSort(workSpace,0,0)

此時,棧中的狀態如下:

 

程序運行到這裏,終於有一個方法可以返回了結果了—— 滿足終止條件①: recursiveMergeSort(workSpace,0,0)

該方法的執行的邏輯是對數組中的下標從0到0的元素進行歸併,該段只有一個元素,所以不用歸併,立即return。

方法一旦return,就意味着方法結束,recursiveMergeSort(workSpace,0,0)從棧中彈出

這時候,程序跳到了代碼片段(二)中的第二行: 滿足終止條件②:recursiveMergeSort(workSpace,1,1),該方法入棧recursiveMergeSort(workSpace,0,0)類似,不用歸併,直接返回,方法出棧

這時候程度跳到了代碼片段(二)中的第三行:歸來時處理:merge(workSpace,0,0,1),即對數組中的前兩個元素進行合併(自然,merge(workSpace,0,0,1)也伴隨着一次入棧與出棧)。

至此,代碼片段(二)執行完畢,歸來①:recursiveMergeSort(workSpace,0,1)方法出棧,程序跳到代碼片段(三)的第二行:recursiveMergeSort(workSpace,2,3)=> 遞去+歸來,然在歸來時merge(2,2,3)第三和第四個元素,該方法是對數組中的第三個、第四個元素進行歸併,與執行recursiveMergeSort(workSpace,0,1)的過程類似,最終會將第三個、第四個元素歸併排序。

然後,程序跳到程序跳到代碼片段(三)的第三行:merge(workSpace,0,1,3)

將前面已經排好序的兩個子序列(【第一第二】個元素爲一組、【第三第四】個元素爲一組)合併。

然後recursiveMergeSort(workSpace,0,3)出棧,程序跳到代碼片段(四)的第二行:recursiveMergeSort(workSpace,4,7),對數組的右半部分的四個元素進行歸併排序,伴隨着一系列的入棧、出棧,最後將後四個元素排好。此時,數組的左半部分與右半部分已經有序。

然後程序跳到代碼片段(四)第三行:merge(workSpace,0,3,7),對數組的左半部分與右半部分合並。

然後recursiveMergeSort(workSpace,4,7)出棧,mergeSort()出棧,最後main()方法出棧,程序結束。

4、算法分析

先來分析一下複製的次數。

如果待排數組有8個元素,歸併排序需要分3層

第一層四個包含兩個數據項子數組第二層包含兩個包含四個數據項子數組

第三層包含一個 8個數據項數組。合併子數組的時候,每一層的所有元素都要經歷一次複製(從原數組複製到workSpace數組),複製總次數爲3* 8=24次,即:層數乘以元素總數

設元素總數爲N,則層數爲log2N,複製總次數爲N log2N

其實,除了從原數組複製到workSpace數組,還需要從workSpace數組複製到原數組,所以,最終的複製複製次數爲2Nlog2N

在大O表示法中,常數可以忽略,所以歸併排序的時間複雜度爲O(N log2N)

一般來講,複製操作的時間消耗要遠大於比較操作的時間消耗,時間複雜度是由複製次數主導的。

下面我們再來分析一下比較次數。

在歸併排序中,比較次數總是比複製次數少一些。現在給定兩個各有四個元素的子數組,首先來看一下最壞情況和最好情況下的比較次數爲多少。

 

第一種情況,數據項大小交錯,所以必須進行7次比較,第二種情況中,一個數組比另一個數組中的所有元素都要小,因此只需要4次比較。

當歸並兩個子數組時,如果元素總數爲N,則最好情況下的比較次數爲N/2,最壞情況下的比較次數爲N-1。

假設待排數組的元素總數爲N,則第一層需要N/2次歸併,每次歸併的元素總數爲2;則第一層需要N/4次歸併,每次歸併的元素總數爲4;則第一層需要N/8次歸併,每次歸併的元素總數爲8……最後一次歸併次數爲1,歸併的元素總數爲N。總層數爲log2N。

最好情況下的比較總數爲:

N/2*(2/2)+ N/4*(4/2)+ N/8*(8/2)+...+1*(N/2) = (N/2)*log2N

最好情況下的比較總數爲:

N/2*(2-1)+ N/4*(4-1)+ N/8*(8-1)+...+1*(N-1) = (N-N/2)+ (N-N/4)+(N-N/8)+...+(N-1) = N*log2N-(1+ N/2+N/4+..)< N*log2N

可見,比較次數介於(N/2)log2N與Nlog2N之間。如果用大O表示法,時間複雜度也爲 O(Nlog2N)



作者:冰河winner
鏈接:https://www.jianshu.com/p/4e286f27b3df
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

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