「这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战」
简介
归并排序,主要利用了分治的思想,先将数组依次对半拆分,最终得到一个个单个的元素。
然后再将元素一组一组的排序并合并。
图解
步骤详解
如上图,数组nums = [5,2,3,1,7,6,9,8]
-
思想上,定义块是block,开始数组是一个block,先将数组对半拆分成两块,再逐个对半拆成两块,最后拆成一个块是一个元素(这一步有思想就行了,代码上不需要做什么)。
-
数组拆到每个块block里只有一个元素,我们开始合并
-
将相邻两个块合并,合并规则如下
- 比较两个block的第一个元素,谁小就拿出谁,一样就拿左边的,放到暂存数组result里
- 只要有一个块元素空了,就停止
- 看看另一个块还有没有剩的,有的话直接都推入result里
-
这一轮归并完成,我们将result的存放的结果给到nums保管,将result置空,继续下一轮。
-
此时我们一个block里就有两个元素了,再继续比较相邻的block,原则和上面一样
-
最终归并完就排序好了
有同学肯定会问,如果元素不是刚好2的次方关系呢?
比如nums元素有9个,或者15个怎么办?
其实是一样的。我画一个9个元素的图示例一下
图画的有点丑,但意思绝对到位的。
分的这一步可以不用看,代码也不需要去做额外的拆分处理,我们直接就可以开始做合并操作的。
由于是9个元素
那么第一轮,相邻两组block合并,block长度变成2,合并到最后,4单独为一组 第二轮,同理,4还是单独为一组 ... 最后一轮,block为8,目标是合并成为成都为16的数组,然后就将4合并进来了
代码
迭代法。
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function(nums) {
let len = nums.length;
// result用于每次合并过程中存放结果
// 每次针对一个block宽度循环结束,将result的结果交给nums暂存,result需要清零
let result = [];
// block代表的是当次循环每个排序快的长度
// 我们分析以下合并终止条件
// 其实简单分析即可得出,只要最一次循环的block长度满足block*2 >= len,即代表我们这次合并能覆盖整个数组,将整个数组都合并完
// 最后一次循环,完成任务后,由于会执行block *= 2,所以必然满足 block >= len
// 那此时已经没有必要进行下一轮
// 因此循环终止条件是block < len
for(let block = 1; block < len ; block *= 2) {
// 因为我们每次会将2个block合并,所以left每次结束都加上2*block
// left超出数组那必然循环结束,所以left<len
for(let left = 0; left <len; left += 2 * block) {
// 定义区间
// all in [start1,end1] is leftBlock
// all in [start2,end2] is rightBlock
let start1 = left;
let end1 = (left + block - 1) < len-1 ? (left + block - 1) : len-1;
let start2 = end1+1;
let end2 = (left + 2 * block -1 ) < len-1 ? (left + 2 * block - 1) : len-1;
// 每次比较两个block的start位置的数,谁小谁就推进result数组
// 只要有一个块遍历结束,那么这一轮就结束
while (start1 <= end1 && start2 <= end2) {
if(nums[start1] <= nums[start2]){
result.push(nums[start1])
start1++
} else {
result.push(nums[start2])
start2++
}
}
// 如果块1还有剩余,则全部推入
while(start1 <= end1) {
result.push(nums[start1])
start1++;
}
// 如果块2还有剩余,则全部推入
while(start2 <= end2) {
result.push(nums[start2])
start2++;
}
}
// result是这一轮处理的结果,将result交给nums保管,
nums = result;
// 然后重置result,继续去装下一轮的结果
result = [];
}
return nums;
};
递归法
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function(nums) {
let len = nums.length;
let result = [];
dfs(nums, result, 0, len - 1);
return nums;
};
function dfs(nums, result, start, end) {
if (start >= end){
return;
}
// 当前区间的宽度
let block = end - start+1;
// all in [start1, end1] is leftBlock
// all in [start2, end2] is rightBlock
let start1 = start;
// 当前区间末尾元素下标
let end1 = (Math.ceil(block/2)-1) + start;
let start2 = end1 + 1;
let end2 = end;
dfs(nums, result, start1, end1);
dfs(nums, result, start2, end2);
// 记录当前合并在result数组里的起点
let k = start;
while (start1 <= end1 && start2 <= end2) {
if(nums[start1] < nums[start2]){
result[k] = nums[start1];
k++
start1++;
} else {
result[k] = nums[start2];
k++
start2++
}
}
// 如果块1还有剩余,则全部推入
while(start1 <= end1) {
result[k] = nums[start1];
k++;
start1++;
}
// 如果块2还有剩余,则全部推入
while(start2 <= end2) {
result[k] = nums[start2];
k++;
start2++;
}
for (k = start; k <= end; k++){
// 没完成一次,都动态的去修改nums
nums[k] = result[k];
}
}
复杂度分析
时间复杂度:O(nlogn),数组长度为n,那么每次减半去,直到block为1,次数为logn。如8个数,每次除以2,就要除3次,即log28。那么每一次减半,我们都需要遍历所有的元素,即n。故总共是O(nlogn)。
空间复杂度:O(n),需要一个数组来存储中间的结果,另外还有递归栈的高度logn,n更大,所以总共还是O(n)。