设A[1..n]是一个包含N个非负整数的数组。如果在i<j的情况下,有A[i]>A[j],则(i,j)就称为A中的一个逆序对(inversion)。
a)列出数组[2,3,8,6,1]的5个逆序。b)如果数组的元素取自集合{1,2,...,n},那么,怎样的数组含有最多的逆序对?它包含多少个逆序对?
c)插入排序的运行时间与输入数组中逆序对的数量之间有怎样的关系?说明你的理由。
d)给出一个算法,它能用O(nlogn)的最坏情况运行时间,确定n个元素的任何排列中逆序对的数目(提示:修改归并排序)
——《算法导论》,思考题2-4
逆序对的应用很多,比如各类OJ中的逆序对题目:http://www.cppblog.com/ickchen2/articles/62422.html,再比如《编程之美》1.7光影切割问题解法二的求交点个数。从上面的思考题入手来理解逆序对算法很简单,这也是标题中所展示的思路历程。本文主要面对的是之前对逆序对基本没接触和不了解O(nlogn)解法的读者,可能显得有些啰嗦。
问题a)直接根据定义可得<2,1>,<3,1>,<8,6>,<8,1>,<6,1>。
问题b)为了逆序对最多,那么应使任一个数在所有比它小的数前面,从而构成所有可能逆序,即[n,n-1,...,1],这样一共有(n-1)+(n-2) + ... + 1 =n(n-1)/2个。
问题c),在插入排序进行时,数组分为两部分:已排序部分和待排序部分。每次将待排序部分的第一个元素插入到已排序部分时,需要找出其插入的位置,并把这之后的已排序元素依次后移。并且,对于一个元素,插入过程中后移的元素数目就是它在原数组中它前面的逆序对的数目。这是因为,根据逆序对定义,可以写出O(n2)的检测方式,二者是一样的。
for(i=0;j<n;j++) for(i=0;i<j;i++) if(A[i]>A[j]) count++;
其实对于问题c,本意并不是告诉读者使用插入排序来找逆序对:同样是O(n2)的算法,这样做没有任何改进之处;而是在于启发对问题d)的解答。
1.在归并排序中,同样是对一个数组分为两段处理,在处理这两段时,并不会影响右段元素与左段元素的逆序关系,只有在归并时才会改变。
2.归并时的改变方式和插入排序是类似的:右段中取出元素放在左段其余所有元素前面时,相当于左段整体后移,后移的元素数就是这个逆序数。
3.由于归并排序使用的是分治法,将每次归并的逆序数累加,最后结果就是总的逆序数。并且,归并排序的时间复杂度是O(nlogn),优于插入排序。
根据以上的探讨,归并排序稍作修改,就获得了时间复杂度为O(nlogn)的寻找逆序对总数的算法了,下面是一个简单示例。
#include <stdio.h> #include <stdlib.h> #define MAXNUM 65535 #define length 8 static int data[length] ={5,2,4,7,1,3,2,6}; //#define length 5 //static int data[length] ={2,3,8,6,1}; int show_out(int *array,int n); int merge(int *array, int nBegin, int nMid, int nEnd) { int n1,n2; int i,j,k; int count =0; n1 = nMid-nBegin+1; n2= nEnd-nMid; int *left,*right; left = (int *)malloc((n1+1) * sizeof(int)); right = (int *)malloc ((n2+1) * sizeof(int)); for (i=0;i<n1;i++) left[i] = array[nBegin+i]; for (j=0;j<n2;j++) right[j] = array[nMid+j+1]; left[n1] = MAXNUM; right[n2] = MAXNUM; i = 0; j = 0; for (k = nBegin;k<=nEnd;k++) { if (left[i] <= right[j]) { array[k] = left[i]; i++; //从left中拷贝至array,没有改变逆序数 } else { array[k] = right[j]; j++; count += n1-i; //left中n1-i个在right[j]前面 //拷贝时会减少n1-i个逆序数 } } free(left); free(right); show_out(array,length); return count; } int inversion(int *array, int nBegin, int nEnd) { int nMid,count = 0; if (nBegin < nEnd) { nMid = ((nEnd - nBegin)>>1) + nBegin;
//nMid = (nBegin+nEnd)/2; count = inversion(array,nBegin,nMid) + inversion(array,nMid+1,nEnd) + merge(array,nBegin,nMid,nEnd); } return count; } int show_out(int *array,int n) { int k; for (k = 0; k<n; k++) printf("%d ",array[k]); printf(" "); } int main() { int result; result = inversion(data,0,length-1); printf("inversions:%d ",result); return result; }