题目描述
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例 1:
输入: [7,5,6,4]
输出: 5
题解思路
方法一:暴力解法(超时)
使用两层 for 循环枚举所有的数对,逐一判断是否构成逆序关系。
复杂度分析:
- 时间复杂度: O ( N 2 ) O(N^2) O(N2),这里 N 是数组的长度;
- 空间复杂度: O ( 1 ) O(1) O(1)
代码实现:
class Solution {
public:
int reversePairs(vector<int>& nums) {
int count = 0;
for(int i = 0; i < nums.size(); ++i){
for(int j = i+1; j < nums.size(); ++j){
if(nums[i] > nums[j]) ++ count;
}
}
return count;
}
};
方法二:归并排序
- 本题其实就是在对一个数组进行归并排序的基础上,增加了一个统计逆序对数目的障眼法,其实还是归并排序。
- 如果了解归并排序的话,就会想到我们可以用分治的思想,将给定的 nums 先一分为二,统计左半部分的逆序对数目,再统计右半部分的逆序对数目,以及统计跨越左半部分和右半部分的逆序对数目,然后将三者相加即可。
案例演示:假设 nums = [8,7,6,5,4,3,2,1]
- 将 nums 一分为二。则有 nums1 = [8,7,6,5],nums2 = [4,3,2,1]。那么 nums 的逆序对总数就等于 nums1 中的逆序对数目 + nums2 中的逆序对数目 + 跨越 nums1 和 nums2 的逆序对数目。接下来我们只演示 nums1 的后续步骤,因为 nums2 同理。
- 将 nums1 再一分为二。则有 nums3 = [8,7],nums4 = [6,5]。那么 nums1 的逆序对总数就等于 nums3 中的逆序对数目 + nums4 中的逆序对数目 + 跨越 nums3 和 nums4 的逆序对数目。接下来我们只演示 nums3 的后续步骤,因为 nums4 同理。
- 将 nums3 再一分为二。则有 nums5 = [8],nums6 = [7],而且 nums5 和 nums6 不能接着分了,因为它们分别只剩一个元素,不构成数对。这个时候,对于 nums5 和 nums6 来说,它们各自的逆序对数目都是 0。而对于 nums3 来说,它左半部分的逆序对数目等于 nums5 的逆序对数目(也就是 0);它右半部分的逆序对数目等于 nums6 的逆序对数目(也就是 0);它跨越左半部分和右半部分的逆序对数目是 1(因为 [8,7] 构成了一个逆序对),所以 nums3 的逆序对数目为 0 + 0 + 1 = 1。
- 而对于 nums1 来说,nums3 的逆序对数目等于 nums1 左半部分的逆序对数目,也就是我们刚刚求出来的 1。这时候我们还需要去求 nums1 右半部分的逆序对数目(也就是 nums4 的逆序对数目),再求跨越 nums3 和 nums4 的逆序对数目,然后再如法炮制算 nums2,然后才能得到最终 nums 的逆序对数目。
复杂度分析:
- 时间复杂度:同归并排序 O ( n l o g n ) O(nlogn) O(nlogn)。这里 n 是数组的长度;
- 空间复杂度:同归并排序 O ( n ) O(n) O(n),因为归并排序需要用到一个临时数组。
代码实现:
class Solution {
public:
int reversePairs(vector<int>& nums){
int sz = nums.size();
// 若不存在数对,直接 return 0
if (sz < 2) return 0;
vector<int> tmp(sz);
return mergeSort(nums, tmp, 0, sz-1);
}
private:
int mergeSort(vector<int>& nums, vector<int>& tmp, int l, int r){
if (l >= r) return 0; // 递归终止条件是只剩一个元素了(即不存在数对了)
int mid = l + (r - l) / 2; // 此算式等同于 (l + r) / 2,是为了避免溢出
int leftPairs = mergeSort(nums, tmp, l, mid); // 计算左半部分的逆序对
int rightPairs = mergeSort(nums, tmp, mid+1, r); // 计算右半部分的逆序对
if(nums[mid] <= nums[mid+1]){
// 即如果左右都已排好序,而且左边的最大值 <= 右边的最小值,那么就不存在跨越左边和右边的逆序对了
return leftPairs + rightPairs;
}
int crossPairs = mergeAndCount(nums, tmp, l, mid, r); // 计算跨越左边和右边的逆序对
return leftPairs + rightPairs + crossPairs;
}
// 本函数的前提条件是:左半部分和右半部分都是已经按升序排好序了的
int mergeAndCount(vector<int>& nums, vector<int>& tmp, int l, int mid, int r){
// 因为本函数是从左右部分都只有一个元素的情况开始运行的(自底向上),所以是可以保证前提条件的
for(int i = l; i <= r; ++i){
tmp[i] = nums[i]; // 先填充 tmp 辅助数组
}
int i = l, j = mid+1; // i 和 j 分别是左半部分和右半部分的起点
int count = 0; // count 用来统计逆序对数量
for(int k = l; k <= r; ++k){
if(i == mid + 1){
// 假如 i 已经越过左边的边界,直接填充右半部分进 nums;
nums[k] = tmp[j];
++j;
}
else if(j == r + 1){
// 假如 j 已经越过右边的边界,直接填充左半部分进 nums
nums[k] = tmp[i];
++i;
}
else if(tmp[i] <= tmp[j]){
// 假如左边小于等于右边,那就不是逆序对,不用修改 count
nums[k] = tmp[i];
++i;
}
else if(tmp[i] > tmp[j]) {
// 假如左边大于右边,是逆序对,count += 当前左边 [i, mid] 的所有元素
// 因为假如左边是 [7,8],右边是[5,6],然后 i 指向 7,j 指向 5
// 那么 5 和 7、8 都构成了逆序对,也就是此时有两对新的逆序对
// 所以可以总结出规律:count += mid - i + 1
nums[k] = nums[j];
count += mid - i + 1;
++j;
}
}
return count;
}
};
方法三:散化树状数组(未掌握)
预备知识
「树状数组」是一种可以动态维护序列前缀和的数据结构,它的功能是:
- 单点更新
update(i, v)
: 把序列 i 位置的数加上一个值 v,这题 v=1 - 区间查询
query(i)
: 查询序列 [1⋯i] 区间的区间和,即 i 位置的前缀和
修改和查询的时间代价都是 O(logn),其中 n 为需要维护前缀和的序列的长度。
思路
记题目给定的序列为 a,我们规定 ai 的取值集合为 a 的「值域」。我们用桶来表示值域中的每一个数,桶中记录这些数字出现的次数。假设 a={5,5,2,3,6},那么遍历这个序列得到的桶是这样的:
index -> 1 2 3 4 5 6 7 8 9
value -> 0 1 1 0 2 1 0 0 0
我们可以看出它第 i−1 位的前缀和表示「有多少个数比 i 小」。那么我们可以从后往前遍历序列 a,记当前遍历到的元素为 ai ,我们把 ai 对应的桶的值自增 1,把 i−1 位置的前缀和加入到答案中算贡献。为什么这么做是对的呢,因为我们在循环的过程中,我们把原序列分成了两部分,后半部部分已经遍历过(已入桶),前半部分是待遍历的(未入桶),那么我们求到的 i−1 位置的前缀和就是「已入桶」的元素中比 ai 大的元素的总和,而这些元素在原序列中排在 ai 的后面,但它们本应该排在 ai 的前面,这样就形成了逆序对。
我们显然可以用数组来实现这个桶,可问题是如果 ai 中有很大的元素,比如 109 ,我们就要开一个大小为 109 的桶,内存中是存不下的。这个桶数组中很多位置是 0,有效位置是稀疏的,我们要想一个办法让有效的位置全聚集到一起,减少无效位置的出现,这个时候我们就需要用到一个方法——离散化。
离散化一个序列的前提是我们只关心这个序列里面元素的相对大小,而不关心绝对大小(即只关心元素在序列中的排名);离散化的目的是让原来分布零散的值聚集到一起,减少空间浪费。那么如何获得元素排名呢,我们可以对原序列排序后去重,对于每一个 ai 通过二分查找的方式计算排名作为离散化之后的值。当然这里也可以不去重,不影响排名。
分析复杂度:
- 时间复杂度:离散化的过程中使用了时间代价为 O(nlogn) 的排序,单次二分的时间代价为 O(logn),一共有 n 次,总时间代价为 O(nlogn);循环执行 n 次,每次进行 O(logn) 的修改和 O(logn) 的查找,总时间代价为 O(nlogn)。故渐进时间复杂度为 O(nlogn)。
- 空间复杂度:树状数组需要使用长度为 n 的数组作为辅助空间,故渐进空间复杂度为 O(n)。
代码实现:
class BIT {
private:
vector<int> tree;
int n;
public:
BIT(int _n): n(_n), tree(_n + 1) {}
static int lowbit(int x) {
return x & (-x);
}
int query(int x) {
int ret = 0;
while (x) {
ret += tree[x];
x -= lowbit(x);
}
return ret;
}
void update(int x) {
while (x <= n) {
++tree[x];
x += lowbit(x);
}
}
};
class Solution {
public:
int reversePairs(vector<int>& nums) {
int n = nums.size();
vector<int> tmp = nums;
// 离散化
sort(tmp.begin(), tmp.end());
for (int& num: nums) {
num = lower_bound(tmp.begin(), tmp.end(), num) - tmp.begin() + 1;
}
// 树状数组统计逆序对
BIT bit(n);
int ans = 0;
for (int i = n - 1; i >= 0; --i) {
ans += bit.query(nums[i] - 1);
bit.update(nums[i]);
}
return ans;
}
};
如有帮助到您,可以多多点赞、评论鼓励哟~~~