1. 程式人生 > >演算法導論習題---求n個元素任何排列中逆序對的數量

演算法導論習題---求n個元素任何排列中逆序對的數量

問題描述:

設A[1…n]是一個包含n個不同數的陣列。如果在i < j的情況下,有A[i] > A[j],則(i,j)就稱為A中的一個逆序對(inversion),(逆序對的元素是下標,而不是數組裡的值)。給出一個演算法,它能用Θ(nlgn)的最壞情況執行時間,確定n個元素的任何排列中逆序對的數目。

問題求解:

方法一:
迴圈從陣列中取出一個元素k,然後從k之後的元素中找到比k小的元素個數,最後統計所有的個數即為排列中逆序對的數目。從陣列中取元素的次數為n,每次取出一個元素後,需要遍歷(n-i)次(i為當前元素的位置),其時間複雜度為
這裡寫圖片描述
演算法實現:

int CountInversions(const
vector<int>& a) { int cnt = 0; for (size_t i = 0; i < a.size(); i++) { for (size_t j = i + 1; j < a.size(); j++) { if (a[i] > a[j]) cnt++; } } return cnt; }

方法二:插入排序

利用插入排序的方式來解決。

對陣列用插入排序來做升序排列時,如果有元素需要移動,則每次移動時都相當於是找到了一個逆序對,因此插入佇列中移動元素的次數,也就是排列中逆序對的數量。

但插入排序的時間複雜度為Θ(n^2),不符合題目要求。

方法三:修改歸併排序(時間複雜度O(lgn))

歸併排序的基本思想就是 Divide & Conquer: 將陣列劃分為左右兩部分,歸併排序左部分,歸併排序右部分,然後 Merge 左右兩個陣列為一個新的陣列,從而完成排序。

按照這個基本思想,我們也可以運用到計算逆序對中來。假設將陣列劃分左右兩部分:
(1)左邊部分的逆序對有 inv1 個
(2)右邊部分的逆序對有 inv2 個
(3)剩餘的逆序對必然是一個數出現在左邊部分,一個數出現在右邊部分,並且滿足出現左邊部分的數 a[i] > a[j]。左右兩部分排序與否,不會影響這種情況下的逆序對數,因為左右兩部分的排序只是消除了兩部分內部的逆序對,而對於 a[i] 來自左邊, a[j] 來自右邊構成的逆序,各自排序後還是逆序。
這裡寫圖片描述

接下來就需要在 Merge 的過程中計算 a[i], a[j] 分別來自左右兩部分的逆序對數。

如下圖所示,如果所有 a[i] 都小於 b[j] 的第一個數,顯然是沒有逆序的。只有當 a[i] > b[j] 是才會發生逆序,由於我們事先對 a[] 和 b[] 已經排好序,而所以如果發生 a[i] > b[j], 那麼所有 a[ii] (ii > i) 也都滿足 a[ii] > b[j], 也就是說和 b[j] 構成逆序的數有 {a[i], a[i+1]…a[end]},所以逆序數增加 end- i + 1個, 所以我們每次碰到 a[i] > b[j] 的情況, 逆序對數增加 (end - i + 1) 個即可。
這裡寫圖片描述

演算法的框架圖:

這裡寫圖片描述

虛擬碼:

這裡寫圖片描述

這裡寫圖片描述
這裡寫圖片描述

The initial call is COUNT-INVERSIONS(A,1,n)