具代表性的面試主題

演算法面試:如何用雙指標解決接雨水?

程式題困難
OfferCC 編輯團隊發佈 更新

題幹

給定 n 個非負整數 height,每個整數表示寬度為 1 的柱子高度,請計算下雨後能接住的總水量。要求實作 O(n) 時間、O(1) 額外空間的演算法,證明雙指標移動規則正確,並說明邊界案例、複雜度和替代方案。

題目與適用場景

給定長度為 n 的非負整數陣列 height,height[i] 表示第 i 根寬度為 1 的柱子高度。 計算這些柱子下雨後能接住的總水量。例如:

text
height = [4, 2, 0, 3, 2, 5]
結果 = 9

目標是 O(n) 時間和 O(1) 額外空間。標準限制為 1 <= n <= 20000、 0 <= height[i] <= 100000;輸入不含負高度,也不要求回傳每一欄的水量。

這道題有直接的公開面試證據:2025 年 6 月的一篇百度 Go 後端實習面經把「接雨水」記錄為三道 現場手寫題之一;另一份公開電話面試記錄還追加了「從指定位置倒入有限水量」的變體。力扣中英文 題目把它標為困難題,並同時列出陣列、雙指標、動態規劃、堆疊和單調堆疊等標籤。核心考查是從局部 公式推導線性演算法並證明指標移動,因此分類為 coding,與實作語言和職位方向無關。

面試官考察點

第一,能否先寫出每一欄的正確水量。第 i 欄能達到的水面由左側最高柱和右側最高柱中較矮的 一根決定,不是由相鄰柱決定。若 leftMax[i] 與 rightMax[i] 都包含目前位置,則該欄水量是 min(leftMax[i], rightMax[i]) - height[i]。

第二,能否把前後綴陣列壓縮為常數空間。儲存所有位置的左右最高值很容易得到 O(n) 時間, 雙指標進一步利用「較矮的已知邊界已經足以確定一側答案」,每輪只結算一欄。

第三,是否真正理解移動規則。只背誦「移動較矮指標」不夠;強回答會給出不變量,並分別討論目前 柱子更新邊界和不更新邊界的情況,證明尚未掃描區域不會改變剛結算的水量。

最後,能否準確比較替代方案。前後綴陣列最容易解釋;單調堆疊按凹槽橫向結算,適合相關堆疊題; 雙指標最省空間。三種方法都可以是正確答案,但複雜度、證明方式和擴充能力不同。

回答前需要釐清的問題

  • 柱子寬度都為 1 嗎? 是;若寬度不同,每欄的水深還要乘以對應寬度。
  • 高度是否保證非負? 是;負高度沒有約定的物理含義,不能默默當成更深的坑。
  • 空陣列是否允許? 標準限制不允許;本文實作對空陣列自然回傳 0,但介面契約仍應明確。
  • 回傳總量還是逐欄結果? 主問題只回傳總量;逐欄結果需要輸出陣列,因此空間至少為 O(n)。
  • 是否必須達到常數額外空間? 是;否則前後綴陣列是更容易講清楚的線性方案。
  • 數值型別會溢位嗎? 應按真實限制估算上界;限制變化或使用較窄整數型別時要改用更寬累加器。
  • 輸入是一維高度圖還是二維網格? 本題是一維;二維接雨水需要從外邊界向內擴展,演算法不同。
  • 是否允許修改輸入? 不需要,示範實作只讀取 height。

30 秒回答框架

「某欄的水量等於左右最高柱較小值減去本欄高度。前後綴陣列能在線性時間算出所有左右最高值, 但要 O(n) 空間。我用左右指標、leftMax 和 rightMax 壓縮空間:它們分別記錄已經掃描過的 左側和右側最高值。若 leftMax <= rightMax,右側已知邊界已經不低於 leftMax;左指標目前柱 若不更新左界,水量就確定為 leftMax - height[left],若更新左界則水量為零。然後左指標右移; 另一側完全對稱。每欄只處理一次,所以時間 O(n)、額外空間 O(1)。」

分步深入解答

第一步:先定義逐欄答案。

令:

text
L[i] = max(height[0..i])
R[i] = max(height[i..n-1])
water[i] = min(L[i], R[i]) - height[i]

因為 L[i] 和 R[i] 都包含 height[i],兩者都不會小於目前高度,所以公式不需要額外取零。 總水量是所有 water[i] 的總和。這個公式也解釋了為什麼只看左右相鄰柱會失敗:遠處更高的邊界 可能決定整個凹槽的水面。

第二步:用基礎方案建立正確基準。

方案時間額外空間特點
每欄向兩側掃描最高值O(n^2)O(1)公式直接,但重複掃描
前後綴最高值陣列O(n)O(n)最容易實作和證明
單調遞減堆疊O(n)O(n)按凹槽橫向結算寬度和深度
雙指標O(n)O(1)每輪確定一側的一欄

前後綴方案先從左到右建立 L,再從右到左建立 R,最後套公式。雙指標不是換了一套水量 定義,只是在尚未儲存完整 L、R 時,利用已經足夠的邊界提前結算。

第三步:建立迴圈不變量。

每輪開始時維護:

  1. left 左側的所有位置已經按逐欄公式正確結算。
  2. right 右側的所有位置已經正確結算。
  3. leftMax 是已掃描左區間 height[0..left-1] 的最大值;空區間取 0。
  4. rightMax 是已掃描右區間 height[right+1..n-1] 的最大值;空區間取 0。
  5. water 是所有已結算位置的水量總和。

未處理區間始終是 [left, right]。每輪必須證明其中至少有一端可以永久結算,隨後縮小這個區間。

第四步:證明為什麼可以移動較小邊界的一側。

若 leftMax <= rightMax,考慮 height[left]:

  • 若目前柱高於 leftMax,更新後它成為新的左側最高柱。該欄的左邊界就是自身,水量為 0。
  • 若目前柱不高於 leftMax,真實右側最高值至少為已經見到的 rightMax,而

rightMax >= leftMax。因此左右邊界較小值確定為 leftMax,該欄水量正好是 leftMax - height[left]。

兩種情況都不需要知道未掃描中間區域的具體形狀,所以左欄可以永久結算。若 leftMax > rightMax,對右欄做完全對稱的推理。這個證明也說明比較的是已知最高邊界,不是簡單 比較兩個相鄰柱或隨意猜測哪邊更低。

第五步:實作雙指標。

python
def trap(height: list[int]) -> int:
    left = 0
    right = len(height) - 1
    left_max = 0
    right_max = 0
    water = 0

    while left <= right:
        if left_max <= right_max:
            left_max = max(left_max, height[left])
            water += left_max - height[left]
            left += 1
        else:
            right_max = max(right_max, height[right])
            water += right_max - height[right]
            right -= 1

    return water

迴圈使用 left <= right,確保指標相遇時最後一欄也會結算。先更新目前側最高值再累加,既能讓 新最高柱貢獻零,也能保證差值非負。空陣列時 right = -1,迴圈不執行並回傳 0。

第六步:走查 [4, 2, 0, 3, 2, 5]。

text
處理位置  目前高度  結算側  更新後邊界  新增水量  累計
0         4         左      leftMax=4   0         0
5         5         右      rightMax=5  0         0
1         2         左      leftMax=4   2         2
2         0         左      leftMax=4   4         6
3         3         左      leftMax=4   1         7
4         2         左      leftMax=4   2         9

右邊的高度 5 為左側剩餘各欄提供了足夠高的已知邊界,所以後續連續結算左側。每欄只出現一次, 不會按凹槽重複計水。

第七步:證明終止、正確性與複雜度。

初始化時兩側已處理區間都為空,不變量成立。第四步證明每輪新增的一欄按逐欄公式結算正確,且 更新後的 leftMax 或 rightMax 滿足下一輪定義。每輪至少讓 left 增加一或讓 right 減少一, 所以有限步後 left > right;此時所有位置都已正確結算,總和正確。

每個位置只被存取一次,時間複雜度為 O(n)。除指標、兩個邊界和累加器外不配置隨輸入增長的 儲存,額外空間為 O(1)。

第八步:用公式 oracle 和邊界案例驗證。

固定案例至少涵蓋:單柱、兩柱、全零、嚴格遞增、嚴格遞減、全等高、多個獨立凹槽、平底凹槽、 標準範例,以及 [3, 0, 3]。最後一個案例能發現把迴圈誤寫成 left < right 而漏算相遇位置的錯誤。

再對短隨機非負陣列建立前後綴 L、R,用逐欄公式作為 oracle,與雙指標結果比較。同時檢查結果 非負、鏡像陣列的總水量相同、在陣列兩端加入高度為零的柱子不改變原有總水量。隨機差分用於發現 實作錯誤,正確性仍由不變量證明承擔。

高品質示範回答

「我先把問題變成逐欄計算:位置 i 的水深是 min(max(height[0..i]), max(height[i..n-1])) - height[i]。儲存兩個前後綴陣列能做到 O(n) 時間和 O(n) 空間;若要求常數空間,我會用雙指標。

迴圈中,leftMax 和 rightMax 分別記錄指標外側已經掃描過的最高柱。若 leftMax <= rightMax,我結算左指標:目前柱若更新左界,水量為零;否則右側已有的 rightMax 已經不低於左界,所以未知中間區域不可能把較小邊界降到 leftMax 以下,該欄水量確定 為 leftMax - height[left]。然後左指標右移。另一側完全對稱。

每輪永久處理一欄,終止時所有欄都被處理,因此總時間 O(n);只儲存常數個變數,額外空間 O(1)。我會測試空或短輸入、單調陣列、等高陣列、多個凹槽和 [3, 0, 3],並用前後綴公式對 隨機小陣列做差分驗證。」

常見錯誤

  • 用左右最高值中的較大者減目前高度 → 水會從較矮邊界溢出 → 始終使用兩側最高值的較小者。
  • 只比較相鄰柱 → 遠處邊界被忽略 → 先寫出包含整個左右區間的逐欄公式。
  • 先累加再更新目前側最高值 → 新最高柱可能產生負水量 → 先更新邊界,再計算非負差值。
  • 迴圈寫成 left < right → [3, 0, 3] 的中間欄可能未處理 → 讓相遇位置也進入迴圈。
  • 移動較大邊界的一側卻不給證明 → 未知另一側可能仍決定較小水面 → 只結算已有另一邊界支撐的較小側。
  • 把「盛最多水的容器」公式套進來 → 寬度 × 邊界高 會把柱體和多個欄位重複計算 → 本題按每欄水深求和。
  • 說雙指標每輪常數時間就結束分析 → 沒有解釋為什麼答案不會被未來邊界改變 → 給出邊界不變量和兩分證明。
  • 聲稱單調堆疊空間也是 O(1) → 嚴格單調輸入可儲存 n 個下標 → 準確寫為最壞 O(n)。
  • 只跑一個標準範例 → 相遇、單調和等高邊界未涵蓋 → 加入固定案例和前後綴 oracle。

追問及應對

追問一:為什麼不直接比較 height[left] 和 height[right]?

另一種正確寫法可以比較兩端目前高度,但它需要配套的不變量和更新順序。本文比較 leftMax 與 rightMax,因為它們直接對應逐欄公式中的邊界。不能把一種寫法的比較條件與另一種 寫法的證明混在一起;面試時選擇一套並保持程式碼、口述和證明一致。

追問二:如果要回傳每一欄接住的水量呢?

處理某欄時把增量寫入長度為 n 的結果陣列,再求和或同時累加。時間仍為 O(n),但輸出本身需要 O(n) 空間。若呼叫方只消費串流結果,還要注意雙指標的結算順序不是從左到右,必須同時回傳下標 或最後重新排序。

追問三:如果高度只能從左到右串流到達呢?

精確答案依賴未來的右邊界,不能在固定記憶體下對每一欄立即定案。可以用單調堆疊儲存尚未閉合的凹槽, 在更高右邊界到達時結算;最壞仍需 O(n) 記憶體。若記憶體有硬上限,就要允許近似、外部儲存或第二遍 掃描,不能繼續承諾原問題的精確常數空間。

追問四:柱子寬度不同怎麼辦?

若第 i 根柱覆蓋獨立寬度 width[i],邊界高度邏輯不變,該欄體積改為 waterDepth[i] * width[i]。若輸入描述的是不規則橫座標和柱間空隙,則要先明確定義每段橫向區間 的高度;不能直接把相鄰中心點距離當成整根柱寬。

追問五:二維高度網格如何接雨水?

二維單元的水面由整個外邊界約束,兩個方向的指標不夠。常用方案把所有邊界格放入最小堆,每次 取目前最低邊界向內擴展;較低鄰格貢獻高度差,並以兩者較大值作為新的有效邊界。配合存取標記, m × n 網格的時間為 O(mn log(mn)),空間為 O(mn)。

追問六:什麼時候更適合用單調堆疊?

如果追問要求列出每個被左右邊界閉合的凹槽、解釋橫向寬度,或繼續轉到柱狀圖矩形等單調堆疊模式, 堆疊方案更自然。它維護高度單調遞減的下標;遇到更高柱時彈出凹槽底,使用新堆疊頂端和目前柱作為左右 邊界,按「有效寬度 × 新增水層高度」結算。總時間仍為 O(n),最壞額外空間為 O(n)。

公開來源

同類題目

相關面試工具

用 Screenshot 處理演算法題

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

查看工具