跳到主要內容

演算法

同一個問題,不同的解題思路

主題 · 考題:接雨水

考題:接雨水

每一格能積多少水,取決於左右兩邊最高的牆:暴力法 O(n²)、前綴最大值用 O(n) 空間、雙指標只要 O(1) 空間,加上單調堆疊,四種做法放在一起比。

解法
輸入
題目
給 n 道寬度為 1 的牆的高度(非負整數),下雨後總共能積多少水?例如 [0,1,0,2,1,0,1,3,2,1,2,1] 能積 6 格。
1/13
0L1234567891011R
這一步在看的格子目前積的水

height[L] ≤ height[R],右邊一定有一道至少一樣高的牆,所以第 0 格由左牆(0)決定,積 0。L 往右。

同一份輸入,所有解法
解法答案讀取高度次數額外記憶體Big O
暴力法61560O(n²)
前綴最大值63624 格O(n)
雙指標6240O(n)
單調堆疊6474 格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 On = 250n = 500n = 1,000n = 2,000成長倍數:實測(理論)
暴力法O(n²)62,750250,5001,001,0004,002,000×64 (×64)
前綴最大值O(n)7501,5003,0006,000×8.0 (×8.0)
雙指標O(n)5001,0002,0004,000×8.0 (×8.0)
單調堆疊O(n)1,5533,0886,30812,638×8.1 (×8.0)

和其他做法比

n = 250n = 500n = 1,000n = 2,000
暴力法62,750250,5001,001,0004,002,000
前綴最大值7501,5003,0006,000
雙指標5001,0002,0004,000
單調堆疊1,5533,0886,30812,638

隨機高度(0–6),數的是讀取高度的次數。n 從 250 變成 2,000,暴力法變成 64 倍,其他三種都約 8 倍。雙指標讀得最少,而且只用兩個變數。

真實世界裡的它

  • 變形:二維版本(LeetCode 407)要用堆積從外圍往內灌水;Container With Most Water(LeetCode 11)是同樣的雙指標思路。
  • 單調堆疊的寫法和「每日溫度」「最大矩形」是同一個模式:每來一個更高的值,就把比它低的都結算掉。

取捨與陷阱

  • 雙指標要從比較矮的那一邊移動:矮的那邊的牆已經確定,另一邊至少一樣高,所以水量只由這一邊決定。
  • 兩端的格子永遠積不了水;邊界處理錯會讓第 0 格或最後一格出現負數。
  • 單調堆疊算的是一層一層的水,不是一格一格:彈出時只算寬度 × 高度差那一層。

LeetCode 練習