题目描述
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例 1:
输入: [7,5,6,4]
输出: 5
限制:
0 <= 数组长度 <= 50000
方法一(归并排序)
1.解题思路
归并排序的思想是将原数组不断二分,直到只有一个或2个元素的时候,再比较排序,最后将排好序的数组依次两两合并。我们只需要在比较排序的过程中,加一个统计逆序对的过程。
当左边下标为i,右边下标为j,并且nums[i]>nums[j]的时候,下标i处元素与范围在[mid+1,j]的所有元素构成j-mid组逆序对。所以这个过程中会产生j-mid组逆序对,再加上每个子区间的逆序对数量,即为整个区间的逆序对数量。
2.代码实现
class Solution {
int[] temp;
public int reversePairs(int[] nums) {
int n=nums.length;
return merge(nums,0,n-1);
}
private int merge(int[] nums,int l,int r){
//1.终止条件
if(l>=r) return 0;
//2.分治
int mid=l+(r-l)/2;
int count=merge(nums,l,mid)+merge(nums,mid+1,r);
//3.排序,并统计逆序对
int i=mid,j=r,index=r-l;
temp=new int[r-l+1];
while(i>=l&&j>=mid+1){
if(nums[i]>nums[j]){
temp[index--]=nums[i--];
count+=j-mid;
}
else{
temp[index--]=nums[j--];
}
}
while(i>=l){
temp[index--]=nums[i--];
}
while(j>=mid+1){
temp[index--]=nums[j--];
}
//4.合并
for(int k=0;k<temp.length;k++){
nums[k+l]=temp[k];
}
return count;
}
}
3.复杂度分析
- 时间复杂度:需要进行logn次二分,每一次需要花费O(n)时间进行排序,统计逆序对,并合并,所以时间复杂度为O(nlogn)。
- 空间复杂度:需要额外长度为n的中间数组,所以空间复杂度为O(n)。
方法二(树状数组)
1.解题思路
- 树状数组简介:树状数组的提出是为了更方便地进行区间查找,最初的区间查找使用前缀和的方法,做法是初始化一个到当前下标的前缀和数组,然后可以在O(1)的时间内找到任何区间元素之和。但是初始化需要O(n)的时间,树状数组可以在O(logn)时间内进行初始化,并且在O(logn)时间内找到区间和。
- 树状数组结构:
蓝色表示原始数组,红色表示树状数组
- 树状数组计算:
数组 C计算 | 数组 A 的元素个数 |
---|---|
C[1] =A[1] | 1 |
C[2] =A[1] + A[2] | 2 |
C[3] =A[3] | 1 |
C[4] =A[1] + A[2] + A[3] + A[4] | 4 |
C[5] =A[5] | 1 |
C[6] =A[5] + A[6] | 2 |
C[7] =A[7] | 1 |
C[8] =A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8] | 8 |
- 树状数组查找前缀和:比如查找A[1]到A[6],只需计算C[4]+C[6]即可。那么这个过程是怎么完成的呢,首先从当前下标6(二进制表示是110)开始,不断减去最右1,直到为0。把这个过程中所有的C的索引相加即为前缀和。由于是按最右1进行遍历,最多需要logn次操作。
- 树状数组插入新元素:我们观察A[5]只在C[5]、C[6]、C[8]中的计算被用到,所以插入A[5]时,只需跟新C[5]、C[6]、C[8],也就是当前索引的父索引。同样这些索引之间只差二进制的最右1的距离,所以以最右1为步长进行跟新,直到最大索引n。
- 本题基本思路:首先复制一个一模一样的数组,进行排序,然后遍历原数组,通过二分查找,找到每一个元素的排名,写入原数组(比如{7,5,6,4}排名之后变为{4,2,3,1})。接着从后向前遍历,通过树状数组查找逆序对。比如排名1,前面没有比它小的了,当前没有逆序对;排名3,按最右1进行查找(3-1=2,二进制表示为10),从10到0,总共有1个逆序对(A[1]+A[2],A[2]=0),也就是(3,1),然后将排名3加入树状数组;排名2,按最右1进行查找(2-1=1,二进制表示为01),从01到0,总共有1个逆序对(A[1]),也就是(2,1),然后将排名2加入树状数组;排名4,按最右1进行查找(4-1=3,二进制表示为11),从11到0,总共有3个逆序对(A[1]+A[2]+A[3]),也就是(4,1),(4,3),(4,2)。
树状数组详细介绍: 请戳这里
2.代码实现
class Solution {
public int reversePairs(int[] nums) {
int n=nums.length;
int[] tmp=new int[n];
//复制原数组
for(int i=0;i<n;i++){
tmp[i]=nums[i];
}
//排序
Arrays.sort(tmp);
//二分查找元素在数组中排名,写入数组(计算排名是为了压缩树状数组的空间)
for(int i=0;i<n;i++){
nums[i]=Arrays.binarySearch(tmp,nums[i])+1;
}
Bit bit=new Bit(n);
int res=0;
//从后往前遍历
for(int i=n-1;i>=0;i--){
//查找小于等于当前排名的所有元素个数,即为当前逆序对个数
res+=bit.query(nums[i]-1);
//将当前元素加入树状数组
bit.update(nums[i],1);
}
return res;
}
}
class Bit{
private int n;
private int[] tree;
public Bit(int n){
this.n=n;
this.tree=new int[n+1];
}
//计算最右1
private int lowbit(int x){
return x&(-x);
}
//查找前缀和
public int query(int x){
int res=0;
while(x!=0){
res+=tree[x];
x-=lowbit(x);
}
return res;
}
//插入元素
public void update(int x,int delta){
while(x<=n){
tree[x]+=delta;
x+=lowbit(x);
}
}
}
3.复杂度分析
- 时间复杂度:第一次排序的时间复杂度为O(nlogn),然后二分查找排名,一次查找复杂度为O(logn),进行了n次,所示是O(nlogn),接着用树状数组计算前缀和,查找和跟新都是O(logn)的复杂度,总共进行n次,故有O(nlogn)的复杂度,所以最终的时间复杂度为O(nlogn)。
- 空间复杂度:建立树状数组需要额外长度为n的数组,所以空间复杂度为O(n)。
剑指offer全集入口: 请戳这里