剑指offer35_数组中逆序对

本文探讨了三种求解数组逆序对数量的算法:朴素做法(O(n^2))、归并排序(O(n log n))以及树状数组(O(n log n))。分别介绍了每种方法的原理、代码实现和复杂度分析,对比了它们在解决逆序对问题上的效率提升。
摘要由CSDN通过智能技术生成

题目陈述

大意:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007

算法一:朴素做法

算法思路

  • 最显然的思路就是枚举,枚举第i个数,下标比他大的所有数中,其数字比他小,则ans就+1,即下标 [ i + 1 , n ) [i+1,n) [i+1,n)中比a[i]小的数的数
  • 实际上,冒泡排序的交换次数,就是该逆序对的对数
  • 算法时间复杂度 O ( n 2 ) O(n^2) O(n2)

代码实现

typedef long long ll;
const ll md=1e9+7;
class Solution {
public:
    int InversePairs(vector<int> a) {
        ll ans=0;//答案
        int n=a.size();
        for(int i=0;i<n-1;i++){
            for(int j=i+1;j<n;j++){//下标比i大
                if(a[i]>a[j]){//数值比i小
                    ans=(ans+1)%md;//别忘记取模
                }
            }
        }
        return ans;
    }
};

算法二:归并排序

算法思路

  • 其实我们只需要在归并排序的模板上面稍加修改即可得到本题的答案,有的地方也写作CDQ分治做法,这是一个很经典的CDQ分治问题,此处不做展开。
  • 我们都知道,归并排序过程中,会有个合并左右子区间的过程,对于这个过程,我们详细分析
  • i , j i,j i,j指针分别在左右儿子区间上面移动,当 a [ i ] > a [ j ] a[i]>a[j] a[i]>a[j]时,此时我们就需要将a[j]放入合并后的数组中
  • 不难发现,左区间的数的下标都是小于右区间的并且满足单调性
  • 我们如果需要知道第一个大于 a [ j ] a[j] a[j]的数,设为 a [ i ] a[i] a[i],那么左区间中 a [ i ] a[i] a[i]以后的所有数,都比a[j]大(因为左区间已经满足单调性)
  • 故此时,左区间中,与 a [ j ] a[j] a[j]构成逆序对的数字的个数为左半边剩下的数 左 半 边 剩 余 的 数 = = ( 右 端 − 左 端 + 1 ) = ( m i d − i + 1 ) 即 左半边剩余的数==(右端-左端+1)=(mid-i+1)即 ==(+1)=midi+1mid-i+1

复杂度分析

  • 时间复杂度 O ( n log ⁡ n ) O(n \log n) O(nlogn),即归并排序的时间复杂度,(简单理解下,区间分治,想象成一颗完全二叉树,每个位置都会被分治 log ⁡ n \log n logn次,所以为 O ( n log ⁡ n ) O(n \log n) O(nlogn)
  • 空间复杂度 O ( n ) O(n) O(n),定义了一个辅助数组

代码实现

C++

typedef vector<int> vci;
typedef long long ll;
const ll md=1e9+7;
ll ans;
class Solution {
public:
    int InversePairs(vci a) {
        int n=a.size();
        vci b(n,0);//辅助数组
        merge_sort(a,b,0,n-1);//归并排序
        return ans;
    }
    void merge_sort(vci &a,vci &b,int l,int r){//归并排序
        int mid;
        if(l<r){
            mid=l+r>>1;
            merge_sort(a,b,l,mid);//先排序左半边
            merge_sort(a,b,mid+1,r);//在排序右半边
            merge(a,b,l,r);//合并左右区间
        }
        return ;
    }
    void merge(vci &a,vci &b,int l,int r){//合并过程,b为辅助数组,暂时储存合并后的结果
        int i=l,mid=l+r>>1;//计算中点
        int k=l,j=mid+1;
        while(i<=mid&&j<=r){
            if(a[i]>a[j]){//右半边的数更小
                b[k++]=a[j++];
                ans+=(mid-i+1);//左半边剩余的数,都比这个数大,构成逆序,左半边剩余的数==(右端-左端+1)=(mid-i+1)
                ans%=md;//别忘记取模
            }
            else b[k++]=a[i++];//左半边的数更小
        }
        while(i<=mid)b[k++]=a[i++];//如果左半边还有剩余
        while(j<=r)b[k++]=a[j++];//右半边还有剩余
        for(i=l;i<=r;i++)a[i]=b[i];//辅助数组拷贝到原数组
        return ;
    }
};

Python

# -*- coding:utf-8 -*-
class Solution:
    def InversePairs(self, a):
        return self.merge_sort(a,[0]*len(a),0,(len(a)-1))%1000000007;
        #[0]*len(a)代表里面有len(a)个0的列表
        #因为py自带大数,所以只需要最后取模即可,中间无需取模
    def merge_sort(self,a,b,l,r):
        if(l>=r):# 非法区间
            return 0
        mid=(l+r)//2 #整数除法
        ans_l=self.merge_sort(a,b,l,mid)#排序左区间
        ans_r=self.merge_sort(a,b,mid+1,r)#排序右区间
        return ans_l+ans_r+self.merge(a,b,l,r)#合并左右区间
    
    def merge(self,a,b,l,r):
        mid=(l+r)//2#区间中点
        i=l#左区间开头指针
        j=mid+1#右区间开头指针
        k=l#合并后的新数组的头指针
        ans=0
        while i<=mid and j<=r:
            if a[i]>a[j]:#右半边的数更小
                b[k]=a[j]
                k+=1
                j+=1
                ans+=mid-i+1#左半边剩余的数,都比这个数大,构成逆序,左半边剩余的数==(右端-左端+1)=(mid-i+1)
            else :#左半边的数更小
                b[k]=a[i]
                k+=1
                i+=1
        while i<=mid:#左区间还有剩余
            b[k]=a[i]
            k+=1
            i+=1
        while j<=r:#右区间还有剩余
            b[k]=a[j]
            k+=1
            j+=1
        a[l:r+1]=b[l:r+1]#辅助数组拷贝到原数组
        return ans

算法三:树状数组

算法思路

  • 树状数组和线段树,往往用于解决区间问题,这题也容易想到
  • 因为题目保证没有重复数字,所以此处问题就得以简化
  • 我们来想一想,树状数组的性质是什么?可以快速( log ⁡ n \log n logn)求出一个区间的和[1,n]
  • 如果我们把所有下标比i小但是比 a [ i ] a[i] a[i]大的数字,都标记出来,标为1其余标记为0,那么我们只需要统计区间[1,i-1]中1的个数,即可知道a[i]构成了多少对逆序对
  • 我们不需要统计下标比i大的构成的逆序对(在后续会别统计到),因为一对逆序对只需被统计一次即可
  • 此处我的写法是反过来的,将比我小的标记为1,这样我就只需要在 [ 1 , i ] [1,i] [1,i]中找0即可,我知道i的下标,也就知道前面有多少个数,即计算,前面有多少个数-前面比我小的数==前面比我大的数
  • 但是数据范围可能很大,甚至会到INT_MAX,这样就会导致空间不够的情况,但是保证了 s i z e < = 1 e 5 size<=1e5 size<=1e5,所以此处我们需要离散化,将空间复杂度从 O ( 2 e 9 ) O(2e9) O(2e9)降到 O ( 1 e 5 ) O(1e5) O(1e5)
  • 离散化,我们排序一遍,就可以将下标作为新的值(可以理解为一种没有冲突哈希函数)
  • 为了保证在第i个数,插入之前,其余比他小的数子都被插入了,所以我们先插入小的数字,即排序从小到大

复杂度分析

  • 时间复杂度为 O ( n log ⁡ n ) O(n \log n) O(nlogn),排序为 O ( n log ⁡ n ) O(n \log n) O(nlogn),树状数组每次操作为 log ⁡ n \log n logn,一个位置执行两次,一次add,一次sum,所以总得还是 O ( n log ⁡ n ) O(n \log n) O(nlogn)
  • 空间复杂度 O ( n ) O(n) O(n),定义了int树状数组

动画演示

在这里插入图片描述

代码实现

typedef vector<int> vci;
typedef long long ll;
const ll md=1e9+7;
const int N=1e5+10;
ll ans;

int t[N],n;
struct node {
	int v,i;
} a[N];
bool  cmp(node a,node b) {//权值从小到大
	return a.v<b.v;
}
#define lowbit(x) ((x)&(-x))
void add(int x) {
	while(x<=n) {//更新树状数组
		t[x]++;
		x+=lowbit(x);
	}
}
ll sum(int x) {//通过树状数组的性质,log n计算[1,x]里面有多少个1
	ll res=0;
	while(x>0) {//此处依旧是大于0,因为我们不能随便改变树状数组的边界,所以下面的结构体需要从下标1开始
		res+=t[x];
		x-=lowbit(x);
	}
	return res;
}
class Solution {
	public:
		int InversePairs(vci vec) {
			n=vec.size();
			for(int i=0; i<n; i++) {//此处整体向右偏移一个单位,因为树状数组二进制的性质原因
				a[i+1].v=vec[i];
				a[i+1].i=i+1;//记录下标,即为离散化后的结果
			}
			sort(a+1,a+n+1,cmp);//此处的排序作用,就是离散化(没有重复元素)
			for(int i=1; i<=n; i++) {//现在我们的问题就已经转化为,离散化后,求一个1-n的序列的逆序对
				add(a[i].i);//小的数先插入,插入第i小的数字,就可以通过他的下标,知道前面有多少个数比他小
				ans=(ans+i-sum(a[i].i))%md;//前面有多少个数-前面比我小的数==前面比我大的数
			}
			return ans;
		}
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值