
Stack 是最單純的資料結構之一,但它撐起了你程式裡最多事情。
先講結論
Stack = 後進先出(LIFO)。所有操作 O(1)。你每次呼叫函式、按 Ctrl+Z、甚至瀏覽器按上一頁,底層都是 Stack。它的規則簡單到只有一條:只能從最上面放,只能從最上面拿。
一疊盤子的規則
┌────┐
│ 30 │ ← top(最後放的,最先拿)
├────┤
│ 20 │
├────┤
│ 10 │ ← bottom(最先放的,最後拿)
└────┘
四個操作,全部 O(1):
| 操作 | 做什麼 |
|---|---|
push(x) | 放到最上面 |
pop() | 拿走最上面的 |
peek() | 看一眼最上面(不拿走) |
isEmpty() | 是不是空的 |
就這樣。沒有「從中間拿」這種操作。想從中間拿?那你要的是 Array。
用 Array 還是 Linked List 實作?
兩種都行,各有適合的場景:
// Array 版:用一個 top 指標追蹤頂端位置
class ArrayStack {
Object[] data;
int top = 0;
void push(x) { data[top++] = x; }
Object pop() { return data[--top]; }
}
// Linked List 版:head 就是 top
class LinkedStack {
Node head;
void push(x) { addFirst(x); }
Object pop() { return removeFirst(); }
}實務上,Array 版因為 cache 友善、不需要額外指標空間,幾乎總是更快。Java 的 Deque(ArrayDeque)就是官方推薦拿來當 Stack 用的——比那個老舊的 Stack 類別好用多了。
你每天都在用 Stack
Call Stack:函式呼叫堆疊
你寫的每一個函式呼叫,背後都在操作 call stack:
void a() { b(); }
void b() { c(); }
void c() { throw new Error(); }
// Error 的 stack trace:
// at c() ← top,最後進的
// at b()
// at a()
// at main() ← bottom,最先進的遞迴太深炸掉的 StackOverflowError?就是這個 stack 滿了。
括號匹配
這是 Stack 最經典的應用題。遇到左括號就 push,遇到右括號就 pop 出來看配不配:
輸入: {[()]}
{ → push stack: ['{']
[ → push stack: ['{', '[']
( → push stack: ['{', '[', '(']
) → pop='(' ✓ stack: ['{', '[']
] → pop='[' ✓ stack: ['{']
} → pop='{' ✓ stack: []
結束時 stack 空了 → 匹配成功
如果你用過 ESLint 或任何 linter,它檢查括號配對的原理就是這個。
瀏覽器上一頁
瀏覽 A → B → C,按上一頁:
back stack: [A, B, C] → pop C,顯示 B
forward stack: [C] → 存起來,按下一頁用
Undo/Redo
每次操作 push 到 undo stack。按 Ctrl+Z 就 pop 出來,同時 push 到 redo stack。
Stack 和遞迴的關係
任何遞迴都可以用 Stack 改寫成迭代。因為遞迴本身就是在用 call stack——你只是把系統幫你管的 stack 改成自己管而已。DFS(深度優先搜尋)就是最典型的例子。
Stack 的哲學:最後進來的人最先處理。聽起來很不公平?但你的瀏覽器、編輯器、編譯器都靠它活著。