给定一个数组 nums
,如果 i < j
且 nums[i] > 2*nums[j]
我们就将 (i, j)
称作一个重要翻转对。
你需要返回给定数组中的重要翻转对的数量。
示例 1:
输入: [1,3,2,3,1] 输出: 2
示例 2:
输入: [2,4,3,5,1] 输出: 3
注意:
- 给定数组的长度不会超过
50000
。 - 输入数组中的所有数字都在32位整数的表示范围内。
暴力解法与优化思路
1. 暴力解法(不可行)
直接遍历所有 i < j
的组合,检查条件 nums[i] > 2 * nums[j]
。时间复杂度为 O(n^2)
,在 n=50000
时无法通过。
2. 归并排序的分治优化
归并排序的核心思想是 分而治之:
- 分:将数组递归拆分为左右子数组。
- 治:分别对左右子数组排序。
- 合:合并两个有序子数组。
在合并过程中,可以利用 子数组的有序性 高效统计逆序对。
为什么普通逆序对可以直接统计,而重要翻转对不行?
普通逆序对(nums[i] > nums[j]
)
- 合并阶段统计:当左右子数组为升序时,若
nums[i] > nums[j]
,则左子数组后续元素均大于nums[j]
,可立即统计剩余元素数量。 - 时间复杂度:
O(n log n)
。
重要翻转对(nums[i] > 2 * nums[j]
)
- 无法直接合并统计:即使左右子数组有序(如升序),也无法保证
nums[i] > 2 * nums[j]
的后续元素满足条件。 - 独立统计的必要性:必须 单独遍历有序子数组,利用有序性快速统计。
归并排序的优化实现
关键步骤
- 降序排序:保证左右子数组为降序排列。
- 独立统计阶段:合并前,遍历左右子数组,统计满足条件的对数。
- 合并阶段:仅合并,不统计。
代码实现
class Solution {
private int[] tmp;
private int count;
public int reversePairs(int[] nums) {
tmp = new int[nums.length];
mergeSort(nums, 0, nums.length - 1);
return count;
}
private void mergeSort(int[] nums, int left, int right) {
if (left >= right) return;
int mid = left + (right - left) / 2;
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
countPairs(nums, left, mid, right); // 独立统计阶段
merge(nums, left, mid, right); // 合并为降序数组
}
// 统计重要翻转对(左右子数组已降序)
private void countPairs(int[] nums, int left, int mid, int right) {
int i = left, j = mid + 1;
while (i <= mid) {
// 右子数组降序,找到第一个不满足 nums[j] >=nums[j]/2.0 的 j
while (j <= right && nums[j] >=nums[i]/2.0) {
j++;
}
count += right-j+1; // 统计满足条件的数量
i++;
}
}
// 降序合并两个子数组
private void merge(int[] nums, int left, int mid, int right) {
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right) {
if (nums[i] >= nums[j]) {
tmp[k++] = nums[i++];
} else {
tmp[k++] = nums[j++];
}
}
while (i <= mid) tmp[k++] = nums[i++];
while (j <= right) tmp[k++] = nums[j++];
System.arraycopy(tmp, 0, nums, left, right - left + 1);
}
}
代码解析
1. 独立统计阶段 countPairs
- 输入:已排序的左右子数组(降序)。
- 双指针遍历:
- 左指针
i
遍历左子数组。 - 右指针
j
从右子数组起始位置开始,找到第一个不满足nums[i] > 2 * nums[j]
的位置。 - 统计
j
之前的元素数量(均满足条件)。
- 左指针
2. 合并阶段 merge
- 降序合并:将左右子数组合并为一个降序数组,为后续操作提供有序性保证。
3. 复杂度分析
- 时间复杂度:
O(n log n)
,归并排序的递归深度为log n
,每层操作复杂度为O(n)
。 - 空间复杂度:
O(n)
,用于临时数组tmp
。
关键点总结
- 降序排序的必要性:保证右子数组元素从前往后递增,使得统计时能利用有序性快速定位。
- 独立统计阶段:必须在合并前单独统计,避免遗漏可能的翻转对。
- 防止整数溢出:比较时使用
long
类型(如2L * nums[j]
)。