剑指offer-面试题51:数组中的逆序对

题目描述

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例 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全集入口: 请戳这里

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值