cover

Stack 是最單純的資料結構之一,但它撐起了你程式裡最多事情。

最後放的最先拿

Stack = 後進先出(LIFO)。所有操作 O(1)。你每次呼叫函式、按 Ctrl+Z、甚至瀏覽器按上一頁,底層都是 Stack。它的規則簡單到只有一條:只能從最上面放,只能從最上面拿。

一疊盤子的規則

      ┌────┐
      │ 30 │  ← top(最後放的,最先拿)
      ├────┤
      │ 20 │
      ├────┤
      │ 10 │  ← bottom(最先放的,最後拿)
      └────┘

四個操作,全部 O(1):

操作做什麼
push(x)放到最上面
pop()拿走最上面的
peek()看一眼最上面(不拿走)
isEmpty()是不是空的

就這樣。沒有「從中間拿」這種操作。想從中間拿?那你要的是 Array。

push 和 pop 全都只動最上面那一格,這就是它們 O(1) 的原因:

push(40)              pop() → 40
┌────┐  ┌────┐        ┌────┐  ┌────┐
│ 30 │  │ 40 │←新top   │ 40 │←取出 │ 30 │←新top
├────┤  ├────┤        ├────┤  ├────┤
│ 20 │→ │ 30 │        │ 30 │→ │ 20 │
├────┤  ├────┤        ├────┤  ├────┤
│ 10 │  │ 20 │        │ 20 │  │ 10 │
└────┘  ├────┤        ├────┤  └────┘
        │ 10 │        │ 10 │
        └────┘        └────┘

下面那疊完全沒被碰到——不管堆多高,push/pop 的成本都一樣。

🎬 互動視覺化線性結構操作動畫 — 切到 Stack 分頁,看 push/pop 只在頂端進出,對照 Queue 從兩端動的差別。

用 Array 還是 Linked List 實作?

兩種都行,各有適合的場景:

// Array 版:用一個 top 指標追蹤頂端位置
class ArrayStack {
    Object[] data;
    int top = 0;
    void push(Object x) { data[top++] = x; }
    Object pop() { return data[--top]; }
}
 
// Linked List 版:head 就是 top
class LinkedStack {
    Node head;
    void push(Object x) { addFirst(x); }    // addFirst/removeFirst 為示意
    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 的哲學:最後進來的人最先處理。聽起來很不公平?但你的瀏覽器、編輯器、編譯器都靠它活著。

接下來往哪走