【每日一題】LeetCode 面試51. 數組中的逆序對

每日一題,防止癡呆 = =

一、題目大意

在數組中的兩個數字,如果前面一個數字大於後面的數字,則這兩個數字組成一個逆序對。輸入一個數組,求出這個數組中的逆序對的總數。

二、題目思路以及AC代碼

這個題目就很明確,就是要求一個數組裏的逆序對,按道理是有套路方法的,不過很可惜,我之前並沒有看過 = =,所以就學習了一下基於歸併排序思路的解法和樹狀數組的解法。

首先,基於歸併排序思路的解法是這樣的。考慮在一般的歸併排序算法,首先將要排序的數組分爲左右兩部分,然後遞歸分別對左右兩部分的子數組進行排序,然後再將兩個有序的子數組進行合併。那麼在合併的時候,我們就可以看出這個數組中的逆序對個數。

舉個例子。
Left: [1, 4, 5, 7, 8]
Right: [2, 3, 6, 9, 10]

假設以上是在合併過程中需要進行合併的兩個子數組,那麼合併過程應該是這樣的

  1. 兩個指針lptr和rptr分別指向左數組和右數組的第一個元素,即1和2,比較,發現1比較小,然後把1放入合併之後的數組,lptr++
  2. 然後繼續比較4和2,此時2比較小,那麼把2放入合併之後的數組,rptr++
  3. 繼續比較4和3,此時3比較小,把3放入合併之後的數組,rptr++
  4. 繼續比較4和6,此時4比較小,把4放入合併之後的數組,但此時發現逆序對了沒有?不要忘了,Left數組中的元素本來都是在Right數組的右邊的,但此時經過排序卻有兩個右邊的數(2,3)被排到了左邊,說明在原數組中(4,2),(4,3)就是兩個逆序對。

用這樣的思路,我們只需要對歸併排序的merge過程做一些操作,就可以把數組中的逆序對個數找出來啦!

然後,在來一個樹狀數組的思路,其實我感覺一般情況下是會先想到這個思路的。我們要找逆序對,無非是想找給定了一個位置,出現在這個位置之後,比這個位置的數小的數有多少個(這句話比較繞,好好捋捋,語文水平有限= =),那麼我們可以這樣,建立一個計次數組用來輔助,還是舉個例子說明比較好。

[5, 5, 2, 4, 6]
那麼我們可以建立一個計次數組cnt,[0, 0, 1, 0, 1, 2, 1],其中cnt[i]表示 i 在原數組中出現的次數,那麼我再對原數組從後向前遍歷,遍歷到nums[i]的時候,cnt[nums[i]]++,並且將結果加上在cnt數組中nums[i]-1的前綴和即可。因爲我們從後向前遍歷,所以保證了當我們遍歷到nums[i]的時候,已經在cnt中計數的都是在原數組中出現在nums[i]之後的數,所以如果在cnt數組中nums[i]-1的前綴和有值,那就說明在原數組中,nums[i]之後存在比nums[i]小的值,也就是逆序對。

但是這樣會發現幾個問題:

  1. 計次數組的大小是根據原數組中數的取值範圍決定的,可能會很大
  2. 如果單純的用cnt數組的話,是一個O(n^2)時間複雜度的結果

針對第一個問題,我們因爲只需要考慮元素的大小關係,所以可以排序之後,用他們的排名替換他們的值,那樣原數組的值域就限制在[0, size-1]了,當然爲了之後樹狀數組的使用,這裏把其限制在[1, size]。

針對第二個問題,最好的方法就是採用樹狀數組了,因爲樹狀數組建立好後,查找一次前綴和的複雜度是O(logn),所以總的時間複雜度是O(nlogn)。

如果對樹狀數組不是很熟悉的,建議看我轉載的另一篇博客,講的很清楚。
https://blog.csdn.net/m0_38055352/article/details/105726733

下面給出AC代碼:

歸併排序:

int merge_sort(vector<int>& a, vector<int>& tmp, int l, int r) {
    int a_size = a.size();
    if (l >= r) return 0;

    int mid = (l + r) / 2;
    int inv_count = merge_sort(a, tmp, l, mid) + merge_sort(a, tmp, mid+1, r);
    int i = l, j = mid+1;
    
    // merge
    int pos = l;
    while (i <= mid && j <= r) {
        if (a[i] <= a[j]) {
            tmp[pos++] = a[i];
            i++;
            inv_count += j - (mid + 1);
        }
        else {
            tmp[pos++] = a[j];
            j++;
        }
    }

    for (;i<=mid;i++) {
        tmp[pos++] = a[i];
        inv_count += (j - (mid + 1));
    }

    for (;j<=r;j++) {
        tmp[pos++] = a[j];
    }

    copy(tmp.begin() + l, tmp.begin() + r + 1, a.begin() + l);
    return inv_count;
}

class Solution {
public:
    int reversePairs(vector<int>& nums) {
        int n_size = nums.size();
        vector<int> tmp(n_size);
        int inv_count = merge_sort(nums, tmp, 0, n_size-1);
        return inv_count;
    }
};

樹狀數組

class BIT {
private:
    vector<int> _tree;
    int _n;

public:
    BIT(int n) : _n(n), _tree(n+1) {}

    int lowbit(int m) {
        return m & (-m);
    }

    int get_sum(int x) {
        int res = 0;
        while (x) {
            res += _tree[x];
            x -= lowbit(x);
        }
        return res;
    }

    void update(int x, int add) {
        while (x <= _n) {
            _tree[x] += add;
            x += lowbit(x);
        }
    }
};

class Solution {
public:
    int reversePairs(vector<int>& nums) {
        int n = nums.size();
        // 離散化
        vector<int> tmp = nums;
        sort(tmp.begin(), tmp.end());

        for (int& num: nums) {
            // 這裏+1是因爲lowbit(0)是0
            num = lower_bound(tmp.begin(), tmp.end(), num) - tmp.begin() + 1;
        }

        // 求逆序對
        BIT tree(n);
        int ans = 0;
        for (int i=n-1;i>=0;i--) {
            ans += tree.get_sum(nums[i] - 1);
            tree.update(nums[i], 1);
        }

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