考題:接雨水
每一格能積多少水,取決於左右兩邊最高的牆:暴力法 O(n²)、前綴最大值用 O(n) 空間、雙指標只要 O(1) 空間,加上單調堆疊,四種做法放在一起比。
解法
輸入
題目
給 n 道寬度為 1 的牆的高度(非負整數),下雨後總共能積多少水?例如 [0,1,0,2,1,0,1,3,2,1,2,1] 能積 6 格。1/13
這一步在看的格子目前積的水
height[L] ≤ height[R],右邊一定有一道至少一樣高的牆,所以第 0 格由左牆(0)決定,積 0。L 往右。
| 解法 | 答案 | 讀取高度次數 | 額外記憶體 | Big O |
|---|---|---|---|---|
| 暴力法 | 6 | 156 | 0 | O(n²) |
| 前綴最大值 | 6 | 36 | 24 格 | O(n) |
| 雙指標 | 6 | 24 | 0 | O(n) |
| 單調堆疊 | 6 | 47 | 4 格 | O(n) |
亮起來的是這一步執行的程式碼
// O(n²): for every cell, scan both sides for the tallest wall.function trapBrute(h: number[]): number { let total = 0; for (let i = 0; i < h.length; i++) { let left = 0, right = 0; for (let j = 0; j <= i; j++) left = Math.max(left, h[j]); for (let j = i; j < h.length; j++) right = Math.max(right, h[j]); total += Math.min(left, right) - h[i]; } return total;} // O(n) time, O(n) space: precompute both walls.function trapPrefix(h: number[]): number { const n = h.length, leftMax = new Array(n).fill(0), rightMax = new Array(n).fill(0); for (let i = 0; i < n; i++) leftMax[i] = Math.max(i ? leftMax[i - 1] : 0, h[i]); for (let i = n - 1; i >= 0; i--) rightMax[i] = Math.max(i < n - 1 ? rightMax[i + 1] : 0, h[i]); let total = 0; for (let i = 0; i < n; i++) total += Math.min(leftMax[i], rightMax[i]) - h[i]; return total;} // O(n) time, O(1) space: move inward from the lower side, whose wall is known.function trapTwoPointers(h: number[]): number { let lo = 0, hi = h.length - 1, leftWall = 0, rightWall = 0, total = 0; while (lo <= hi) { if (h[lo] <= h[hi]) { leftWall = Math.max(leftWall, h[lo]); total += leftWall - h[lo++]; } else { rightWall = Math.max(rightWall, h[hi]); total += rightWall - h[hi--]; } } return total;} // O(n): a stack of falling walls; each taller wall fills one layer per pop.function trapStack(h: number[]): number { const stack: number[] = []; let total = 0; for (let i = 0; i < h.length; i++) { while (stack.length && h[stack[stack.length - 1]] < h[i]) { const bottom = stack.pop()!; if (!stack.length) break; const left = stack[stack.length - 1]; total += (Math.min(h[left], h[i]) - h[bottom]) * (i - left - 1); } stack.push(i); } return total;}模型假設與範圍
- Big O 與實測步數以頁面列出的基本操作為單位;視覺化的快照、畫圖、程式高亮和輸出成本另計,不是執行時間 benchmark。
什麼時候用
- 面試官要看的是你能不能說出每一格的水由什麼決定:min(左邊最高, 右邊最高) − 自己的高度。說清楚這一句,四種解法都只是「怎麼找到那兩道牆」的差別。
- 建議的講法:先說暴力法(O(n²)),指出重複在掃同樣的兩邊,改成前綴最大值(O(n) 時間、O(n) 空間),再用雙指標把空間降到 O(1)。
和其他主題的關係
時間與空間複雜度(Big O)
| 操作 | 平均 | 最差 |
|---|---|---|
| 暴力法 每一格都把兩邊掃一遍;額外空間 O(1) | O(n²) | O(n²) |
| 前綴最大值 三趟掃描;兩個長度 n 的陣列 | O(n) | O(n) |
| 雙指標 一趟;只要兩個變數記牆高 | O(n) | O(n) |
| 單調堆疊 每格最多進出堆疊一次;最差 O(n) 空間(高度一路遞減時) | O(n) | O(n) |
空間:O(1) / O(n) / O(1) / O(n),依序是暴力法、前綴最大值、雙指標、單調堆疊
Big O 實測:n 變大時步數怎麼長
數的是:讀取高度的次數(隨機高度)
| Big O | n = 250 | n = 500 | n = 1,000 | n = 2,000 | 成長倍數:實測(理論) | |
|---|---|---|---|---|---|---|
| 暴力法 | O(n²) | 62,750 | 250,500 | 1,001,000 | 4,002,000 | ×64 (×64) |
| 前綴最大值 | O(n) | 750 | 1,500 | 3,000 | 6,000 | ×8.0 (×8.0) |
| 雙指標 | O(n) | 500 | 1,000 | 2,000 | 4,000 | ×8.0 (×8.0) |
| 單調堆疊 | O(n) | 1,553 | 3,088 | 6,308 | 12,638 | ×8.1 (×8.0) |
和其他做法比
| n = 250 | n = 500 | n = 1,000 | n = 2,000 | |
|---|---|---|---|---|
| 暴力法 | 62,750 | 250,500 | 1,001,000 | 4,002,000 |
| 前綴最大值 | 750 | 1,500 | 3,000 | 6,000 |
| 雙指標 | 500 | 1,000 | 2,000 | 4,000 |
| 單調堆疊 | 1,553 | 3,088 | 6,308 | 12,638 |
隨機高度(0–6),數的是讀取高度的次數。n 從 250 變成 2,000,暴力法變成 64 倍,其他三種都約 8 倍。雙指標讀得最少,而且只用兩個變數。
真實世界裡的它
- 變形:二維版本(LeetCode 407)要用堆積從外圍往內灌水;Container With Most Water(LeetCode 11)是同樣的雙指標思路。
- 單調堆疊的寫法和「每日溫度」「最大矩形」是同一個模式:每來一個更高的值,就把比它低的都結算掉。
取捨與陷阱
- 雙指標要從比較矮的那一邊移動:矮的那邊的牆已經確定,另一邊至少一樣高,所以水量只由這一邊決定。
- 兩端的格子永遠積不了水;邊界處理錯會讓第 0 格或最後一格出現負數。
- 單調堆疊算的是一層一層的水,不是一格一格:彈出時只算寬度 × 高度差那一層。