这里是题目描述:剑指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的子数组内部不存在逆序对,只需统计这两个子数组之间存在的逆序对数量
定义指针p1
、p2
、p3
分别指向两个子数组的末尾和新开辟的存放归并排序结果的数组的末尾,两子数组从最后一个数字向前开始比较,归并结果也从后向前填充。若子数组1指针指向的数字较大,则构成逆序对,若子数组2指针指向的数字较大,则不构成逆序对 (1)p1
指向的数组7
大于p2
指向的数字6
,将7
放入p3
指向的位置,p3
和p1
向左移动一位;又因为7
大于p2
和p2
左边的所有数字(即子数组2中的剩余数字个数),所以逆序对数+2 (2)p2
指向的数组6
大于p1
指向的数字5
,将6
放入p3
指向的位置,p3
和p2
向左移动一位;逆序对数不增加 (3)p1
指向的数组5
大于p2
指向的数字5
,将7
放入p3
指向的位置,p3
和p1
向左移动一位;子数组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)