具代表性的面試主題

編碼面試:如何實作一個隨機可合併堆?

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

題幹

請實作支援 meld、insert、find-min 與 extract-min 的隨機可合併最小堆,並解釋隨機化如何影響複雜度、如何處理重複鍵、節點別名與深遞迴。

題幹與適用場景

你要為任務排程器實作可合併的最小優先佇列。呼叫方會頻繁合併兩個佇列,再插入工作並取出最小優先級。請實作 meldinsertfind-minextract-min,說明隨機化、空堆、重複鍵與節點所有權。

隨機可合併堆以二元樹表達堆序,不維護左偏堆的秩等額外中繼資料。隨機選擇遞迴合併進入左子樹或右子樹,重點考察不變量、機率分析與可測試性。

面試官考察點

重點包括根鍵最小不變量、meld 的交換與結合語義、隨機數產生器邊界、重複鍵、節點歸屬、遞迴深度、銷毀策略,以及期望複雜度和最壞情況的區分。候選人也應能比較二元堆、左偏堆與配對堆的適用條件。

回答前需要釐清的問題

  • meld 是否可以消耗輸入堆,還是必須保留兩個原堆?
  • 是否能注入隨機種子,以便重播失敗測試?
  • 最大節點數與遞迴堆疊預算是多少?
  • 是否需要穩定控制代號、任意刪除或 decrease-key
  • 目標是教學實作、工程吞吐量,還是需要嚴格最壞界?

30 秒回答框架

「每個節點保存鍵、值、左右子樹。meld(a,b) 先處理空樹,再讓較小根成為結果根;隨機決定把另一棵樹與左子樹或右子樹遞迴合併。insert 用單節點與根 meld,extract-min 用根的左右子樹 meld。如此根始終最小,操作通常為期望對數時間,但遞迴深度與隨機種子必須測試與限制。」

分步驟深入解答

第一步:定義節點和所有權

節點至少保存 keyvalueleftright。堆物件保存根與節點數。若 meld 採用可變實作,輸入堆的根會被重新連接,介面必須明確輸入是否失效;若要求持久化,則需要路徑複製,複雜度和空間都會改變。

text
meld(a, b):
  if a is empty: return b
  if b is empty: return a
  if b.key < a.key: swap(a, b)
  if randomBit() == 0:
    a.left = meld(a.left, b)
  else:
    a.right = meld(a.right, b)
  return a

第二步:維護 meld 不變量

先比較兩個根,較小鍵保留為根;相等鍵可以採固定平局規則或隨機規則,但不能破壞堆序。遞迴返回後,結果子樹的所有鍵仍不小於目前根,因此不變量沿路徑保持。

實作中要避免同一節點同時掛到兩個父節點下。可變 meld 應記錄輸入所有權,除錯版本可檢查節點計數和無環;持久化版本則不能直接重用會被修改的子樹。

第三步:實作 insert 與 find-min

insert 建立單節點樹並與目前根 meld,節點數加一。find-min 讀取根鍵;空堆依介面回傳空值或錯誤。重複鍵不應被去重,值相同也不代表節點相同。

若隨機源是全域狀態,測試難以重播。較穩妥的方式是把隨機源作為依賴注入,並在測試使用固定種子;生產環境仍要避免低品質或偏斜的隨機位實作。

第四步:實作 extract-min

取出根後,新根是原根左右子樹的 meld。先斷開舊根的兩個指標,再減少節點數;若堆擁有節點記憶體,最後釋放舊根。輸入堆被消耗時,舊根控制代號必須標記失效,避免再次參與合併。

若需要保留舊版本,就採持久化路徑複製,不能在共享節點上原地修改指標。這個選擇應在介面層寫清楚,否則呼叫方會遇到別名造成的隱蔽資料損壞。

第五步:說明複雜度邊界

隨機可合併堆的 meldinsertextract-min 通常可給出期望對數時間或高機率對數時間的分析,具體表述取決於隨機模型與實作細節。find-min 是常數時間。空間為節點數的線性級別。

不要把期望界說成每次操作的最壞界;惡劣隨機序列可能形成較深樹。生產實作要限制遞迴風險,必要時改成顯式堆疊或設定深度監控,並用基準和隨機測試驗證分布。

第六步:測試與對照實作

把堆與標準優先佇列對拍,覆蓋空堆、重複鍵、交替 meld、連續 extract-min、固定隨機種子和極端深度。每次操作後檢查根最小、節點數正確、無環和所有權約束。

與配對堆相比,本題使用二元樹和隨機分支,不需要兄弟鏈表、兩遍合併或控制代號切斷;與左偏堆相比,不維護秩中繼資料,換取機率化分析。比較時要同時說明快取局部性、可變語義和證明要求。

高品質示範回答

我會把最小鍵留在根,meld 對空樹直接返回,非空時交換根使 a 較小,再隨機把 ba.lefta.right 合併。insertextract-min 都重用 meldfind-min 讀根。實作前確認 meld 是否消耗輸入;測試注入固定隨機源,對拍標準優先佇列,檢查無環、計數和根最小。複雜度表述為隨機模型下的期望或高機率對數時間,並單獨處理遞迴深度風險。

常見錯誤

  • 隨機分支放在比較根之前 → 根可能不是最小鍵 → 先交換根,再隨機選擇子樹。
  • 可變 meld 後繼續使用舊堆 → 節點同時擁有兩個父節點 → 明確消耗語義或實作持久化。
  • 把期望界寫成最壞 O(log n) → 機率假設被遺漏 → 註明隨機模型和高機率邊界。
  • 隨機源不可注入 → 失敗用例無法重播 → 依賴注入並固定種子測試。
  • 遞迴沒有深度策略 → 極端樹可能耗盡呼叫堆疊 → 顯式堆疊、深度監控或文件化限制。

追問及應對

追問一:如何讓測試完全確定?

把隨機位產生器作為堆實例依賴,測試傳入固定序列或種子;生產再使用獨立實例,避免全域隨機狀態讓多個測試互相影響。

追問二:meld 必須保留輸入時怎麼辦?

採用持久化實作,對沿遞迴路徑的節點做路徑複製,未修改的子樹共享。需要同步更新空間複雜度和垃圾回收策略,不能繼續宣稱原地常數額外空間。

追問三:如何處理兩個堆都引用同一節點?

可變 API 應禁止跨堆共享並在除錯模式記錄歸屬;持久化 API 允許結構共享,但所有節點必須不可變。發現歸屬衝突時回傳錯誤,不要靜默修復。

追問四:為什麼不用配對堆?

配對堆適合需要 decrease-key 的場景,但要維護多叉子鏈和刪除後的重組;隨機可合併堆的二元 meld 更短,適合只需要合併、插入和取最小值且能接受機率化保證的介面。

公開來源

同類題目

相關面試工具

用 Screenshot 處理演算法題

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

查看工具