LeetCode 493. 翻转对
题目描述
✅ 493. 翻转对

题意分析
统计原数组中满足
i < j且nums[i] > 2 * nums[j]的下标对数量。每一对按两个位置区分,元素可以重复,也可以为负数。这里比较的是右侧值的两倍,不能当成普通逆序对,也不能仅根据两个值是否相等决定是否计数。直接枚举所有下标对需要平方时间,可以在归并排序过程中利用两个有序半区批量统计。
解法:归并排序计数
核心思路
[!blue]
把当前区间按下标分成左右两半,所有答案恰好分为三类:两个位置都在左半、都在右半、一个在左半而另一个在右半。递归分别统计前两类,并将各自区间排好序,本层再统计跨半区的配对,三类之间不会重复。
对跨区间配对,左半的所有元素原本都位于右半元素之前,所以
i < j已由原来的区间归属保证。两半各自在内部排序不会改变这种归属,只需比较左值是否严格大于右值的两倍。两半排序后,固定一个左值时,符合条件的右值一定构成右半的一个前缀:右值从小到大排列,乘以正数二仍然保持这个顺序。用
j指向第一个不满足条件的位置,或右半末尾之后的位置,本次贡献就是j - (mid + 1)。按从小到大枚举左值时,之前符合条件的右值仍然符合,满足条件的前缀只可能变长,
j不需要回退。每个左元素都要加上整个有效前缀的长度,而不是只加本轮新增长度,因为同一个右元素可以分别与多个左侧位置组成不同下标对。跨区间统计完成后,再用普通归并把两半按数值大小合为有序区间,交给上一层使用。如果提前将两半混合,就会失去原区间的左右归属;归并比较也仍用普通大小关系,不能改成翻转对的两倍条件。
比较时在乘法前转换为 64 位,避免右值乘二先在 32 位中溢出。负数同样满足上述单调关系,两个相等的负值甚至也可能构成翻转对,因此必须始终按原不等式判断。
解题步骤
- 空区间或单节点区间返回零。
- 递归处理左右半区,累加各自的计数,此时两半分别有序。
- 令右指针从
mid + 1开始,依次枚举左半元素,持续推进仍满足两倍条件的右指针。- 对每个左元素累加整个有效右前缀长度,右指针不重置。
- 用共享临时数组完成普通有序归并,再复制回当前区间并返回计数。
代码实现
class Solution {
public int reversePairs(int[] nums) {
return mergeSort(nums, new int[nums.length], 0, nums.length - 1);
}
private int mergeSort(int[] nums, int[] temp, int left, int right) {
if (left >= right) {
return 0;
}
int mid = left + (right - left) / 2;
int count = mergeSort(nums, temp, left, mid) + mergeSort(nums, temp, mid + 1, right);
int j = mid + 1;
// 右指针不随左指针重置;满足条件的右侧前缀只会扩大。
for (int i = left; i <= mid; i++) {
// 在乘法前使用 64 位整数,严格大于才计数。
while (j <= right && (long) nums[i] > 2L * nums[j]) {
j++;
}
// 当前左元素对应整个有效前缀,不能只加本轮新增长度。
count += j - (mid + 1);
}
// 统计结束后再归并,供上一层继续利用有序性。
merge(nums, temp, left, mid, right);
return count;
}
private void merge(int[] nums, int[] temp, int left, int mid, int right) {
int i = left;
int j = mid + 1;
int k = left;
while (i <= mid && j <= right) {
if (nums[i] <= nums[j]) {
temp[k++] = nums[i++];
} else {
temp[k++] = nums[j++];
}
}
while (i <= mid) {
temp[k++] = nums[i++];
}
while (j <= right) {
temp[k++] = nums[j++];
}
System.arraycopy(temp, left, nums, left, right - left + 1);
}
}
func reversePairs(nums []int) int {
temp := make([]int, len(nums))
return mergeSort(nums, temp, 0, len(nums)-1)
}
func mergeSort(nums []int, temp []int, left int, right int) int {
if left >= right {
return 0
}
mid := left + (right-left)/2
count := mergeSort(nums, temp, left, mid) + mergeSort(nums, temp, mid+1, right)
j := mid + 1
// 右指针不随左指针重置;满足条件的右侧前缀只会扩大。
for i := left; i <= mid; i++ {
// 在乘法前使用 64 位整数,严格大于才计数。
for j <= right && int64(nums[i]) > 2*int64(nums[j]) {
j++
}
// 当前左元素对应整个有效前缀,不能只加本轮新增长度。
count += j - (mid + 1)
}
// 统计结束后再归并,供上一层继续利用有序性。
merge(nums, temp, left, mid, right)
return count
}
func merge(nums []int, temp []int, left int, mid int, right int) {
i, j, k := left, mid+1, left
for i <= mid && j <= right {
if nums[i] <= nums[j] {
temp[k] = nums[i]
i++
} else {
temp[k] = nums[j]
j++
}
k++
}
for i <= mid {
temp[k] = nums[i]
i++
k++
}
for j <= right {
temp[k] = nums[j]
j++
k++
}
copy(nums[left:right+1], temp[left:right+1])
}
复杂度分析
- 时间复杂度:$O(n\log(n + 1))$,每层跨区间统计中两个指针只向前移动,随后归并也是线性操作,总共对数层。
- 空间复杂度:$O(n)$,临时数组供所有递归区间复用,递归栈另占 $O(\log n)$。本实现结束后原数组会被排序。
关键点总结
[!green]
- 固定顺序:递归、统计、归并。
- 指针不回退:右半有效前缀单调扩大,避免重复扫描。
- 计数与比较分开看:n 最多 50000 时,配对数最多 1,249,975,000,可用 int;两倍比较必须提升位宽。
易错点总结
[!yellow]
- 按普通逆序对计数:这里要求严格大于右值的两倍,不能只判断左值较大。
- 两倍乘法之后再转换类型:溢出已发生时无法恢复,应先用宽整数运算。
- 直接排除所有相等值:相等的负数仍可能满足两倍条件,应按数值不等式比较。
- 每次重新扫描右半:会失去单调指针带来的线性统计成本。
- 只加右指针本轮推进量:会漏掉当前左值与先前已经满足条件的右值之间的配对。
- 统计之前先归并混合两半:原左右区间的下标先后关系会被破坏,无法直接计跨区间对。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 315. 计算右侧小于当前元素的个数 | 困难 | 同样在归并时计跨区间数对,本题比较左值与右值两倍,不能直接按普通逆序关系累计。 |
| 327. 区间和的个数 | 困难 | 同样用排序后的两侧窗口批量统计满足数值范围的组合。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!