剑指Offer-面试题51.数组中的逆序对 基于归并排序

这里是题目描述:剑指Offer-面试题51.数组中的逆序对

这道题的直观方法是将数组nums中的每个数和位于它后面的数字进行比较,若位于它后面的数字小于它,则逆序总数加1。这种方法的时间复杂度为O(n2),但我们根据题干可知,0 <= 数组长度 <= 50000,因此这种方法最多会带来超过10e10的运算,超出时间限制

基于归并排序的方法统计逆序对

我们使用一种在归并排序基础上的方法,在每趟归并排序时,统计两个相邻待归并子数组之间的逆序对数;而每个子数组内已经完成排序,没有逆序对;最终整个数组完成归并排序,整个数组中所有的逆序对都已被统计

对于数组中的一个数字(子数组),我们不将它和位于它后面的所有数字进行比较,因为这样的时间复杂度为O(n2),我们只拿它和与它相邻的数字(子数组)进行比较。

以题干中给出的nums=[7,5,6,4]为例,首先将它分成长度为1的4个子数组,也即四个数字[7][5][6][4],将它们按顺序两两进行归并排序得到[5,7][4,6],在归并的过程中,有[7]大于[5][6]大于[4],它们分别是一对逆序,因此逆序对总数加2
在这里插入图片描述

接下来,我们对两个长度为2的子数组[5,7][4,6]进行归并排序,因为[5,7][4,6]都是由归并排序得到并在排序过程中统计了每两个长度为1的子数组的逆序对数,因此长度为2的子数组内部不存在逆序对,只需统计这两个子数组之间存在的逆序对数量
在这里插入图片描述
定义指针p1p2p3分别指向两个子数组的末尾和新开辟的存放归并排序结果的数组的末尾,两子数组从最后一个数字向前开始比较,归并结果也从后向前填充。若子数组1指针指向的数字较大,则构成逆序对,若子数组2指针指向的数字较大,则不构成逆序对 (1)p1指向的数组7大于p2指向的数字6,将7放入p3指向的位置,p3p1向左移动一位;又因为7大于p2p2左边的所有数字(即子数组2中的剩余数字个数),所以逆序对数+2 (2)p2指向的数组6大于p1指向的数字5,将6放入p3指向的位置,p3p2向左移动一位;逆序对数不增加 (3)p1指向的数组5大于p2指向的数字5,将7放入p3指向的位置,p3p1向左移动一位;子数组2中剩余数字个数为1,所以逆序对数+1 **(4)**最后剩下了子数组2中的数字4,将其放入归并结果数组中

最后统计逆序对总数为5个

题解代码:

class Solution {
    public int reversePairs(int[] nums) {
        if(nums.length<=1)
        {
            return 0;
        }
        int numRevPair=0; //统计逆序对数量
        for(int subLen=1;subLen<nums.length;subLen*=2) //subLen是两两进行归并的子数组长度
        {
            for(int i=0;i<nums.length;i+=subLen*2)
            {
                numRevPair+=mergeCount(nums,i,subLen);
            }
        }
        return numRevPair;
    }
    int mergeCount(int[] nums,int begin,int subLen) //两个子数组的归并排序,并统计子数组内的逆序对数
    {
        int count=0;
        int[] subArr1,subArr2; //等待被归并排序的两个子数组,注意数组越界问题
        subArr1=Arrays.copyOfRange(nums,begin,Math.min(begin+subLen,nums.length));
        subArr2=Arrays.copyOfRange(nums,Math.min(begin+subLen,nums.length),Math.min(begin+subLen*2,nums.length));
        int p1=subArr1.length-1,p2=subArr2.length-1;
        int[] merged=new int[subArr1.length+subArr2.length]; //存储归并的结果
        int p=merged.length-1;
        while(p1>=0 && p2>=0)
        {
            if(subArr1[p1]>subArr2[p2]) //发现逆序
            {
                count+=(p2+1);
                merged[p]=subArr1[p1];
                p1--;
            }
            else
            {
                merged[p]=subArr2[p2];
                p2--;
            }
            p--;
        }
        if(p1>=0)
        {
            for(;p>=0;p--,p1--)
            {
                merged[p]=subArr1[p1];
            }
        }
        else
        {
            for(;p>=0;p--,p2--)
            {
                merged[p]=subArr2[p2];
            }
        }
        for(int i=begin,j=0;j<merged.length;i++,j++) //将排好的序写回原数组nums
        {
            nums[i]=merged[j];
        }
        return count;
    }
}

时间复杂度:需要进行logn次归并,每次归并进行n次比较运算,因此为O(nlogn)
空间复杂度:归并排序需要辅助空间存储每次归并的结果,因此为O(n)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值