归并排序(MergeSort) Java实现及解析

一、归并排序的理解
本质而言可以看作二叉树遍历的一种应用。

二、merge操作

merge方法用于将已经排好序的左右两边进行整理和合并。

假设要被排序的数组被分成了很多个小块, 在进行当前递归步骤之前,每一个小块上的元素已经是有序的了,需要通过merge操作将两个已经排号序的小块整理为一整块有序部分。
这里的merge操作在非递归实现和递归实现中是通用的。

 // 
    public static void merge(int[] arr, int L, int M, int R){
        int[] help = new int[R - L + 1]; // 设置一个长度为L到R元素个数的辅助数组help
        int i = 0; // 为help使用所准备的指针
        int p1 = L; // 设置一个左指针从左往右移动
        int p2 = M + 1; // 设置一个右指针从右往左移动
        while (p1 <= M && p2 <= R){ // 当左右指针都没有越界时
            // help中放入p1或p2指针所对应的元素中教小的那个,i以及对应指针p1或p2移动一位
            help[i++] = arr[p1] <= arr[p2] ? arr[p1++]:arr[p2++];
        }
        // 上面的while循环终止的条件是,要么p1越界,要么p2越界
        // p1,p2不可能都越界,所以下面两个while循环只会有一个执行
        // 然后将没越界的那个要走的剩余部分放入help中
        while (p1 <= M){
            help[i++] = arr[p1++];
        }
        while (p2 <= R){
            help[i++] = arr[p2++];
        }
        // 然后将help数组中的内容重新刷入arr中
        for (i = 0; i < help.length; i++){
            arr[L + i] = help[i];
        }
    }

三、递归实现

public class Code01_MergeSort {
    // 递归方法实现
    public static void mergeSort1(int[] arr){
        if(arr == null || arr.length < 2){
            return;
        }
        process(arr, 0, arr.length - 1);
    }

    // 递归函数的定义:让arr在[L..R]范围上, 变成有序的
    public static void process(int[] arr, int L, int R) {
        if (L == R){ // base case: 当范围上只有一个数时停止划分
            return;
        }
        int mid = L + ((R - L) >> 1); // 如果范围上不止一个数, 先选出中点
        process(arr, L, mid);
        process(arr, mid+1, R);
        merge(arr, L, mid, R);
    }
}

四、非递归(迭代)实现

用迭代的方法进行归并排序的话,可以理解为,自初始的有序长度1开始,不断对已经有序的左组和长度位置,未经排序的右组进行合并,在合并的过程中左右组会一起变得有序,每完成一次进行合并的区域长度倍增。

    public static void mergeSort2(int[] arr){
        if(arr == null || arr.length < 2){
            return;
        }
        int N = arr.length;
        // 如果设置组长度K,应从2开始
        int mergeSize = 1; // 当前有序的,左组长度,从1开始
        while(mergeSize < N){ // log N
            int L = 0; // L用于枚举第N个左组的位置从哪里开始,一次归并完成会扩大mergeSize回到下标0位置重新开始新一轮归并
            // 0 - ..
            while(L < N){
                // L .. M 当前的左组,其长度为mergeSize
                int M = L + mergeSize - 1; // M的位置可以直接算出
                if (M >= N){ // 当前左组的右端位置已经到达arr末端,可以不用再排序了
                    break;
                }
                // L..M M+1..R(mergeSize)
                int R = Math.min(M + mergeSize, N - 1);// 右组右端的位置取决于剩余元素数量够不够和左组一样长
                merge(arr, L, M, R);
                L = R + 1;// 一次左右组归并完成,将L移动至R+1位置
            }
            // 加上下面这一句是为了防止溢出:可能会有未知的错误, 例如超过Int类型最大值(21亿左右)
            if (mergeSize > N / 2){ // 判断2 * mergeSize是否大于arr长度
                break;
            }
            mergeSize <<= 1; //每次完成一组排序后如果组长度仍然不比arr长度大,mergeSize*2
        }
    }

五、总结

递归操作的时间复杂度公式:

若递归操作符合: T ( N ) = a ∗ T ( N b ) + O ( N d ) T(N) = a*T(\frac{N}{b}) + O(N^d) T(N)=aT(bN)+O(Nd)

  1. log ⁡ b a > d \log_b^a>d logba>d ⇒ 时间复杂度为: O ( N l o g b a ) O(N^{log_b^a}) O(Nlogba)
  2. log ⁡ b a < d \log_b^a < d logba<d ⇒ 时间复杂度为: O ( N d ) O(N^d) O(Nd)
  3. log ⁡ b a = d \log_b^a = d logba=d ⇒ 时间复杂度为: O ( N d ∗ l o g N ) O(N^d*log^N) O(NdlogN)

d 表示除了递归调用之外剩余所有行为的时间复杂度。

计算时间复杂度: T ( N ) = 2 ∗ T ( N 2 ) + O ( N ) T(N) = 2*T({\frac{N}{2}}) + O(N) T(N)=2T(2N)+O(N)

尾项为merge的时间复杂度,merge的时间复杂度为 O ( N ) O(N) O(N),所以对于递归的归并排序而言,d=1。
两个2的来源是“二分等规模的递归操作”。

l o g b a = l o g 2 2 = d = 1 log_b^a = log_2^2 = d= 1 logba=log22=d=1 所以时间复杂度为: O ( N ∗ l o g N ) O(N*log^N) O(NlogN)
归并排序的时间复杂度为 O ( N ∗ l o g N ) O(N*log^N) O(NlogN)

归并排序相较于冒泡排序、选择排序和插入排序的时间复杂度 O ( N 2 ) O(N^2) O(N2),其优势在于把比较的结果作为有效信息固定了下来 ,而上面的三种排序,在一次遍历的比较操作中只有一次是有效的。

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值