今天看啥  ›  专栏  ›  兔尔曼

归并排序解决逆序对问题

兔尔曼  · 掘金  ·  · 2021-04-05 14:51
阅读 7

归并排序解决逆序对问题

归并排序与逆序对问题

归并排序

    
		private void sort(int[] nums) {
        int[] temp = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
          temp[i] = nums[i];
        }
        sort(nums, temp, 0, nums.length-1);
    }

		private void sort(int[] nums, int[] temp, int left, int right) {
        if (left >= right) return;

        int mid = left + (right - left) / 2;
        sort(nums, temp, left, mid);
        sort(nums, temp, mid + 1, right);

        merge(nums, temp, left, mid, right);
    }
		
		// 合并两个有序数组,左数组的下标[left, mid],右数组的下标[mid+1, right]
    private void merge(int[] nums, int[] temp, int left, int mid, int right) {
        // temp数组保存nums合并前从left到right的值,即保存左、右两个子数组,用作赋值和值比较
        for (int i = left; i<= right; i++) {
            temp[i] = nums[i];
        }

        // i,j分别是左右两个有序的子数组待插入nums的下标
        int i = left;
        int j = mid+1;

        for (int k = left; k <= right; k++) {
            if (i > mid) {
                // 左子数组已经全部插入nums,剩下的只有右子数组
                nums[k] = temp[j++];
            } else if (j > right) {
                // 右子数组已经全部插入nums,剩下的只有左子数组
                nums[k] = temp[i++];
            } else if (temp[i] < temp[j]) {
                // 左边的值更小,插入左边的i
                nums[k] = temp[i++];
            } else if (temp[i] == temp[j]){
                // 左右的值相等,插入i和j均可,这里插入i
                nums[k] = temp[i++];
            } else {
                // 右边的值更小,插入右边的j
                nums[k] = temp[j++];
            }
        }
    }
复制代码

逆序对问题

leetcode:剑指 Offer 51. 数组中的逆序对 - 力扣(LeetCode) (leetcode-cn.com)

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

输入: [7,5,6,4]
输出: 5
复制代码

有两种解法:

解法1考虑的是,j插入时,根据左数组比j大的值来更新逆序对。

解法2考虑的是,i插入时,根据右数组比i小的值来更新逆序对。

解法1: 注意到在归并排序合并的阶段,如果temp[i] > temp[j] ,则出现了逆序对,此时从[i,mid]的所有元素,都比右数组中剩下的元素要大,逆序对的数量可以增加mid-i+1个。如果右子数组已经全部合并,即temp[j]不存在,逆序对的数量+0。

对上述归并排序的代码稍作修改:

    
		int ans = 0; // 逆序对的个数

		private int reversePairs(int[] nums) {
        int[] temp = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
          temp[i] = nums[i];
        }
        sort(nums, temp, 0, nums.length-1);
        return ans;
    }

		private void sort(int[] nums, int[] temp, int left, int right) {
        if (left >= right) return;

        int mid = left + (right - left) / 2;
        sort(nums, temp, left, mid);
        sort(nums, temp, mid + 1, right);

        merge(nums, temp, left, mid, right);
    }

    private void merge(int[] nums, int[] temp, int left, int mid, int right) {
        for (int i = left; i<= right; i++) {
            temp[i] = nums[i];
        }

        int i = left;
        int j = mid+1;

        for (int k = left; k <= right; k++) {
            if (i > mid) {
                nums[k] = temp[j++];
                ans += 0; // i后面没有元素了,逆序对+0
            } else if (j > right) {
                nums[k] = temp[i++];
            } else if (temp[i] < temp[j]) {
                nums[k] = temp[i++];
            } else if (temp[i] == temp[j]){
                nums[k] = temp[i++];
            } else {
                ans += (mid - i + 1); // 增加逆序对
                nums[k] = temp[j++];
            }
        }
    }
复制代码

解法2:

解法1考虑的是,j插入时,根据左数组比j大的值来更新逆序对。

解法2考虑的是,i插入时,根据右数组比i小的值来更新逆序对。

当i、j对应的值相等时,先插入了i,所以当temp[i]==temp[j]时,比i小的j一定已经插入了,同理,当temp[i] < temp[j]时,比i小的j也一定已经被插入了,所以对于temp[i]<=temp[j],j的左边都比当前i要小,逆序对的数量等于j左边的元素的数量: j-mid-1

如果j已经全部被插入:此时需要增加逆序对的数量为:right-mid

        for (int k = left; k <= right; k++) {
            if (i > mid) {
                nums[k] = temp[j++];
            } else if (j > right) {
                nums[k] = temp[i++];
              	ans += right-mid;
            } else if (temp[i] <= temp[j]) {
                nums[k] = temp[i++];
              	ans += j-mid-1;
            } else {
                nums[k] = temp[j++];
            }
        }
复制代码

计算数组中右侧比当前元素小的个数

leetcode:315. 计算右侧小于当前元素的个数 - 力扣(LeetCode) (leetcode-cn.com)

给定一个整数数组 nums,按要求返回一个新数组 counts。数组 counts 有该性质: counts[i] 的值是  nums[i] 右侧小于 nums[i] 的元素的数量。


示例:
输入:nums = [5,2,6,1]
输出:[2,1,1,0] 
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素
复制代码

归并排序分成了左右两个有序数组,左数组中一定不存在右侧的比当前元素小,本题就变成了,求归并排序合并时,左数组插入时,右数组当前元素小的个数。利用逆序对的解法2,求右数组比当前元素小的个数。

如果本题换成:数组中左侧比当前元素大的个数,那么就需要用到逆序对的解法1。

同时,本题还考察了知识点:索引数组。

本题要求的是每一个元素,右边比他小的元素的个数,所以归并排序的过程中,关心的是每一个下标,需要对下标构建一个索引数组,每次合并时,比较的是nums的值,更新的是index的下标,tmp也不再保存值而是保存下标。(归并排序时,比较的是值,更新的是值)。

int n = nums.length;
int[] index = new int[n]; // 构建一个索引数组,index[i]表示,数组nums下标i,在排序后的位置为index[i]
int[] tmp = new int[n];

for (int i = 0; i < nums.length; i++) {
  index[i] = i;
  tmp[i] = i;
}
复制代码

那么,当更新时:

    int[] count = new int[n]; // count[i] : nums[i] 右边比nums[i]小的元素个数
		private void merge(int left, int mid, int right) {
        for (int i = left; i <= right; i++) {
            tmp[i] = index[i];
        }

        int i = left;
        int j = mid+1;
       // 比较的是值,更新的是index中保存的下标
        for (int k = left; k <= right; k++) {
            if (i > mid) {
                index[k] = tmp[j++];
            } else if (j > right) {
                index[k] = tmp[i++];
                count[index[k]] += right-mid; // 已合并的j都比当前合并的元素i小
            } else if (nums[tmp[i]] > nums[tmp[j]]) {
                index[k] = tmp[j++];
            } else {
                index[k] = tmp[i++];
                count[index[k]] += (j-mid-1); // 已合并的j都比当前合并的元素i小
            }
        }
    }
复制代码



原文地址:访问原文地址
快照地址: 访问文章快照