归并排序

来源:互联网 发布:php 汉字长度 编辑:程序博客网 时间:2024/06/08 07:00

http://www.cnblogs.com/zichi/p/4796727.html
我们再进一步看,其实递归调用的结果形成了一棵二叉树!我们以数组[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));}

代码最底层的left和right是只有一项的数组,merge函数给这两项排序组成一个新的有序数组;新的有序数组成为上一个递归中的left或者right中的某一项,慢慢向上之后left和right中的项越来越多,但此时left、right都是有序的数组了,merge每次取出left和right中的最小值,知道某一个数组全部取完,那说明剩下的数组即使最小的第0项也比之前取出的项大,直接concat(left,right),此时left、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()函数功能与前例相同却没有使用递归。尽管迭代版本的合并排序算法比递归实现要慢一些,但它并不会像递归版本那样受调用栈限制的影响。把递归算法改用迭代实现是实现栈溢出错误的方法之一。

原创粉丝点击