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

開篇

上篇聊到的堆排序僅用線性對數(shù)級別的時(shí)間復(fù)雜度 O(n log n) 和常數(shù)級別的額外輔助空間即可將一個(gè)數(shù)組排序泰佳,已然十分高效阳柔。這篇我們來聊一種同樣高效但要更古老的排序算法——?dú)w并排序。

正文

何為歸并排序

此算法于 1945 年由計(jì)算機(jī)科學(xué)的祖師爺——約翰·馮·諾伊曼(對就是那個(gè)大名鼎鼎的馮·諾依曼)首次提出樟蠕,看年代確實(shí)挺古老的柒凉!

將兩個(gè)已經(jīng)(整體)有序的數(shù)組合并成一個(gè)更大的有序數(shù)組稠腊,這就叫歸并

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

歸并操作:[1, 2, 3, 4, 5, 6, 7, 8]

自頂向下的遞歸實(shí)現(xiàn)

歸并排序是算法里面分治法的典型應(yīng)用,一種經(jīng)典的實(shí)現(xiàn)是采用遞歸的方法自頂向下分而治之是

來張動(dòng)圖具象化展示下以幫助理解畸陡,圖源維基百科:

歸并排序具象化展示鹰溜,圖源維基百科

具體邏輯如此,下面我們直接上代碼(Java)來看看歸并排序到底是怎么一回事丁恭,實(shí)現(xiàn)中有個(gè)將兩個(gè)有序數(shù)組歸并成一個(gè)有序數(shù)組的操作我們將其抽象成一個(gè)單獨(dú)的工具方法曹动,命名為 merge(其實(shí)是將當(dāng)前數(shù)組兩個(gè)有序的子數(shù)組歸并)。一點(diǎn)注意牲览,此方法需要額外的輔助空間:

/**
     * @param array:待歸并數(shù)組墓陈,我們需要將此數(shù)組的[start, mid] 和 [mid + 1, end] 兩個(gè)已經(jīng)有序的子數(shù)組歸并起來
     * @param aux:輔助數(shù)組,完成歸并操作的額外輔助空間第献,其大小應(yīng)和 array 一致
     * @param start:歸并區(qū)間起始位置贡必,inclusive
     * @param mid:歸并區(qū)間第一個(gè)子數(shù)組的有邊界,inclusive
     * @param end:歸并區(qū)間終止位置痊硕,inclusive
     */
    private static void merge(int[] array, int[] aux, int start, int mid, int end){
        //將 array 的 [start, mid] 和 [mid + 1, end] 兩個(gè)已有序的子數(shù)組歸并
        int s1 = start;//start1
        int s2 = mid + 1;//start2

        for (int i = start; i <= end; i++){//拷貝待排序數(shù)組
            aux[i] = array[i];
        }
        //開始?xì)w并兩個(gè)(子)數(shù)組
        for (int i = start; i <= end; i++){
            if (s1 > mid){               //第一個(gè)子數(shù)組(左半邊)已遍歷完
                array[i] = aux[s2++];
            }else if (s2 > end){         //第二個(gè)子數(shù)組(右半邊)已遍歷完
                array[i] = aux[s1++];
            }else if (aux[s1] > aux[s2]){//平凡情況赊级,取右半邊元素
                array[i] = aux[s2++];
            }else {                      //平凡情況,取左半邊元素
                array[i] = aux[s1++];
            }
        }
    }

    /**
     * <p>歸并排序自頂向下的遞歸實(shí)現(xiàn)</p>
     * @param array:待排序數(shù)組岔绸,將數(shù)組的 [start, end] 區(qū)間排序
     * @param aux:輔助數(shù)組理逊,完成歸并操作的額外輔助空間橡伞,其大小應(yīng)和 array 一致
     * @param start:待排序區(qū)間起始位置,inclusive
     * @param end:待排序區(qū)間終止位置晋被,inclusive
     */
    public static void sortMerge(int[] array, int[] aux, int start, int end){
        if(end <= start){//遞歸結(jié)束條件
            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);//歸并兩個(gè)已排序的子數(shù)組
    }

其中 sortMerge 方法的遞歸邏輯可能不是那么容易理解,需要好好消化一下羡洛。以數(shù)組 [6, 5, 3, 1, 8, 7, 2, 4] 為例挂脑,我們一起來捋下其排序遞歸操作的函數(shù)調(diào)用軌跡來幫助理解:

-------------a = [6, 5, 3, 1,  8, 7, 2, 4] 
-------------sortMerge(a, aux, 0, 7)//為此數(shù)組初始調(diào)用歸并排序,設(shè)輔助數(shù)組為 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)
歸并結(jié)果----:merge(array, aux, 0, 3, 7)

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

上面提到了自頂向下這種說法刁俭,仔細(xì)觀察算法的執(zhí)行過程,我們是將一個(gè)大問題分割成(兩個(gè))小問題來分別解決韧涨,然后用所有小問題的解來得到整個(gè)大問題的解(典型的分而治之)牍戚。其實(shí)反之亦是一種不錯(cuò)的實(shí)現(xiàn)思路,也即自底向上:首先我們進(jìn)行兩兩歸并(把數(shù)組每個(gè)元素看成一個(gè)大小為 1 的子數(shù)組虑粥,將相鄰兩個(gè)子數(shù)組歸并到一起如孝,每次歸并兩個(gè)元素)然后四四歸并、八八歸并(粒度越來越粗)娩贷,直至數(shù)組整體有序第晰。

自底向上的嵌套循環(huán)實(shí)現(xiàn)

/**
     *<p>歸并排序自底向上的嵌套循環(huán)實(shí)現(xiàn)</p>
     * @param array:待排序數(shù)組,將數(shù)組的 [start, end] 區(qū)間排序
     * @param aux:輔助數(shù)組育勺,完成歸并操作的額外輔助空間但荤,其大小應(yīng)和 array 一致
     */
    public static void sortMerge_(int[] array, int[] aux){
        for (int size = 1; size < array.length; size <<= 1){//子數(shù)組的大小每次都翻倍
            //根據(jù)當(dāng)前每個(gè)子數(shù)組的大小 size,按順序?qū)ο噜弮蓚€(gè)子數(shù)組應(yīng)用歸并操作涧至,注意每個(gè)子數(shù)組在當(dāng)前 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));//最后一次歸并時(shí)腹躁,第二個(gè)子數(shù)組可能比第一個(gè)體積要小,或者跟第一個(gè)相等南蓬,我們的歸并操作支持為兩個(gè)大小不同的數(shù)組應(yīng)用
            }
        }
    }

上面的代碼跟我們一開始實(shí)現(xiàn)的自頂向下版本是基本等價(jià)的纺非,可以看到其代碼要精簡許多。還是以數(shù)組 [6, 5, 3, 1, 8, 7, 2, 4] 為例赘方,其方法執(zhí)行軌跡如下:

-------------自底向上對數(shù)組歸并排序
-------------a = [6, 5, 3, 1,  8, 7, 2, 4]
-------------sortMerge(a, aux)//自底向上歸并排序烧颖,設(shè)輔助數(shù)組為 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)
-------------數(shù)組已整體有序
*/

總結(jié)

如上所述,歸并排序是建立在歸并操作基礎(chǔ)上的一種高效窄陡、穩(wěn)定的排序算法炕淮,其時(shí)間復(fù)雜度恒為線性對數(shù)級別的 O(n log n) ,與輸入無關(guān)跳夭。與我們之前討論的排序算法不同涂圆,其實(shí)現(xiàn)需要額外的輔助空間们镜,空間復(fù)雜度最壞為線性級別的 O(n)。

尾巴

因其高效性润歉,歸并排序是當(dāng)下應(yīng)用非常廣泛的排序算法模狭,很多語言的的標(biāo)準(zhǔn)函數(shù)庫中涉及到排序的地方一般都有其實(shí)現(xiàn)(比如Java)。那歸并排序是應(yīng)用最廣泛的排序算法嗎踩衩?答案是否定的嚼鹉,下篇我們就來聊一種更加高效,且是目前應(yīng)用最廣泛的排序算法——快速排序(你看這名字G弧)锚赤。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市萌朱,隨后出現(xiàn)的幾起案子宴树,更是在濱河造成了極大的恐慌策菜,老刑警劉巖晶疼,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異又憨,居然都是意外死亡翠霍,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門蠢莺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寒匙,“玉大人,你說我怎么就攤上這事躏将〕酰” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵祸憋,是天一觀的道長会宪。 經(jīng)常有香客問我,道長蚯窥,這世上最難降的妖魔是什么掸鹅? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮拦赠,結(jié)果婚禮上巍沙,老公的妹妹穿的比我還像新娘。我一直安慰自己荷鼠,他們只是感情好句携,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著允乐,像睡著了一般矮嫉。 火紅的嫁衣襯著肌膚如雪牡辽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天敞临,我揣著相機(jī)與錄音态辛,去河邊找鬼。 笑死挺尿,一個(gè)胖子當(dāng)著我的面吹牛奏黑,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播编矾,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼熟史,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了窄俏?” 一聲冷哼從身側(cè)響起蹂匹,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎凹蜈,沒想到半個(gè)月后限寞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡仰坦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年履植,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片悄晃。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡玫霎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出妈橄,到底是詐尸還是另有隱情庶近,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布眷蚓,位于F島的核電站鼻种,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏溪椎。R本人自食惡果不足惜普舆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望校读。 院中可真熱鬧沼侣,春花似錦、人聲如沸歉秫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至轧膘,卻和暖如春钞螟,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背谎碍。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工鳞滨, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蟆淀。 一個(gè)月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓拯啦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親熔任。 傳聞我的和親對象是個(gè)殘疾皇子褒链,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 1 初級排序算法 排序算法關(guān)注的主要是重新排列數(shù)組元素,其中每個(gè)元素都有一個(gè)主鍵疑苔。排序算法是將所有元素主鍵按某種方...
    深度沉迷學(xué)習(xí)閱讀 1,408評論 0 1
  • 一. 寫在前面 要學(xué)習(xí)算法甫匹,“排序”是一個(gè)回避不了的重要話題,在分析完并查集算法和常用數(shù)據(jù)結(jié)構(gòu)之后惦费,今天我們終于可...
    Leesper閱讀 2,533評論 0 40
  • 一些概念 數(shù)據(jù)結(jié)構(gòu)就是研究數(shù)據(jù)的邏輯結(jié)構(gòu)和物理結(jié)構(gòu)以及它們之間相互關(guān)系兵迅,并對這種結(jié)構(gòu)定義相應(yīng)的運(yùn)算,而且確保經(jīng)過這...
    Winterfell_Z閱讀 5,816評論 0 13
  • 概述 排序有內(nèi)部排序和外部排序趁餐,內(nèi)部排序是數(shù)據(jù)記錄在內(nèi)存中進(jìn)行排序喷兼,而外部排序是因排序的數(shù)據(jù)很大,一次不能容納全部...
    蟻前閱讀 5,186評論 0 52
  • 1.插入排序—直接插入排序(Straight Insertion Sort) 基本思想: 將一個(gè)記錄插入到已排序好...
    依依玖玥閱讀 1,258評論 0 2