一天一算法 - 归并排序

归并排序最吸引人的性质就是能够保证将任意长度为 N 的数组排序所需时间和 NLogN 成正比,它的主要缺点是所需的额外空间和 N 成正比。

其实归并排序的思想非常简单,直接了当的说就是将两个不同的有序数组归并到第三个数组当中去。那么怎么才能够从一个无序的数组获取到两个有序的数组呢?这就需要借助递归的思想,我们可以递归切割数组,直到子数组中只有一个元素,这个时候我们自然而然的就得到了有序的数组。比如我们要排序数组 [5, 4, 3, 2, 1, 0], 归并排序会按照下图切割数组。

demo.png

当切割的只剩下一个元素时,返回并与自己同级的兄弟节点两两整合,最后返回的就是一个有序的大数组,可以明显看出,这是非常典型的分治思想。

首先,我们来看看将两个有序数组整合成一个有序大数组的代码。

var merge = function(ltr, rtr) {
    var ltrLength = ltr.length,
        rtrLength = rtr.length,
        li = 0,
        rj = 0,
        result = [];

    while (li < ltrLength && rj < rtrLength) {
        if (ltr[li] < rtr[rj]) {
            result.push(ltr[li++]);
        } else {
            result.push(rtr[rj++]);
        }
    }

    while (li < ltrLength) {
        result.push(ltr[li++]);
    }

    while (rj < rtrLength) {
        result.push(rtr[rj++]);
    }

    return result;
};

代码的逻辑主要分为三步:

  1. 当左数组和右数组都还有元素的时候,比较两数组元素的大小,将其中较小的元素推进辅助数组(为了合并两数组而创建的数组)。

  2. 当左数组中尚有元素剩余而右数组已经没有元素了,这个时候将左数组中的元素依次推进辅助数组。

  3. 当右数组中尚有元素剩余而左数组已经没有元素了,这个时候将右数组中的元素依次推进辅助数组。

现在已经有了合并的方法,那么还需要一个能够将数组切割成小数组的方法,下面来看看实现代码。

var mergeSortRec = function(arr) {
    var length = arr.length;

    if (length === 1) {
        return arr;
    }

    var mid = Math.floor(length / 2),
        ltr = arr.slice(0, mid),
        rtr = arr.slice(mid, length);

    return merge(mergeSortRec(ltr), mergeSortRec(rtr));
};

这个函数使用了递归切割的方法,它可以保证将一个大数组一步步切割成只剩一个元素的数组,并且返回提供给 merge() 函数令其归并。

这个时候,归并排序就已经完成了,现在可以去测试以下归并排序排序百万数量级的数据所需时间,我用以下代码进行了测试。

console.time();
array.mergeSort();
console.timeEnd();

注意,上面只是核心代码,在这之前我已经在数组中存放了一百万个元素,并且已经随机打乱。我们来看看浏览器返回给我的结果。

sort.png

我尝试了好几次,大概都在 550ms 左右。按理来说,这已经很快了,但是仍然存在一些优化可以让归并排序更快。我在上一篇文章介绍插入排序算法的时候曾经说过,当插入排序面临基本有序的数组时可能比任何排序算法都要快。根据这一思想,我们可以在归并排序中使上一些手段,比如当数组的长度比较小时,改用插入排序进行排序。

修改后的代码像下面这样。

var mergeSortRec = function(arr) {
    var length = arr.length;

    if (length === 1) {
        return arr;
    }

    if(length <= 10) {
        return insertionSort(arr);
    }

    var mid = Math.floor(length / 2),
        ltr = arr.slice(0, mid),
        rtr = arr.slice(mid, length);

    return merge(mergeSortRec(ltr), mergeSortRec(rtr));
};

现在再来看看排序同样的数组需要多长时间。

sort-2.png

我运行了很多次,其值一直在 270ms 左右浮动。非常令人惊讶,只是这么一个小小的改动,归并排序的性能竟获得如此大的提升!

不过,这个程序还是有一点不完美,比如对于一个已经有序的数组,它排序话费的时间和排序无序的数组是差不多的,所以我们还是有必要在排序前对数组进行检测,如果其确实是个无序数组,我们再调用算法进行排序也不迟。

var isSorted = function() {
    var length = array.length;
    
    for(var i = 1; i < length; i++) {
         if(array[i] < array[i - 1]) {
             return false;
         }
          
        return true;
    }
}

这样一来,对于一个有序的大数组,我们只需要花 1-2 ms 的时间就可以确定是否需要调用排序算法,相比于不检测就直接调用归并排序省了大把时间。

我在写 isSorted() 函数的时候突然冒出一个想法,我能不能在检测是否排序的时候记录一个数组中到底有多少逆序呢?这样我就可以择机使用插入排序替代希尔排序,这对于基本有序数组又是一个性能优化。最终我没有记录有多少个逆序,但我声明了一个变量,用于记录有多少个元素不在其最终位置上,当其值小于 100 时,我选择用插入排序替代了归并排序,像下面这样。

this.isSorted = function() {
    var length = array.length,
        reverse = 0;
    for (var i = 1; i < length; i++) {
        if (array[i] < array[i - 1]) {
            reverse++;
        }
    }
    if (reverse === 0) {
        return true;
    } else {
        if (reverse < 100) {
            insertionSort(array);
            return true;
        }
        return false;
    }
}

我测试了让前一百万个数据中的前几十个元素随机打乱,然后再把最后的几十个元素随机打乱,其性能仍然优于归并排序!


如果你想查看源码,可以打开 我的 Github

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

推荐阅读更多精彩内容

  • 最近在读< >时,了解到了很多常用的排序算法,故写一篇读书笔记记录下这些排序算法的思路和实现. 冒泡排序 冒泡排序...
    SylvanasSun阅读 676评论 0 0
  • 一. 写在前面 要学习算法,“排序”是一个回避不了的重要话题,在分析完并查集算法和常用数据结构之后,今天我们终于可...
    Leesper阅读 2,508评论 0 40
  • Ba la la la ~ 读者朋友们,你们好啊,又到了冷锋时间,话不多说,发车! 1.冒泡排序(Bub...
    王饱饱阅读 1,782评论 0 7
  • 排序算法说明 (1)排序的定义:对一序列对象根据某个关键字进行排序; 输入:n个数:a1,a2,a3,…,an输出...
    BULL_DEBUG阅读 759评论 0 3
  • 没有一艘舰船 能像一本书 带我们遨游远方 没有一匹骏马 能像一页诗行 如此欢跃飞扬 即使一贫如洗 它也可以带你走上...
    慕容兰馨阅读 212评论 4 3