具代表性的面試主題

程式面試:如何實作支援 O(1) 操作的稀疏集合?

程式題困難
Offer.cc 編輯團隊發佈 更新

題幹

給定整數範圍 [0, U),請實作支援 insert、remove、contains、clear 和 iterate 的集合,所有更新與查詢都要求 O(1)。

題目與範圍

給定固定整數宇宙 [0, U),實作集合,支援 insert(x)remove(x)contains(x)clear() 與走訪目前元素。前四項要求最壞 O(1),走訪目前元素數為 O(k)。集合不允許重複值,刪除不存在的值不報錯。

公開面經記錄了 Pure Storage 的同型題目;真正考察的是 dense/sparse 陣列不變量,而不是記住某個函式庫的類別名稱。

面試官考察什麼

  • 是否說明固定宇宙是前提,不能把 O(1) 結論無條件推廣到任意整數。
  • 是否同時維護 dense[sparse[x]] == x,並用它避免刪除後的誤判。
  • 刪除是否採用末元素交換,保持 dense 前綴連續,讓走訪為 O(k)
  • 是否說明空間為 O(U),集合很密或宇宙未知時要考慮其他結構。

作答前澄清

  1. U 是否已知,且能接受兩條長度為 U 的陣列?這是稀疏集合成立的資源前提。
  2. iterate() 是否要求排序?本方案只保證走訪所有成員,不保證順序。
  3. 是否需要穩定迭代器或並行存取?若需要,交換刪除和同步策略都要重新定義。
  4. clear() 是否必須避免掃描 U?本題要求常數時間,因此只重設 size

30 秒回答

「我維護長度為 U 的 sparse 索引和長度為 U 的 dense 陣列,再保存目前 size。元素 x 存在,當且僅當 sparse[x] < sizedense[sparse[x]] == x。插入把 x 寫到 dense[size] 並記錄索引;刪除時用最後一個元素覆蓋被刪位置並修正它的索引;清空只把 size 設為零。四個核心操作都是最壞 O(1),走訪 dense 前 size 個位置是 O(k),空間是 O(U)。」

分步深答

第一步:定義不變量。

dense[0..size) 保存全部成員且沒有重複;若 x 在集合中,sparse[x] 是它在 dense 中的位置,且 dense[sparse[x]] == x。不在集合的值可能保留舊 sparse 內容,所以 contains 不能只檢查索引範圍。

第二步:查詢與插入。

contains(x) 先檢查 0 <= x < U,再驗證 sparse[x] < size 和反向連結。插入先呼叫 contains;若不存在,把它寫入 dense[size],設定 sparse[x] = size,最後遞增 size。

第三步:交換刪除。

x 在位置 i,令 last = dense[size - 1],把 last 寫入 dense[i],更新 sparse[last] = i,再遞減 size。不必清理 sparse[x],因為 size 改變後反向連結驗證會使它失效;刪除最後元素也遵循相同流程。

第四步:常數時間清空與線性走訪。

clear() 只設定 size = 0,舊陣列內容不會被視為有效成員。走訪只掃描 dense[0]dense[size - 1],因此耗時是 O(k),不是 O(U)

第五步:複雜度與適用邊界。

containsinsertremoveclear 都是最壞 O(1);走訪是 O(k);空間是 O(U)。GCC 的實作也指出,稀疏集合適合固定宇宙且需要密集走訪的場景;宇宙未知、需要擴容或 U 遠大於可用記憶體時,雜湊集合或位圖可能更合適。

第六步:測試不變量。

用參考 Set 逐次對照隨機操作。涵蓋空集合、重複插入、刪除不存在值、刪除中間元素、刪除最後元素、清空後重用、0U-1,並在每次操作後驗證 dense 前綴無重複、每個成員的反向連結成立。

高品質示範回答

「固定宇宙 [0, U) 讓我能用兩個陣列換取確定的常數時間。dense 保存目前成員的緊湊前綴,sparse 把值映射回 dense 下標;成員判斷必須同時檢查邊界、下標小於 size 和反向連結,不能只看 sparse 數值。刪除用最後元素覆蓋目標位置並更新其 sparse 下標,clear 只重設 size。如此更新和查詢是最壞 O(1),走訪是 O(k),空間是 O(U);宇宙不可控時我會改用雜湊表或位圖。」

常見錯誤

  • 只判斷 sparse[x] < size 未加入的值可能殘留合法下標 → 補上 dense[sparse[x]] == x
  • 刪除後整體搬移 → 刪除退化為 O(U)O(k)用最後元素交換。
  • 把 clear 寫成填零迴圈 → 清空變成 O(U)只重設 size。
  • 忽略固定宇宙 → 陣列索引越界或記憶體不可接受 → 先確認 [0, U) 與容量限制。
  • 聲稱走訪是 O(1) → 取到視圖是常數,處理全部元素仍需 O(k)區分返回視圖與消費元素。

追問與回答

追問 1:如何支援任意整數?

先做座標壓縮,把實際值映射到 [0, U);若值域持續增長或無法預掃描,雜湊集合更自然,但其常數時間是攤銷或期望意義,不能複用本題的最壞 O(1) 結論。

追問 2:如何保證走訪順序?

目前 dense 的順序會被交換刪除改變。若要求插入順序,需要額外鏈結串列或穩定陣列,刪除和空間開銷都會變化;應先確認順序是否屬於介面契約。

追問 3:什麼時候選擇位圖?

若只需要成員判斷、宇宙規模適中且希望每個值佔一位,位圖更省空間。若還需要快速列舉成員,稀疏集合的 dense 前綴通常更適合;最終選擇取決於 U、成員數和存取模式。

公開來源

同類題目

相關面試工具

用 Screenshot 處理演算法題

截圖題目後,依序看約束、解法、程式碼、邊界條件和複雜度。

查看工具