前言
之前写过一次。。。
https://blog.csdn.net/weixin_44176696/article/details/105092613
因为写的太糟糕了,今天来个精简重制版。。。
题目描述
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例1:
输入: [7,5,6,4]
输出: 5
限制:
0 <= 数组长度 <= 50000
思路
求一个数组中逆序对,即求两个下标 (i, j),其中 i,j 的分布分为三种情况:
- i,j 位于数组左半边
- i,j 位于数组右半边
- 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);
}
};