前文我们了解了快速排序算法的实现,本文我们来了解下另一种流行的排序算法-归并排序算法。

我们先来回顾下快排。快排的核心是找出一个基准元素,把数组中比该元素小的放到左边数组,比该元素大的放到右边数组,如果左边数组和右边数组分别有序,那么leftArray+midItem+rightArray就是我们要的排序结果了。要使得左右数组有序,只需要对它们分别调用快排函数就可以了。递归调用需要一个出口,当数组长度<=1的时候,就是递归出口。

我们再进一步看,其实递归调用的结果形成了一棵二叉树!我们以数组[2, 1, 3, 4, 7, 6, 5]为例,代入数据到之前的快排算法中,堆栈中其实形成了一棵如下二叉树(二叉搜索树):

  4
 /  \
1    6
 \  / \
  2 5  7
   \
    3

当递归到最底层向上回溯时,其实我们只需把父节点和左子树右子树的元素合并成一个数组就行了。而更令人激动的是,左子树的值 <= midItem <= 右子树的值(因为是一棵二叉搜索树)!于是我们只需要简单地将它们按序concat就ok了。


说了这么多,我们回到本文的主题上——归并排序。之所以说到二叉树,是因为归并排序同样可以用构成一棵二叉树来解释,只不过快排的复杂度花在了成树(二叉搜索树)上(从上往下),而归并排序的复杂度花在了归并上(从下往上)。

我们以数组[1, 5, 6, 2, 4, 3]举例,归并排序的第一步,将数组一分为2:

[1, 5, 6] [2, 4, 3]

接着将分成的数组继续一分为2,直到长度为1,我们构成如下二叉树(成树 从上往下):

       [1, 5, 6, 2, 4, 3]
       /                 \
[1, 5, 6]             [2, 4, 3]
/       \            /         \
[1]    [5, 6]      [2]       [4, 3]
       /    \                /    \
      [5]   [6]             [4]   [3] 

当递归到了尽头,我们向上回溯,对于两个有序的数组,我们将它们合并成一个有序数组,从而完成整个归并排序(归并 从下往上):

       [1, 2, 3, 4, 5, 6]
       /                 \
[1, 5, 6]             [2, 3, 4]
/       \            /         \
[1]    [5, 6]      [2]       [3, 4]
       /    \                /    \
      [5]   [6]             [4]   [3] 

代码不难,直接上代码:

function merge(left, right) {
  var tmp = [];

  while (left.length && right.length) {
    if (left[0] < right[0])
      tmp.push(left.shift());
    else
      tmp.push(right.shift());
  }

  return tmp.concat(left, right);
}

function mergeSort(a) {
  if (a.length === 1)
    return a;

  var mid = ~~(a.length / 2)
    , left = a.slice(0, mid)
    , right = a.slice(mid);

  return merge(mergeSort(left), mergeSort(right));
}

这段合并排序的代码相当简单直观,但是mergeSort()函数会导致很频繁的自调用。一个长度为n的数组最终会调用mergeSort() 2*n-1次,这意味着如果需要排序的数组长度很大会在某些栈小的浏览器上发生栈溢出错误。

这里插个话题,关于递归调用时浏览器的栈大小限制,可以用代码去测试:

var cnt = 0;
try {
  (function() {
    cnt++;
    arguments.callee();
  })();
} catch(e) {
  console.log(e.message, cnt);
}

// chrome: Maximum call stack size exceeded 35992
// firefox: too much recursion 11953

遇到栈溢出错误并不一定要修改整个算法,只是表明递归不是最好的实现方式。这个合并排序算法同样可以迭代实现,比如(摘抄自《高性能JavaScript》):

function merge(left, right) {
  var result = [];

  while (left.length && right.length) {
    if (left[0] < right[0])
      result.push(left.shift());
    else
      result.push(right.shift());
  }

  return result.concat(left, right);
}

function mergeSort(a) {
  if (a.length === 1)
    return a;

  var work = [];
  for (var i = 0, len = a.length; i < len; i++)
    work.push([a[i]]);

  work.push([]); // 如果数组长度为奇数

  for (var lim = len; lim > 1; lim = (lim + 1) / 2) {
    for (var j = 0, k = 0; k < lim; j++, k += 2)
      work[j] = merge(work[k], work[k + 1]);

    work[j] = []; // 如果数组长度为奇数
  }

  return work[0];
}

console.log(mergeSort([1, 3, 4, 2, 5, 0, 8, 10, 4]));

这个版本的mergeSort()函数功能与前例相同却没有使用递归。尽管迭代版本的合并排序算法比递归实现要慢一些,但它并不会像递归版本那样受调用栈限制的影响。把递归算法改用迭代实现是实现栈溢出错误的方法之一。

【前端也要学点算法】 归并排序的JavaScript实现的更多相关文章

  1. 前端要不要学数据结构&算法

    我们都知道前端开发工程师更多偏向 DOM 渲染和 DOM 交互操作,随之 Node 的推广前端工程师也可以完成服务端开发.对于服务端开发而言大家都觉得数据结构和算法是基础,非学不可.所以正在进行 No ...

  2. 【前端也要学点算法】快速排序的JavaScript实现

    作为算法目录下的第一篇博文,快速排序那是再合适不过了.作为最基本最经典的算法之一,我觉得每个程序员都应该熟悉并且掌握它,而不是只会调用库函数,知其然而不知其所以然. 排序算法有10种左右(或许更多), ...

  3. python实现折半查找算法&&归并排序算法

    今天依旧是学算法,前几天在搞bbs项目,界面也很丑,评论功能好像也有BUG.现在不搞了,得学下算法和数据结构,笔试过不了,连面试的机会都没有…… 今天学了折半查找算法,折半查找是蛮简单的,但是归并排序 ...

  4. 做acm 需要学的算法

    做acm 需要学的算法 转一个搞ACM需要的掌握的算法.  要注意,ACM的竞赛性强,因此自己应该和自己的实际应用联系起来.  适合自己的才是好的,有的人不适合搞算法,喜欢系统架构,因此不要看到别人什 ...

  5. 经典排序算法 - 归并排序Merge sort

    经典排序算法 - 归并排序Merge sort 原理,把原始数组分成若干子数组,对每个子数组进行排序, 继续把子数组与子数组合并,合并后仍然有序,直到所有合并完,形成有序的数组 举例 无序数组[6 2 ...

  6. 前端开发周报: CSS 布局方式方式与JavaScript数据结构和算法

    前端开发周报:CSS 布局方式与JavaScript动画库 1.常见 CSS 布局方式详见: 一些常见的 CSS 布局方式梳理,涉及 Flex 布局.Grid 布局.圣杯布局.双飞翼布局等.http: ...

  7. web前端入坑第二篇:web前端到底怎么学?干货资料! 【转】

    http://blog.csdn.net/xllily_11/article/details/52145172 版权声明:本文为博主[小北]原创文章,如要转载请评论回复.个人前端公众号:前端你别闹,J ...

  8. 数据结构和算法(Golang实现)(23)排序算法-归并排序

    归并排序 归并排序是一种分治策略的排序算法.它是一种比较特殊的排序算法,通过递归地先使每个子序列有序,再将两个有序的序列进行合并成一个有序的序列. 归并排序首先由著名的现代计算机之父John_von_ ...

  9. 第十四章 web前端开发小白学爬虫

    老猿从事IT开发快三十年了,接触互联网也很久了,但自己没有做过web前端开发,只知道与前端开发相关的一些基本概念,如B/S架构.html标签.js脚本.css样式.xml解析.cookies.http ...

随机推荐

  1. [Linux 性能检测工具]TOP

    TOP NAME 显示linux任务 语法 top -hv | -abcHimMsS -d delay -n iterations -p pid [, pid ...] 描述 top程序提供了系统实时 ...

  2. php 升级到 5.3+ 后出现的一些错误,如 ereg(); ereg_replace(); 函数报错

    在php5.3环境下运行,常常会出现 Deprecated: Function ereg() is deprecated in...和Deprecated: Function ereg_replace ...

  3. [转]Membership三步曲之入门篇 - Membership基础示例

    本文转自:http://www.cnblogs.com/jesse2013/p/membership.html Membership三步曲之入门篇 - Membership基础示例   Members ...

  4. MMORPG大型游戏设计与开发(客户端架构 part15 of vegine)

    一个接口需要统一的派生接口,这样做的好处在于能够统一的进行管理.我所知的脚本语言中,接口有多重接口,也还有所谓的虚基类,这些都是方便类的管理.在vengine(微引擎)中,统一的的接口管理为kerne ...

  5. 【2016-10-12】【坚持学习】【Day3】【命令模式】

    今天学习了第二个模式,命令模式 例子: 开关==>发送者 电线==>命令传输 电灯==>接受者 一个开关可以通过不同电线控制不同电器, 结构: 发送者类, 抽象命令类 具体命令类:继 ...

  6. POJ1160 Post Office[序列DP]

    Post Office Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 18680   Accepted: 10075 Des ...

  7. UI坐标变换/转换

    InverseTransformPoint Transform.InverseTransformPoint :相对于谁的坐标.如果是相对2D UI,请使用localposition,如果是3D场景,请 ...

  8. Unity3D FPS帧数修改

    修改Unity的FPS FPS是游戏运行的帧数,本文讲解如何修改Unity引擎的FPS. 步骤 1.在 Edit/Project Settings/Quality  质量设置里把帧数设定关闭,关闭之后 ...

  9. Ahead-of-time compilation(AOT)

    Ahead-of-time (AOT) compilation is the act of compiling a high-level programming language such as C  ...

  10. [No000059]知道这些,你的时间会比别人多一大截

    大噶猴,这里是最近不爱断案,爱上了号脉问诊的包大人.来看看下面这些症状,你中了几条? 字的快餐阅读 2.微博.微信.QQ空间.微博.微信.QQ空间……陷在这样的循环里 3.每天好像接收了很多信息,然而 ...