题目链接
题目描述
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
解题思路
暴力法(超时)
public class Solution {
public int reversePairs(int[] nums) {
int cnt = 0;
int len = nums.length;
for (int i = 0; i < len - 1; i++) {
for (int j = i + 1; j < len; j++) {
if (nums[i] > nums[j]) {
cnt++;
}
}
}
return cnt;
}
}
归并排序(分治法)
高级排序算法(快速排序,归并排序)中,能够看到非常明显的阶段排序结果的算法就是归并排序。
预备知识
「归并排序」是分治思想的典型应用,它包含这样三个步骤:
- 分解: 待排序的区间为 [l, r][l,r],令 m=⌊(l+r)/2⌋,我们把 [l, r] 分成 [l, m] 和 [m + 1, r]
- 解决: 使用归并排序递归地排序两个子序列
- 合并: 把两个已经排好序的子序列 [l, m] 和 [m + 1, r] 合并起来
在待排序序列长度为 11 的时候,递归开始「回升」,因为我们默认长度为 11 的序列是排好序的。
而计算逆序数就发生在排序的过程中,利用了「排序」以后数组的有序性。
- 关键在于「合并两个有序数组」的步骤,利用数组的部分有序性,一下子计算出一个数之前或者之后元素的逆序的个数;
- 前面「分」的时候什么都不做,「合」的过程中计算「逆序对」的个数;
- 「排序」的工作是必要的,正是因为「排序」才能在下一轮利用顺序关系加快逆序数的计算,也能避免重复计算;
- 在代码实现上,只需要在「归并排序」代码的基础上,加上「逆序对」个数的计算,计算公式需要自己在草稿纸上推导。
下面提供两种写法:
1、在第 2 个子区间元素归并回去的时候,计算逆序对的个数(参考代码 2);
2、在第 1 个子区间元素归并回去的时候,计算逆序对的个数(参考代码 3)。
注意:两者不能同时计算,否则会计算重复。
参考代码 2:在第 2 个子区间元素归并回去的时候,计算逆序对的个数。
即在 j
指向的元素赋值回去的时候,给计数器加上 mid - i + 1
。
class Solution {
public:
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过大时,l+r溢出
// 递归计算左右两边
int inv_count = mergeSort(nums, tmp, l, mid) + mergeSort(nums, tmp, mid + 1, r);
int i = l,j=mid+1,pos=l;
while (i <= mid && j <= r) { // 归并
if (nums[i] <= nums[j]) { // 当左边小于等于右边时,插入左边
tmp[pos] = nums[i];
++i;
}
else { // 当右边小于左边时,插入右边,并将逆序数加上左边剩余的元素个数
tmp[pos] = nums[j];
++j;
inv_count += (mid - i + 1);
}
++pos;
}
// 将剩余的元素插入
for (int k = i; k <= mid; ++k) {
tmp[pos++] = nums[k];
}
for (int k = j; k <= r; ++k) {
tmp[pos++] = nums[k];
}
// 将辅助数组排序好的部分复制给元素数组
// [ _First,_Last) DestBeg 指出复制到的目标区间起始位置
// copy(First,Last,DestBeg);
copy(tmp.begin() + l, tmp.begin() + r + 1, nums.begin() + l);
// 等同于下面
// for(int i=l;i<=r;i++){
// nums[i]= tmp[i];
// }
return inv_count;
}
int reversePairs(vector<int>& nums) {
int n = nums.size();
if(n<2)
return 0;
vector<int> tmp(n); //辅助数组
return mergeSort(nums, tmp, 0, n - 1);
}
};
class Solution {
public int reversePairs(int[] nums) {
return binarySort(nums, 0, nums.length - 1);
}
private int binarySort(int[] nums, int left, int right){
if(left >= right){
return 0;
}
int res = 0;
int mid = left + (right - left)/2;
res += binarySort(nums, left, mid);
res += binarySort(nums, mid+1, right);
int[] result = new int[right - left + 1];
int l = left, r = mid + 1;
int pos = 0;
while(l <= mid && r <= right){
if(nums[r] < nums[l]){
result[pos++] = nums[r++];
res += (mid - l + 1);
}else{
result[pos++] = nums[l++];
}
}
while(l <= mid){
result[pos++] = nums[l++];
}
while(r <= right){
result[pos++] = nums[r++];
}
pos = 0;
for(int i = left; i <= right; ++i){
nums[i] = result[pos++];
}
return res;
}
}
- 时间复杂度:O(NlogN): 这里 N 是数组的长度。复杂度是归并排序的时间复杂度,直接看递归树的结点个数或者使用主定理分析,归并的回收每一步计算逆序对的个数是 O(1) 的;
- 空间复杂度:O(N)。