归并排序解决重要翻转对
题目描述
给定一个数组 nums,如果满足:
text
i < j 且 nums[i] > 2 * nums[j]
那么 (i, j) 就是一对重要翻转对。
请返回数组中重要翻转对的数量。
例如:
text
输入:[1,3,2,3,1]
输出:2
满足条件的下标对为:
text
(1, 4):3 > 2 * 1
(3, 4):3 > 2 * 1
小记:今天这题的hard难题和前几天类似,但是没有了下标这种比较难操作的元素,只需要我在比较完二倍大小后再重新归并排序,简单了很多,今天过的也很充实,还不错
基本思路
最直接的方法是枚举所有下标对:
cpp
for (int i = 0; i < n; i++)
{
for (int j = i + 1; j < n; j++)
{
if (nums[i] > 2 * nums[j])
ret++;
}
}
这种方法的时间复杂度是 O(N^2)。
当数组长度达到 50000 时,效率无法接受。
这道题可以使用归并排序,将数组分成左右两个有序区间,然后统计跨越左右区间的重要翻转对。
核心思想是:
利用归并排序,把逐个比较转化为按段统计。
归并过程中的统计
假设左右两个区间已经按照降序排列:
text
左区间:[10, 6, 3]
右区间:[4, 2, 1]
对于左区间中的某个数字 nums[cur1],从右区间的 cur2 开始判断。
如果:
cpp
nums[cur1] > 2LL * nums[cur2]
由于右区间是降序排列的,所以 cur2 后面的所有数字都不大于当前数字。
因此,后面的所有数字也都满足条件,可以一次性统计:
cpp
ret += right - cur2 + 1;
如果当前条件不满足,就让 cur2 向后移动,尝试右区间中更小的数字。
为什么使用降序排列
本代码在归并时选择较大的数字:
cpp
if (nums[cur11] > nums[cur22])
{
tmp[i++] = nums[cur11++];
}
因此左右区间最终都是降序排列。
在降序数组中:
- 左区间指针向后移动,左边的数字变小;
- 右区间指针向后移动,右边的数字也变小。
当某个左边数字满足:
cpp
nums[cur1] > 2LL * nums[cur2]
右区间从 cur2 到 right 的数字全部满足条件,所以可以直接统计这一整段。
完整代码
cpp
class Solution
{
int ret = 0;
vector<int> tmp;
public:
int mergeSort(vector<int>& nums, int left, int right)
{
if (left >= right)
return 0;
int mid = (right - left) / 2 + left;
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
// 统计跨越左右区间的重要翻转对
int cur1 = left;
int cur2 = mid + 1;
while (cur1 <= mid && cur2 <= right)
{
if (nums[cur1] > 2LL * nums[cur2])
{
ret += right - cur2 + 1;
cur1++;
}
else
{
cur2++;
}
}
// 合并两个降序区间
int cur11 = left;
int cur22 = mid + 1;
int i = 0;
while (cur11 <= mid && cur22 <= right)
{
if (nums[cur11] > nums[cur22])
{
tmp[i++] = nums[cur11++];
}
else
{
tmp[i++] = nums[cur22++];
}
}
while (cur11 <= mid)
{
tmp[i++] = nums[cur11++];
}
while (cur22 <= right)
{
tmp[i++] = nums[cur22++];
}
// 将临时数组中的内容复制回原数组
for (int u = left; u <= right; u++)
{
nums[u] = tmp[u - left];
}
return ret;
}
int reversePairs(vector<int>& nums)
{
ret = 0;
int n = nums.size();
tmp.resize(n);
return mergeSort(nums, 0, n - 1);
}
};
代码中的关键细节
1. 使用 2LL 防止溢出
题目中的数字可能达到 32 位整数范围。
如果直接写:
cpp
nums[cur1] > 2 * nums[cur2]
那么乘法可能在 int 范围内发生溢出。
因此写成:
cpp
nums[cur1] > 2LL * nums[cur2]
让运算转换为 long long 类型,更安全。
2. 统计和归并使用不同的指针
统计重要翻转对时使用:
cpp
cur1、cur2
真正归并时使用:
cpp
cur11、cur22
这是因为统计过程会移动指针,但不能影响后面的正常归并。
3. 必须先统计,再归并
递归完成后,左右区间已经有序。
此时先统计跨区间的重要翻转对,再将两个区间合并。
如果先合并,就会破坏原来的左右区间边界,导致统计逻辑混乱。
复杂度分析
归并排序会将数组不断二分,并在每一层进行线性合并。
因此:
text
时间复杂度:O(N log N)
空间复杂度:O(N)
其中 O(N) 的额外空间主要来自临时数组 tmp。
总结
这道题不能直接使用普通的两重循环,否则时间复杂度为 O(N^2)。
归并排序的关键在于:
- 递归地将数组划分为左右两个区间;
- 保证左右区间有序;
- 利用有序性批量统计重要翻转对;
- 最后合并两个区间。
本题的核心不是单纯排序,而是利用排序后的区间关系,减少不必要的逐个比较。