常见算法题---归并排序的扩展

前言

归并排序是一种建立在归并操作上的稳定性算法,是典型分治法的应用,把一个大的集合分成左右两个小的集合,然后分别排序两个小集合,最终再把已经排序好的集合合并成一个大集合。

先简单看一下归并排序

假设有如下一串数字 8 3 7 5 6 9 2 1

在这里插入图片描述

每次都找到一个中间点,然后拆分,直到不能拆分为止

在这里插入图片描述
一次合并过程结果
在这里插入图片描述

合并过程主要依靠两个指针分别指向左边的开始处和右边的开始处,如果左边指针指向的的数小于右边指针指向的数,则记录左边的数,并把左边的指针向后移动一位,如果左边指针指向的的数大于右边指针指向的数,则记录右边的数,并把右边的指针向后移动一位。

因为3小于5,所以左边指针移动一位指向8
在这里插入图片描述

因为8大于5,所以右边指针向后移动一位指向7
在这里插入图片描述

最终一次遍历完成,向上返回,继续合并遍历。
在这里插入图片描述

代码实现


import java.util.Arrays;

public class MergeSort {
    public static void main(String[] args) {
        int[] arr = {8, 3, 7, 5, 6, 9, 2, 1};
        process(arr, 0, arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }

    private static void process(int[] arr, int left, int right) {
        //不能拆分了
        if (left == right) {
            return;
        }
        /*
        找中间点left + ((right - left) >> 1) 就等于 left + ((right - left) / 2),等于 (left + right) / 2,
        这样写的目的是为了防止left + right溢出
         */
        int mid = left + ((right - left) >> 1);
        //相对左边继续拆分
        process(arr, left, mid);
        //相对右边继续拆分
        process(arr, mid + 1, right);
        //排序 合并
        merge(arr, left, mid, right);
    }

    private static void merge(int[] arr, int left, int mid, int right) {
        //辅助数组,记录排序好的结果
        int[] help = new int[right - left + 1];
        //辅助数组的下标
        int index = 0;
        //左指针下标
        int p1 = left;
        //右指针下标
        int p2 = mid + 1;
        /*
        当左、右指针都没有达到边界时
         */
        while (p1 <= mid && p2 <= right) {
            //谁小就赋值给help数组,并移动它的指针
            help[index++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
        }

        //一次性把余下的全部赋值给help数组
        while (p1 <= mid) {
            help[index++] = arr[p1++];
        }

        //一次性把余下的全部赋值给help数组
        while (p2 <= right) {
            help[index++] = arr[p2++];
        }

        //从left位置开始,赋值原数组
        for (int i = 0; i < help.length; i++) {
            arr[left + i] = help[i];
        }

    }
}

扩展

1、求数组小和的问题

数组中,每一个数左边比当前数小的数累加起来,最终得到的结果就叫做数组的小和。

示例

还是以8 3 7 5 6 9 2 1为例

8左边没有数,记:0
3左边一个8,比3大,记:0
7左边是8、3,其中比8小,比3大,所以记:1个3
5左边是8、3、7,其中比8小,比7小,比3大,所以记:1个3
6左边是8、3、7,5,其中比8小,比7小,比3大,比5大,所以记:1个3,1个5
9左边是8、3、7,5,6、其中比8大,比3大,比7大,比5大,比6大,所以记:1个8,1个3,1个7,1个5,1个6
2左边的数都比它大,记:0
1左边的数都比它大,记:0

最终结果就是
0+0+3+3+3+5+8+3+7+5+6+0+0 = 43

利用归并排序过程,可以转换为如果右边指针指向的数大于左边指针指向的数,则右边指针直到结束之处,所有数都比左边指针当前指向的数要大。

图解

在归并排序的过程中处理小和问题。

左指针指向8,右指针指向3,8大于3,所以不产生小和,可以记录1个0
在这里插入图片描述

同理7大于5,记录1个0
在这里插入图片描述

3小于5,所以要记录下来,又由于5和7是排好序的,所以满足3小于5,也必然满足3小于5之后的所有数,所以可以直接记录产生2个3。
在这里插入图片描述

处理右边的6、9同理,产生1个6
在这里插入图片描述

2 1不产生小和,可以记录1个0
在这里插入图片描述

6 9 和 1 2,同理也不产生小和,都记录0
在这里插入图片描述

现在比较 3 5 7 8和1 2 6 9,根据上面的分析,当右边指针指向1和2时不产生小和,指向6时,分别产生2个3和2个5
在这里插入图片描述

在这里插入图片描述

指向9时,分别产生1个7、1个8
在这里插入图片描述

最终一共记录了,3 3 6 3 3 5 5 7 8 = 43

代码实现


public class SmallSum {
    public static void main(String[] args) {
        int[] arr = {8, 3, 7, 5, 6, 9, 2, 1};
        int sum = process(arr, 0, arr.length - 1);
        System.out.println(sum);
    }

    private static int process(int[] arr, int left, int right) {
        if (left == right) {
            return 0;
        }

        int mid = left + ((right - left) >> 1);
        return process(arr, left, mid) +
                process(arr, mid + 1, right) +
                merge(arr, left, mid, right);

    }

    private static int merge(int[] arr, int left, int mid, int right) {
        int[] help = new int[right - left + 1];
        int index = 0;
        int p1 = left;
        int p2 = mid + 1;
        int res = 0;
        while (p1 <= mid && p2 <= right) {
            //在原归并排序的基础上,只加了这一行处理逻辑,即完成了数组小和的统计
            res += arr[p1] < arr[p2] ? (right - p2 + 1) * arr[p1] : 0;
            help[index++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
        }

        while (p1 <= mid) {
            help[index++] = arr[p1++];
        }

        while (p2 <= right) {
            help[index++] = arr[p2++];
        }

        for (int i = 0; i < help.length; i++) {
            arr[left + i] = help[i];
        }
        return res;

    }
}

2、数组中的逆序对(剑指offer中的第51题)

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

示例

输入:[7,5,6,4]
输出:5
分别为:(7,5)(7,6)(7,4)(5,4)(6,4)

思想与前一题类似,直接上代码,大家可以参考前一题的分析过程,自己分析一遍。


public class ReversePairs {

    public static void main(String[] args) {
        int[] arr = {8, 3, 7, 5, 6, 9, 2, 1};
        ReversePairs p = new ReversePairs();
        System.out.println(p.reversePairs(arr));
    }

    public int reversePairs(int[] nums) {
        if (nums == null || nums.length < 2) {
            return 0;
        }
        return process(nums, 0, nums.length - 1);
    }

    private static int process(int[] arr, int left, int right) {
        if (left == right) {
            return 0;
        }
        int mid = (left + right) / 2;
        return process(arr, left, mid) +
                process(arr, mid + 1, right) +
                merge(arr, left, right, mid);
    }

    private static int merge(int[] arr, int left, int right, int mid) {
        int index = 0;
        int[] help = new int[right - left + 1];
        int p1 = left;
        int p2 = mid + 1;
        int sum = 0;
        while (p1 <= mid && p2 <= right) {
            //如果左指针指向的数,大于右指针指向的数,则记录左指针到其边界的距离。
            sum += arr[p1] > arr[p2] ? (mid - p1 + 1) : 0;
            help[index++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
        }
        while (p1 <= mid) {
            help[index++] = arr[p1++];
        }

        while (p2 <= right) {
            help[index++] = arr[p2++];
        }

        for (int i = 0; i < help.length; i++) {
            arr[left + i] = help[i];
        }
        return sum;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码拉松

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值