1. 题目来源
链接:数组中的逆序对
来源:LeetCode——《剑指-Offer》专项
2. 题目说明
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例 1:
输入: [7,5,6,4]
输出: 5
限制:
0 <= 数组长度 <= 50000
3. 题目解析
方法一:TLE+暴力+常规解法
暴力解法首先试试,虽然肯定 AC
不了。顺序遍历数组元素,拿到一位数组元素后向后遍历剩余的所有数组元素,比较大小,计数即可。时间复杂度
O
(
n
2
)
O(n^2)
O(n2),空间复杂度
O
(
1
)
O(1)
O(1)。
参见代码如下:
// TLE
// 35 / 139 个通过测试用例
class Solution {
public:
int reversePairs(vector<int>& nums) {
int cnt = 0, tmp = 0;;
for (int i = 0; i < nums.size(); ++i) {
tmp = nums[i];
for (int j = i; j < nums.size(); ++j) {
if (tmp > nums[j]) ++cnt;
}
}
return cnt;
}
};
方法二:思维+归并排序+巧妙解法
来自《剑指-Offer》的解法,利用 归并排序 的思想解题。以数组 {7, 5, 6, 4}
为例来分析统计逆序对的过程。每次扫描到一个数字的时候,我们不能拿它和后面的每一个数字作比较,否则时间复杂度就是
O
(
n
2
)
O(n^2)
O(n2),因此我们可以考虑先比较两个相邻的数字。
上图中省略了最后一步,即复制第二个子数组最后剩余的 4 到辅助数组中。
- ( a )
P1
指向的数字大于P2
指向的数字,表明数组中存在逆序对。P2
指向的数字是第二个子数组的第二个数字,因此第二个子数组中有两个
数字比 7 小。把逆序对数目加 2,并把 7 复制到辅助数组,向前移动P1
和P3
- ( b )
P1
指向的数字小于P2
指向的数字,没有逆序对。把P2
指向的数字复制到辅助数组,并向前移动P2
和P3
- ( c )
P1
指向的数字大于P2
指向的数字,因此存在逆序对。由于P2
指向的数字是第二个子数组的第一个数字,子数组中只有一个数字比 5小。把逆序对数目加 1,并把 5 复制到辅助
数组,向前移动P1
和P3
。
接下来我们统计两个长度为 2 的子数组之间的逆序对。在第二张图片中细分第一张图片的步骤 (d)
的合并子数组及统计逆序对的过程:
- 我们先用两个指针分别指向两个子数组的末尾,并每次比较两个指针指向的数字。
- 如果第一个子数组中的数字大于第二个子数组中的数字,则构成逆序对,并且逆序对的数目等于第二个子数组中剩余数字的个数(如第二张图
(a)
和第二张图(c)
所示) - 如果第一个数组中的数字小于或等于第二个数组中的数字,则不构成逆序对(如第二张图
(b)
所示)
- 如果第一个子数组中的数字大于第二个子数组中的数字,则构成逆序对,并且逆序对的数目等于第二个子数组中剩余数字的个数(如第二张图
- 每一次比较的时候,都把较大的数字从后往前复制到一个辅助数组中去,确保辅助数组中的数字是递增排序的。在把较大的数字复制到辅助数组之后,把对应的指针向前移动一位,接下来进行下一轮比较。
经过前面详细的讨论,可以总结出统计逆序对的过程:
- 先把数组分隔成子数组,先统计出子数组内部的逆序对的数目
- 再统计出两个相邻子数组之间的逆序对的数目。在统计逆序对的过程中,还需要对数组进行排序。
如果对排序算法很熟悉,我们不难发现这个排序的过程实际上就是归并排序。关于归并排序,博主有相关总结,比较详细,推荐参考:[排序算法] 9. 归并排序递归与非递归实现及算法复杂度分析(分治算法、归并排序、复杂度分析)
故此,我们可以基于归并排序写代码了。
参见代码如下:
// 执行用时 :144 ms, 在所有 C++ 提交中击败了94.34%的用户
// 内存消耗 :47.3 MB, 在所有 C++ 提交中击败了100.00%的用户
class Solution {
public:
int reversePairs(vector<int>& nums) {
if (nums.size() == 0) return 0;
vector<int> vt(nums);
int cnt = help(nums, vt, 0, nums.size() - 1);
return cnt;
}
int help(vector<int>& nums, vector<int>& vt, int left, int right) {
if (left == right) {
vt[left] = nums[left];
return 0;
}
int len = (right - left) / 2;
int start = help(vt, nums, left, left + len);
int end = help(vt, nums, left + len + 1, right);
int i = left + len;
int j = right;
int index_vt = right;
int cnt = 0;
while (i >= left and j >= left + len + 1) {
if (nums[i] > nums[j]) {
vt[index_vt--] = nums[i--];
cnt += j - left - len;
}
else {
vt[index_vt--] = nums[j--];
}
}
for (; i >= left; --i) vt[index_vt--] = nums[i];
for (; j >= left + len + 1; --j) vt[index_vt--] = nums[j];
return start + end + cnt;
}
};