[路飞]_我不信你看不懂我这篇归并排序

206 阅读3分钟

「这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战

简介

归并排序,主要利用了分治的思想,先将数组依次对半拆分,最终得到一个个单个的元素。

然后再将元素一组一组的排序并合并。

图解

归并排序.png

步骤详解

如上图,数组nums = [5,2,3,1,7,6,9,8]

  1. 思想上,定义块是block,开始数组是一个block,先将数组对半拆分成两块,再逐个对半拆成两块,最后拆成一个块是一个元素(这一步有思想就行了,代码上不需要做什么)。

  2. 数组拆到每个块block里只有一个元素,我们开始合并

  3. 将相邻两个块合并,合并规则如下

    • 比较两个block的第一个元素,谁小就拿出谁,一样就拿左边的,放到暂存数组result里
    • 只要有一个块元素空了,就停止
    • 看看另一个块还有没有剩的,有的话直接都推入result里
  4. 这一轮归并完成,我们将result的存放的结果给到nums保管,将result置空,继续下一轮。

  5. 此时我们一个block里就有两个元素了,再继续比较相邻的block,原则和上面一样

  6. 最终归并完就排序好了

有同学肯定会问,如果元素不是刚好2的次方关系呢?

比如nums元素有9个,或者15个怎么办?

其实是一样的。我画一个9个元素的图示例一下

图画的有点丑,但意思绝对到位的。

分的这一步可以不用看,代码也不需要去做额外的拆分处理,我们直接就可以开始做合并操作的。

由于是9个元素

那么第一轮,相邻两组block合并,block长度变成2,合并到最后,4单独为一组 第二轮,同理,4还是单独为一组 ... 最后一轮,block为8,目标是合并成为成都为16的数组,然后就将4合并进来了

归并排序奇数个元素.png

代码

迭代法。

/**
 * @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)。