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

我们先来回顾下快排。快排的核心是找出一个基准元素,把数组中比该元素小的放到左边数组,比该元素大的放到右边数组,如果左边数组和右边数组分别有序,那么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()函数功能与前例相同却没有使用递归。尽管迭代版本的合并排序算法比递归实现要慢一些,但它并不会像递归版本那样受调用栈限制的影响。把递归算法改用迭代实现是实现栈溢出错误的方法之一。

最新文章

  1. VB编程的键盘控制
  2. Redis为什么使用单进程单线程方式也这么快
  3. org.hibernate.QueryException: could not resolve property
  4. 基于PHP以及Mysql,使用WordPress搭建站点
  5. 约瑟夫环(Josehpuse)的模拟
  6. 软件工程(GZSD2015)第二次作业文档模板
  7. Convert Sorted Array to Binary Search Tree With Minimal Height
  8. JavaScript(1)
  9. xamarin.ios 豆瓣电台视频教程
  10. Android 如何添加一种锁屏方式
  11. 仿QQ迷你首页(VC++,MFC)(迷你资讯)的开发与实现(源代码)
  12. 理解 Git
  13. python day32--struct,文件上传下载
  14. 我发起并创立了一个 Javascript 前端库 开源项目 jWebForm
  15. CAS跳转流程
  16. [luoguU48574][藏妹子之处]
  17. Http Header 之 Requests Header 和 Responses Header
  18. SpringBoot结合Swagger2自动生成api文档
  19. MySQL双主.md
  20. BZOJ1337: 最小圆覆盖

热门文章

  1. 30-算法训练 最短路 spfa
  2. webpack.dev.conf.js
  3. 关于js动画简单理解;
  4. c# 24种设计模式
  5. ofo退押金脚本
  6. 使用vim编程步骤
  7. gdal source code c++ make windows
  8. PHP Cron Expression Parser ( LARAVEL )
  9. Spring PropertyResolver 占位符解析(一)API 介绍
  10. Hive 系列(二)权限管理