cover

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 的 DequeArrayDeque)就是官方推薦拿來當 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 的哲學:最後進來的人最先處理。聽起來很不公平?但你的瀏覽器、編輯器、編譯器都靠它活著。