具代表性的面試主題

演算法面試:如何除錯雙堆動態中位數實作?

程式題中等
OfferCC 編輯團隊發佈 更新

題幹

一個雙堆 MedianFinder 能通過有序範例,卻在重複值、交錯極值和偶數長度時回傳錯誤結果。你會如何定位問題、修復並證明正確性?

題幹與適用情境

你接手一個 MedianFinder:較小的一半放最大堆,較大的一半放最小堆。它能處理 1, 2, 3,卻會在 10, 1, 9, 2、大量重複值和極端整數下失敗。本題要求除錯既有實作,不再重複從零推導雙堆。

面試官考察點

  • 能否先寫清楚不變量,再修改程式碼。
  • 能否縮小失敗序列,找到第一次非法狀態。
  • 能否涵蓋空查詢、重複值和偶數長度溢位。
  • 證明、複雜度和實作是否一致。

回答前需要釐清的問題

  • 第一次插入前呼叫 findMedian() 應回傳什麼?
  • 哪個堆允許多一個元素?
  • 輸入整數寬度與回傳型別是什麼?
  • 是否允許重複值,並行是否在範圍內?

本題假設重複值有效,下半堆可多一個元素,查詢回傳浮點數,空查詢拋出明確錯誤;並行不在範圍內。

30 秒回答框架

「我會在每次插入後檢查兩個不變量:下半堆和上半堆大小相等,或下半堆多一個;兩個堆都非空時,下半堆頂不能大於上半堆頂。先把失敗輸入縮成最小反例,再統一用『插入下半堆、把最大值移到上半堆、必要時把上半堆最小值移回』修復。偶數長度先擴寬數值型別再算平均。最後用排序陣列作真值,對短序列窮舉並涵蓋交錯極值。」

分步驟深入解答

第一步:讓錯誤狀態可見

每次插入後記錄輸入前綴、兩個堆的大小和堆頂,一旦不變量失效就停止。不斷刪除輸入值並重跑,直到得到仍可重現的最短序列。四個數的反例比一千個隨機數更容易說明。

第二步:使用確定的插入流程

所有新值先進入下半堆,再把下半堆最大值移到上半堆;若上半堆變大,就把它的最小值移回。這個順序先修復左右順序,再修復大小。只有最小堆的語言可在下半堆儲存負數,但符號轉換要集中在邊界。

第三步:處理查詢與數值安全

兩個堆都空時拒絕查詢。總數為奇數時回傳下半堆頂;偶數時先把兩個端點轉成更寬或浮點型別再相加,避免固定寬度整數在中位數可表示時仍發生加法溢位。

第四步:證明並驗證修復

把下半堆最大值移到上半堆後,留下的下半部不會越過邊界;再移回一個上半堆最小值,只會恢復大小規則,不會破壞順序。每次插入執行常數次堆操作,因此是 O(log n);查詢 O(1),空間 O(n)。

每個前綴都與排序陣列結果比對。測試空查詢、一個值、兩個極值、遞增、遞減、交錯高低、全重複、負數,以及奇偶長度反覆切換。

高品質示範回答

「我不會只修補當前失敗分支。我先斷言兩個堆大小相等或下半堆多一個,並在兩邊非空時斷言 max(lower) <= min(upper)。插入時統一先進 lower,再把最大值移到 upper;只有 upper 較大時才把最小值移回。這樣正確性不依賴輸入順序。

空結構查詢會報錯。奇數長度取 lower 堆頂,偶數長度先擴寬兩個堆頂再算平均,避免極端整數溢位。我會讓每個輸入前綴都和排序陣列真值比較,並對包含負數、零、重複值和極值的小集合窮舉。複雜度仍是插入 O(log n)、查詢 O(1)、空間 O(n)。」

常見錯誤

  • 只平衡大小 → 兩堆的值仍可能交叉 → 單獨檢查堆頂順序不變量。
  • 只根據新值選擇分支 → 舊堆狀態可能繼續非法 → 統一執行堆間移動。
  • 用輸入整數型別直接相加 → 極值平均會溢位 → 相加前擴寬。
  • 只測有序且不重複的值 → 分支缺陷無法暴露 → 每個前綴對照並加入交錯極值。
  • 聲稱插入 O(1) → 堆的 push/pop 是對數級 → 依真實操作計算複雜度。

追問及應對

如何找到最小失敗輸入?

逐個或分段刪除輸入並重跑不變量,保留仍然失敗的最短前綴,再只分析造成第一次失效的那次插入。

為什麼重複值不需要特殊分支?

不變量使用 <=,相等值可以落在任一側。中位數由堆大小決定,與重複值的身分無關。

不信任另一份雙堆實作時如何測試?

對小輸入直接複製並排序,再按數學定義計算中位數。先窮舉小字母表序列,再補固定寬度極值與大規模隨機資料。

這套實作支援滑動視窗嗎?

不支援。刪除任意過期值需要可索引刪除,或在兩個堆增加延遲刪除計數;那是另一道獨立題。

公開來源

同類題目

相關面試工具

用 Screenshot 處理演算法題

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

查看工具