这是算法导论第二章后面的思考题,问题描述:设A[1..n]是一个包含n个不同数的数组。如果在i<j的情况下,有A[i]>A[j],则(i,j)就称为A的一个逆序对(inversion)。例如 ( 2,3,8,6,1 ) 中包含5个逆序:(1,5),(2,5),(3,5),(3,4),(4,5).
现在要求给出一个算法,能够用O(nlgn)的最坏情况运行时间,确定n个元素的任何排列中逆序对的数目。(提示:修改归并排序)
由给出的提示,以及给出的时间复杂度O(nlgn)的要求,可以想到分治的策略。进一步可以由主定理猜测,递归式为T(n)=2T(n/2)+O(n)。其中T(n/2)表示子问题的规模,O(n)是合并子问题的开销。
和归并排序是一个框架,大致如下:
(1)将数组A分成两个子数组;
(2)分别求两个子数组中的逆序;
(3)将子数组合并起来,求整体的逆序;
要注意的是,在第二步求子数组逆序时,两个数组之间的逆序没有考虑,这需要在合并的时候进行处理。若子数组left中的第i个大于子数组right中的第j个,这两个数构成一个逆序。合并子数组时,子数组已经排好序,可知left中下标大于i的元素都大于right[j]。
由归并操作的流程,涉及left[i]和right[j]的归并操作有且只有一次。合并之后,将按照升序一同出现在后续操作的同一个子数组中。因此,每次遇到left[i]>right[j]的时候,要将left[i]之后元素和right[j]构成的逆序一起进行统计。
代码如下:
public class CountInversion {
static int countInver(int[] a,int l,int r)
{
int inversion=0;
if(l<r){
int mid=(l+r)/2;
inversion+=countInver(a,l,mid);
inversion+=countInver(a,mid+1,r);
inversion+=mergeInver(a,l,mid,r);
}
return inversion;
}
static int mergeInver(int[] a,int l,int mid,int r)
{
int n1=mid-l+1;
int n2=r-mid;
int[] left=new int[n1];
int[] right=new int[n2];
for(int i=0;i<n1;i++)
left[i]=a[l+i];
for(int j=0;j<n2;j++)
right[j]=a[mid+1+j];
int inversion=0;
int i=0,j=0,k=l;
while(i<n1&&j<n2)
{
if(left[i]<right[j])
{
a[k++]=left[i++];
}
else
if(left[i]>right[j])
{
inversion+=n1-i; //[i~n1-1]的元素都大于right[j]
a[k++]=right[j++];
}
else
{
a[k++]=right[j++];
a[k++]=left[i++];
}
}
while(i<n1)
a[k++]=left[i++];
while(j<n2)
a[k++]=right[j++];
return inversion;
}
public static void main(String[] args)
{
int[] test1=new int[]{1,2,3,4,5,6,7};
int[] test2=new int[]{6,5,4,3,2,1,0};
int[] test3=new int[]{5,6,4,3,7,1,2};
int ans1=countInver(test1,0,test1.length-1);
int ans2=countInver(test2,0,test2.length-1);
int ans3=countInver(test3,0,test3.length-1);
System.out.println(ans1);
System.out.println(ans2);
System.out.println(ans3);
}
}
本文探讨了如何利用归并排序策略解决算法导论中关于逆序对的问题。通过将数组分成两半,分别计算子数组的逆序对,然后在合并过程中统计跨子数组的逆序对,实现了O(nlgn)的最坏情况运行时间。关键在于合并阶段,当left[i]>right[j]时,需要累计left[i]之后所有元素与right[j]的逆序对。
1284

被折叠的 条评论
为什么被折叠?



