1.題目描述
設計一個支持在平均 時間復雜度 O(1) 下, 執行以下操作的數據結構。
注意: 允許出現重復元素。
-
insert(val):向集合中插入元素 val。remove(val):當 val 存在時,從集合中移除一個 val。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所有出現位置集合之間的哈希映射。
只是寫法略有不同,不同點如下:
- 由“一對一映射”變換成“一對多映射”;
因為有重復數字,不能像之前那樣建立每個數字和其坐標的一對一映射,而是建立數字和其所有出現位置的集合之間的映射。
為了嚴格的遵守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 常數時間內插入刪除和獲得隨機數 - 允許重復
