邏輯之美(6)_歸併排序

開篇

上篇聊到的堆排序僅用線性對數級別的時間複雜度 O(n log n) 和常數級別的額外輔助空間便可將一個數組排序,已然十分高效。這篇咱們來聊一種一樣高效但要更古老的排序算法——歸併排序。java

正文

何爲歸併排序

此算法於 1945 年由計算機科學的祖師爺——約翰·馮·諾伊曼(對就是那個大名鼎鼎的馮·諾依曼)首次提出,看年代確實挺古老的!算法

將兩個已經(總體)有序的數組合併成一個更大的有序數組,這就叫歸併數組

原始數組:[6, 5, 3, 1,  8, 7, 2, 4]
---------------------|
---------------------|
左半排序:[1, 3, 5, 6]|------------
右半排序:------------|[2, 4, 7, 8]

歸併操做:[1, 2, 3, 4, 5, 6, 7, 8]
複製代碼

自頂向下的遞歸實現

歸併排序是算法裏面分治法的典型應用,一種經典的實現是採用遞歸的方法自頂向下分而治之是函數

來張動圖具象化展現下以幫助理解,圖源維基百科:工具

歸併排序具象化展現,圖源維基百科

具體邏輯如此,下面咱們直接上代碼(Java)來看看歸併排序究竟是怎麼一回事,實現中有個將兩個有序數組歸併成一個有序數組的操做咱們將其抽象成一個單獨的工具方法,命名爲 merge(實際上是將當前數組兩個有序的子數組歸併)。一點注意,此方法須要額外的輔助空間:spa

/** * @param array:待歸併數組,咱們須要將此數組的[start, mid] 和 [mid + 1, end] 兩個已經有序的子數組歸併起來 * @param aux:輔助數組,完成歸併操做的額外輔助空間,其大小應和 array 一致 * @param start:歸併區間起始位置,inclusive * @param mid:歸併區間第一個子數組的有邊界,inclusive * @param end:歸併區間終止位置,inclusive */
    private static void merge(int[] array, int[] aux, int start, int mid, int end){
        //將 array 的 [start, mid] 和 [mid + 1, end] 兩個已有序的子數組歸併
        int s1 = start;//start1
        int s2 = mid + 1;//start2

        for (int i = start; i <= end; i++){//拷貝待排序數組
            aux[i] = array[i];
        }
        //開始歸併兩個(子)數組
        for (int i = start; i <= end; i++){
            if (s1 > mid){               //第一個子數組(左半邊)已遍歷完
                array[i] = aux[s2++];
            }else if (s2 > end){         //第二個子數組(右半邊)已遍歷完
                array[i] = aux[s1++];
            }else if (aux[s1] > aux[s2]){//平凡狀況,取右半邊元素
                array[i] = aux[s2++];
            }else {                      //平凡狀況,取左半邊元素
                array[i] = aux[s1++];
            }
        }
    }

    /** * <p>歸併排序自頂向下的遞歸實現</p> * @param array:待排序數組,將數組的 [start, end] 區間排序 * @param aux:輔助數組,完成歸併操做的額外輔助空間,其大小應和 array 一致 * @param start:待排序區間起始位置,inclusive * @param end:待排序區間終止位置,inclusive */
    public static void sortMerge(int[] array, int[] aux, int start, int end){
        if(end <= start){//遞歸結束條件
            return;
        }
        int mid = start + (end - start)/2;//歸併左半部分的終止位置,右半部分的起始位置天然是 mid + 1
        sortMerge(array, aux, start, mid);//左半部分排序
        sortMerge(array, aux, mid + 1, end);//右半部分排序
        merge(array, aux, start, mid, end);//歸併兩個已排序的子數組
    }
複製代碼

其中 sortMerge 方法的遞歸邏輯可能不是那麼容易理解,須要好好消化一下。以數組 [6, 5, 3, 1, 8, 7, 2, 4] 爲例,咱們一塊兒來捋下其排序遞歸操做的函數調用軌跡來幫助理解:code

-------------a = [6, 5, 3, 1,  8, 7, 2, 4] 
-------------sortMerge(a, aux, 0, 7)//爲此數組初始調用歸併排序,設輔助數組爲 aux
-------------
左半部分排序:sortMerge(array, aux, 0, 3)----------------------->瞧見沒,典型的分而治之
-------------		sortMerge(array, aux, 0, 1)
-------------			merge(array, aux, 0, 0, 1)
-------------		sortMerge(array, aux, 2, 3)
-------------			merge(array, aux, 2, 2, 3)
右半部分排序:sortMerge(array, aux, 4, 7)----------------------->瞧見沒,典型的分而治之
-------------		sortMerge(array, aux, 4, 5)
-------------			merge(array, aux, 4, 4, 5)
-------------		sortMerge(array, aux, 6, 7)
-------------			merge(array, aux, 6, 6, 7)
歸併結果----:merge(array, aux, 0, 3, 7)
複製代碼

爲避免遞歸帶來的額外開銷,還請讀者自行把上面的代碼改形成非遞歸版本。cdn

上面提到了自頂向下這種說法,仔細觀察算法的執行過程,咱們是將一個大問題分割成(兩個)小問題來分別解決,而後用全部小問題的解來獲得整個大問題的解(典型的分而治之)。其實反之亦是一種不錯的實現思路,也即自底向上:首先咱們進行兩兩歸併(把數組每一個元素當作一個大小爲 1 的子數組,將相鄰兩個子數組歸併到一塊兒,每次歸併兩個元素)而後四四歸併、八八歸併(粒度愈來愈粗),直至數組總體有序。blog

自底向上的嵌套循環實現

/** *<p>歸併排序自底向上的嵌套循環實現</p> * @param array:待排序數組,將數組的 [start, end] 區間排序 * @param aux:輔助數組,完成歸併操做的額外輔助空間,其大小應和 array 一致 */
    public static void sortMerge_(int[] array, int[] aux){
        for (int size = 1; size < array.length; size <<= 1){//子數組的大小每次都翻倍
            //根據當前每一個子數組的大小 size,按順序對相鄰兩個子數組應用歸併操做,注意每一個子數組在當前 size 下只參與一次歸併操做
            for (int start = 0; start < array.length - size; start += size + size){
                int end = start + size + size - 1;
                //這裏的 merge 方法跟上面的自頂向下的一致
                merge(array, aux, start, start + size - 1, Math.min(end, array.length - 1));//最後一次歸併時,第二個子數組可能比第一個體積要小,或者跟第一個相等,咱們的歸併操做支持爲兩個大小不一樣的數組應用
            }
        }
    }
複製代碼

上面的代碼跟咱們一開始實現的自頂向下版本是基本等價的,能夠看到其代碼要精簡許多。仍是以數組 [6, 5, 3, 1, 8, 7, 2, 4] 爲例,其方法執行軌跡以下:排序

-------------自底向上對數組歸併排序
-------------a = [6, 5, 3, 1,  8, 7, 2, 4]
-------------sortMerge(a, aux)//自底向上歸併排序,設輔助數組爲 aux
-------------
-------------       size = 1
-------------       merge(array, aux, 0, 0, 1)
-------------       merge(array, aux, 2, 2, 3)
-------------       merge(array, aux, 4, 4, 5)
-------------       merge(array, aux, 6, 6, 7)
-------------   size = 2
-------------   merge(array, aux, 0, 1, 3)
-------------   merge(array, aux, 4, 5, 7)
-------------size = 4
-------------merge(array, aux, 0, 3, 7)
-------------數組已總體有序
*/
複製代碼

總結

如上所述,歸併排序是創建在歸併操做基礎上的一種高效、穩定的排序算法,其時間複雜度恆爲線性對數級別的 O(n log n) ,與輸入無關。與咱們以前討論的排序算法不一樣,其實現須要額外的輔助空間,空間複雜度最壞爲線性級別的 O(n)。

尾巴

因其高效性,歸併排序是當下應用很是普遍的排序算法,不少語言的的標準函數庫中涉及到排序的地方通常都有其實現(好比Java)。那歸併排序是應用最普遍的排序算法嗎?答案是否認的,下篇咱們就來聊一種更加高效,且是目前應用最普遍的排序算法——快速排序(你看這名字!)。

相關文章
相關標籤/搜索