學點算法(五)——使用歸併算法求數組的逆序對數

今天來學習歸併算法的一個應用,求數組中的逆序對數。

首先我們需要知道逆序對是什麼東西:在一個數組中,有兩個元素,索引小的元素比索引大的元素大,那這兩個元素就構成一個逆序對。而一個數組中所有逆序對的個數就叫做逆序對數

暴力求解法

我們可以很容易地想出暴力求解的方法:遍歷數組,依次取數組中的每一個數,然後與索引排在其後的元素比較,如果比它小,則逆序對數+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),那麼我們有沒有更優的解法了呢?

學點算法(三)——數組歸併排序裏面我們提到了分而治之的思想,在求逆序對的問題上,是否也可以用此思想呢?

歸併算法求數組逆序對數

答案是可以的。我們一步步來分析:

  1. 要求一個數組的所有逆序對數,可以先將它分爲兩部分,左子數組和右子數組。
  2. 分割完畢後,一個數組的總逆序對數 = 左子數組的總逆序對數 + 右子數組的總逆序對數 + 左子數組和右子數組中交叉的逆序對數。
  3. 要求左(右)子數組的逆序對數我們可以使用遞歸的方法繼續求,而左子數組和右子數組中交叉的逆序對數這個該怎麼求呢?如果不施加任何條件,我們還是需要遍歷左子數組中的元素,然後依次取右子數組中的元素進行對比,這樣比較下來我們還是需要O(n2)的算法複雜度。
  4. 而我們知道如果經過歸併排序後,左子數組和右子數組會分別變爲有序的,那麼我們在對比一組元素之後,如果發現逆序(左子數組中的元素比右子數組的元素大),那麼右子數組的該元素會與左子樹中後續所有元素構成逆序對。通過這個發現,我們可以利用歸併排序算法代碼,來求逆序對數,而該算法複雜度也優化到了和歸併排序一致,爲O(nlogn)。

我們取[53, 89][32, 45, 67]的歸併來說明這個過程:

  1. 左子數組的53和右子數組的32對比,發現53大於32,那麼32將和左子數組中53及其以後的元素構成逆序對,即[53, 32][89, 32]在這裏插入圖片描述
  2. 左子數組的53和右子數組的45對比,發現53大於45,那麼45將和左子數組中53及其以後的元素構成逆序對,即[53, 45][89, 45]在這裏插入圖片描述
  3. 左子數組的53和右子數組的67對比,發現53小於45,則無逆序對。
    在這裏插入圖片描述
  4. 左子數組的89和右子數組的67對比,發現89大於67,那麼67將和左子數組中89及其以後的元素構成逆序對,即[89, 67]
    在這裏插入圖片描述
  5. 右子數組中已無元素,則後續無逆序對。
    在這裏插入圖片描述
    累加每一次對比的結果: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

符合我們的預期。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章