380. Insert Delete GetRandom O(1)
題目 / Problem
中文:
請實作 RandomizedSet 類別,支援以下操作:
RandomizedSet():建構函式,初始化物件。bool insert(int val):若val不在集合中則插入並回傳true;否則回傳false。bool remove(int val):若val在集合中則移除並回傳true;否則回傳false。int getRandom():等機率地隨機回傳目前集合中的某個元素(呼叫時保證至少有一個元素)。
每個操作的平均時間複雜度都必須是 O(1)。
English:
Design a RandomizedSet class that supports:
RandomizedSet(): constructor.bool insert(int val): insertvalif not present, returningtrue/falseaccordingly.bool remove(int val): removevalif present, returningtrue/falseaccordingly.int getRandom(): return a uniformly random element of the current set (guaranteed non-empty when called).
Each operation must run in average O(1) time.
Constraints:
- -2^31 <= val <= 2^31 - 1
- At most 2 * 10^5 calls in total.
- The set is non-empty whenever getRandom is called.
Example:
Input
["RandomizedSet","insert","remove","insert","getRandom","remove","insert","getRandom"]
[[],[1],[2],[2],[],[1],[2],[]]
Output
[null, true, false, true, 2, true, false, 2]
名詞解釋 / Glossary
- 動態陣列 / dynamic array (vector):可以隨時在尾端追加(push_back)或移除(pop_back)的可變長度陣列。在 C 中我們手動
realloc;在 C++ 中用std::vector。 - 雜湊表 / hash map (
unordered_map, uthash):把「鍵」對應到「值」的查表結構,平均查找/插入/刪除為O(1)。本題用它記錄「某個 val 目前存在動態陣列的哪個 index」。 - 均勻隨機 / uniform random:每個元素被選中的機率相同。若集合大小為
n,那每個元素被選的機率必須是1/n。 - swap-and-pop 技巧 / swap with last and pop:要從動態陣列中刪除「中間」元素時,若改成「跟最後一個元素交換,再砍掉最後一個」,就能在
O(1)完成(因為不用搬動其他元素)。但前提是順序不重要——本題剛好不在乎順序,所以可以用。 - 平均 O(1) / amortized O(1):單次操作偶爾可能慢一點(例如雜湊表 rehash 或陣列 resize),但長期平均下來每次仍是常數時間。
- 指標 / pointer (C):儲存「某塊記憶體位址」的變數。
*p取出該位址裡的值,p->x等同於(*p).x。 malloc/realloc/free:C 標準函式庫的記憶體分配函式。malloc配置一塊新的記憶體,realloc調整既有區塊的大小,free歸還記憶體以避免洩漏。
思路
最直覺的作法是只用一個陣列:插入時走訪全部元素檢查重複(O(n)),刪除時也要先找到位置再把後面所有元素往前搬(O(n))。getRandom 雖然能 O(1)(隨機 index 即可),但 insert/remove 都不是 O(1),不符合要求。改用雜湊表(如 unordered_set)的話,insert/remove 變成 O(1),但要做 getRandom 卻很麻煩——標準雜湊容器無法直接「取第 k 個元素」,只能走訪到第 k 個,變成 O(n)。所以單一資料結構辦不到,我們必須同時用兩種:用「動態陣列 nums」儲存實際元素,因為陣列可以用 index 直接抽到隨機元素;再用「雜湊表 pos」記錄「每個 val 目前在 nums 的哪個 index」,這樣插入時能 O(1) 判重,刪除時也能 O(1) 找到位置。刪除最關鍵:不能直接從中間挖洞(會連動 index),而是把要刪的元素跟「最後一個元素」交換,更新雜湊表中「最後那個元素」的新 index,再把陣列尾端 pop 掉,整個過程都是 O(1)。getRandom 就是 nums[rand() % nums.size()]。不變式(invariant):對所有 val ∈ nums,恆有 nums[pos[val]] == val。
The naive idea—use a single array and linearly scan for duplicates—gives O(n) insert/remove. Using only a hash set fixes insert/remove but breaks getRandom, because hash containers don't support "give me the k-th element" in O(1)—you'd have to iterate. The trick is to combine two structures: a dynamic array nums that holds the actual values (so we can pick a random element by indexing nums[rand() % size]), and a hash map pos mapping val -> index in nums (so insert can detect duplicates and remove can locate the slot in O(1)). The deletion step is the clever part: you cannot just erase a middle element of an array in O(1) without shifting everything. Instead, swap the doomed element with the last element, update pos so that the moved-from-last element now points to its new index, then pop_back. Order doesn't matter for a set, so the swap is harmless. Invariant: for every value v in the set, nums[pos[v]] == v. As long as you maintain this on every insert/remove, all three operations are amortized O(1).
逐步走查 / Walkthrough
我們追蹤 nums(動態陣列)和 pos(雜湊表 val→index)兩個結構。
| 步驟 / Step | 操作 / Op | 動作 / Action | nums 之後 |
pos 之後 |
回傳 / Return |
|---|---|---|---|---|---|
| 1 | insert(1) |
1 不在 pos → 追加到 nums,pos[1]=0 |
[1] |
{1:0} |
true |
| 2 | remove(2) |
2 不在 pos → 什麼都不做 |
[1] |
{1:0} |
false |
| 3 | insert(2) |
2 不在 pos → 追加到 nums,pos[2]=1 |
[1, 2] |
{1:0, 2:1} |
true |
| 4 | getRandom() |
size=2,rand()%2 為 0 或 1,回傳 nums[那個 index] |
[1, 2] |
{1:0, 2:1} |
1 或 2 (例:2) |
| 5 | remove(1) |
1 在 pos,index=0;最後一個元素是 2 (index=1)。把 nums[0]=2,更新 pos[2]=0,pop_back;最後從 pos 抹掉 1 |
[2] |
{2:0} |
true |
| 6 | insert(2) |
2 已在 pos → 直接拒絕 |
[2] |
{2:0} |
false |
| 7 | getRandom() |
size=1,唯一可選 | [2] |
{2:0} |
2 |
注意第 5 步:被刪的是 nums[0],我們先把最後一個 2 搬到 index 0、更新 pos[2] 從 1 改成 0,然後 pop_back,全程 O(1)。
Notice step 5: we did not shift elements. We copied the last element into the slot of the removed element, fixed up pos for that moved element, and shrank the array by one — three O(1) operations.
Solution — C
// 演算法 / Algorithm:
// nums: 動態陣列存放實際的值 / dynamic array of the actual values
// pos : 雜湊表 val -> 在 nums 的 index / hash map from val to its index in nums
// 刪除時把目標跟最後一個元素交換再 pop_back / on remove, swap-with-last then pop_back
// 我們用 LeetCode 預先提供的 uthash.h 來實作雜湊表 / we use uthash.h (provided by LeetCode's C judge).
#include <stdlib.h> // malloc, realloc, free, rand / 記憶體配置與隨機數
#include <stdbool.h> // bool, true, false / 布林型別
#include "uthash.h" // uthash 雜湊表巨集 / uthash macros for the hash map
typedef struct {
int key; // 鍵就是 val 本身 / the key is the value itself
int idx; // 該 val 在 nums 中的 index / its index inside nums[]
UT_hash_handle hh; // uthash 內部用的 handle,必須叫 hh / required handle field, must be named hh
} Entry;
typedef struct {
int *nums; // 指向動態陣列的指標 / pointer to the dynamic array
int size; // 目前元素個數 / current number of elements
int cap; // 已配置的容量 / currently allocated capacity
Entry *map; // uthash 的「表頭」指標,初始必須是 NULL / hash table head pointer, must start as NULL
} RandomizedSet;
RandomizedSet* randomizedSetCreate(void) {
RandomizedSet *obj = (RandomizedSet*)malloc(sizeof(RandomizedSet)); // 配置物件 / allocate the struct
obj->cap = 16; // 初始容量設 16 / start with capacity 16
obj->size = 0; // 一開始沒元素 / no elements yet
obj->nums = (int*)malloc(sizeof(int) * obj->cap); // 配置初始陣列 / allocate the underlying array
obj->map = NULL; // uthash 規定表頭一開始要設 NULL / uthash requires NULL to start
return obj;
}
bool randomizedSetInsert(RandomizedSet* obj, int val) {
Entry *e = NULL; // 暫存查詢結果的指標 / pointer to receive lookup result
HASH_FIND_INT(obj->map, &val, e); // 在雜湊表中找 key=val / look up val in the hash map
if (e != NULL) return false; // 已存在就不能再插入 / already present, reject
if (obj->size == obj->cap) { // 陣列滿了要擴容 / grow the array if full
obj->cap *= 2; // 容量加倍(攤銷 O(1)) / double the capacity (amortized O(1))
obj->nums = (int*)realloc(obj->nums, sizeof(int) * obj->cap); // 重新配置更大的記憶體 / reallocate to the new size
}
obj->nums[obj->size] = val; // 把 val 放到陣列尾端 / append val at the end of nums
e = (Entry*)malloc(sizeof(Entry)); // 為雜湊表新節點配置記憶體 / allocate a new hash entry
e->key = val; // 設定鍵 / set the key
e->idx = obj->size; // 記住它在 nums 的 index / remember its index in nums
HASH_ADD_INT(obj->map, key, e); // 加入雜湊表 / insert it into the hash table
obj->size++; // 元素數 +1 / size grows by one
return true; // 成功插入 / inserted successfully
}
bool randomizedSetRemove(RandomizedSet* obj, int val) {
Entry *e = NULL; // 接收查詢結果 / lookup target
HASH_FIND_INT(obj->map, &val, e); // 找 val 是否存在 / look up val
if (e == NULL) return false; // 不存在就不能刪 / not in set, nothing to remove
int idx = e->idx; // 要被刪除的位置 / index slot to vacate
int lastIdx = obj->size - 1; // 陣列最後一個位置 / index of the last element
int lastVal = obj->nums[lastIdx]; // 拿到最後一個值 / grab the last value
obj->nums[idx] = lastVal; // 把最後一個值搬到要刪的位置 / move last value into the hole
if (idx != lastIdx) { // 若不是同一位置才需更新對應 / only update map if it actually moved
Entry *eLast = NULL; // 拿 lastVal 的雜湊節點 / fetch lastVal's hash entry
HASH_FIND_INT(obj->map, &lastVal, eLast); // 從雜湊表找 lastVal / look up the moved value
eLast->idx = idx; // 更新它的新 index / update its index to the freed slot
}
HASH_DEL(obj->map, e); // 從雜湊表移除舊節點 / remove the old entry
free(e); // 釋放它的記憶體 / free its memory
obj->size--; // 元素數 -1(相當於 pop_back) / shrink size (logical pop_back)
return true; // 成功刪除 / removed successfully
}
int randomizedSetGetRandom(RandomizedSet* obj) {
int r = rand() % obj->size; // 取 [0, size) 的均勻隨機 index / uniform random index in [0, size)
return obj->nums[r]; // 直接以 index 取值即 O(1) / O(1) lookup by index
}
void randomizedSetFree(RandomizedSet* obj) {
Entry *cur, *tmp; // 走訪雜湊表用的兩個指標 / iterators for safe deletion
HASH_ITER(hh, obj->map, cur, tmp) { // 遍歷整張雜湊表 / iterate through every entry
HASH_DEL(obj->map, cur); // 從表中移除節點 / unlink it
free(cur); // 釋放節點記憶體 / free its memory
}
free(obj->nums); // 釋放動態陣列 / free the underlying array
free(obj); // 釋放物件本身 / free the struct itself
}
Solution — C++
// 演算法 / Algorithm:
// nums: vector<int> 存實際值 / vector storing the actual values
// pos : unordered_map<int,int> val -> nums 的 index / map from value to its index
// 刪除採用 swap-with-last + pop_back / removal swaps target with last then pop_back
#include <vector> // std::vector / 動態陣列容器
#include <unordered_map> // std::unordered_map / 雜湊表容器
#include <cstdlib> // std::rand / C 風格隨機數
class RandomizedSet {
private:
std::vector<int> nums; // 動態陣列存元素 / array of current elements
std::unordered_map<int, int> pos; // val -> nums 的 index / map: value to its index in nums
public:
RandomizedSet() {} // 預設建構即可(容器自帶初始化) / default ctor; containers self-initialize
bool insert(int val) {
if (pos.count(val)) return false; // 已存在就拒絕 / reject if already present
pos[val] = nums.size(); // 新元素的 index 是目前 size / its index will be the current size
nums.push_back(val); // 追加到陣列尾端 / append at the end
return true; // 成功插入 / inserted
}
bool remove(int val) {
auto it = pos.find(val); // 用 find 拿到 iterator 避免兩次查找 / single lookup via find
if (it == pos.end()) return false; // 找不到就不能刪 / not present, nothing to do
int idx = it->second; // 取得要刪位置 / index of the slot to vacate
int lastVal = nums.back(); // 取得最後一個值 / grab last value of nums
nums[idx] = lastVal; // 把最後一個值搬到要刪位置 / move last value into the hole
pos[lastVal] = idx; // 更新被搬動值的新 index / update map for the moved value
nums.pop_back(); // 陣列尾端去掉一個 / shrink the array by one
pos.erase(it); // 從雜湊表抹掉 val / erase val from the map
return true; // 成功刪除 / removed
}
int getRandom() {
return nums[std::rand() % nums.size()]; // 隨機 index 後直接取值 / O(1) random index then index lookup
}
};
小提醒 / note:上面
remove中pos[lastVal] = idx;即使val == lastVal(只有一個元素或刪的就是最後一個)也是安全的——我們在pos.erase(it)之前更新,這個 entry 馬上就會被 erase 掉,不會留下髒值。在 C 版本中我們特別用if (idx != lastIdx)是因為若先HASH_DEL後再寫idx可能讀到已釋放的記憶體;條件分支更清楚。
複雜度 / Complexity
- Time: 每個
insert/remove/getRandom都是 amortized O(1)。雜湊表的 find/insert/erase 平均 O(1);vector::push_back偶爾要 resize(成本攤平後仍 O(1));getRandom是一次rand()加一次 index 取值。 Each operation is amortized O(1): hash lookup is O(1) average, vector growth is amortized O(1), andgetRandomis justrand()plus an array index. - Space: O(n),其中 n 為集合中當下的元素數。
nums與pos各存 n 筆資料。 O(n) where n is the current number of elements; bothnumsandposhold n entries.
Pitfalls & Edge Cases
- 刪除時忘記更新
pos/ Forgetting to updateposfor the moved element:把最後一個元素搬到被刪的位置後,雜湊表必須改寫它的新 index,否則之後再刪該元素會抓到錯誤位置,破壞不變式nums[pos[v]] == v。 - 刪除時順序錯誤 / Wrong order of operations on remove:必須先「搬移、更新 map」再
erase被刪 entry /pop_back陣列。順序顛倒會讀到錯的舊值或越界。 - 被刪元素剛好就是最後一個 / Removing the last element:
idx == lastIdx,搬移其實是自我覆寫無害;但在 C 版本若先HASH_DEL再去查lastVal會讀到剛釋放的節點。我們用if (idx != lastIdx)跳過更新,安全又清楚。 - 雜湊表只允許每個 val 一次 / Set semantics:
insert對重複值要回傳false;不要呼叫成「總是 push_back」否則nums會出現重複,getRandom機率就不均勻了。 getRandom機率均勻性 / Uniform probability:因為nums中每個值都恰好出現一次,且我們用rand() % size,所以每個元素機率剛好是1/size。- C 中忘記
realloc或忘記free/ Memory issues in C:陣列滿了沒擴容會寫越界;randomizedSetFree沒走訪雜湊表會記憶體洩漏。 val可達INT_MIN~INT_MAX/ Full int range:本題沒有運算只儲存比較,所以不會溢位;雜湊表也以 int 為 key,沒問題。rand()偏差 /rand()quality:對 OJ 來說足夠均勻;正式應用可改用<random>的std::mt19937+std::uniform_int_distribution取得更佳分佈。