归并排序
归并算法应用到分治策略,简单说就是把一个答问题分解成易于解决的小问题后一个个解决,最后在把小问题的一步步合并成总问题的解。归并排序应用递归来把数组分解成一个个小数组,直到小数组的数位有序,在把有序的小数组两两合并而成有序的大数组。
最差、最优、平均时间复杂度都为O(nlogn)。辅助空间O(n)。稳定性:稳定。
算法实现
function merge(left,right){ let 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; let mid=a.length>>1, 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) { let 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; let work = [],len=a.length; for (let i = 0; i < len; i++) work.push([a[i]]); work.push([]); // 如果数组长度为奇数 let last=0; for (let lim = len; lim > 1; lim = (lim + 1)>>1) { for (let j = 0, k = 0; k < lim; j++, k += 2) { work[j] = merge(work[k], work[k + 1]); last=j; } work[last+1] = []; // 如果数组长度为奇数 } return work[0]; }
这个版本的mergeSort()函数功能与前例相同却没有使用递归。尽管迭代版本的合并排序算法比递归实现要慢一些,但它并不会像递归版本那样受调用栈限制的影响。把递归算法改用迭代实现是实现栈溢出错误的方法之一。