逻辑之美(6)_归并排序

开篇

上篇聊到的堆排序仅用线性对数级别的时间复杂度 O(n log n) 和常数级别的额外辅助空间即可将一个数组排序,已然十分高效。这篇我们来聊一种同样高效但要更古老的排序算法——归并排序。

正文

何为归并排序

此算法于 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(其实是将当前数组两个有序的子数组归并)。一点注意,此方法需要额外的辅助空间:

/**
     * @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] 为例,我们一起来捋下其排序递归操作的函数调用轨迹来帮助理解:

-------------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)

为避免递归带来的额外开销,还请读者自行把上面的代码改造成非递归版本。

上面提到了自顶向下这种说法,仔细观察算法的执行过程,我们是将一个大问题分割成(两个)小问题来分别解决,然后用所有小问题的解来得到整个大问题的解(典型的分而治之)。其实反之亦是一种不错的实现思路,也即自底向上:首先我们进行两两归并(把数组每个元素看成一个大小为 1 的子数组,将相邻两个子数组归并到一起,每次归并两个元素)然后四四归并、八八归并(粒度越来越粗),直至数组整体有序。

自底向上的嵌套循环实现

/**
     *<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)。那归并排序是应用最广泛的排序算法吗?答案是否定的,下篇我们就来聊一种更加高效,且是目前应用最广泛的排序算法——快速排序(你看这名字!)。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 1 初级排序算法 排序算法关注的主要是重新排列数组元素,其中每个元素都有一个主键。排序算法是将所有元素主键按某种方...
    深度沉迷学习阅读 1,389评论 0 1
  • 一. 写在前面 要学习算法,“排序”是一个回避不了的重要话题,在分析完并查集算法和常用数据结构之后,今天我们终于可...
    Leesper阅读 2,516评论 0 40
  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 5,646评论 0 13
  • 概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    蚁前阅读 5,164评论 0 52
  • 1.插入排序—直接插入排序(Straight Insertion Sort) 基本思想: 将一个记录插入到已排序好...
    依依玖玥阅读 1,235评论 0 2