归并排序

102 阅读2分钟

归并排序可以理解为将一个数组依次分成两部分,其中的一部分又分成两部分......,然后反过来依次排好这些小小部分,最后整个大部分就排好了,可以说是先局部后整体,这是整个递归的整体思路。时间复杂度是O(N * logN),空间复杂度是O(N),稳定。

下面用图直观感受一下:

image.png

下面我们来看一下动态展示:

归并排序.gif

代码的相关展示如下:

    public static void main(String[] args) {
        int[] nums = new int[]{1,2,5,2,0,0,1};
        mergeSort(nums);
        for (int i : nums){
            System.out.println(i);
        }
    }
    public static void mergeSort(int[] arr){
        if (arr == null || arr.length < 2){
            return;
        }
        porcess(arr, 0, arr.length - 1);
    }   
    //注意递归次数不可以太多,否则会导致栈的溢出
    //递归从整体上来理解,porcess(arr, L, mid)相当于排好序的左部分, porcess(arr,mid + 1
    //, R)相当于排好序的右部分,merge相当于将左部分和右部分整合起来排好序
    public static void porcess(int[] arr, int L, int R) {
        if(L == R){
            return;
        }
        int mid = L + ((R - L) >> 1);
        porcess(arr, L, mid);
        porcess(arr,mid + 1, R);
        //每一块都依次排好了
        merge(arr, L, mid, R);
    }
    //核心代码 可以宏观的看成两部分,而这两部分已经是排好序的,假设p1在第一部分的最开始
    //p2在第二部分的最开始,依次比较p1和p2,谁小放入新的数组空间中,对应的p1++或p2++,
    // 再依次比较
    public static void merge(int[] arr, int L, int M, int R){
        int[] help = new int[R - L + 1];
        int i = 0;
        int p1 = L;
        int p2 = M + 1;
        while (p1 <= M && p2 <= R){
            //这里的<=可以保证如果左边的等于右边的,就将左边的先存入新数组,故保证了稳定性
            help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
        }
        //跳出了第一个while,说明有一个部分比较完了,就将剩下的部分都移到新空间数组中
        while (p1 <= M){
            help[i++] = arr[p1++];
        }
        while (p2 <= R){
            help[i++] = arr[p2++];
        }
        //移到arr对应的位置,因为还在递归中
        for (i = 0; i < help.length; i++){
            arr[L + i] = help[i];
        }
    }

代码的时间复杂度分析,这个要用上master公式得到:

T(N)= 2 * (T/2)+ O(N)

即a = 2; b = 2; d = 1;

因为log(b,a) = d,所以时间复杂度是O(N * logN)

这里不清楚的可以看:递归行为时间复杂度的估算

下面补充一道利用归并排序的算法题:

求以数组中每一位为基准,可以知道每一位左侧的比它小的值,求所有这些值的和,例如[0,4,2,3],这个和就是0 * 4 + 2 * 1 = 2;例如[1,5,7,3,8],这个和就是1 * 4 + 5 * 2 + 7 * 1 + 3 * 1 = 24。

相关的代码是:

    public static void main(String[] args) {
        int[] nums = new int[]{1,5,7,3,8};
        System.out.println(minSum(nums));
    }
    public static int minSum(int[] arr){
        if (arr == null || arr.length < 2){
            return 0;
        }
        return porcess(arr, 0, arr.length - 1);
    }
    //注意递归次数不可以太多,否则会导致栈的溢出
    //递归从整体上来理解,porcess(arr, L, mid)返回左部分的最小和, porcess(arr,mid + 1, 
    //R)返回右部分的最小和,merge相当于将左部分和右部分整合起来返回最小和
    public static int porcess(int[] arr, int L, int R) {
        if(L == R){
            return 0;
        }
        int mid = L + ((R - L) >> 1);
        return porcess(arr, L, mid) + porcess(arr, mid + 1, R) + merge(arr, L, mid, R);
    }
    //归并排序的核心部分改进
    public static int merge(int[] arr, int L, int M, int R){
        int[] help = new int[R - L + 1];
        int i = 0;
        int p1 = L;
        int p2 = M + 1;
        int res = 0;
        while (p1 <= M && p2 <= R){
            //将右侧比左侧这个数大的个数乘上对应左侧的数依次相加,因为已经排好序,所以当右侧一个数比这             //个数大时,右侧紧跟着的数都会比它大
            //注意两行代码的顺序
            //注意两行代码的顺序
            res += arr[p1] < arr[p2] ? (R - p2 + 1) * arr[p1] : 0;
            //这里和归并排序有区别,注意是<而不是<=这是因为和题目的要求有关系,左侧和右侧相等的话,将             //右侧的先存入新数组中是为了方便统计右侧部分比左侧的这个数大的个数,直接R - p2 + 1就OK
            help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
        }
        //跳出了第一个while,说明有一个部分比较完了,就将剩下的部分都移到新空间数组中
        while (p1 <= M){
            help[i++] = arr[p1++];
        }
        while (p2 <= R){
            help[i++] = arr[p2++];
        }
        //移到arr对应的位置,因为还在递归中
        for (i = 0; i < help.length; i++){
            arr[L + i] = help[i];
        }
        return res;
    }
}