一、题目
题目:
给定数组 nums,如果 i < j 且 nums[i] > 2*nums[j]就将 (i, j) 称作一个翻转对。
返回数组中翻转对数量。
示例 :
输入: [2,4,3,5,1]
输出: 3
二、分析
1) 暴力法
数组左侧有一个i下标,右侧有一个j下标,找出所有满足nums[i] > 2*nums[j]
的个数,最简单的思想就是暴力,遍历每一个i,向后一直寻找满足条件的j,但时间复杂度是O(n^2)
2) 归并分治
利用归并分治的思想,将数组从中间一分为二,假如左右侧范围数据都是有序的(从大到小),那么
总翻转对数 = 左侧范围翻转对数 + 右侧范围翻转对数+跨左右范围翻转对数
这里解释一下公式怎么来的:
① 将数组分为左[left, mid], 右[mid + 1, right]两部分
② 右侧[mid + 1, right]正常求出翻转对个数即可
③ 左侧[left, mid]是i的范围,但是j在i的右侧,所以j的范围为
[i+1, mid] + [mid + 1, right],
所以在求出j在[i + 1, mid]范围后,还需要求出跨左右范围[mid + 1, right]
这里将一下跨左右范围翻转对怎么求:
因为左右范围数据是有序的,假设左范围为[9, 8, 7, 6], 右范围为[5, 4, 3, 2],L指针指向左侧范围数据,R指针指向右侧范围数据,
当L=0时,只需要让R在右侧范围向右寻找第一个nums[L] > 2*nums[R]
的位置
那么可以知道R往后所有的数据都与L满足翻转对,因此L位置跨左右范围翻转对的数量为right-R + 1
当L=1时,R并不需要回退,R之前的位置nums[L] <= 2*nums[R]
,现在L后移之后nums[L]又变小了,所以R之前范围肯定不成立,
因此R只需要从当前位置一直向后找即可
④ 既然假设了范围要有序,那么要求出跨左右范围后,要对左右范围有序序列进行从大到小贪心合并
三、代码实现
class Solution {
// 归并分治的辅助数组
private static final int[] help = new int[50000];
public static int reversePairs(int[] nums) {
return reversePairs(nums, 0, nums.length - 1);
}
private static int reversePairs(int[] nums, int left, int right) {
// 范围内只有一个数据,翻转对个数一定为0,并且自然有序
if (left == right) {
return 0;
}
// 为了防止范围溢出,这里用left加上范围的一半,求出中间下标
int mid = left + ((right - left) / 2);
// 求出左侧范围翻转对数量、右侧范围翻转对数量
int leftCount = reversePairs(nums, left, mid);
int rightCount = reversePairs(nums, mid + 1, right);
// 求出跨左右范围翻转对数量
int allCount = leftCount + rightCount;
// 遍历左侧范围的每一个位置i,找到右侧满足翻转对的个数
for (int i = left, j = mid + 1; i <= mid; i++) {
// 从j往后找到第一个满足nums[i] > 2 * nums[j]条件的
while (j <= right && nums[i] <= 2L * nums[j]) {
j++;
}
// 记录这个i位置对应的翻转对个数
allCount += right - j + 1;
}
// 将序列按照从大到小进行合并
// 1.将两个序列合并到辅助数组help
for (int i = left, j = mid + 1, c = left; i <= mid || j <= right; c++) {
int lValue = i <= mid ? nums[i] : Integer.MIN_VALUE;
int rValue = j <= right ? nums[j] : Integer.MIN_VALUE;
if (lValue >= rValue) {
help[c] = lValue;
i++;
} else {
help[c] = rValue;
j++;
}
}
// 2.将合并好的序列拷贝至原数组
for (int i = left; i <= right ; i++) {
nums[i] = help[i];
}
// 返回翻斗数总个数
return allCount;
}
}