算法学习笔记--归并排序及其应用

前言

在学习左神的算法课程,关于归并排序有些收获,在此记录,以备后查。

原理

分治的思想,将对数组arr[]排序的任务(规模为N),分为对左、右半边排序(规模各位N/2)、合并(规模为N)这三步操作。对左(或右)半边排序又可以拆分为两个更小规模的排序与一个合并的操作。如此递归,直到待排序的规模为1.

递归实现

    /**
     * 假定左边和右边分别已经排序好,将左右两边合并成有序的序列
     * @param arr	待排序的数组
     * @param left  arr左边界 inclusive
     * @param right arr右边界 inclusive
     */
    public static void mergeSort(int[] arr,int left,int right){
        //边界条件
        if(left>=right){
            return;
        }
        int mid =left+((right-left)>>1);
        //左边排序
        sort(arr, left, mid);
        //右边排序
        sort(arr, mid + 1, right);
        //左右合并
        merge(arr,left,mid,right);
    }

    /**
     * 假定左边和右边分别已经排序好,将左右两边合并成有序的序列
     * @param arr
     * @param left
     * @param mid
     * @param right
     */
    private static void merge(int[] arr, int left, int mid, int right) {

        int i = left, j = mid + 1, k=0;
        int[] tmp = new int[right - left + 1];
        while (i<=mid&&j<=right){
            tmp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
        }

        while (i <= mid) tmp[k++] = arr[i++];
        while (j <= right) tmp[k++] = arr[j++];

        for (int l = 0; l < tmp.length; l++) {
            arr[left + l] = tmp[l];
        }
    }

非递归实现

所有递归都可以改成非递归写法。递归是自上而下的,那么对应的非递归写法就是自下而上的逻辑。

    /**
     * 归并排序的非递归版本
     * @param arr
     */
    public static void mergeSort2(int[] arr){
        if(arr==null||arr.length<2){
            return;
        }
		//i指左边要合并的数据的size,以2^i的趋势变化,它控制了归并操作要进行几趟
        for (int i = 1; i < arr.length; i=i<<1) {
            //j指每一趟归并过程中,左半边的开始的指针,所以左半边的范围是[j,j+i-1],右半边的范围要考虑到长度不够的情况,所以是[j+i,min(j+2*i-1,arr.length-1)]
            for (int j = 0; j +i < arr.length; j=j+2*i) {
                merge(arr,j,j+i-1,Math.min(j+2*i-1,arr.length-1));
            }
        }
    }

    /**
     * 假定左边和右边分别已经排序好,将左右两边合并成有序的序列
     * @param arr
     * @param left
     * @param mid
     * @param right
     */
    private static void merge(int[] arr, int left, int mid, int right) {

        int i = left, j = mid + 1, k=0;
        int[] tmp = new int[right - left + 1];
        while (i<=mid&&j<=right){
            tmp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
        }

        while (i <= mid) tmp[k++] = arr[i++];
        while (j <= right) tmp[k++] = arr[j++];

        for (int l = 0; l < tmp.length; l++) {
            arr[left + l] = tmp[l];
        }
    }

拓展面试题

求数组小和

示例:

设数组arr={1,3,4,2,5},数组的小和指每一个数左边比它小的数的和。如
1:0
3:1
4:1、3
2:1
5:1、3、4、2
数组arr的小和为1+1+3+1+1+3+4+2=16。

思路:

简答方法,可以通过二重遍历进行暴力破解,时间复杂度是N2。这在面试中显然是没分的,有没有更好地方法呢?整个题目,是要获知数组中每一个数左边有哪些数比它小,并把他们累加。用同样分治的思想,总的小和应该等于左半边的小和+右半边的小和+merge过程中的小和。
其中实现优化的关键点在于merge过程,这一步求的小和,是指对右边每个数而言,左边有哪些数比它小,因此左边或右边自己内部的顺序不影响merge求小和的结果,如此也就可以利用上面的归并排序,压榨出这一步的小和。
归并过程中,左边范围[left,mid],左指针为i,右边范围[mid+1,right],右指针为j。当arr[i]因而不必再对后面遍历,直接将(right-j+1)*arr[i]累加到小和里面,这一步是产生性能优化的关键点

代码:
/**
 * 求数组的小和
 *
 * @param arr
 * @param left
 * @param right
 * @return
 */
public static int getArrSmallerSum(int[] arr, int left, int right) {
    if (left >= right) {
        return 0;
    }
    int mid = left + ((right - left) >> 1);
    int leftSum = getArrSmallerSum(arr, left, mid);
    int rightSum = getArrSmallerSum(arr, mid + 1, right);
    int mergeSum = merge2(arr, left, mid, right);
    return leftSum + rightSum + mergeSum;
}

/**
 * 假定左边和右边分别已经排序好,将左右两边合并成有序的序列,
 * 并返回合并过程中的小和
 *
 * @param arr
 * @param left
 * @param mid
 * @param right
 */
private static int merge2(int[] arr, int left, int mid, int right) {
    int sum = 0;
    int i = left, j = mid + 1, k = 0;
    int[] tmp = new int[right - left + 1];
    while (i <= mid && j <= right) {
        if (arr[i] < arr[j]) {
            //关键是这一行!!!
            sum = sum + arr[i] * (right - j + 1);
            tmp[k++] = arr[i++];
        } else {
            tmp[k++] = arr[j++];
        }
    }
    while (i <= mid) tmp[k++] = arr[i++];
    while (j <= right) tmp[k++] = arr[j++];
    for (int l = 0; l < tmp.length; l++) {
        arr[left + l] = tmp[l];
    }
    return sum;
}

统计数组降序对的数量

示例:

数组arr={3,1,4,2,5,7,4}中,降序对指左边大于右边的数组成的对,如:
3:{3,1}、{3,2}
1:{}
4:{4,2}
5:{5,4}
7:{7,4}
数组arr降序对的数量为2+0+1+1+1=5

思路:

上一题,是要获知数组中,每一个数左边有哪些数比它小。这一题,是要获知数组中,每个数右边有哪些数比它小。换汤不换药,还是基于归并排序修改,只不过这次关注的是归并过程中arr[i]>arr[j]时,应该有(mid-i+1)个数可以和arr[j]组成降序对

代码:
    /**
     * 统计数组中降序对的数量
     * @param arr
     * @param left
     * @param right
     * @return
     */
    public static int getArrDesCount(int[] arr, int left, int right) {
        if (left >= right) {
            return 0;
        }
        int mid = left + ((right - left) >> 1);
        int leftSum = getArrDesCount(arr, left, mid);
        int rightSum = getArrDesCount(arr, mid + 1, right);
        int mergeSum = merge3(arr, left, mid, right);
        return leftSum + rightSum + mergeSum;
    }

    /**
     * 假定左边和右边分别已经排序好,将左右两边合并成有序的序列,
     * 并返回合并过程降序对的数量
     *
     * @param arr
     * @param left
     * @param mid
     * @param right
     */
    private static int merge3(int[] arr, int left, int mid, int right) {
        int sum = 0;
        int i = left, j = mid + 1, k = 0;
        int[] tmp = new int[right - left + 1];
        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j]) {
                tmp[k++] = arr[i++];
            } else {
                //关键是这一行!!!
                sum=sum+(mid-i+1);
                tmp[k++] = arr[j++];
            }
        }
        while (i <= mid) tmp[k++] = arr[i++];
        while (j <= right) tmp[k++] = arr[j++];
        for (int l = 0; l < tmp.length; l++) {
            arr[left + l] = tmp[l];
        }
        return sum;
    }

你可能感兴趣的:(算法,java)