Leetcode 381. O(1) 時間插入、刪除和獲取隨機元素 - 允許重復


1.題目描述

設計一個支持在平均 時間復雜度 O(1) , 執行以下操作的數據結構。

注意: 允許出現重復元素。

    1. insert(val):向集合中插入元素 val。
    2. remove(val):當 val 存在時,從集合中移除一個 val。
    3. getRandom:從現有集合中隨機獲取一個元素。每個元素被返回的概率應該與其在集合中的數量呈線性相關。

示例:

// 初始化一個空的集合。
RandomizedCollection collection = new RandomizedCollection();

// 向集合中插入 1 。返回 true 表示集合不包含 1 。
collection.insert(1);

// 向集合中插入另一個 1 。返回 false 表示集合包含 1 。集合現在包含 [1,1] 。
collection.insert(1);

// 向集合中插入 2 ,返回 true 。集合現在包含 [1,1,2] 。
collection.insert(2);

// getRandom 應當有 2/3 的概率返回 1 ,1/3 的概率返回 2 。
collection.getRandom();

// 從集合中刪除 1 ,返回 true 。集合現在包含 [1,2] 。
collection.remove(1);

// getRandom 應有相同概率返回 1 和 2 。
collection.getRandom();

 

2.解題思路

該題是之前那道Insert Delete GetRandom O(1)的拓展——允許插入重復的數字。

解題思路和之前的一樣,使用的數據結構有兩個:

(1)一個數組nums,用來保存每一個插入的val(可重復),與數組下標由一一對應的關系,便於getRandom返回時,每個元素被返回的概率應該與其在集合中的數量呈線性相關。

(2)一個哈希結構,unordered_map<int, unordered_set<int>>,建立的是val值和val所有出現位置集合之間的哈希映射。

只是寫法略有不同,不同點如下:

  1. 由“一對一映射”變換成“一對多映射”;

             因為有重復數字,不能像之前那樣建立每個數字和其坐標的一對一映射,而是建立數字和其所有出現位置的集合之間的映射。

             為了嚴格的遵守O(1)的時間復雜度,集合使用的unordered_set,其插入刪除操作都是常量級的。

“一對一映射”
a——>1; //(a,1) b——>2; //(b,2)

“一對多(集合)映射” (int ——> unordered_set<int>)
a——>{1,3,5} ; //(a,1),(a,3),(a,5) b——>{2,4}; //(b,2),(b,4)

 

2.對於insert函數,將要插入的數字val加入數組nums中,並且將val在數組中的 位置加入m[val]數組的末尾。另外,判斷是否有重復只要看m[val]數組中val值的個數是1個還是多個。(m是一個上面提及的unordered_map對象,m[val]是unordered_set集合,本質上是數組實現的集合)

 

3.對於remove函數,這是重難點。

  • 首先判斷有無,如果哈希表有無val,沒有直接返回false; 否則進行下一步;
  • 更新哈希表。取出nums的尾元素,把尾元素哈希表中的位置數組(集合,其中表征位置的數字是遞增序列)中的最后一個元素更新為m[val]的尾元素,這樣就可以刪掉m[val]的尾元素了,如果m[val]只有一個元素,直接刪除這個映射;
  • 對於數組nums,都是將數組最后一個位置的元素和要刪除的元素交換位置,然后刪除最后一個位置上的元素;(如果要刪除的元素恰好是數組nums最后一個元素,直接刪除即可。
假定依次插入的值:
(a,0)
(b,1)      
(a,2)        
(a,3)
(b,4)

數組nums中:          哈希表中:
[0,a]                       a——>{0,2,3}
[1,b]
[2,a]                       b——>{1,4}
[3,a]                          
[4,b]


調用remove(a)結果是:(刪除了(a,3))

數組nums中:          哈希表中:
[0,a]                       a——>{0,2}
[1,b]
[2,a]                       b——>{1,3}                        
[3,b]

  

 

3.示例代碼

 

    
class RandomizedCollection {
public:
    /** Initialize your data structure here. */
    RandomizedCollection() {}
    
    
    /** Inserts a value to the set. Returns true if the set did not already contain the specified element. */
    bool insert(int val) {
        m[val].insert(nums.size());
        //nums.size()初始值為0,隨着元素一個個插入nums,size++
        nums.push_back(val);
        return m[val].size()==1;   
        //val值的個數為1,返回true; val值的個數大於1,返回false
    }

    
    /** Removes a value from the set. Returns true if the set contained the specified element. */
    bool remove(int val) {
        //每當要刪除一個值val,用nums的最后一個值last_val替換要刪除的填,最后刪除的nums的最后一個值
        //更新m[val]和m[last_val]兩個集合中的值,m[val]最后一個元素彈出,同時將該元素替換m[last_val]中的最后的一個元素(刪除最后一個,插入idx)
        if(m[val].empty()) return false;
        int idx = *m[val].begin();
        m[val].erase(idx);
        
        //要刪除的不是nums的最后一個元素
        if(nums.size()-1 != idx){
            int t = nums.back();//取nums的最后一個元素
            nums[idx] = t;
            m[t].erase(nums.size()-1);
            m[t].insert(idx);
        }

        nums.pop_back();
        return true;
    }
      
    
    /** Get a random element from the set. */
    int getRandom() {
        if (nums.size() == 0) {
                return NULL;
            }
        int randomIndex = (int) (rand() % nums.size()); // 0 ~ size -1
        return nums[randomIndex];
    }
    
    
//建立兩個數據結構,一個是vector<int>,另一個是unordered_map<int,unordered_set<int>>;  
private:   
    vector<int> nums;
    unordered_map<int,unordered_set<int>> m;
};

 

4.Leetcode上用時更少的范例

        這個雖然是當前我提交時,看到的運行時間最短的實現方法,但是這個方法嚴格來講不是最好的,因為priority_queue<int>的增刪的時間復雜度不是O(1)的,而是O(logN)。這里可以參考文末的參考博客,作者Grandyang一開始用的是優先隊列(priority_queue),后面在博友的建議,改用了集合unordered_set。

        提速的原因之一在於末尾的“static const auto io_sync_off = []()”的代碼,原因可以參考我的另一篇博文Leetcode 295. 數據流的中位數

class RandomizedCollection
{
public:
    RandomizedCollection()
    {
        srand(time(nullptr));
    }
    
    bool insert(int val)
    {
        map[val].push(vOrders.size());
        vOrders.push_back(val);
        return map[val].size() <= 1;
    }
    
    bool remove(int val)
    {
        if ( map[val].empty() )
            return false;
        auto &heap = map[val];
        int i = heap.top();
        heap.pop();
        int val2 = vOrders.back();
        if ( val != val2 )
        {
            vOrders[i] = val2;
            auto &heap2 = map[val2];
            heap2.pop();
            heap2.push(i);
        }
        vOrders.pop_back();
        return true;
    }
    
    int getRandom()
    {
        return vOrders[rand() % vOrders.size()];
    }

protected:
    vector<int> vOrders;
    unordered_map<int, priority_queue<int>> map;    
};

static const auto io_sync_off = []()
    {
        // turn off sync
        std::ios::sync_with_stdio(false);
        // untie in/out streams
        std::cin.tie(nullptr);
        return nullptr;
    }();

 

5.補充說明

        這篇博客更多是學習大牛Grandyang的Leetcode All in One的博客集,絕大部分內容是參考的他的博客,鏈接附在文末。

        這道題是hard級別的,我個人習慣通過寫博客的方式來學習和加深對於問題的理解,用自己能夠快速理解的方式呈現出來,方便自己日后的回顧(寫一篇一年后能看懂的博客),如果有朋友因博文缺乏足夠的原創內容而不滿,還請多多諒解。畢竟,每個人都有自己的Style!

 

參考資料:

1.[LeetCode] Insert Delete GetRandom O(1) - Duplicates allowed 常數時間內插入刪除和獲得隨機數 - 允許重復


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM