算法漫谈 —— 归并排序

引言

本文主要对《算法》(第4版)一书中对归并排序(merge-sort)算法的介绍进行摘录整理,并适当拓展。

概览

顾名思义,归并排序算法基于「归并」这个简单的操作 —— 即将两个有序数组归并成一个更大的有序数组。人们根据这个操作发明了这种简单的递归排序算法:要将一个数组排序,可以先(递归地)将它分成两半分别排序,然后将结果归并起来。归并排序是高效算法设计中分治思想的典型应用,其最吸引人的性质是它能够保证将任意长度为 N 的数组排序所需时间和 NlogN 成正比。

实现

Talk is cheap, show me the code. 以下代码实现了自顶向下的归并排序(top-down merge-sort):

/**
 *  @author Robert Sedgewick
 *  @author Kevin Wayne
 */
public class Merge {

    // * 核心逻辑:使用辅助数组 aux 对数组 a 进行递归式的归并排序
    private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi) {
        if (hi <= lo) return; // 递归调用最简情况
        int mid = lo + (hi - lo) / 2; // 均分数组
        sort(a, aux, lo, mid); // 递归调用,归并排序左半数组
        sort(a, aux, mid + 1, hi); // 递归调用,归并排序右半数组
        merge(a, aux, lo, mid, hi); // 对排序后的有序数组或单元素数组进行归并
    }

    // 使用辅助数组 aux 对有序数组 a[lo .. mid] 及 a[mid+1 .. hi] 进行归并
    private static void merge(Comparable[] a, Comparable[] aux, int lo, int mid, int hi) {
        
        // 前置检验:a[lo .. mid] 及 a[mid+1 .. hi] 须为有序数组或仅包含单个元素
        assert isSorted(a, lo, mid);
        assert isSorted(a, mid+1, hi);

        // 将 a 中元素复制至 aux
        for (int k = lo; k <= hi; k++) {
            aux[k] = a[k]; 
        }

        // 对有序数组 a[lo .. mid] 及 a[mid+1 .. hi] 进行归并操作
        int i = lo, j = mid+1;
        for (int k = lo; k <= hi; k++) {
            if      (i > mid)              a[k] = aux[j++];
            else if (j > hi)               a[k] = aux[i++];
            else if (less(aux[j], aux[i])) a[k] = aux[j++];
            else                           a[k] = aux[i++];
        }
        
    }
    
    // 客户端调用该方法对数组进行归并排序
    public static void sort(Comparable[] a) {
        Comparable[] aux = new Comparable[a.length];
        sort(a, aux, 0, a.length-1);
    }
    
}

以上代码的核心思路是递归地将待排序数组均分为左半数组和右半数组进行排序和归并。观察代码可以发现,我们只用数十行代码就实现了该算法,这是递归代码的简洁优雅之处。进一步通过递归三要素对其中的核心逻辑sort()方法进行分析:

  • 递归总有一个最简单的情况:上述代码中if (hi <= lo) return;语句表示的便是归并排序算法中的最简情况 —— 即被均分后的左半或右半数组(数组边界由lohi界定)中仅存在一个元素;对单个元素无需排序,此时方法直接返回,准备进入归并阶段;
  • 递归调用总是去尝试解决一个规模更小的子问题(这样递归才能收敛到最简单的情况):显然,这也是归并排序算法所依托的分治思想的核心思路,即将一个较大的数组不断切分(分)成较小的数组后进行处理(治);
  • 递归调用尝试解决的子问题间不应该有交集:上述代码总是尝试通过计算数组的中间位置mid将数组均分为左半数组(由lomid界定)和右半数组(由mid+1hi界定),作为子问题(即下层递归调用)的输入,以此保证所处理的数据不存在交集。

merge()函数在该算法中的作用是将单个元素或已经有序的数组进行归并,并保证归并后的数组仍然有序,此处不再赘述。同时,我们也可以发现,sort()方法除了负责对数组不断进行切分()外也巧妙地安排了多次merge())方法的正确调用顺序。

图释

极客时间中《程序员的数学基础课》专栏中对归并排序算法给出了形象的图释,并从的角度对上述自顶向下版本的归并排序算法作出解释:在归并排序的数据分解阶段,初始的数据集可以看做树的根节点,二分之后左、右半边数据集是子节点;分解过程持续到单个数值为止(即上文提到的递归最简情况),也就是最末端的叶子节点。使用递归进行数据切分是数的深度优先搜索的体现。如下图(图中省略了对原始数组的初次切分过程):

布隆过滤
fork/join

待续……

参考资料
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值