数据结构与算法 - 归并排序

79 阅读3分钟

前面讲到了冒泡和快排,后面也会陆陆续续的讲其它排序算法,今天的内容是:归并排序。

核心内容

归并排序的核心内容其实很简单,把数组递归二分,后将二分的数组排序好再重新组合到一起,整个排序就完成了。
听完这个介绍,你可能很懵,但不必担心,下面将带你一步步推导。

组合并排序两个数组

看以下题目:

给定两个数组,这两个数组,都是有序的,如[1, 2, 5],[3, 4, 10],要求将两个数组合并,并依旧保持有序,即输出[1, 2, 3, 4, 5, 10]

题目中最有用的信息,无疑是两个数组都是有序的,那么且将两个数组分别称为arr1和arr2,结果数组为arr3,看看以下思路:

  • 给两个数组都设置一个起始下标0
  • 比较arr1[0]和arr2[0],将较小的数据放入arr3,并将这个数组的下标往后移动一位【此时结果为,arr1的下标为1,arr2的下标为0,arr3为[1]
  • 比较arr1[1]和arr2[0],将较小的数据放入arr3,并将这个数组的下标往后移动一位【此时结果为,arr1的下标为2,arr2的下标为0,arr3为[1, 2]
  • 比较arr[2]和arr2[0],将较小的数据放入arr3,并将这个数组的下标往后移动一位【此时结果为,arr1的下标为2,arr2的下标为1,arr3为[1, 2, 3]
  • 重复以上动作,直到某个数组的数据都被移动到了arr3
  • 将另一个数组的数据全部放入arr3

很明显,我们利用了两个数组都是有序的信息,每次,我们能依次找出最小的数据,并把它放到结果数组,直到某一个数组的所有数据都被放入了结果数组,再将另外一个数组剩余的数据全部放入结果数组,下面我们看一下代码实现:

function sortTwoArr(arr1, arr2) {
    let index1 = 0;
    let index2 = 0;
    let result = [];
    while (index1 < arr1.length && index2 < arr2.length) {
        // 将较小的数据放入result,并将其数组的下标往后移动
        if (arr1[index1] < arr2[index2]) {
            result.push(arr1[index1]);
            index1++;
        } else {
            result.push(arr2[index2]);
            index2++;
        }
    }
    
    // 会存在一个数组,还有剩余数据没被放入result,只需要按顺序放入即可
    let tarArr = index1 === arr1.length ? arr2 : arr1;
    let tarStartIndex = index1 === arr1.length ? index2 : index1;
    for (let i = tarStartIndex; i < tarArr.length; i++) {
        result.push(tarArr[i]);
    }
    
    return result;
}

回到归并排序

存在以下数组[5, 2, 1, 4, 10, 3],现要求将整个数组排序

可以发现,这个数组,和我们上面的例子,数据很相似。
把这个数组对半分,就是[5, 2, 1],[4, 10, 3],如果我们能将其分别排序成[1, 2, 5][3, 4, 10],那就能通过组合两个有序数组的方式,把整个数组排序好了!
以数组[5, 2, 1]的排序任务为例,其实又可以将其拆分成两个数组[5],[2, 1]
[2, 1]又可以拆分为[2], [1]

梳理一下

  • 最开始的任务: 排序一个大数组【[5, 2, 1, 4, 10, 3]
  • 任务拆分,[5, 2, 1, 4, 10, 3]拆成2个数组[5, 2, 1],[4, 10, 3],分别排序好【把[5, 2, 1]变成[1, 2, 5], 把[4, 10, 3]变成[3, 4, 10]】,再组合起来
  • 任务拆分,[5, 2, 1]拆成2个数组拆分成[5], [2, 1],分别排序好【[5]不变,把[2, 1]变成[1, 2]】,再组合起来
  • 任务拆分,[2, 1]拆分成两个数组[2],[1],分别排序好,再组合起来
  • 一直细分数组,直到细分的长度为1

让我们尝试写一下代码:

function sort(source) {
    if (source.length <= 1) return source;
    
    let mid = Math.floor(source.length / 2);
    let half1 = sort(source.slice(0, mid));
    let half2 = sort(source.slice(mid, source.length));
    
    return sortTwoArr(half1, half2);
}

上面的代码,就是归并排序的全部了。

注意:代码中频繁的创建了数组,如将数组一分为二时,创建了两个,将两个数组合并时,又创建了一个新的。在数据量比较大的时候,这样的操作会很浪费性能,但其实它是可以省掉的,只需要一个数组即可完成,希望你可以自己写一写。

总结

归并排序是一种分治的思想,它将一个大问题,分成了一些很小的问题,最后将小问题的“答案”组合起来以解决原始的大问题,是一种很高效的算法,后面我们也会在其它算法中接触和学习到它。

下一章

树,bfs和dfs