归并排序

归并排序

1. 基本思想

归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

在归并排序中主要注重治(conquer)的阶段,在将数组不断二分为小的数组,再将二分后的小数组再通过归并程序使得其变得有序。通过递归方法得出算法复杂度递推公式为:

\[T(n) = 2T(\frac{n}{2})+O(n) \]

通过master公式可得,归并排序的时间复杂度为\(O(nlogn)\),额外空间复杂度为\(O(n)\),实现可以做到稳定性。

2. java代码实现

import java.util.Arrays;

/**
 * @author: syx
 * @date: 2021/6/4 11:03
 * @version: v1.0
 */
public class MergeSort {
    public static void main(String[] args) {
        int[] nums = new int[]{5, 4, 3, 2, 5, 6, 6};
        MergeSort(nums);
        System.out.println(Arrays.toString(nums));
    }

    //归并排序的主要入口
    public static void MergeSort(int[] nums) {
        //当待排序数组为空,或者只有一个值时,直接返回,不排序。
        if (nums == null || nums.length < 2) {
            return;
        }
        //否则开始归并排序
        MergeSort(nums, 0, nums.length - 1);
    }

    /**
     * 归并排序的主体
     *
     * @param nums
     * @param start
     * @param end
     */
    public static void MergeSort(int[] nums, int start, int end) {
        //当排序的数组只有一个元素则直接返回
        if (start == end) {
            return;
        }
        //取中值
        int mid = start + ((end - start) >> 1);
        //将数组的两半部分分别归并排序后,再进行归并操作
        MergeSort(nums, start, mid);
        MergeSort(nums, mid + 1, end);
        Merge(nums, start, mid, end);
    }

    /**
     * 归并算法
     *
     * @param nums
     * @param start
     * @param mid
     * @param end
     */
    public static void Merge(int[] nums, int start, int mid, int end) {
        int[] help = new int[end - start + 1];// 定义一个辅助空间,空间的大小与需要归并的长度相同
        int i = 0;//辅助空间的指针
        int p1 = start;//待排序数组中左半部分的指针
        int p2 = mid + 1;//待排序数组中右半部分的指针
        while (p1 <= mid && p2 <= end) {//当两个指针都没有过界
            // 将两个指针指向的数字比较,较小的数字存入辅助数组中,指向小的数字的指针右移一位
            if (nums[p1] < nums[p2]) {
                help[i++] = nums[p1];
                p1++;
            } else {
                help[i++] = nums[p2];
                p2++;
            }
        }
        //进行完上一个循环后,至少由一个指针过界,将另一个没有过界的指针继续移动,将余下的数字放入辅助数组中
        while (p1 <= mid) {
            help[i++] = nums[p1];
            p1++;
        }
        while (p2 <= end) {
            help[i++] = nums[p2];
            p2++;
        }
        // 再将辅助数组中的元素,按位置放回原数组中。
        for (int j = 0; j < help.length; j++) {
            nums[start + j] = help[j];
        }
    }
}

2. 归并排序的相关扩展

1. 小和问题

描述

在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。

例子

[1,3,4,2,5]
1左边比1小的数:没有
3左边比3小的数:1
4左边比4小的数:1,3
2左边比2小的数:1
5左边比5小的数:1,3,4,2
所以小和为1+1+3+1+1+3+4+2=16

方法与代码

可以通过在归并排序的代码上修改来解出这道题,在归并排序时,每次归并时,因为左右两边的数组都是有序的,在查出左边的数(下标为\(m\))比右边的数(下标为\(n\))小的时候,可以通过下标计算来确定左边的数(下标为\(m\))比右边的\(k\)个数小。

\[k=end-n+1 \]

在归并排序使得局部有序的过程中就可以求得小和的局部解\(k*nums[m]\),将部分解相加就可以得到数组的小和。

代码如下:

/**
 * @author: syx
 * @date: 2021/6/8 11:33
 * @version: v1.0
 */
public class minSum {
    static int minum = 0;
    public static void main(String[] args) {
        int[] nums = new int[]{1,3,4,2,5};
        MergeSort(nums);
        System.out.println(Arrays.toString(nums));
        System.out.println(minum);
    }

    //归并排序的主要入口
    public static void MergeSort(int[] nums) {
        //当待排序数组为空,或者只有一个值时,直接返回,不排序。
        if (nums == null || nums.length < 2) {
            return;
        }
        //否则开始归并排序
        MergeSort(nums, 0, nums.length - 1);
    }

    /**
     * 归并排序的主体
     *
     * @param nums
     * @param start
     * @param end
     */
    public static void MergeSort(int[] nums, int start, int end) {
        //当排序的数组只有一个元素则直接返回
        if (start == end) {
            return;
        }
        //取中值
        int mid = start + ((end - start) >> 1);
        //将数组的两半部分分别归并排序后,再进行归并操作
        MergeSort(nums, start, mid);
        MergeSort(nums, mid + 1, end);
        Merge(nums, start, mid, end);
    }

    /**
     * 归并算法
     *
     * @param nums
     * @param start
     * @param mid
     * @param end
     */
    public static void Merge(int[] nums, int start, int mid, int end) {
        int[] help = new int[end - start + 1];// 定义一个辅助空间,空间的大小与需要归并的长度相同
        int i = 0;//辅助空间的指针
        int p1 = start;//待排序数组中左半部分的指针
        int p2 = mid + 1;//待排序数组中右半部分的指针
        while (p1 <= mid && p2 <= end) {//当两个指针都没有过界
            // 将两个指针指向的数字比较,较小的数字存入辅助数组中,指向小的数字的指针右移一位
            if (nums[p1] < nums[p2]) {
                help[i++] = nums[p1];
                minum += nums[p1]*(end-p2+1);// 只修改了一行,就可以实现小和计算
                p1++;
            } else {
                help[i++] = nums[p2];
                p2++;
            }
        }
        //进行完上一个循环后,至少由一个指针过界,将另一个没有过界的指针继续移动,将余下的数字放入辅助数组中
        while (p1 <= mid) {
            help[i++] = nums[p1];
            p1++;
        }
        while (p2 <= end) {
            help[i++] = nums[p2];
            p2++;
        }
        // 再将辅助数组中的元素,按位置放回原数组中。
        for (int j = 0; j < help.length; j++) {
            nums[start + j] = help[j];
        }
    }
}

3. 逆序对问题

描述

\(A\)为一个有 n 个数字的有序集 (n>1),其中所有数字各不相同。

如果存在正整数 \(i<j\) 使得 \(i<j\) 而且 \(A[i]>A[j]\),则 \(A[i],A[j]\) 这一个有序对称为\(A\)的一个逆序对,也称作逆序。逆序对的数量称作逆序数。

例子

[1,3,4,2,5,4]
逆序对有 <4,2>,<5,4>

方法与代码

逆序对与小和问题类似,当左边的数大于右边的数的时候,就产生了逆序对。同样的,可以在归并中将逆序对来计数,对于左半部分中的一个数,如果这个数大于右半部分的数时,由于左右两边数组都是有序的,所以可以计算出左边有多少个数字大于右边的这个数。

package syx.com;

import java.util.Arrays;

/**
 * @author: syx
 * @date: 2021/6/8 11:53
 * @version: v1.0
 */
public class ReversePair {
    static int count = 0;
    public static void main(String[] args) {
        int[] nums = new int[]{3,2,1};
        MergeSort(nums);
        System.out.println(Arrays.toString(nums));
        System.out.println(count);
    }

    //归并排序的主要入口
    public static void MergeSort(int[] nums) {
        //当待排序数组为空,或者只有一个值时,直接返回,不排序。
        if (nums == null || nums.length < 2) {
            return;
        }
        //否则开始归并排序
        MergeSort(nums, 0, nums.length - 1);
    }

    /**
     * 归并排序的主体
     *
     * @param nums
     * @param start
     * @param end
     */
    public static void MergeSort(int[] nums, int start, int end) {
        //当排序的数组只有一个元素则直接返回
        if (start == end) {
            return;
        }
        //取中值
        int mid = start + ((end - start) >> 1);
        //将数组的两半部分分别归并排序后,再进行归并操作
        MergeSort(nums, start, mid);
        MergeSort(nums, mid + 1, end);
        Merge(nums, start, mid, end);
    }

    /**
     * 归并算法
     *
     * @param nums
     * @param start
     * @param mid
     * @param end
     */
    public static void Merge(int[] nums, int start, int mid, int end) {
        int[] help = new int[end - start + 1];// 定义一个辅助空间,空间的大小与需要归并的长度相同
        int i = 0;//辅助空间的指针
        int p1 = start;//待排序数组中左半部分的指针
        int p2 = mid + 1;//待排序数组中右半部分的指针
        while (p1 <= mid && p2 <= end) {//当两个指针都没有过界
            // 将两个指针指向的数字比较,较小的数字存入辅助数组中,指向小的数字的指针右移一位
            if (nums[p1] <= nums[p2]) {
                help[i++] = nums[p1];
                p1++;
            } else {
                help[i++] = nums[p2];
                count += mid - p1 +1;
                p2++;
            }
        }
        //进行完上一个循环后,至少由一个指针过界,将另一个没有过界的指针继续移动,将余下的数字放入辅助数组中
        while (p1 <= mid) {
            help[i++] = nums[p1];
            p1++;
        }
        while (p2 <= end) {
            help[i++] = nums[p2];
            p2++;
        }
        // 再将辅助数组中的元素,按位置放回原数组中。
        for (int j = 0; j < help.length; j++) {
            nums[start + j] = help[j];
        }
    }
}

4. 总结

在归并排序的扩展题目中,小和问题的核心是计算出右边有多少个数比一个数大,逆序对问题是计算左边有多少个数比一个数大,利用归并算法的框架可以快速的将这些问题计算出来。在排序的分治过程中,对归并部分的比较做一些改动,根据比较结果做一个计数,利用下标来计算个数多少从而得出问题的解。

posted @ 2021-06-08 14:44  锤子布  阅读(67)  评论(0编辑  收藏  举报