系列文章链接
1. 原理
归并排序(Merge Sort
)是利用分治法(Divide and Conquer
)的一种非常经典的排序算法。通过几句话地描述这个算法就是:
- 先不断
拆分
数组成为多个只有一个元素最小数组,然后最小数组两两比较
进行排序,合并
成排好序的较大数组
较大数组
继续两两比较,合并成多个排好序的更大数组
- 以此类推,直至完成整个数组的排序。
以这张图为例,将[5,3,8,6,2,7,1,4]
通过归并排序
转为[1,2,3,4,5,6,7,8]
排序过程主要分为拆分
(Divide
)和治理
(Conquer
)两个步骤,笔者认为治理
并不是一个容易理解的词,实际上它的主要工作还是合并
,因此后文统一用合并
表示治理
这一步骤
拆分:把数组对半拆解为小数组
(1) 先把[5,3,8,6,2,7,1,4]
分成[5,3,8,6]
和[2,7,1,4]
;
(2) 再把[5,3,8,6]
分成[5,3]
和[8,6]
;
(3) [2,7,1,4]
分成[2,7]
和[1,4]
;
(4) 以此类推,直到每个数组只有一个元素。
合并:按从小到大排序,合并小数组
(1) 对[5]
和[3]
,比较大小后,合并成数组[3,5]
;
(2) 对[8]
和[6]
,比较大小后,合并成数组[6,8]
;
(3) 对[3,5]
和[6,8]
,比较大小后,合并成数组[3,5,6,8]
;
(4) 以此类推,最后对[3,5,6,8]
和[1,2,4,7]
,比较大小排序后,合并成最终结果[1,2,3,4,5,6,7,8]
,完成排序
2. 详细分析
对于拆分这一步比较容易实现,比如,我们可以使用递归
的方式
function mergeSort(arr, l, r) { // arr为完整数组,l为左索引,r为右索引
var mid = l + ((r - 1) / 2); // 获取中点
if (r - l > 1) { // 递归直到左索引大于右索引
mergeSort(arr, l, mid); // 递归处理数组左半边
mergeSort(arr, mid + 1, r); // 递归处理数组右半边
}
merge(arr, l, mid, r); // 做排序和合并的处理,后面会讲
return arr; // 返回排序合并好结果
}
通过减少索引l
和r
的距离,拆解大数组,直到得到仅有一个元素的小数组。
在拆分之后需要将数组排序并合并起来,上面代码中的merge
方法实现了这些功能。以合并[2,7]
和[1,4]
为例,见下图
我们一步步分析合并
这一过程
- 对于最小数组
[2]
、[7]
、[1]
、[4]
[2]
和[7]
比较,2
比7
小,本身已经排好序,合并成[2,7]
;[1]
和[4]
比较,也是已经排好序,合并成[1,4]
。
- 接下来合并
[2,7]
和[1,4]
,在[2,7]
和[1,4]
合并前要先排好序。
通过左指针left
指向[2,7]
的元素2
,通过右指针right
指向[1,4]
的最元素1
。
新建一个辅助数组Help
,比较Array[4]
和Array[6]
,2
比1
大,将1
放到Help
中
右指针right
右移到位置7
,继续比较Array[4]
和Array[7]
,2
比4
小,将2
放到Help
中
左指针left
右移到位置5
,再比较Array[5]
和Array[7]
,7
比4
大,将4
放到Help
中,此时发现右指针right
没办法再右移
直接将左指针left
右边剩余部分,全部放到Help
里面,Help
完成[2,7]
和[1,4]
的排序和合并。
最后将Help
刷回到原数组,完成原数组[2,7]
和[1,4]
的合并,其它小数组的合并以此类推。
3. 代码实现
function main(arr) {
var l = 0;
var r = arr.length - 1;
return mergeSort(arr, l, r);
}
function mergeSort(arr, l, r) {
var mid = l + ((r - l) >> 1); // 等价于l + ((r - 1) / 2)
if (r - l > 1) {
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
}
merge(arr, l, mid, r);
return arr;
}
function merge(arr, l, mid, r) {
var left = l;
var right = mid + 1;
var helpInd = 0;
var help = [];
while (left <= mid && right <= r) {
help[helpInd++] = arr[left] < arr[right] ? arr[left++] : arr[right++];
}
while (left <= mid && right > r) {
help[helpInd++] = arr[left++];
}
while (left > mid && right <= r) {
help[helpInd++] = arr[right++];
}
var i = 0;
while (help[i] !== undefined) {
arr[l + i] = help[i];
i++;
}
}
console.log(main([5, 1, 4, 3, 0])); // [0,1,2,3,4,5]
4 时间复杂度
我们可以通过两种方法计算归并排序的时间复杂度
,分别是直观理解
和递归复杂度公式
4.1 直观理解
假设一颗二叉树的节点数量为N
,那么它的高度就是logN + 1
。
在拆分
阶段,一共做了logN
次(log8 = 3
)的操作,该阶段时间复杂度是logN
在合并
阶段,我们要遍历二叉树的每一层节点,每层节点数都是N
,需要遍历的层数有logN
层(log8 = 3
),所以这个阶段的时间复杂度就是NlogN
两个阶段的时间复杂度相加logN + NlogN
,可以得到时间复杂度O(NlogN)
4.2 递归复杂度公式
如果 ,递归函数时间复杂度为
如果 ,递归函数时间复杂度为
如果 ,递归函数时间复杂度为
在介绍公式之前,我们先了解递归函数规模
这个概念,以上面提到的递归函数为例
function mergeSort(arr, l, r) { // arr为完整数组,l为左索引,r为右索引
var mid = l + ((r - 1) / 2); // 获取中点
if (r - l > 1) { // 递归直到左索引大于右索引
mergeSort(arr, l, mid); // 递归处理数组左半边
mergeSort(arr, mid + 1, r); // 递归处理数组右半边
}
merge(arr, l, mid, r); // 做排序和合并的处理,后面会讲
return arr; // 返回排序合并好结果
}
mergeSort
里面有两个子递归mergeSort
,它们分别处理了父函数中数组arr
左半边和右边,那么它们的递归函数规模
就是。
公式 在中的就是递归函数规模
,a
是子递归函数的数量(刚才提到有2个),所以公式前半部分就是。
公式的后半部分,是子递归以外代码
的时间复杂度。比如上面例子中,递归以外的代码有
function mergeSort(arr, l, r) { // arr为完整数组,l为左索引,r为右索引
var mid = l + ((r - 1) / 2); // 获取中点
// if (r - l > 1) { // 递归直到左索引大于右索引
// mergeSort(arr, l, mid); // 递归处理数组左半边
// mergeSort(arr, mid + 1, r); // 递归处理数组右半边
// }
merge(arr, l, mid, r); // 做排序和合并的处理,后面会讲
return arr; // 返回排序合并好结果
}
主要还是看merge
函数,大家可以看下章节的代码实现
,不难看出时间复杂度是O(N)
。
好啦,终于可以套入公式可以得到
其中 a = 2, b = 2, d = 1
如果 ,递归函数时间复杂度为
如果 ,递归函数时间复杂度为
如果 ,递归函数时间复杂度为
套入,所以时间复杂度是O(NlogN)
递归复杂度公式适应于所有
递归规模相同
的递归函数,比如上面例子中每个子递归函数的规模都是。假如是下面这种就不适用了
function mergeSort(arr, l, r) {
// ...
mergeSort(arr, l, (r - l) / 3); // 递归处理数组的1/3
mergeSort(arr, (r - l) / 3, r); // 递归处理数组的2/3
// ...
}
5. 额外空间复杂度
在合并
过程中,我们使用辅助数组Help
用于数组合并前的排序。
从总体上来看,因为二叉树中每层的节点数都恒定为N
,每一层所有小数组的节点总数都是N
,多个辅助数组Help
的占用总空间也是N
。
因此,我们需要的额外空间为N
,额外空间复杂度为O(N)
。
6. 稳定性
稳定性取决于值相同的两个元素在排序时有没有交换位置。
算法主要是在合并阶段利用辅助数组Help
进行排序,在比较大小过程中,如果值相同的元素不需要交换元素,而在合并过程中也不会出现顺序上的变化。
因此归并排序算法具有稳定性
7. 本章小结
- 归并排序是利用分治法的一种的排序,先不断拆分数组称为最小数组,然后最小数组两两比较,合并成多个排好序的较大数组,较大两两比较,合并成多个排好序的更大数组,直至完成整个数组的排序
- 归并排序的时间复杂度是
O(NlogN)
- 归并排序的空间复杂度是
O(N)
- 归并排序具有稳定性