leetcode 剑指offer 51:数组中的逆序对 -- 分治法思路与代码

前言

之前写过一次。。。

https://blog.csdn.net/weixin_44176696/article/details/105092613

因为写的太糟糕了,今天来个精简重制版。。。

题目描述

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

示例1:

输入: [7,5,6,4]
输出: 5

限制:

0 <= 数组长度 <= 50000

思路

求一个数组中逆序对,即求两个下标 (i, j),其中 i,j 的分布分为三种情况:

  1. i,j 位于数组左半边
  2. i,j 位于数组右半边
  3. i 位于数组左半边,j 位于数组右半边

在这里插入图片描述

利用分治法求情况 1 和 2 非常简单,通过递归即可实现。难点主要在于情况3的求解,即 i,j 位于数组两半边的情况。如果使用暴力枚举,那么分治法的合并代价仍然是 O(n^2)

但是可以利用归并排序的特性,即分治之后进行归并排序,那么数组左右是有序递增的。我们只关心 i,j 位于数组两半边的情况,那么他们的顺序就不重要了。

我们枚举右边的点 j,对于每一个点 j,都在左半边找到第一个 i 下标使得 nums[i]<=nums[j],这意味着 [i+1, mid] 区间的点,都满足 nums[i]>nums[j],于是对于右端点 j,有 mid-i 个逆序对:

如图:黄色框图中的点 x 满足 nums[x]>nums[j]

在这里插入图片描述

此外,枚举 j 的顺序应该按照从右往左的顺序,因为左右半边都是单调递增,这么做是因为:

  • 第 n 次枚举 j 时,此时右端点=j,有 nums[i]>nums[j]
  • 第 n+1 次枚举 j 时,此时右端点=j-1,因为 nums[j-1]<nums[j] 故有 nums[i]>nums[j-1] 仍然成立

即复用上一次的结果,减少左端点 i 的重复移动。因为左端点 i 从 mid 开始枚举,最多移动到 l-1 位置,即移动次数不超过 mid-l,所以合并代价仍然是O(n)

代码

class Solution {
    
    
public:
    vector<int> nums;
    int divide(int l, int r)
    {
    
    
        // 边界 -- 长度为1的区间没有逆序对
        if(l<0 || r>=nums.size() || l==r) return 0;

        int ans=0, mid=(l+r)/2;
        ans += divide(l, mid);      // 逆序对两点都在左半边
        ans += divide(mid+1, r);    // 逆序对两点都在右半边

        // 逆序对两点一个在左半边一个在右半边
        int i = mid;
        for(int j=r; j>=mid+1; j--)
        {
    
    
            while(i>=l)
            {
    
    
                if(nums[i]<=nums[j]) break; // 找第一个 nums[i]<=nums[j] 
                i--;
            }
            ans += mid-i;   // [i+1, mid] 区间的数都大于 nums[j]
        }

        // 归并排序
        auto b = nums.begin();
        inplace_merge(b+l, b+mid+1, b+r+1);
        
        return ans;
    }
    int reversePairs(vector<int>& nums) {
    
    
        if(nums.size()==0) return 0;
        this->nums = nums;
        return divide(0, nums.size()-1);
    }
};

猜你喜欢

转载自blog.csdn.net/weixin_44176696/article/details/109556032
今日推荐