← 題庫 / Archive
2026-05-07 TI150 Medium ArrayHash TableMathDesignRandomized

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): insert val if not present, returning true/false accordingly.
  • bool remove(int val): remove val if present, returning true/false accordingly.
  • 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 → 追加到 numspos[1]=0 [1] {1:0} true
2 remove(2) 2 不在 pos → 什麼都不做 [1] {1:0} false
3 insert(2) 2 不在 pos → 追加到 numspos[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} 12 (例:2)
5 remove(1) 1 在 pos,index=0;最後一個元素是 2 (index=1)。把 nums[0]=2,更新 pos[2]=0pop_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:上面 removepos[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), and getRandom is just rand() plus an array index.
  • Space: O(n),其中 n 為集合中當下的元素數。numspos 各存 n 筆資料。 O(n) where n is the current number of elements; both nums and pos hold n entries.

Pitfalls & Edge Cases

  • 刪除時忘記更新 pos / Forgetting to update pos for 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 取得更佳分佈。