設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("\n"); } int main() { int result; result = inversion(data,0,length-1); printf("inversions:%d\n",result); return result; }