「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战」
前言
前端算法系列是我对算法学习的一个记录, 主要从常见算法、数据结构、算法思维、常用技巧几个方面剖析学习算法知识, 通过LeetCode平台实现刻意练习, 通过掘金和B站的输出来实践费曼学习法, 我会在后续不断更新优质内容并同步更新到掘金、B站和Github, 以记录学习算法的完整过程, 欢迎大家多多交流、点赞、收藏, 让我们共同进步, daydayup👊
目录地址:目录篇
相关代码地址: Github
相关视频地址: 哔哩哔哩-百日算法系列
一、思路
归并排序作为经典的分治算法之一, 主要的设计思路就是分而治之, 通过将原数组分成两堆, 然后分别排序, 在对两个数组分别排序的过程中我们可以使用任意一种排序方式, 也可以通过递归的方式继续使用归并排序. 最后再将排好序的两个有序数组合并起来. 这样通过分成两组分别排序的方式我们称之为二路归并, 此外在特定的场景下, 我们也可以将其划分为多个区块分别排序,我们称之为多路归并.
二、实现二路归并
function mergeSort(nums) {
// 1、将数组拆分为两部分 递归
// 2、合并最终的结果
dfs(nums, 0, nums.length)
}
var dfs = function(nums, l, r) {
if (l >= r) return
let mid = Math.floor((l + r) / 2)
dfs(nums, l, mid)
dfs(nums, mid + 1, r)
mergeTwoIntervals(nums, l, mid, mid + 1, r)
}
// 2.合并两个有序数组
var mergeTwoIntervals = function(nums, l1, r1, l2, r2) {
let i = l1, j = l2, k = 0, res = new Array(r2 - l1 + 1)
// 合并
while(i <= r1 || j <= r2) {
if (j > r2 || (i <= r1 && nums[i] <= nums[j])) {
res[k++] = nums[i++]
} else {
// 这里因为不是从第一个数组中取值就是从第二个取值,所以省略判断
// if (i > r1 || (j <= r2 && nums[j] <= nums[i]))
res[k++] = nums[j++]
}
}
// 替换
for(let i=l1; i<r2; i++) {
nums[i] = res[i - l1]
}
}
三、进阶多路归并
1.为什么多路
思考一下这个问题
有一台2GB内存的计算机, 如何对硬盘中一个40GB的文件排序?
对于这道题目, 我们就可以充分利用归并排序的分治思想, 首先将40GB的文件划分为40个1GB的文件块分别进行排序(内排序), 然后再利用归并排序的多路合并思路创建40个指针, 每次求出其中的最小值添加到数组中. 当然, 在最终合并的过程中是需要额外的空间的, 但是我们可以利用硬盘空间(外排序)将合并后的数据填写到一个新的文件之中.
这样的话在我们进行分别排序的时候只需要用到1GB的内存,而在后续合并的时候也只用到了四十个指针的内存.
因此, 归并排序的一大优点就在于:
最终的合并操作是可以使用 外排序 进行的, 因此在面对大量数据的排序的时候拥有一定的优势.
其他说明:
内排序: 利用内存排序, 在排序的过程中需要读取数据
外排序: 利用外存排序, 在排序的过程中可以利用硬盘容量来排序
2.多路的实现
为了保证最少知识原则, 我们这里并不实现关于文件的读取、写入和内排序操作, 另外关于四十个文件的最小值问题, 正常情况下我们可以使用小顶堆来实现, 关于具体的实现方式会在数据结构篇中有讲解.
// 下列函数的具体逻辑不需要关心, 只要明白其输入输出即可
// F.split(file, num) 将文件拆分成多个小文件, 并返回一个文件数组
// F.vals(file) 获取文件中所有的值,返回一个数组
// F.val(file) 获取文件内第一个值,没有返回false
// F.unshift(file) 将文件中第一个值删除并返回
// F.push(name, text) 将内容追加到名为name的文件中
function threeMergeSort(bigFile) {
const num = 40
// 1、将文件拆分为四十个文件
let files = F.split(bigFile, num)
// 2、对每个文件进行内排序
for(let file of files) {
let vals = F.vals(file)
mergeSort(vals) // 使用上面递归版的二路归并对当前文件数据排序
F.push(file.name, ...vals) // 将排好序的数组再写回文件中去
}
// 3、合并所有的文件
while(true) {
let minIdx = -1, minVal = null
for(let i=0; i<num; i++) {
let val = F.val(files[i])
if (!val) continue;
if (minIdx == -1 || val < minVal) {
minIdx = i
minVal = val
}
}
if (minIdx !== -1) {
// 删除文件中最小的一个值, 并添加到输出的文件中去
F.unshift(files[minIdx])
F.push('output.text', minVal)
} else {
// 当minIdx值为-1时表示所有文件中都没有值了,结束循环
break;
}
}
}
3.总结
到这里关于二路归并与多路归并的代码和思路就结束了, 不知道你是否学会了, 不管是二路还是多路, 总体的思想都是 先拆分排序、再合并结果. 另外你还有哪些疑问或补充, 欢迎在评论区留言交流.
其他
关于小顶堆的实现-文章链接TODO