定义
所谓归并排序,就是在递归过程中将数组进行拆分,然后再回溯的过程中将递归的结果合并。因此,归并排序大体上分为两个步骤:
- 递归分解
- 回溯合并
算法优势
大数据排序的霸主级算法
归并排序
二路归并排序
将两个有序数组合并为一个有序数组
原理
# 例如将下面的数组进行排序
[1,4,2,3,5,8,6,7,9,0]
# 进行归并排序的时候,会将对整个数组排序的任务拆分成两个较小的子任务进行排序
[1,4,2,3,5] [8,6,7,9,0]
# 同理,我们再将上面的两个子任务各自拆分成更小的两个子任务
[1,4] [2,3,5] [8,6] [7,9,0]
# ....
# 当我们子任务的数组元素小于或等于3(当然,这个子任务最小元素数量其实我们可以根据实际情况调整,只要能够让我们能够轻易的排序就可以了)个时,就可以停止继续递归拆分了,可以开始进行排序,排序结果如下
[1,4] [2,3,5] [6,8] [0,7,9]
# 然后对左右的子任务分别进行合并,注意,此处借用了额外的数组空间用来临时存储,当合并完成之后,再将这个临时数组的值复制到原本左/右任务的数组中去。这一个步骤是归并排序的重点:将两个有序数组合并成一个有序数组的过程。
# 那么,我们要如何将两个有序数组合并成一个有序数组呢?我们可以让两个指针p1和p2分别指向两个有序数组的第一个元素,然后将这两个元素中较小的一个元素放入到临时数组的末尾,然后让这个较小元素所在数组的指针向后移动一位,继续上述过程进行比较插入,最终就可以将两个有序数组合并成一个有序数组了。
# [【1】,2,3,4,5] [【0】,6,7,8,9]
# 上面两个指针所指向的元素,0比较小,所以先将0放入临时数组末尾,此时临时数组为:[0]
# 然后右边数组的指针向右移动一位
# [【1】,2,3,4,5] [0,【6】,7,8,9]
# 继续上述的比较,直至两个指针都到达了各自数组的末尾时,我们就已经将两个有序数组合并到了一个临时数组中了
[1,2,3,4,5] [0,6,7,8,9]
# 再将左右子任务进行合并
[0,1,2,3,4,5,6,7,8,9]
简单代码实现
function mergeSort(arr, l = 0, r = arr.length - 1) {
// 当数组元素大于等于1,即左指针和右指针相遇或错过时退出递归过程
if (l >= r) return;
// 计算中间值,方便之后将原数组拆分成两个子任务
const mid = (l + r) >> 1;
// 进行左半边数组排序
mergeSort(arr, l, mid);
// 进行右半边数组排序
mergeSort(arr, mid + 1, r);
// 到这里,我们左右两边数组已经排序完成了,我们准备将左右两边的数组进行合并
// p1是左数组的指针,初始指向左数组第一个元素,p2是有数组的指针,初始指向右数组第一个元素
let p1 = l,
p2 = mid + 1;
let tmp = [];
// 左右指针没有到达各自数组的右边界则继续循环,左指针的右边界是mid,右指针的右边界是r
while (p1 <= mid || p2 <= r) {
// 如果右数组为空或者左数组的指针指向的值比较小,就将所指元素放入临时数组tmp
if (
p2 > r || // 右数组为空
(p1 <= mid && arr[p1] <= arr[p2]) // 判断左指针指向的值比较小
) {
tmp.push(arr[p1++]);
} else {
// 否则将右数组元素加入到临时数组
tmp.push(arr[p2++]);
}
}
// 将临时数组中的元素复制到原数组中
arr.splice(l, tmp.length, ...tmp);
// 释放临时数组空间
tmp = null;
}
const arr = [8, 2, 1, 3, 4, 0, 8, 7, 5, 4, 6, 9];
console.time('mergeSort');
mergeSort(arr)
console.timeEnd('mergeSort');// mergeSort: 0.0947265625 ms
console.log(arr);// [0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 8, 9]
多路归并排序
多个有序数组合并成一个有序数组
变路归并排序
在排序的过程中计算当前的数组区间需要拆分为几个子问题进行归并排序,排序过程中拆分的子问题数量是不确定的
归并排序在大数据场景下的应用
在小内存设备上进行大数据排序
在一个内存仅有2GB的电脑上,对40GB大小的文件进行排序
这种场景下,大家来思考一下,能够使用之前学习过的快速排序
进行操作吗?显然是不行的。我们快速排序的基础是Partition
操作,也就是分区操作,是对数组整体进行操作的,我们现在的数据量有40GB,明显是没有办法一次性将所有数据读取到内存中进行排序的,那么,这个时候,我们的归并排序就登场了。
我们来思考一下,我们刚刚在实现二路归并排序
的时候使用的tmp
这个临时数组,是不是只要支持在尾部添加元素就可以了,那么,大家再来想想,我们操作系统中的文件,是不是也支持在文件的尾部添加内容,我们是不是可以借助外部的存储空间硬盘
来存储我们这些临时数据呢?我们的硬盘大小一般是远大于内存大小的。
我们可以将我们40GB
的文件,拆分成20份2GB
的文件进行排序,排好序之后,我们再使用20路归并排序
将这20份有序的文件合并成一个40GB
的有序文件(合并的过程中,如果是二路归并的话,每个子任务的最小值是显而易见的,但因为我们要在20份数据中获取最小值,所以我们要借助之前我们学习过的小顶堆
,这样每次从20份数据中获取最小值也就轻而易举了),这样就解决了我们因为内存不足而导致无法排序超过内存大小数据的问题了。
这也是为什么归并排序特别适合处理大数据问题的原因,因为归并排序最重要的过程就是合并,而合并的时候的临时存储区是可以借助外部存储器存储的,所以我们才把归并排序称为外部排序,而快速排序
称为内部排序
归并排序与快速排序
快速排序
在内部排序表现优秀,因为必须将所有的数据一次性读入内存中进行整体排序,是一个只针对内存级别的排序
归并排序
在外部排序表现优秀,可以借助额外的外部存储空间,如硬盘等进行排序,在内存不足或大数据的场景下是非常有用的
归并排序的思想
先处理左边,得到左边的信息
再处理右边,得到右边的信息
最后处理横跨左右两边的信息
思想:要解决一个大问题,先将这个大问题差分成若干个子问题分别求解,然后将所有子问题的解进行合并得出大问题的解,即分治思想