今天來學習歸併算法的一個應用,求數組中的逆序對數。
首先我們需要知道逆序對是什麼東西:在一個數組中,有兩個元素,索引小的元素比索引大的元素大,那這兩個元素就構成一個逆序對。而一個數組中所有逆序對的個數就叫做逆序對數。
暴力求解法
我們可以很容易地想出暴力求解的方法:遍歷數組,依次取數組中的每一個數,然後與索引排在其後的元素比較,如果比它小,則逆序對數+1,遍歷完畢,得到的逆序對數則是數組的逆序對數。
代碼如下:
/**
* 暴力求解法求逆序對數
* @param nums 數組
* @return 逆序對數
*/
public static int inversionPairCountBruteForce(int[] nums) {
Objects.requireNonNull(nums);
if (nums.length <= 1) {
// 數組最多隻有一個元素,無法構成逆序對
return 0;
}
int inversionPairCount = 0;
// 遍歷數組
for (int i = 0; i < nums.length - 1; i++) {
// 遍歷該元素後續元素,查找逆序對
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] > nums[j]) {
// 找到則逆序對數+1
System.out.println("[" + nums[i] + ", " + nums[j] + "]");
inversionPairCount++;
}
}
}
return inversionPairCount;
}
暴力求解法的算法複雜度爲O(n2),那麼我們有沒有更優的解法了呢?
在學點算法(三)——數組歸併排序裏面我們提到了分而治之的思想,在求逆序對的問題上,是否也可以用此思想呢?
歸併算法求數組逆序對數
答案是可以的。我們一步步來分析:
- 要求一個數組的所有逆序對數,可以先將它分爲兩部分,左子數組和右子數組。
- 分割完畢後,一個數組的總逆序對數 = 左子數組的總逆序對數 + 右子數組的總逆序對數 + 左子數組和右子數組中交叉的逆序對數。
- 要求左(右)子數組的逆序對數我們可以使用遞歸的方法繼續求,而左子數組和右子數組中交叉的逆序對數這個該怎麼求呢?如果不施加任何條件,我們還是需要遍歷左子數組中的元素,然後依次取右子數組中的元素進行對比,這樣比較下來我們還是需要O(n2)的算法複雜度。
- 而我們知道如果經過歸併排序後,左子數組和右子數組會分別變爲有序的,那麼我們在對比一組元素之後,如果發現逆序(左子數組中的元素比右子數組的元素大),那麼右子數組的該元素會與左子樹中後續所有元素構成逆序對。通過這個發現,我們可以利用歸併排序算法代碼,來求逆序對數,而該算法複雜度也優化到了和歸併排序一致,爲O(nlogn)。
我們取[53, 89]
和[32, 45, 67]
的歸併來說明這個過程:
- 左子數組的
53
和右子數組的32
對比,發現53
大於32
,那麼32
將和左子數組中53
及其以後的元素構成逆序對,即[53, 32]
,[89, 32]
。 - 左子數組的
53
和右子數組的45
對比,發現53
大於45
,那麼45
將和左子數組中53
及其以後的元素構成逆序對,即[53, 45]
,[89, 45]
。 - 左子數組的
53
和右子數組的67
對比,發現53
小於45
,則無逆序對。
- 左子數組的
89
和右子數組的67
對比,發現89
大於67
,那麼67
將和左子數組中89
及其以後的元素構成逆序對,即[89, 67]
。
- 右子數組中已無元素,則後續無逆序對。
累加每一次對比的結果:2 + 2 + 1
可以得到交叉的逆序對數爲5
。
我們只需要在歸併排序算法(歸併排序算法請見學點算法(三)——數組歸併排序)的基礎上稍作修改即可得到求逆序數的算法:
/**
* 數組的歸併排序算法(同時求逆序對數)
*
* @param nums 數組
* @param lo 區間的lo索引(包含)
* @param hi 區間的hi索引(不包含)
*/
public static int inversionPairCountMergeSort(int[] nums, int lo, int hi) {
// 數組爲null則直接返回
if (nums == null) {
return 0;
}
// 索引檢查
if (lo < 0 || nums.length <= lo) {
throw new IllegalArgumentException("lo索引必須大於0並且小於數組長度,數組長度:" + nums.length);
}
if (hi < 0 || nums.length < hi) {
throw new IllegalArgumentException("hi索引必須大於0並且小於等於數組長度,數組長度:" + nums.length);
}
if (hi <= lo) {
// lo索引必須小於hi索引(等於也不行,因爲區間是左閉右開,如果等於,區間內元素數量就爲0了)
throw new IllegalArgumentException("lo索引必須小於hi索引");
}
if (lo + 1 >= hi) {
// 區間元素個數最多爲1
// 無需排序,逆序數爲0
return 0;
}
int inversionPairCount = 0;
int mid = (lo + hi) / 2;
// 對左子區間排序,並加上逆序對數
inversionPairCount += inversionPairCountMergeSort(nums, lo, mid);
// 對右子區間排序,並加上逆序對數
inversionPairCount += inversionPairCountMergeSort(nums, mid, hi);
// 對兩個排序好的子區間歸併,得到一個整體有序的區間,並加上兩個子區間交叉的逆序對數
inversionPairCount += merge(nums, lo, mid, hi);
return inversionPairCount;
}
public static int merge(int[] nums, int lo, int mid, int hi) {
// 這裏不用檢查索引,調用方已經決定了索引是有效的
// 結果區間和右子區間使用原有數組
// 左子區間使用臨時數組(因爲結果區間可能會覆蓋左子區間的元素,所以需要開闢新數組保存)
int inversionPairCount = 0;
int leftLen = mid - lo;
int[] left = new int[leftLen];
System.arraycopy(nums, lo, left, 0, leftLen);
// 左子區間索引
int leftIdx = 0;
// 右子區間索引
int rightIdx = mid;
// 結果區間索引
int resultIdx = lo;
while (true) {
if (leftIdx < leftLen && rightIdx < hi) {
// 兩個子區間都存在元素
// 取兩個子區間的有效首元素對比
if (left[leftIdx] <= nums[rightIdx]) {
// 左子區間首元素小於右子區間首元素
// 將左子區間首元素放到結果位置,同時更新索引位置
nums[resultIdx++] = left[leftIdx++];
} else {
// 右子區間首元素小於左子區間首元素
// 將右子區間首元素放到結果位置,同時更新索引位置
nums[resultIdx++] = nums[rightIdx++];
for (int i = leftIdx; i < leftLen; i++) {
System.out.println("[" + left[i] + ", " + nums[rightIdx-1] + "]");
}
inversionPairCount += (leftLen - leftIdx);
}
} else {
if (leftIdx < leftLen) {
// 左子區間還有剩餘元素
// 直接將左區間所有元素一起移動到結果位置
System.arraycopy(left, leftIdx, nums, resultIdx, leftLen - leftIdx);
} else {
// 右子區間還有剩餘元素
// 因爲經過上一次判斷,左子區間和右子區間只會有一個存在剩餘元素
// 直接將右區間所有元素一起移動到結果位置
System.arraycopy(nums, rightIdx, nums, resultIdx, hi - rightIdx);
}
// 全部元素移動完畢,退出
break;
}
}
return inversionPairCount;
}
測試代碼如下:
int[] nums = {2, 343, 4, 1, 3, 5, 7};
System.out.println(inversionPairCountBruteForce(nums));
System.out.println(inversionPairCountMergeSort(nums, 0, nums.length));
輸出如下:
[2, 1]
[343, 4]
[343, 1]
[343, 3]
[343, 5]
[343, 7]
[4, 1]
[4, 3]
暴力求解法求逆序對數:8
[343, 4]
[2, 1]
[4, 1]
[343, 1]
[4, 3]
[343, 3]
[343, 5]
[343, 7]
歸併排序法求逆序對數:8
符合我們的預期。