归并排序是非常经典的基础排序之一,使用分治的思想,分而治之。
先将待排序数组分拆为更小的数组,一直拆分到只有一个元素(只有一个元素的数组就是有序的),然后对分拆的数组按顺序进行合并。这个思想就是分而治之的思想,变成实现的时候,常用递归编程技巧来实现。
直接上图来说这个过程(图是拷贝网上的):
这里重点详细讲一下合并过程,合并被分解好的左右两部分的时候,左右两部分分别已经排好了,因此可以按如下图策略合并:比如需要合并left [1,5,6]的数组和right [2,3,4]数组的过程图:
整体的思路看完了,准备用递归实现,递归实现最重要的是写出递归公式、终止条件,地推公式可以这样写:
sort(arr) = merge(mergeSort(arr1), mergeSort(arr2));
在数组只有一个元素的时候,就不用再排序了。
撸代码,注意,要重点看一下merge的实现,有一点技巧:
function mergeSort(arr){
// 拆到只有一个元素的时候,就不用再拆了。
if(arr.length <2){
return arr;
}
const mid = arr.length/2;
const left = arr.slice(0, mid);
const right = arr.slice(mid);
const leftSplited = mergeSort(left);
const rightSplited = mergeSort(right);
return merge(leftSplited, rightSplited);
}
merge的实现:
// 合并做分支数组和右分支数组,左分支和右分支在自己数组里面已经是有序的。所以就方便很多。
function merge(left, right){
let tmp = [];
let leftIndex =0, rightIndex = 0;
while(leftIndex < left.length && rightIndex < right.length) {
if(left[leftIndex] <= right[rightIndex]) {
tmp.push(left[leftIndex]);
leftIndex ++;
}else{
tmp.push(right[rightIndex]);
rightIndex ++;
}
}
// 最后,检查一下是否有数组内容没有拷贝完
if(leftIndex < left.length){
tmp = tmp.concat(left.slice(leftIndex))
}
if(rightIndex < right.length){
tmp = tmp.concat(right.slice(rightIndex))
}
return tmp;
}
这个实现可以进一步优化,如果说左右数组已经是有大小关系的了,比如,左边数组的最后一个元素比右边的第一个小,那么,就不用while循环了,就直接合并就行。
因此可以优化为:
// 合并做分支数组和右分支数组,左分支和右分支在自己数组里面已经是有序的。所以就方便很多。
function merge(left, right){
// 剪枝优化
if(left[left.length -1] <= right[0]){
return left.concat(right)
}
if(right[right.length-1] <= left[0]){
return right.concat(left)
}
let tmp = [];
let leftIndex =0, rightIndex = 0;
while(leftIndex < left.length && rightIndex < right.length) {
if(left[leftIndex] <= right[rightIndex]) {
tmp.push(left[leftIndex]);
leftIndex ++;
}else{
tmp.push(right[rightIndex]);
rightIndex ++;
}
}
// 最后,检查一下是否有数组内容没有拷贝完
if(leftIndex < left.length){
tmp = tmp.concat(left.slice(leftIndex))
}
if(rightIndex < right.length){
tmp = tmp.concat(right.slice(rightIndex))
}
return tmp;
}
注,关于Merg函数,网上有不少的示例感觉都是有些问题的。比如:
作为对比学习,大家可以先看下这个代码的问题在哪里?
// https://www.cnblogs.com/sunmarvell/p/9248676.html
function merge(left,right){
var temp=[];
while(left.length&&right.length){
if(left[0]<right[0]){
temp.push(left.shift());
}else{
temp.push(right.shift());
}
}
//left和right长度不一样时,直接连接剩下的长的部分(本身有序)
return temp.concat(left,right);
}
这个代码功能上完全没有问题。但是性能上会有问题。while循环每次运行的时候,都会删除数组的第一个元素。删除数组的第一个元素,是会有导致底层需要进行数据搬移,以维持数据的连续性。搬移一次数组的时间复杂度就是T(n) = O(n);事实上,会让merge函数的时间复杂度提升一个量级,从T(n) = O(n)变为T(n) = O(n)*O(n)。
JS在进行算法处理的时候,要慎用原生提供的修改原数组功能的函数,比如shift、splice等。