堆疊(Stack)

雜湊、堆疊與佇列優先度 5/5 — 必備 — 幾乎每一輪面試都會出現必備 更新於 Oct 9, 2026
章節優先度優先度 5/5 — 必備 — 幾乎每一輪面試都會出現必備優先度 4/5 — 高價值 — 這裡有缺口就會掉關高價值優先度 3/5 — 值得會 — 多半是必備模式的變形值得會優先度 2/5 — 冷門 — 讀過一次即可,除非目標公司已知會問冷門只標在真正需要的章節上 —— 沒標的是背景/參考資料。

範圍 — LIFO 的基本功,加上堆疊的經典模板:括號配對、min-stack、單調堆疊的精簡版、用顯式堆疊做走訪,以及作用域/上下文帳本。 另見:stack_expression_parsing.md — 計算機、decode string 與後綴式求值,整個運算式剖析家族;stack_examples.md — 這些模板背後的解題實作庫;monotonic_stack.md — next greater/previous smaller/span 類問題的深入版;queue.md — FIFO 的對照組;iterator.md — 以堆疊為底的迭代器。

LeetCode 題目清單

時間複雜度

資料結構 搜尋 插入 刪除 最小/最大
堆疊 O(n) O(1) O(1) O(n)

插入 = push,刪除 = pop,peek —— 全都發生在頂端,全都是 O(1)。最小/最大值可以靠一個輔助的 min/max 堆疊做到 O(1)(見 monotonic_stack.md)。空間是 O(n)。

總覽

堆疊是具有後進先出(LIFO)性質的資料結構。每個操作都在堆疊頂端加入或移除元素。

關鍵性質

  • 複雜度:見上方的時間複雜度表
  • 核心原理:最後放進去的元素最先被拿出來
  • 適用場景:牽涉到順序反轉、樣式配對,或是要維護上下文的問題

參考資料

題型分類

十種形狀幾乎涵蓋所有堆疊題。在哪裡這一欄告訴你程式碼放在下面哪個模板,或是解法搬到了哪一份表。

題型 堆疊裡放的是什麼 LC 在哪裡
括號/巢狀驗證 還欠一個右括號的左括號 20, 921, 1541, 1614 模板 2
括號修復/計量 未配對括號的索引 1249, 32, 856 模板 2 § LC 1249、stack_examples.md
單調 —— next greater/smaller 還在等答案的元素 496, 503, 739, 84, 907, 2104 模板 3、monotonic_stack.md
單調 —— 貪婪移除 目前建出來最好的前綴 402, 316, 1081, 1673 stack_examples.md
單調 —— span 累積 [value, span] 配對,串流式處理 901, 735 stack_examples.md
放 [element, count] 配對的堆疊 前綴的遊程壓縮表示 1047, 1209, 1544 stack_examples.md
堆疊上的 O(1) 彙總值 每一層一個 min/max/delta 的狀態 155, 716, 1381, 895 模板 4
運算式剖析 運算元/延後的項/未關閉的作用域 224, 227, 772, 394, 150, 682 stack_expression_parsing.md
作用域/上下文帳本 外層的上下文,以深度為鍵 388, 636, 591, 71 模板 6
順序反轉/暫停的走訪 還沒做完的工作 144, 145, 173, 341, 445 模板 5

值得知道的堆疊變形

  • 單一堆疊
  • 用堆疊做出佇列
    • LC 232(用 2 stack)
  • 用佇列做出堆疊
  • 放 (char, count) 配對的堆疊
    • 存 [element, count] 配對,而不是原始元素
    • LC 1047(k=2 的特例,單純 pop)
    • LC 1209(移除 k 個連續重複字元)
    • LC 1544(Make The String Great)
    • LC 394(Decode String,用堆疊記重複次數)
    • LC 726(Number of Atoms)

模板與演算法

模板對照表

模板 堆疊元素 迴圈形狀 複雜度 什麼時候用
1 —— 基本操作 任何東西 — 每次操作 O(1) push/pop/peek 的慣用寫法
2 —— 括號配對 左括號字元 掃一趟,遇右括號就 pop O(n)/O(n) 驗證巢狀,且括號種類 >1
3 —— 單調堆疊 (value, index) for 裡包一層 while O(n)/O(n) next greater/smaller/span
4 —— Min stack 值 + 每一層一個彙總狀態 — 每次操作 O(1) O(1) getMin()/getMax();前綴彙總、路徑狀態
5 —— 顯式堆疊 待處理的節點 while stack O(n)/O(h) 迭代式走訪、順序反轉
6 —— 作用域帳本 每層深度的外層上下文 掃一趟,截到當前深度 O(n)/O(depth) 有縮排的輸入、start/end 事件

模板 1:堆疊基本操作

push(放入):

java
// Java
Stack<Integer> stack = new Stack<>();
stack.push(element);  // O(1)
python
# Python
stack = []
stack.append(element)  # O(1)

pop(移除頂端):

java
// Java
int top = stack.pop();  // O(1), throws if empty
python
# Python
top = stack.pop()  # O(1), raises if empty

peek(看頂端):

java
// Java
int top = stack.peek();  // O(1), throws if empty
if (!stack.isEmpty()) {
    top = stack.peek();
}
python
# Python
top = stack[-1]  # O(1), raises if empty
if stack:
    top = stack[-1]

模板 2:括號配對 —— LC 20 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

面試出現頻率最高的堆疊模式。把左括號 push 進去,遇到右括號就檢查頂端是不是它的另一半。 只要括號不只一種,就非用堆疊不可(計數器不夠),因為順序有意義:([)] 是不合法的。

text
Core Idea:
  - Opener  -> push it (we owe a matching closer)
  - Closer  -> stack must be non-empty AND top must be the matching opener
               (else fail fast)
  - End     -> stack must be EMPTY (no unmatched openers left)

When to Use:
  - Validate / repair / measure balanced sequences
  - Any "nesting must be well-formed" check (brackets, tags, expressions)

Counter vs Stack (interview discriminator):
  - ONE bracket type only  -> a running balance counter is enough, O(1) space
                              (LC 921, LC 1541, LC 1614)
  - MULTIPLE bracket types -> MUST use a stack (LC 20)
  - Need the POSITION of the offending bracket -> stack of INDICES
                              (LC 1249, LC 32)

Similar LC:
  - LC 20    Valid Parentheses                        (base template)
  - LC 1249  Minimum Remove to Make Valid Parentheses (stack of indices -> delete;
                                                    also two counter scans, no stack)
  - LC 921   Minimum Add to Make Parentheses Valid    (balance counter)
  - LC 32    Longest Valid Parentheses                (index stack + `-1` base;
                                                    also 1D DP, or two counter scans for O(1) space)
  - LC 856   Score of Parentheses                     (stack of partial scores)
  - LC 1541  Minimum Insertions to Balance a Parentheses String  ( `(` needs `))` )
  - LC 1614  Maximum Nesting Depth of the Parentheses (max depth = max balance)
java
// java
// LC 20 - Valid Parentheses
// IDEA: STACK — push openers, pop-and-verify on closers, stack must end empty
// time = O(n), space = O(n)
public boolean isValid(String s) {

    // closer -> its matching opener
    Map<Character, Character> pairs = new HashMap<>();
    pairs.put(')', '(');
    pairs.put(']', '[');
    pairs.put('}', '{');

    Deque<Character> st = new ArrayDeque<>();

    for (char c : s.toCharArray()) {
        if (pairs.containsKey(c)) {
            /**
             *  NOTE !!!  a closer needs BOTH checks:
             *   1) stack NOT empty  (e.g. ")" alone)
             *   2) top is the matching opener (e.g. "(]" must fail)
             *
             *  NOTE !!! unbox to `char` before comparing —
             *  comparing two Character objects with `!=` compares REFERENCES.
             */
            if (st.isEmpty()) {
                return false;
            }
            char top = st.pop();
            if (top != pairs.get(c)) {
                return false;
            }
        } else {
            st.push(c);
        }
    }

    /** NOTE !!! leftover openers => invalid (e.g. "(((") */
    return st.isEmpty();
}
python
# python
# LC 20 - Valid Parentheses
# IDEA: STACK — push openers, pop-and-verify on closers, stack must end empty
# time = O(n), space = O(n)
class Solution(object):
    def isValid(self, s):
        pairs = {')': '(', ']': '[', '}': '{'}
        stack = []
        for c in s:
            # closer
            if c in pairs:
                # NOTE !!! empty stack OR wrong partner -> invalid
                if not stack or stack.pop() != pairs[c]:
                    return False
            # opener
            else:
                stack.append(c)
        # NOTE !!! leftover openers -> invalid
        return not stack

括號家族 —— 同一個模板的四種變形(解法在 stack_examples.md):

LC 變形 堆疊裡放什麼
1249 不只驗證,還要修復 —— 見下方 未配對 ( 的索引 —— 或不用堆疊:兩趟計數掃描,每個方向一趟
921 只有一種括號,用計數器就夠 —— O(1) 空間 什麼都不放(堆疊退化成一個 size)
32 最長合法區段的長度 索引,外加一個 -1 當基準哨兵
856 從巢狀結構摺出一個分數 每一層的部分結果

括號修復變形 —— push 的是索引,不是字元(LC 1249)優先度 4/5 — 高價值 — 這裡有缺口就會掉關

跟 LC 20 差在哪:LC 20 只回答是/否,所以堆疊可以只放左括號字元、不必記得它們在哪。LC 1249 必須刪掉出問題的括號,所以需要它們的位置 —— push 每個 ( 的索引。這題只有一種括號,pop 時不用比對:任何一個還開著的 ( 都能配任何一個 )。

LC 20 —— 驗證 LC 1249 —— 修復
堆疊裡放 左括號字元 未配對 ( 的索引
遇到 ) 而堆疊是空的 return False 把索引 i 標記為要刪,繼續掃
遇到 ) 而堆疊非空 pop,檢查是不是另一半 pop —— 這一對保留
掃描結束 堆疊必須是空的 還留在堆疊上的每個索引都是未配對的 ( → 一樣刪掉
輸出 一個布林值 s 去掉被標記的位置

為什麼這就是最少。 一個 ) 如果前面沒有開著的 (,右邊的任何東西都不可能配到它;掃完後還留在堆疊上的 (,後面也沒有 ) 可配。每一次刪除都是被迫的,其餘全部成對,所以沒有刪掉任何不必刪的括號。

python
# python
# LC 1249 - Minimum Remove to Make Valid Parentheses
# IDEA: STACK OF '(' INDICES — blank out unmatched ')' on sight, unmatched '(' after the scan
# time = O(n), space = O(n)
class Solution(object):
    def minRemoveToMakeValid(self, s):
        chars = list(s)      # mutable copy, so a position can be blanked in O(1)
        open_idx = []        # indices of '(' still waiting for a ')'

        for i in range(len(chars)):
            if chars[i] == '(':
                open_idx.append(i)
            elif chars[i] == ')':
                if open_idx:
                    open_idx.pop()     # matched -> keep both
                else:
                    chars[i] = ''      # NOTE !!! no '(' open before it -> can never match
            # letters never affect the balance

        # NOTE !!! the second flush: what is left on the stack are unmatched '('
        for i in open_idx:
            chars[i] = ''

        return ''.join(chars)  # '' entries vanish in the join
text
s = "a)b(c)d"

i  ch   action                          open_idx   chars
0  a    letter                          []         a ) b ( c ) d
1  )    stack empty -> blank index 1    []         a _ b ( c ) d
2  b    letter                          []
3  (    push 3                          [3]
4  c    letter                          [3]
5  )    pop 3 (matched)                 []
6  d    letter                          []
end     nothing left to flush                      -> "ab(c)d"

s = "))(("   ->  ')' x2 blanked on sight, '(' x2 left on the stack and flushed  ->  ""

陷阱:

  • 忘了第二次清掃。 迴圈本身只抓得到多出來的 );"(()" 需要在迴圈結束後把剩下的索引刪掉。
  • 不要寫 len(s) <= 1: return s 這種捷徑。 "(" 和 ")" 都只有一個字元,而且都不合法 —— 答案是 ""。一般迴圈本來就處理得了短字串。
  • 不要在迴圈裡刪。 s = s[:i] + s[i+1:] 每刪一次是 O(n)(總共 O(n²)),而且會讓已經在堆疊上的每個索引全部位移。先標記,最後一次重建。
  • 用一個存要刪索引的 set 也行(to_remove.update(open_idx),再保留 i not in to_remove)。演算法一樣,只是多一個結構。把 list 副本的位置清成空字串是比較精簡的寫法。

Java 版本,以及不用堆疊的兩趟計數掃描(左→右刪掉多出來的 ),右→左刪掉多出來的 ( —— 面試官接下來會問的 follow-up)在 stack_examples.md § 13;同一個「先標記再重建」的想法,從字串技巧的角度講,在 string.md 模板 7。


模板 3:單調堆疊 —— Next Greater/Smaller —— LC 739 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

核心想法:堆疊裡放的是還在等答案的元素,並保持單調順序。把比較方向翻過來就換一個方向 —— top < cur 時 pop 是找 next greater,top > cur 時 pop 是找 next smaller。每個元素只 push 一次、最多 pop 一次,所以 for 裡包 while 仍然是 O(n)。

這份表只保留一個精簡版。完整家族(previous smaller、span、直方圖、子陣列最小/最大值總和,以及貪婪移除的各種變形)是 monotonic_stack.md 的主題;解題實作在 stack_examples.md。

  • 存 (value, index) —— 索引才能把「找到了」變成一段距離或一個寬度。
python
# python
# LC 739, LC 503 - Find next `big number`
# ...
stack = [] # [[idx, val]]
for i, val in enumerate(tmp):
    while stack and stack[-1][1] < val:
        _idx, _val = stack.pop(-1)
        res[tmp[_idx]] = i - _idx
    stack.append([i, val]) 
# ...
java
// java
// LC 239
// LC 496
// ...

// Traverse the array from right to left
for (int i = 0; i < n; i++) {
    // Maintain a decreasing monotonic stack
    /** NOTE !!! below */
    while (!stack.isEmpty() && nums[stack.peek()] <= nums[i]) {
        stack.pop();  // Pop elements from the stack that are smaller or equal to the current element
    }
    
    // If stack is not empty, the next greater element is at the top of the stack
    if (!stack.isEmpty()) {
        result[i] = nums[stack.peek()];
    }
    
    // Push the current element's index onto the stack
    stack.push(i);
}

// ...
想找的東西 pop 的條件 答案怎麼讀出來 LC
next greater 元素/下一個更暖的日子 top < cur cur 把 top pop 掉的當下 496, 503, 739
next smaller 元素,或左右邊界 top > cur cur 把 top pop 掉的當下 84, 907, 2104
往回到上一個更大值的 span top <= cur,並累加被 pop 掉的 span 累加起來的計數 901
環狀陣列 一樣的做法,跑 nums * 2 並用 idx % n 一樣 503

模板 4:Min Stack —— O(1) getMin —— LC 155 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

模式:每一層一個彙總值 —— min_values[i] == min(stack[0..i])

題目要的那個堆疊就是普通堆疊。竅門在於再開一個逐層對映的陣列,裡面放的不是元素本身, 而是*「這一層以下所有東西」對這個查詢的答案*。LC 155 的查詢是「最小值」,所以 min_values = 每一層 stack 對應的 minimum。這是前綴彙總在設計題裡的長相, 而同一個想法在堆疊之外也到處出現 —— 見本模板最後的 經典題表。

一般形式 —— 每一層一個彙總值

text
Invariant:  min_values[i] = min(stack[0], stack[1], ..., stack[i])
            -> min_values is NOT the elements sorted; it is a STATE per layer

push(v):    min_values.append(min(v, min_values[-1]))   # new layer, new state
pop():      min_values.pop()                             # the popped layer's state goes with it
getMin():   min_values[-1]                               # the top layer's state IS the answer

push -2, 0, -3        then pop()
  stack      = [-2,  0, -3]      stack      = [-2,  0]
  min_values = [-2, -2, -3]      min_values = [-2, -2]   <- getMin() = -2, nothing recomputed
                  ^    ^    ^
                  |    |    min of all three
                  |    min of the first two (0 did not beat -2)
                  min of the first one

從這個不變量直接推出三件事:

  • 兩個陣列永遠等長 —— 每次 push 兩邊都 append,每次 pop 兩邊都移掉。pop() 裡不需要 if,也不用把 pop 出來的值跟最小值比:min_values[-1] 是屬於那一層的狀態,不是某個元素的 副本,所以把那一層 pop 掉就是全部的工作。這就是為什麼下面的 V0 不需要省空間變形那個 stack.pop() == mins[-1] 的檢查。
  • 重複值不用特別處理。 push(0); push(0); pop() 之後 min_values = [0],因為每個 0 都有自己的一層。只存新最小值的那種變形必須寫 <= 才能做對 —— 這是 LC 155 的經典 bug(見 monotonic_stack.md § 2-16)。
  • 任何是前綴函數的彙總值都一樣可行 —— max、累加和、累積 GCD、待套用的 delta(LC 1381)、 出現次數(LC 895)。把 push 裡的 min(...) 換掉,其他一行都不用動。

為什麼是 O(1) —— 狀態在 push 時就算好了

getMin() 只讀一個陣列格子。不需要掃描,因為工作已經搬到 push 那邊去做了,而 push 只要合併 兩個數字:新值,以及下一層的答案。整個論證就這樣 —— 每個操作只碰兩個陣列的頂端,其他什麼 都不碰。

保留每一層的狀態而不是只用一個 self.min 變數,關鍵在 pop()。單一變數在更小的值進來時可以往下 調,但那個值離開時卻調不回去 —— 更早的最小值已經被覆寫掉了。陣列記得,因為它從來沒被覆寫; 它一直就在下一層。

做法 push pop getMin 差在哪裡
單一 self.min 變數 O(1) O(n) O(1) 把最小值 pop 掉之後,得重新掃一遍找前一個最小值
heap(heapq) O(log n) O(log n),需搭配 lazy deletion O(1) 為一個堆疊根本用不到的排序付 log 的代價
{value: count} map O(1) O(1) O(n) min(counts) 是一次掃描(min-stack.py 的 V0-1 就是這個)
逐層的 min_values O(1) O(1) O(1) 當前頂端的答案在它被 push 的那一刻就定下來了
python
# LC 155. Min Stack
# V0
# IDEA: 2 ARRAYS — stack holds the elements, min_values holds each layer's minimum
# time = O(1) per operation, space = O(n)
class MinStack(object):

    def __init__(self):
        self.stack = []
        # min_values[i] == min(stack[0..i]) — a STATE per layer, not the elements sorted
        self.min_values = []

    def push(self, val):
        self.stack.append(val)
        if not self.min_values:
            self.min_values.append(val)
        else:
            self.min_values.append(min(val, self.min_values[-1]))

    def pop(self):
        # the popped layer's state leaves with it — no comparison needed
        self.min_values.pop()
        return self.stack.pop()

    def top(self):
        return self.stack[-1]

    def getMin(self):
        # the top layer's state IS the current minimum — O(1)
        return self.min_values[-1]
python
# V1: the same invariant in one stack of (value, min_so_far) tuples
# time = O(1) per operation, space = O(n)
class MinStack(object):

    def __init__(self):
        self.stack = []

    def push(self, x):
        if not self.stack:
            self.stack.append((x, x))
        else:
            self.stack.append((x, min(x, self.stack[-1][1])))

    def pop(self):
        self.stack.pop()

    def top(self):
        return self.stack[-1][0]

    def getMin(self):
        return self.stack[-1][1]

同一個想法走出堆疊 —— 經典題 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

這個不變量需要一個只在一端變動的容器,這樣頂端消失時「下一層的狀態」才仍然成立。LC 題庫裡有 四種東西是這樣運作的。看表時從第一欄下手:找到你的題目裡是什麼在扮演堆疊,每層該帶什麼彙總值就 跟著出來了。

什麼在扮演堆疊 每層的狀態 回答的查詢 經典 LC
一個顯式堆疊 至今的 min/max O(1) 的 getMin()/getMax() 155 Min Stack、716 Max Stack(peekMax)
對下方所有元素待套用的 delta O(1) 的整批 increment(k, val) 1381 Design a Stack With Increment Operation
每個頻率等級一個堆疊 O(1) pop 出現最多次的元素 895 Maximum Frequency Stack
兩個堆疊組成的佇列(LC 232) 每個堆疊各自至今的 min/max 攤銷 O(1) 的視窗 min/max,不需要單調 deque 239 Sliding Window Maximum、1438 Longest Continuous Subarray With Absolute Diff ≤ Limit
一個只會變長的陣列 —— 索引 = 層 前綴 min/max/sum/product 對每個 i 回答「i 之前(含)的最佳值」 121 Best Time to Buy and Sell Stock、2016 Maximum Difference Between Increasing Elements、42 Trapping Rain Water、238 Product of Array Except Self、303 Range Sum Query、769 Max Chunks To Make Sorted、915 Partition Array into Disjoint Intervals、1477 Find Two Non-overlapping Sub-arrays Each With Target Sum
遞迴堆疊 —— 根到節點的路徑 沿路徑的 (lo, hi)/max/前綴值 每個節點關於其祖先的答案 1026 Maximum Difference Between Node and Ancestor、98 Validate Binary Search Tree、1448 Count Good Nodes in Binary Tree、129 Sum Root to Leaf Numbers

該保留多少狀態,由兩條規則決定:

  • 之後的查詢會問到「不是頂端」的那一層時,整個陣列都要留著。 LC 42 第二趟需要每個 i 的 left_max[i];LC 238 需要每個前綴乘積;LC 769/915 在每個切點把前綴 max 跟後綴 min 比。單一變數 回答不了它已經走過的那一層。
  • 只會查頂端、而且永遠不 pop 時,可以塌成一個變數。 LC 121 每個索引問一次「至今最小」,從不回頭, 所以 min_values 就是單一個 lo —— 只留陣列的最後一格,其餘丟掉。LC 155 不能這樣做,因為 pop() 確實會回頭。
python
# LC 239 (as a min queue) / LC 1438 - a queue built from two min stacks
# IDEA: a queue is two stacks (LC 232). Give each stack its per-layer min and the
#       queue's min is the smaller of the two tops — no monotonic deque needed.
#       Swap min -> max for LC 239 itself; run one of each for LC 1438.
# time = O(1) amortised per operation, space = O(n)
class MinQueue(object):

    def __init__(self):
        self.inbox, self.outbox = [], []      # each entry: (value, min_so_far)

    @staticmethod
    def _push(st, v):
        st.append((v, v if not st else min(v, st[-1][1])))

    def push(self, v):
        self._push(self.inbox, v)

    def pop(self):
        if not self.outbox:                   # refill: each element moves once
            while self.inbox:
                self._push(self.outbox, self.inbox.pop()[0])
        return self.outbox.pop()[0]

    def getMin(self):
        return min(st[-1][1] for st in (self.inbox, self.outbox) if st)
python
# LC 1026 - Maximum Difference Between Node and Ancestor
# IDEA: the recursion stack IS the stack; (lo, hi) is that layer's aggregate over the
#       root-to-node path. Returning from the call pops it — no bookkeeping at all.
# time = O(n), space = O(h)
def maxAncestorDiff(root):
    def dfs(node, lo, hi):
        if not node:
            return hi - lo
        lo, hi = min(lo, node.val), max(hi, node.val)
        return max(dfs(node.left, lo, hi), dfs(node.right, lo, hi))
    return dfs(root, root.val, root.val)

各列的深入內容在哪裡:LC 1381 的 delta 技巧在 design_examples.md § 6, LC 239 的 deque 寫法在 monotonic_queue.md, 前綴陣列在 prefix_sum.md,LC 121 在 stock_trading.md,(lo, hi) 邊界傳遞的模式在 bst_advanced.md。


模板 5:顯式堆疊 —— 迭代式走訪 —— LC 144, LC 145 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

核心想法:把遞迴的呼叫堆疊攤開來自己管。把還沒做的工作 push 進去,pop 出來就做。前序要先 push right 再 push left,因為堆疊會把你餵進去的東西反過來吐。後序是樹裡最划算的小把戲:用 root → right → left 跑一次前序,然後把輸出反轉。

同一個堆疊的暫停版本 —— 必須在元素之間停下來的迭代器 —— 是 stack_examples.md 裡的 LC 173/LC 341,更完整的家族在 iterator.md。

python
# python
# LC 144 - Binary Tree Preorder Traversal
# IDEA: EXPLICIT STACK — push RIGHT before LEFT so LEFT is popped first
# time = O(n), space = O(h)
class Solution(object):
    def preorderTraversal(self, root):
        if not root:
            return []
        res, stack = [], [root]
        while stack:
            node = stack.pop()
            res.append(node.val)
            # NOTE !!! right first -> left ends up on TOP
            if node.right:
                stack.append(node.right)
            if node.left:
                stack.append(node.left)
        return res


# LC 145 - Binary Tree Postorder Traversal
# IDEA: preorder variant (root -> RIGHT -> LEFT), then REVERSE => left-right-root
# time = O(n), space = O(h)
class Solution(object):
    def postorderTraversal(self, root):
        if not root:
            return []
        res, stack = [], [root]
        while stack:
            node = stack.pop()
            res.append(node.val)
            # NOTE !!! mirrored order compared with preorder
            if node.left:
                stack.append(node.left)
            if node.right:
                stack.append(node.right)
        return res[::-1]   # root-right-left  ->  left-right-root
java
// java
// LC 144 - Binary Tree Preorder Traversal
// IDEA: EXPLICIT STACK — push RIGHT before LEFT so LEFT is popped first
// time = O(n), space = O(h)
public List<Integer> preorderTraversal(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    if (root == null) {
        return res;
    }
    Deque<TreeNode> st = new ArrayDeque<>();
    st.push(root);
    while (!st.isEmpty()) {
        TreeNode node = st.pop();
        res.add(node.val);
        /** NOTE !!! right pushed FIRST, so left is popped FIRST */
        if (node.right != null) {
            st.push(node.right);
        }
        if (node.left != null) {
            st.push(node.left);
        }
    }
    return res;
}

// LC 145 - Binary Tree Postorder Traversal
// IDEA: preorder with LEFT/RIGHT swapped (root-right-left), then REVERSE
// time = O(n), space = O(h)
public List<Integer> postorderTraversal(TreeNode root) {
    LinkedList<Integer> res = new LinkedList<>();
    if (root == null) {
        return res;
    }
    Deque<TreeNode> st = new ArrayDeque<>();
    st.push(root);
    while (!st.isEmpty()) {
        TreeNode node = st.pop();
        /** NOTE !!! addFirst == "append then reverse", done incrementally */
        res.addFirst(node.val);
        if (node.left != null) {
            st.push(node.left);
        }
        if (node.right != null) {
            st.push(node.right);
        }
    }
    return res;
}

模板 6:作用域/上下文帳本 —— LC 388, LC 636 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

核心想法:堆疊裡放的不是字元,而是外層的上下文(一段路徑前綴、一個正在執行的函式、一個未關閉的標籤)。進入一個作用域就 push 上下文,離開就 pop,答案是拿 stack[-1]/stack[depth] —— 也就是你當下所在的那層上下文 —— 算出來的。

這是 Google 出現頻率最高的那批堆疊題背後的模式,而且它不是括號配對:這裡的「括號」是隱含的(縮排深度、start/end 的 log 事件)。

text
Core Idea:
  - Stack index == NESTING DEPTH. stack[d] = accumulated context at depth d.
  - On entering depth d : trim the stack down to d, then push the new context
  - On leaving  a scope : pop, and hand the accumulated value back to the parent
  - The parent is ALWAYS stack[-1] — that is what makes it a stack problem

When to Use:
  - Indented / tab-delimited input   -> depth = number of leading tabs (LC 388)
  - start/end (or open/close) events -> the "running" item is the stack top (LC 636)
  - Any "who is my parent?" question during a single left-to-right scan

Similar LC:
  - LC 388  Longest Absolute File Path       (depth-indexed prefix lengths)
  - LC 636  Exclusive Time of Functions      (running function = stack top)
  - LC 591  Tag Validator                    (open-tag stack + scope rules)
  - LC 71   Simplify Path                    (stack_examples.md; ".." pops the parent dir)
java
// java
// LC 388 - Longest Absolute File Path
// IDEA: SCOPE STACK indexed by DEPTH — stack.get(d) = length of the path prefix at depth d
// time = O(n), space = O(depth)
public int lengthLongestPath(String input) {

    int res = 0;

    /**
     *  NOTE !!!
     *   stack.get(d) = length of "dir1/dir2/.../" for the current branch at depth d
     *   (already includes the trailing '/')
     *   -> index in the list IS the nesting depth
     */
    List<Integer> stack = new ArrayList<>();
    stack.add(0); // depth 0 has an empty prefix

    for (String line : input.split("\n")) {

        // depth = number of leading '\t'
        int depth = 0;
        while (depth < line.length() && line.charAt(depth) == '\t') {
            depth++;
        }
        String name = line.substring(depth);

        /** NOTE !!! we LEFT the previous deeper scopes -> pop back to `depth` */
        while (stack.size() > depth + 1) {
            stack.remove(stack.size() - 1);
        }

        if (name.contains(".")) {
            // a FILE never becomes a parent -> just measure it
            res = Math.max(res, stack.get(depth) + name.length());
        } else {
            // a DIRECTORY becomes the context of depth+1 (+1 for the '/')
            stack.add(stack.get(depth) + name.length() + 1);
        }
    }

    return res;
}
python
# python
# LC 388 - Longest Absolute File Path
# IDEA: SCOPE STACK indexed by DEPTH — stack[d] = length of the path prefix at depth d
# time = O(n), space = O(depth)
class Solution(object):
    def lengthLongestPath(self, input):
        res = 0
        stack = [0]   # stack[d] = prefix length at depth d (trailing '/' included)

        for line in input.split('\n'):
            name = line.lstrip('\t')
            depth = len(line) - len(name)   # number of leading tabs

            # NOTE !!! we left deeper scopes -> trim the stack back to this depth
            while len(stack) > depth + 1:
                stack.pop()

            if '.' in name:
                # file: measure, never push (a file has no children)
                res = max(res, stack[depth] + len(name))
            else:
                # dir: becomes the prefix for depth+1, +1 for the '/'
                stack.append(stack[depth] + len(name) + 1)

        return res

# Trace: "dir\n\tsubdir2\n\t\tfile.ext"
#   "dir"        d=0 -> stack = [0, 4]           ("dir/")
#   "subdir2"    d=1 -> stack = [0, 4, 12]       ("dir/subdir2/")
#   "file.ext"   d=2 -> res = 12 + 8 = 20

變體 —— 用覆寫取代 pop(README 把 LC 388 歸在 Hash_table 底下,就是這個寫法)。修剪堆疊的那個迴圈其實可以省掉。輸入是前序(pre-order)列出的,所以深度 d 的一行只會讀 path_len[d],而那個位置最後一次寫入正是它自己的父目錄 —— 兩者之間不會夾著任何深度 ≤ d-1 的行。更深層的過期位置在被讀到之前一定會先被覆寫,所以一個以深度為索引的 map(或陣列)就能取代堆疊。複雜度一樣是 O(n);它要教的是:這本帳其實就是每個深度最後一次的寫入。

java
// java
// LC 388 - Longest Absolute File Path
// IDEA: HASH MAP by DEPTH — pathLen[d] = prefix length for children at depth d; overwrite, never pop
// time = O(n), space = O(depth)
public int lengthLongestPath(String input) {
    int res = 0;
    Map<Integer, Integer> pathLen = new HashMap<>();
    pathLen.put(0, 0); // depth-0 entries have an empty prefix

    for (String line : input.split("\n")) {
        int depth = line.lastIndexOf('\t') + 1;   // tabs are leading and contiguous
        String name = line.substring(depth);

        if (name.contains(".")) {
            res = Math.max(res, pathLen.get(depth) + name.length());
        } else {
            // NOTE !!! overwrite the slot for depth+1 — any stale deeper slot is rewritten before it is read
            pathLen.put(depth + 1, pathLen.get(depth) + name.length() + 1);
        }
    }
    return res;
}
python
# python
# LC 388 - Longest Absolute File Path
# IDEA: HASH MAP by DEPTH — path_len[d] = prefix length for children at depth d; overwrite, never pop
# time = O(n), space = O(depth)
class Solution(object):
    def lengthLongestPath(self, input):
        res = 0
        path_len = {0: 0}   # depth-0 entries have an empty prefix

        for line in input.split('\n'):
            name = line.lstrip('\t')
            depth = len(line) - len(name)

            if '.' in name:
                res = max(res, path_len[depth] + len(name))
            else:
                # NOTE !!! overwrite the slot for depth+1 — any stale deeper slot is rewritten before it is read
                path_len[depth + 1] = path_len[depth] + len(name) + 1

        return res
java
// java
// LC 636 - Exclusive Time of Functions
// IDEA: SCOPE STACK of function ids — the RUNNING function is always the stack top
// time = O(n), space = O(n)
public int[] exclusiveTime(int n, List<String> logs) {

    int[] res = new int[n];
    Deque<Integer> stack = new ArrayDeque<>(); // ids of functions currently RUNNING
    int prev = 0;  // timestamp where the current "run slice" started

    for (String log : logs) {
        String[] p = log.split(":");       // {id, "start"|"end", timestamp}
        int id = Integer.parseInt(p[0]);
        int t = Integer.parseInt(p[2]);

        if (p[1].equals("start")) {
            /**
             *  NOTE !!!
             *   the caller (stack top) ran during [prev, t) -> credit it,
             *   then it gets PREEMPTED by the new callee
             */
            if (!stack.isEmpty()) {
                res[stack.peek()] += t - prev;
            }
            stack.push(id);
            prev = t;
        } else {
            /**
             *  NOTE !!!
             *   "end at t" is INCLUSIVE -> the slice is [prev, t], hence `+ 1`
             *   and the caller resumes at t + 1
             */
            res[stack.pop()] += t - prev + 1;
            prev = t + 1;
        }
    }

    return res;
}
python
# python
# LC 636 - Exclusive Time of Functions
# IDEA: SCOPE STACK of function ids — the RUNNING function is always the stack top
# time = O(n), space = O(n)
class Solution(object):
    def exclusiveTime(self, n, logs):
        res = [0] * n
        stack = []    # ids of functions currently RUNNING (top = executing now)
        prev = 0      # start of the current time slice

        for log in logs:
            fid, typ, t = log.split(':')
            fid, t = int(fid), int(t)

            if typ == 'start':
                # the caller ran on [prev, t) before being preempted
                if stack:
                    res[stack[-1]] += t - prev
                stack.append(fid)
                prev = t
            else:
                # 'end' timestamp is INCLUSIVE -> [prev, t] => + 1
                res[stack.pop()] += t - prev + 1
                prev = t + 1

        return res

LC 636 —— 堆疊和 prev 各自存什麼。 堆疊就是題目描述的那個呼叫堆疊:頂端是此刻在 CPU 上執行的函式。prev 是時間軸上的單一游標 —— 還沒有被算給任何人的第一個時間單位。每一行 log 都會把 [prev, …] 這一段收尾、算給當時在頂端的函式,然後移動游標。

text
n = 2, logs = ["0:start:0","0:start:2","0:end:5","1:start:6","1:end:6","0:end:7"]

log          credit (to stack top)        stack      prev   res
0:start:0    (stack empty)                [0]        0      [0, 0]
0:start:2    res[0] += 2 - 0     = 2      [0, 0]     2      [2, 0]   <- recursive call, same id
0:end:5      res[0] += 5 - 2 + 1 = 4      [0]        6      [6, 0]
1:start:6    res[0] += 6 - 6     = 0      [0, 1]     6      [6, 0]   <- zero-length slice, harmless
1:end:6      res[1] += 6 - 6 + 1 = 1      [0]        7      [6, 1]
0:end:7      res[0] += 7 - 7 + 1 = 1      []         8      [7, 1]   -> answer [7, 1]

為什麼只有 end 要 + 1。 start:t 指的是時間單位 t 的開頭,end:t 指的是時間單位 t 的結尾。所以 start 的邊界在 t,end 的邊界在 t + 1。一解析到 end:t 就把它換成邊界 t + 1,兩個分支就變成同一個半開區間 [prev, boundary) 的相減 —— 也就是下面的寫法。複雜度一樣是 O(n);值得學它的理由是:off-by-one 直接消失,而不是靠記憶。

python
# python
# LC 636 - Exclusive Time of Functions
# IDEA: normalise every log to a half-open BOUNDARY (end:t -> t + 1), so one formula credits both
# time = O(n), space = O(n)
class Solution(object):
    def exclusiveTime(self, n, logs):
        res = [0] * n
        stack = []
        prev = 0      # boundary where the current slice began
        for log in logs:
            fid, typ, t = log.split(':')
            fid, t = int(fid), int(t)
            boundary = t if typ == 'start' else t + 1
            if stack:
                # the stack top ran on [prev, boundary)
                res[stack[-1]] += boundary - prev
            if typ == 'start':
                stack.append(fid)
            else:
                stack.pop()
            prev = boundary
        return res

LC 636 特有的陷阱:

  • 先記帳,再 push 或 pop。 剛結束的那一段屬於原本的頂端;先 push 的話,呼叫者的時間就被算給了被呼叫者。
  • 每次呼叫一個 frame,而不是每個 id 一筆。 函式可以呼叫自己(範例 2),所以用 {id: start_time} 的 map 會覆寫外層那次呼叫。同一個 id 有兩個 frame 才是對的。
  • prev 是一個全域游標,不是每個函式各自的開始時間。 另一種寫法是存 [id, resume_time] frame,並在每次 pop 之後把呼叫者的 resume_time 改成 t + 1 —— 同一個想法,只是游標存在 frame 上,而且多一行容易忘記的程式。
  • 長度為零的一段沒關係。 在 prev = 6 之後緊接著 1:start:6,算進去的是 0;不需要特別處理。

摘要與速查

決策表 —— 該用哪一種堆疊模式?

問題類型 模式 核心想法 例題
找 next greater/smaller 元素 單調堆疊 維持遞增/遞減的順序 LC 496, 503, 739
移除相鄰重複字元 放 [element, count] 配對的堆疊 記次數,湊到 k 就 pop LC 1047, 1209, 1544
帶括號的字串解碼 帶計數的堆疊 用配對處理巢狀重複 LC 394, 726
算術運算式 帶運算子的堆疊 處理優先序與求值 LC 224, 227
移除 k 位數使數字最小 貪婪 + 單調 划算就把較大的數字 pop 掉 LC 402
有重複字元時求字典序最小 單調 + 最後出現位置 貪婪移除,搭配「後面還會出現」的檢查 LC 316, 1081
串流/線上頻率統計 帶 span 配對的堆疊 用配對累加計數 LC 901
用 LIFO 做出 FIFO 兩個堆疊 用 input/output 兩個堆疊模擬佇列 LC 232
push/pop 之外還要 O(1) min/max 逐層彙總值 min_values[i] = min(stack[0..i]),跟著那一層一起 pop LC 155, 716, 1381
括號平衡驗證 括號配對 push 左括號,遇右括號 pop 並驗證 LC 20, 1249, 32
巢狀上下文(縮排、start/end 事件) 作用域/上下文帳本 stack[depth] = 外層上下文 LC 388, 636, 591
反轉一個只能往前走的序列 全部 push,再全部 pop pop 出來就是逆序 LC 445, 234, 143
迭代式走訪/延遲式迭代器 顯式堆疊 堆疊裡放的是還沒做的工作 LC 144, 145, 173, 341
後綴式/RPN 求值 運算元堆疊 遇運算子就 pop 兩個、push 結果 LC 150, 682

怎麼用:在最左欄找到你的問題目標,再拿對應的模式和例題當起點。

各模式的複雜度

這裡列的是每個模式的成本。結構本身每個操作的成本在最上面的時間複雜度表。

模式 時間 空間 為什麼
括號配對 O(n) O(n),只有一種括號時 O(1) 掃一趟,每個字元最多 push 一次
單調堆疊 O(n) O(n) 每個元素 push 一次、最多 pop 一次
貪婪移除(丟掉 k 個) O(n) O(n) 同樣的攤還分析;k 限制了 pop 的次數
[element, count] 配對 O(n) O(n) 堆疊就是前綴的遊程編碼
Min stack 每次操作 O(1) O(n) 每次 push 多存一筆輔助資料
顯式堆疊走訪 O(n) O(h) 待處理的只有當前那條 root 到節點的路徑
作用域帳本 O(n) O(depth) 每個未關閉的作用域一筆
運算式剖析 O(n) O(n) 堆疊深度 = 巢狀深度

常見陷阱

  • 單調堆疊:處理「next greater/smaller」問題的關鍵模式 —— 先確認題目要的是遞增還是遞減
  • 配對堆疊:移除相鄰重複或巢狀計數的題目,堆疊裡存 [element, count] 配對
  • 貪婪移除:有些題目適合在維持某個不變量的前提下,貪婪地把元素丟掉
  • 先檢查堆疊是否為空:每個由右括號觸發的 pop()/peek() 前面都要有 !stack.isEmpty()。
  • Java 的 Character vs char:對兩個裝箱的 Character 用 != 比的是參考 —— 比較前先拆箱。
  • 這類題目的整數除法是往零截斷;Python 的 // 是往下取整,所以要寫 int(a / b)。
  • 當堆疊必須吐出由左到右的順序時,子節點要反著 push。

其他內容在哪裡

你在找 檔案
計算機(LC 224/227/772)、decode string(LC 394)、後綴式(LC 150) stack_expression_parsing.md
上面提到那些題目的解題實作 stack_examples.md
next greater/previous smaller/直方圖的理論 monotonic_stack.md
迭代器設計(LC 173, 341, 284) iterator.md
FIFO、雙端佇列、單調佇列 queue.md、monotonic_queue.md