英文原文
Given an integer array nums
, return the number of reverse pairs in the array.
A reverse pair is a pair (i, j)
where 0 <= i < j < nums.length
and nums[i] > 2 * nums[j]
.
Example 1:
Input: nums = [1,3,2,3,1] Output: 2
Example 2:
Input: nums = [2,4,3,5,1] Output: 3
Constraints:
1 <= nums.length <= 5 * 104
-231 <= nums[i] <= 231 - 1
中文题目
给定一个数组 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位整数的表示范围内。
通过代码
高赞题解
思路
- 想要更快地统计翻转对,就要去掉一些不必要的判断!
找到一对翻转对 $i、j$,如果能因此 保证 一些 “pair” 也一定是翻转对,就很 nice。
nums[i] > 2*nums[j]
,那些比nums[i]
大的数,且在nums[j]
左边,就一定和nums[j]
组成翻转对,而且这些数的个数最好是好算的。
想到维护两个升序的序列,如下图,
nums[i]
在左序列,nums[j]
在右序列如果恰好满足:
nums[i] > 2*nums[j]
,则有:- 左序列中
nums[i]
右侧的数,必然能和nums[j]
组成翻转对,且 个数 好统计。
- 左序列中
想到归并排序——不断二分,直到不能二分,就形成了有序序列(单个元素即有序),然后不断合并两个有序序列。
我们为处于右序列的
nums[j]
,去找处于左序列的nums[i]
,找到了就统计一下个数。对于
nums[j]
,它先是在规模小的左序列中找目标数,下次,它被放在一个新的有序右序列里,它又会在规模更大的左序列中找目标数,找到了,就又统计一下随着递归结束,它会把左侧所有的数都考察了,找完了属于它的翻转对。
因为每次都二分,找到了,就有一段不用考察,时间复杂度是 $O(nlgn)$。
统计翻转对的时机
在得到左右有序序列之后,合并左右有序序列之前。
怎么统计呢?
i、j 指针分别扫描左右序列,为当前的 nums[j]
寻找 nums[i]
。
找到了,那 i 到左序列的末尾,都能和nums[j]
构成翻转对,通过索引之差统计个数。
i := start // i 指向左序列的开头
j := mid + 1 // j 指向右序列的开头
for i <= mid && j <= end { // i扫描左序列,j扫描右序列
if nums[i] > 2*nums[j] {
count += mid - i + 1 // i到mid,都能和j构成翻转对
j++ // 考察下一个j,为它找i
} else { // 当前i不满足,考察下一个i
i++
}
}
需要一个辅助数组
每次两个有序序列,合并成一个新的有序序列,保存在 temp 数组里。再更新到 nums 原数组。
归并排序就这么做的。
其实,去掉统计翻转对的部分,这就是一个单纯的归并排序。
代码
func reversePairs(nums []int) int {
if len(nums) == 0 { // 没有元素,没有翻转对
return 0
}
count := 0 // 翻转对个数
mergeSort(nums, &count, 0, len(nums)-1) // 归并的范围:0到末尾
return count
}
// 对当前的序列(start到end)进行归并排序
func mergeSort(nums []int, count *int, start, end int) {
if start == end { // 递归的出口:不能再二分了,返回
return
}
mid := start + (end-start)>>1 // 当前序列的中点索引
mergeSort(nums, count, start, mid) // 递归左序列
mergeSort(nums, count, mid+1, end) // 递归右序列
// 此时左右序列已升序,现在做:合并前的统计、以及合并
i := start // 左序列的开头
j := mid + 1 // 右序列的开头
for i <= mid && j <= end { // i j 都不越界
if nums[i] > 2*nums[j] {
*count += mid - i + 1 // i 到 mid,都ok
j++ // 考察下一个j,继续找 i
} else { // 当前i不满足,考察下一个i
i++
}
}
i = start
j = mid + 1 // 复原 i j 指针,因为现在要合并左右序列
temp := make([]int, end-start+1) // 辅助数组,存放合并排序的数
index := 0 // 从0开始
for i <= mid && j <= end { // 如果 i j 都没越界
if nums[i] < nums[j] { // nums[i]更小
temp[index] = nums[i] // 取nums[i],确定了temp[index]
index++ // 确定下一个
i++ // 考察下一个i,j不动
} else {
temp[index] = nums[j]
index++
j++
}
}
for i <= mid { // 如果 i 没越界,j越界了
temp[index] = nums[i] // i 和 i右边的都取过来
index++ // 确定下一个数
i++
}
for j <= end { // j 没越界,i越界了
temp[index] = nums[j] // j 和 j右边的都取过来
index++ // 确定下一个数
j++
}
k := 0
for i := start; i <= end; i++ { // 根据合并后的情况,更新nums
nums[i] = temp[k]
k++
}
}
var reversePairs = function (nums) {
if (nums.length == 0) {
return 0;
}
let count = 0;
function mergeSort(nums, start, end) {
if (start == end) {
return 0;
}
const mid = start + ((end - start) >> 1);
mergeSort(nums, start, mid);
mergeSort(nums, mid + 1, end);
let i = start;
let j = mid + 1;
while (i <= mid && j <= end) {
if (nums[i] > 2 * nums[j]) {
count += mid - i + 1;
j++;
} else {
i++;
}
}
i = start;
j = mid + 1;
const temp = new Array(end - start + 1);
let index = 0;
while (i <= mid && j <= end) {
if (nums[i] < nums[j]) {
temp[index] = nums[i];
index++;
i++;
} else {
temp[index] = nums[j];
index++;
j++;
}
}
while (i <= mid) {
temp[index] = nums[i];
index++;
i++;
}
while (j <= end) {
temp[index] = nums[j];
index++;
j++;
}
for (let i = start, k = 0; i <= end; i++, k++) {
nums[i] = temp[k];
}
}
mergeSort(nums, 0, nums.length - 1);
return count;
};
复盘总结
随着递归的出栈,归并排序会「并」两个有序的序列,形成新的有序序列,再和别的有序序列合并,我们在合并之前,在两个有序序列中寻找 i j pair,找到了,就统计一下,每次递归的「并」之前都找,就累加统计完了。
统计个数,只是归并排序的副操作,需要你对归并排序有比较好的理解。
下图是归并排序的递归树,对归并排序不熟的朋友可以看看:
谢谢收看,再见。如果觉得不错的话,帮我用赞顶上去。
最后修改于:2021-11-01
统计信息
通过次数 | 提交次数 | AC比率 |
---|---|---|
27249 | 78618 | 34.7% |
提交历史
提交时间 | 提交结果 | 执行时间 | 内存消耗 | 语言 |
---|
相似题目
题目 | 难度 |
---|---|
计算右侧小于当前元素的个数 | 困难 |
区间和的个数 | 困难 |