归并排序详解

前言

前面的两篇文章,给大家具体的讲述了快排和堆,堆排以及加强堆的实现过程。基于比较排序的排序算法中,最后再向大家介绍一下归并排序,之所以放到最后,并不是因为归并排序比快排、堆排复杂,恰恰相反,归并排序从理解层面上和前面介绍两种排序算法相比,应该是最简单的,归并排序重要的是背后的分治思想,解释过来就是分而治之。
当下流行的微服务、分布式以及各种技术架构,背后或多或少都有着分治的理念,也是希望大家重视这种思想,培养整体思维,遇到一个大问题,我们可以分解成一个个小问题去解决。好了,废话不多说,正式开始归并排序的旅途。

原理

前面说了归并排序的流程理解起来很简单,怎么个简单法呢?还是大家来个例子,给你一个数组 arr,给它排有序。怎么搞呢?整体流程很简单:先把数组对半砍分成左右两半,想办法让左半部分排有序,右半部分排有序,最后把左半部分和右半部分合并起来做到整体有序即可。怎么左半部分排有序呢?哎,是不是递归就行了。
我们先不看递归,不过这里既然提到了递归,也是想给大家一个提醒,对于递归函数,我们要具备黑盒思维,要先想明白递归的含义,这很重要!!!递归函数的含义一旦定了,递归的base case就很好想,剩下的就是子过程的调用流程,一个递归的要素也就是这几点。这里先假设有这么一个黑盒结构,把数组的左半部分和右半部分丢进去,他就会排好序。
我们先看排好序的左组和右组合并过程,其实要点也很简单,准备一个辅助数组,再准备两个指针,分别卡着左组和右组的起始位置,开始遍历,如果左组的数小于等于右组的数,copy左组的数,否则copy右组的数,具体代码如下:

// 合并左组和右组,做到整体有序
public static void merge(int[] arr, int L, int M, int R) {
    // 准备一个辅助数组
    int[] help = new int[R - L + 1];
    int index = 0;
    // 左组的起始位置
    int p1 = L;
    // 右组的起始位置
    int p2 = M + 1;
    // 左组 右组合并
    while (p1 <= M && p2 <= R) {
        // 规定左组的数小于等于右组的数,copy左组的数
        help[index++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
    }
    // 下面两个循环只会发生一个!
    // 从第一个while跳出的情况:p1先越界或p2先越界
    // p2先越界,左组还有数
    while (p1 <= M) {
        help[index++] = arr[p1++];
    }
    // p1先越界,右组还有数
    while (p2 <= R) {
        help[index++] = arr[p2++];
    }
    // 把辅助数组copy回原数组
    for (int i = 0; i < help.length; i++) {
        arr[L + i] = help[i];
    }
}

有了合并的过程,我们再来解决递归的问题,根据我们的需求,我们的递归得能让左部分排有序、右部分排有序。综和上述情况,不妨这么定义递归,给定一个数组,再给定L,R两个参数表示数组的范围,具体含义:数组L到R范围上的数排有序,具体代码如下:

// arr[L...R]上排有序
public static void process(int[] arr, int L, int R) {
    // base case
    if (L == R) {
        return;
    }
    // 求中点位置
    int mid = L + ((R - L) >> 1);
    // 左部分排有序
    process(arr, L, mid);
    // 右部分排有序
    process(arr, mid + 1, R);
    // 左、右部分合并
    merge(arr, L, mid, R);
}

public static void mergeSort1(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    process(arr, 0, arr.length - 1);
}

没错,就是这么简单,这里为了帮助大家理解,以arr[6,3,7,9,0,4,1,8]为例,体会一下整个递归的运行过程,详情如下图所示:归并排序
以图为例,我们这里讨论一下归并排序的时间复杂度吧,我们再看看上面递归的流程图:哎,像不像一颗二叉树?对于算法而言,当你面对新的题目,要具备一定的敏感度,这点很重要,它能很快给我们一种思维上的启发。回到正题,数据量为N,挂到树上,树高是不是为logN?每次分组之后是不是随后都会伴随一次左组右组合并的行为,这里注意:在合并的过程中,左组的指针是不会回退的,右组的指针也是不会回退的,所以合并的过程是不是O(N)?所有流程整合起来,归并排序的时间复杂度就是O(N * logN)。

最后我们再来看看归并排序迭代版本的实现:准备一个变量mergeSize,称之为步长,用来划分左组和右组,一个左组配一个右组,怎么理解呢?还是以数组arr[6,3,7,9,0,4,1,8]为例,当mergeSize=1,arr就可以划分为{6,3},{7,9},{0,4},{1,8}四个组,在{6,3}这个组中:{6}是左组,{3}是右组;在{7,9}这个组中{7}是左组,{9}是右组,就是说mergeSize的值就代表着当前左组和右组的长度,随后一个左组和一个右组会进行合并操作变成一个有序的整体,也就相当于例子中:mergeSize=1时,左组{6}和右组{3}合并后会变成{3,6}。具体怎么使用,代码如下:

// 非递归方式实现
public static void mergerSort2(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    int N = arr.length;
    // 步长
    int mergeSize = 1;
    while (mergeSize < N) {
        // 当前左组的位置
        int L = 0;
        while (L < N) {
            if (mergeSize > N - L) break;
            // 当前左组的最后位置:中心点
            int M = L + mergeSize - 1;
            // 当前右组的最后位置
            // 边界条件:右组不够步长时,取原数组最后位置
            int R = M + Math.min(mergeSize, N - M - 1);
            merge(arr, L, M, R);
            // 当前组合并完成之后,来到下个左组的起始位置
            L = R + 1;
        }
        // 1.防止mergeSize整型溢出的情况
        // 2.mergeSize > N/2:意味着当前的左组和右组合起来的总体长度已经超过了原数组长度
        if (mergeSize > N / 2) {
            break;
        }
        // 不要忘记mergeSize左移更新
        mergeSize <<= 1;
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值