逆序对之冒泡和归并排序

逆序对之冒泡和归并排序

  • 前言
  • 一、例题
    • 1、原题
      • A.题目
      • B.示例
  • 二、题解
    • 1、冒泡法
      • A.Code
      • B.Estimate
    • 2、基于分治的二路归并
      • A.Code
      • B.Estimate
  • 总结
  • 参考文献

前言

逆序对是一种常见的需求,比如行列式降阶法中确定正负号时需要逆序对的个数。

一、例题

1、原题

A.题目

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

B.示例

示例 1:
输入: [7,5,6,4]
输出: 5

二、题解

分析该问题的核心----逆序对,转化成升序排序时,逆序对的个数就是两数之间的交换次数。而这种两数之间按大小关系交换的排序算法就有冒泡法基于分治的二路归并法

1、冒泡法

Core:相邻两元素之间通过比较大小来交换位置。

A.Code

	/**
     * 剑指offer 51 数组中的逆序对
     *
     * @param nums
     * @return
     */
    public int reversePairs(int[] nums) {
        //core:将nums进行冒泡升序排序,每对逆序对都会交换一次,记录排序完交换的次数。
        int count = 0;
        int n = nums.length;
        for (int i = 0; i < n - 1; i++) {
            for (int j = 0; j < n - 1 - i; j++) {
                if (nums[j] > nums[j + 1]) {
                    int temp = nums[j];
                    nums[j] = nums[j + 1];
                    nums[j + 1] = temp;
                    count++;
                }
            }
        }
        return count;
    }

B.Estimate

1)Time -> O(n2),找每一个元素的逆序对需要O(n),一共有n个元素。
2)Space -> O(1),只用到常数个辅助空间。

2、基于分治的二路归并

思想:通过分治,将长度为n的无序表分为n个长度为1的有序表,然后相邻有序表之间两两归并得到一个新的有序表。反复归并,最终得到一个长度为n的有序表。
Core:
一个相对有序的表 + 相邻有序的表 + 辅助数组 = 一个相对有序的表。(利用了相对有序+辅助数组快速合并成一个相对有序的表,而减少重复比较时间。

A.Code

	//用于记录有多少此交换
    private int count = 0;
    //归并必须要用到的辅助数组
    private int[] temp = null;
	
    public int reversePairs(int[] nums) {
        //core:将nums进行归并排序,减少时间复杂度。
        int n = nums.length;
        temp = new int[n];
        MergeSort(nums, 0, n - 1);
        return count;
    }
	//归并的核心逻辑
    private void Merge(int[] nums, int low, int high) {
        //计算中间值,便于归并左右段。
        int mid = (high + low) / 2;
        //把数据复制到辅助数组。
        for (int i = low; i <= high; i++) {
            temp[i] = nums[i];
        }
        //开始归并两段数据
        int i, j, k;
        for (i = low, j = mid + 1, k = i; i <= mid && j <= high; k++) {
            if (temp[i] > temp[j]) {
                nums[k] = temp[j++];
            } else {
                //左边的值进入数组,可能已经有很多右边比较小的值已经进入数组了,所以存在逆序对。
                count += j - (mid + 1);
                nums[k] = temp[i++];
            }
        }
        //把剩下每比较的一段数据直接加在后面。
        while (i <= mid) {
            //同理,说明右边的值全部小于此刻i及i后面的值,即存在逆序对。
            nums[k++] = temp[i++];
            count += j - (mid + 1);
        }
        while (j <= high) nums[k++] = temp[j++];
    }
	//基于分治来归并
    private void MergeSort(int[] nums, int low, int high) {
        //只要还没有把数据分成只有一个的时候,就递归分组及归并。
        if (low < high) {
            int mid = (high + low) / 2;
            MergeSort(nums, low, mid);
            MergeSort(nums, mid + 1, high);
            //最后把low -- high的两段数组归并
            Merge(nums, low, high);
        }
    }

注:模拟归并

初始元素 一趟归并后 两趟归并后 三趟归并后
[65] [49] [97] [38] [88] [49 65] [38 97] [88] [38 49 65 97] [88] [38 49 65 88 97]

B.Estimate

1)Time -> O(nlog2n),一趟归并需要O(n),一共需要log2n趟归并。
2)Space -> O(n),需要O(n)大小的辅助数组才能发挥两相邻表的有序性。

总结

1)冒泡排序
2)基于分治的二路归并排序
3)转化问题,逆序对个数 -> 排序交换次数。

参考文献

[1] Leetcode 原题

你可能感兴趣的:(数据机构与算法,算法,冒泡排序,归并排序,逆序对,Java)