Stack — 實戰題解
範圍 — stack.md 背後的題庫:不是任何模板錨點題的堆疊題,各自以題目卡的形式放在它所歸約到的模板底下;單調堆疊題在 monotonic_stack.md 裡完整解出,這裡只放指向它們的連結。 另見:stack.md — 母頁:這些題目所歸約到的六份模板;monotonic_stack.md — next greater/previous smaller、span、直方圖與貪婪移除,完整解在那裡;stack_expression_parsing.md — 計算機、decode string 與後綴運算求值,那是自成一家的題型;iterator.md — 以堆疊為底的迭代器(LC 173、LC 341);queue.md — FIFO 那一側,包括 LC 232。
LeetCode 題目清單
總覽
stack.md 放六份模板;這份表是套用它們的題目。底下每個 ## 小節對應一份模板,順序和核心表相同,每張卡片都寫出該題在模板之上多加的那一步。已經在別份表裡解出的題目,在索引(以及所屬小節的指標表)裡只佔一列,不另抄一份。
索引
| LC | 題目 | 歸約到 | 難度 | 在哪裡 |
|---|---|---|---|---|
| 1047 | Remove All Adjacent Duplicates In String | stack § Template 1 | Easy | 錨點題,完整解在 stack.md |
| 844 | Backspace String Compare | advanced_simulation § Template 6 | Easy | 完整解在 advanced_simulation.md |
| 946 | Validate Stack Sequences | advanced_simulation § Template 6 | Medium | 完整解在 advanced_simulation.md |
| 1910 | Remove All Occurrences of a Substring | advanced_simulation § Template 6 | Medium | 完整解在 advanced_simulation.md |
| 1209 | Remove All Adjacent Duplicates in String II | stack § Template 1 | Medium | 卡片 |
| 735 | Asteroid Collision | stack § Template 1 | Medium | 卡片 |
| 20 | Valid Parentheses | stack § Template 2 | Easy | 錨點題,完整解在 stack.md |
| 921 | Minimum Add to Make Parentheses Valid | greedy_examples § 7 | Medium | 完整解在 greedy_examples.md |
| 1249 | Minimum Remove to Make Valid Parentheses | string § Template 7 | Medium | 索引堆疊在 string.md;兩趟掃描卡片 在這裡 |
| 856 | Score of Parentheses | stack § Template 2 | Medium | 卡片 |
| 1963 | Minimum Number of Swaps to Make the String Balanced | stack § Template 2 | Medium | 卡片 |
| 32 | Longest Valid Parentheses | stack § Template 2 | Hard | 卡片 |
| 496 | Next Greater Element I | monotonic_stack § 2-3 | Easy | 完整解在 monotonic_stack.md |
| 739 | Daily Temperatures | stack § Template 3 | Medium | 錨點題,完整解在 stack.md |
| 503 | Next Greater Element II | monotonic_stack § 2-5 | Medium | 完整解在 monotonic_stack.md |
| 901 | Online Stock Span | monotonic_stack § 2-6 | Medium | 完整解在 monotonic_stack.md |
| 907 | Sum of Subarray Minimums | monotonic_stack § 2-7 | Medium | 完整解在 monotonic_stack.md |
| 2104 | Sum of Subarray Ranges | monotonic_stack § 2-12 | Medium | 完整解在 monotonic_stack.md |
| 402 | Remove K Digits | monotonic_stack § 2-8 | Medium | 完整解在 monotonic_stack.md |
| 316 | Remove Duplicate Letters | monotonic_stack § 2-8-1 | Medium | 完整解在 monotonic_stack.md |
| 84 | Largest Rectangle in Histogram | monotonic_stack § 2-2 | Hard | 完整解在 monotonic_stack.md |
| 155 | Min Stack | stack § Template 4 | Medium | 錨點題,完整解在 stack.md |
| 144 | Binary Tree Preorder Traversal | stack § Template 5 | Easy | 錨點題,完整解在 stack.md |
| 145 | Binary Tree Postorder Traversal | stack § Template 5 | Easy | 錨點題,完整解在 stack.md |
| 232 | Implement Queue using Stacks | queue § Template 5 | Easy | 完整解在 queue.md |
| 173 | Binary Search Tree Iterator | bst_advanced § Template 5b | Medium | 完整解在 bst_advanced.md |
| 341 | Flatten Nested List Iterator | iterator § 2-2 | Medium | 完整解在 iterator.md |
| 445 | Add Two Numbers II | stack § Template 5 | Medium | 卡片 |
| 71 | Simplify Path | string § Variation 6.1 | Medium | 完整解在 string.md |
| 388 | Longest Absolute File Path | stack § Template 6 | Medium | 錨點題,完整解在 stack.md |
| 636 | Exclusive Time of Functions | stack § Template 6 | Medium | 錨點題,完整解在 stack.md |
與頂端抵銷的題目
| LC | 題目 | 完整解在哪裡 |
|---|---|---|
| 1047 | Remove All Adjacent Duplicates In String | stack § 模板 1 — 錨點題 |
| 844 · 946 · 1910 | Backspace String Compare · Validate Stack Sequences · Remove All Occurrences of a Substring | advanced_simulation § 模板 6 |
Remove All Adjacent Duplicates in String II — LC 1209 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
關鍵 — 堆疊裡每一筆是一整段連續段
[char, length],所以堆疊就是化簡後前綴的遊程編碼。相同字元把頂端那段加長;一旦長度達到k,整段就被 pop 掉,下一段露出來接受下一個字元 —— 這就是不用第二趟掃描也能連鎖消除的原因。
歸約到:stack § 模板 1 — 抵銷需要 k 個相鄰相同字元而不是兩個,所以頂端要帶一個計數。LC 1047 就是 k = 2。
# python
# LC 1209 - Remove All Adjacent Duplicates in String II
# IDEA: stack of [char, run length]; a run that reaches k is popped whole
# time = O(n), space = O(n)
class Solution(object):
def removeDuplicates(self, s, k):
stack = [] # [char, run length] — the reduced prefix, run-length encoded
for ch in s:
if stack and stack[-1][0] == ch:
stack[-1][1] += 1
if stack[-1][1] == k:
stack.pop() # the run reached k: it vanishes, exposing the run below
else:
stack.append([ch, 1])
return ''.join(ch * cnt for ch, cnt in stack)
// java
// LC 1209 - Remove All Adjacent Duplicates in String II
// IDEA: stack of {char, run length}; a run that reaches k is popped whole
// time = O(n), space = O(n)
public String removeDuplicates(String s, int k) {
Deque<int[]> stack = new ArrayDeque<>(); // {char, run length}, top = last run
for (char ch : s.toCharArray()) {
if (!stack.isEmpty() && stack.peek()[0] == ch) {
stack.peek()[1]++;
if (stack.peek()[1] == k) {
stack.pop(); // the run vanishes, exposing the run below
}
} else {
stack.push(new int[]{ch, 1});
}
}
StringBuilder sb = new StringBuilder();
while (!stack.isEmpty()) {
int[] run = stack.pollLast(); // the bottom is the leftmost run
for (int i = 0; i < run[1]; i++) {
sb.append((char) run[0]);
}
}
return sb.toString();
}
試跑
s = "deeedbbcccbdaa", k = 3
ch stack (runs) note
d [d1]
e [d1 e1]
e [d1 e2]
e [d1] e reaches 3 -> popped
d [d2] the pop exposed d, so this d extends it
b [d2 b1]
b [d2 b2]
c [d2 b2 c1]
c [d2 b2 c2]
c [d2 b2] c reaches 3 -> popped
b [d2] b reaches 3 -> popped (cascade)
d [] d reaches 3 -> popped (cascade)
a [a1]
a [a2] -> "aa"
陷阱
- 只 push 單一字元、每一步都重數頂端
k個,是 O(n·k);k = 10⁴、n = 10⁵ 時會超時。把計數存在記錄上。 - 計數歸零而不是 pop(
"deeed",k = 3)會在頂端留下一段空的連續段,第二個d就另起一段,連鎖消成"dd"永遠不會發生。 - Java:用 for-each 走訪
ArrayDeque是頂端 → 底部,答案會反過來;用pollLast()取出。
相似題 — LC 1047(k = 2,不需要計數)· LC 1544(同一個字母、大小寫相反時抵銷)
Asteroid Collision — LC 735
關鍵 — 堆疊裡放的是到目前為止的倖存者,而碰撞只會發生在往左飛的新來者(
a < 0)和往右飛的頂端(top > 0)之間。新來者在while迴圈裡不斷擊毀比較小的頂端,所以一顆小行星可以 pop 掉很多顆;迴圈停下時它還活著才 push。
歸約到:stack § 模板 1 — 抵銷的判斷是比大小,而且雙方都可能被毀,不只頂端。
# python
# LC 735 - Asteroid Collision
# IDEA: stack of survivors; a left-mover fights right-moving tops until one side is gone
# time = O(n), space = O(n)
class Solution(object):
def asteroidCollision(self, asteroids):
stack = [] # survivors, left to right
for a in asteroids:
alive = True
# a collision needs the newcomer moving left and the top moving right
while alive and a < 0 and stack and stack[-1] > 0:
if stack[-1] < -a:
stack.pop() # the top explodes; `a` keeps flying left
continue
if stack[-1] == -a:
stack.pop() # equal size: both explode
alive = False # `a` explodes (or already did with the top)
if alive:
stack.append(a)
return stack
// java
// LC 735 - Asteroid Collision
// IDEA: stack of survivors; a left-mover fights right-moving tops until one side is gone
// time = O(n), space = O(n)
public int[] asteroidCollision(int[] asteroids) {
Deque<Integer> stack = new ArrayDeque<>();
for (int a : asteroids) {
boolean alive = true;
while (alive && a < 0 && !stack.isEmpty() && stack.peek() > 0) {
if (stack.peek() < -a) {
stack.pop(); // the top explodes, `a` keeps flying
continue;
}
if (stack.peek() == -a) {
stack.pop(); // both explode
}
alive = false; // `a` explodes
}
if (alive) {
stack.push(a);
}
}
int[] res = new int[stack.size()];
for (int i = res.length - 1; i >= 0; i--) {
res[i] = stack.pop(); // pop fills from the right end
}
return res;
}
陷阱
- 只要正負號不同就相撞:
[-2, 1]是互相遠離(左邊往左、右邊往右),必須維持[-2, 1]。只有頂端往右、新來者往左才會相遇。 - 大小相等時兩顆都毀掉:
[8, -8]是[],不是[8]也不是[-8]。 - 只打一場就把新來者 push:
[10, 2, -5]必須讓-5先擊毀2,再撞上10而毀掉,得到[10]。碰撞是while,不是if。
括號配對的題目
| LC | 題目 | 完整解在哪裡 |
|---|---|---|
| 20 | Valid Parentheses | stack § 模板 2 — 錨點題 |
| 921 | Minimum Add to Make Parentheses Valid | greedy_examples § 7 — 只有一種括號,所以堆疊退化成計數器 |
| 1249 | Minimum Remove to Make Valid Parentheses(索引堆疊) | string § 模板 7 — 不用堆疊的寫法是下面這張卡片 |
Minimum Remove to Make Valid Parentheses — LC 1249
關鍵 — 索引堆疊的解法在 string § 模板 7; 這張是不用堆疊的版本。只有一種括號時,堆疊可以縮成一個計數器,但計數器記不住多出來的
(在哪裡,所以按方向拆開:左 → 右刪掉每個前面沒有開著的(的),再右 → 左刪掉每個後面沒有開著的)的(。每一趟只刪不可能配對的括號,所以刪得最少。
歸約到:stack § 模板 2 — 堆疊退化成它的大小,每個方向各跑一次。它回答的是 follow-up 「不用索引堆疊做得到嗎?」,而且和下面 LC 32 的計數掃描是同一個雙向技巧。
# python
# LC 1249 - Minimum Remove to Make Valid Parentheses
# IDEA: 2 SCANS with a balance counter — drop unmatched ')' left->right, unmatched '(' right->left
# time = O(n), space = O(n) for the two output buffers; O(1) beyond them
class Solution(object):
def minRemoveToMakeValid(self, s):
# pass 1: left -> right; a ')' with no '(' open before it can never be matched
kept = []
open_cnt = 0
for c in s:
if c == ')':
if open_cnt == 0:
continue # unmatched ')' -> drop
open_cnt -= 1
elif c == '(':
open_cnt += 1
kept.append(c)
# pass 2: right -> left, the mirror; a '(' with no ')' open after it can never be matched
res = []
close_cnt = 0
for c in reversed(kept):
if c == '(':
if close_cnt == 0:
continue # unmatched '(' -> drop
close_cnt -= 1
elif c == ')':
close_cnt += 1
res.append(c)
res.reverse() # pass 2 built it backwards
return ''.join(res)
// java
// LC 1249 - Minimum Remove to Make Valid Parentheses
// IDEA: 2 SCANS with a balance counter — drop unmatched ')' left->right, unmatched '(' right->left
// time = O(n), space = O(n) for the two output buffers; O(1) beyond them
public String minRemoveToMakeValid(String s) {
StringBuilder kept = new StringBuilder();
int openCnt = 0;
for (char c : s.toCharArray()) {
if (c == ')') {
if (openCnt == 0) {
continue; // unmatched ')' -> drop
}
openCnt--;
} else if (c == '(') {
openCnt++;
}
kept.append(c);
}
StringBuilder res = new StringBuilder();
int closeCnt = 0;
for (int i = kept.length() - 1; i >= 0; i--) {
char c = kept.charAt(i);
if (c == '(') {
if (closeCnt == 0) {
continue; // unmatched '(' -> drop
}
closeCnt--;
} else if (c == ')') {
closeCnt++;
}
res.append(c);
}
return res.reverse().toString(); // pass 2 built it backwards
}
試跑
s = ")((a)"
pass 1, left -> right ) open=0 -> drop ( open=1 ( open=2 a ) open=1
kept = "((a)"
pass 2, right -> left ) close=1 a ( close=0 ( close=0 -> drop
res = "(a)"
陷阱
- 只跑一趟左 → 右:
"(()"會留下多出來的(,因為open_cnt最後只是停在 1。反方向的第二趟才會把它刪掉。 - 第二趟之後忘了反轉:
")((a)"會回傳")a("。
Score of Parentheses — LC 856
關鍵 — 每個堆疊槽位存的是這一層目前收集到的分數。
(開一個新框(push0);)關掉它並併進父層:空框代表"()",得1分,非空的框加倍,所以父層加上max(2 * inner, 1)。
歸約到:stack § 模板 2 — 堆疊為每個開著的深度存一份部分結果,而不是左括號本身。
# python
# LC 856 - Score of Parentheses
# IDEA: STACK OF PARTIAL SCORES — one frame per depth, fold child into parent
# time = O(n), space = O(n)
class Solution(object):
def scoreOfParentheses(self, s):
stack = [0] # stack[-1] = score collected so far at the current depth
for c in s:
if c == '(':
stack.append(0) # open a frame for the new depth
else:
inner = stack.pop() # close it: "()" scores 1, "(X)" scores 2 * X
stack[-1] += max(2 * inner, 1)
return stack[0]
// java
// LC 856 - Score of Parentheses
// IDEA: STACK OF PARTIAL SCORES — one frame per depth, fold child into parent
// time = O(n), space = O(n)
public int scoreOfParentheses(String s) {
Deque<Integer> st = new ArrayDeque<>();
st.push(0); // score of the outermost frame
for (char c : s.toCharArray()) {
if (c == '(') {
st.push(0); // open a new (empty) frame
} else {
int inner = st.pop();
st.push(st.pop() + Math.max(2 * inner, 1)); // "()" scores 1, "(X)" scores 2 * X
}
}
return st.pop();
}
陷阱
- 沒有最外層的
0框:"()()"在第一個)就把堆疊 pop 空,分數沒地方加。堆疊要先放[0]。 - 只寫
2 * inner會把"()"算成0;max(..., 1)就是 base case。
Minimum Number of Swaps to Make the String Balanced — LC 1963
關鍵 — 用堆疊把每一對配好的
[]抵銷掉,剩下的永遠是"]" * m + "[" * m:每個沒配對的]都排在每個沒配對的[前面。把最外側的]和最外側的[交換一次,能修好那 m 對中的兩對,所以答案是ceil(m / 2)。
歸約到:stack § 模板 2 — 當作化簡器用:掃描後只剩不平衡的核心,答案是它大小的一個公式。
# python
# LC 1963 - Minimum Number of Swaps to Make the String Balanced
# IDEA: cancel matched pairs; the residue is "]"*m + "["*m and one swap fixes two pairs
# time = O(n), space = O(n)
class Solution(object):
def minSwaps(self, s):
stack = []
for c in s:
if c == ']' and stack and stack[-1] == '[':
stack.pop() # a matched pair cancels
else:
stack.append(c) # an unmatched ']' or a '[' still open
# what is left is "]" * m + "[" * m: every ']' sits before every '['
m = len(stack) // 2
return (m + 1) // 2 # one swap repairs two of the m pairs
陷阱
- 每遇到
]就 pop、不檢查頂端是不是[:在"]]][[["上兩個]會互相抵銷,剩下"][[["而不是"]]][[["。pop 之前先確認頂端配得上。 - 用
m // 2而不是(m + 1) // 2:"][]["的 m = 1,需要交換一次,不是零次。 - 堆疊裡永遠是
]在下、[在上,所以只數未配對的]也能得到同一個m,空間 O(1)。
Longest Valid Parentheses — LC 32 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
關鍵 — push 索引,並讓堆疊最底下那個元素是目前合法區段前一格的索引(基準,初始放
-1)。遇到)先 pop:如果堆疊空了,這個)沒有配對,成為新的基準;否則i - stack[-1]就是以i結尾的合法區段長度。
歸約到:stack § 模板 2 — 存索引而不是字元,再加一個把「配對成功」變成「長度」的基準哨兵。
# python
# LC 32 - Longest Valid Parentheses
# IDEA: STACK OF INDICES + `-1` base sentinel; length = i - stack[-1]
# time = O(n), space = O(n)
class Solution(object):
def longestValidParentheses(self, s):
stack = [-1] # bottom = index just before the current valid run (the base)
best = 0
for i, c in enumerate(s):
if c == '(':
stack.append(i)
else:
stack.pop() # match the top '(' — or pop the base
if not stack:
stack.append(i) # unmatched ')' -> it becomes the new base
else:
best = max(best, i - stack[-1])
return best
// java
// LC 32 - Longest Valid Parentheses
// IDEA: STACK OF INDICES + `-1` base sentinel; length = i - stack.peek()
// time = O(n), space = O(n)
public int longestValidParentheses(String s) {
Deque<Integer> st = new ArrayDeque<>();
st.push(-1); // base: the index just before the current valid run
int best = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '(') {
st.push(i);
} else {
st.pop(); // match the top '(' — or pop the base
if (st.isEmpty()) {
st.push(i); // unmatched ')' -> the new base
} else {
best = Math.max(best, i - st.peek());
}
}
}
return best;
}
試跑
s = ")()())"
i c action stack best
- - init base [-1] 0
0 ) pop -1 -> empty -> new base [0] 0
1 ( push [0, 1] 0
2 ) pop 1, i - top = 2 - 0 = 2 [0] 2
3 ( push [0, 3] 2
4 ) pop 3, i - top = 4 - 0 = 4 [0] 4 <- answer
5 ) pop 0 -> empty -> new base [5] 4
另外兩個值得知道的解法。 堆疊是要先寫的那個;這兩個各自教了堆疊沒教的東西。
| 做法 | 時間 | 空間 | 它教你什麼 |
|---|---|---|---|
索引堆疊 + -1 基準(上面) |
O(n) | O(n) | 基準哨兵把一次配對變成一個長度 |
1D DP — dp[i] = 以 i 結尾的最長合法區段 |
O(n) | O(n) | 跳過以 i-1 結尾的區段,找到 s[i] 的另一半 |
| 兩趟計數掃描(左→右,再右→左) | O(n) | O(1) | 一個計數器看不見多出來的 (,所以要倒著再掃一次 |
1D DP。 合法區段只能以 ) 結尾,所以 s[i] == '(' 時 dp[i] = 0。遇到 ):如果 s[i-1] == '(',就是一對新括號黏在前一段後面,dp[i] = dp[i-2] + 2;否則以 i-1 結尾的區段長 dp[i-1],所以 s[i] 的另一半只可能在 open_idx = i - dp[i-1] - 1,如果那裡是 (,則 dp[i] = dp[i-1] + 2 + dp[open_idx-1] —— 最後一項把另一半之前剛結束的那段也黏上來,這是大家最常忘的部分。
# python
# LC 32 - Longest Valid Parentheses
# IDEA: 1D DP — dp[i] = longest valid substring ENDING at i
# time = O(n), space = O(n)
class Solution(object):
def longestValidParentheses(self, s):
n = len(s)
dp = [0] * n # dp[i] = longest valid run ENDING at i
best = 0
for i in range(1, n):
if s[i] == '(':
continue # a valid run never ends with '('
if s[i - 1] == '(':
# case 1: "...()" -> the new pair + the run ending at i-2
dp[i] = 2 + (dp[i - 2] if i >= 2 else 0)
else:
# case 2: "...))" -> jump over the run ending at i-1 to s[i]'s partner
open_idx = i - dp[i - 1] - 1
if open_idx >= 0 and s[open_idx] == '(':
dp[i] = dp[i - 1] + 2
if open_idx >= 1:
dp[i] += dp[open_idx - 1] # glue on the run that ends before the partner
best = max(best, dp[i])
return best
s = "()(())"
i c case dp
0 ( '(' ends nothing [0,0,0,0,0,0]
1 ) "()": 2 + dp[-1 -> 0] [0,2,0,0,0,0]
2 ( - [0,2,0,0,0,0]
3 ( - [0,2,0,0,0,0]
4 ) "()": 2 + dp[2] = 2 [0,2,0,0,2,0]
5 ) "))": open_idx = 5 - dp[4] - 1 = 2, s[2] = '('
dp[5] = dp[4] + 2 + dp[1] = 2 + 2 + 2 = 6 [0,2,0,0,2,6] <- answer
兩趟計數掃描。 左 → 右數 opens 和 closes:opens == closes 時結束一段長度 2 * closes 的合法區段,closes > opens 代表這個 ) 永遠配不到,兩個都歸零。左 → 右在 "(()" 上永遠看不到 opens == closes,所以再跑一趟右 → 左的鏡像,在 opens > closes 時歸零。每一段合法區段至少會被其中一趟抓到;這是唯一 O(1) 空間的解。
# python
# LC 32 - Longest Valid Parentheses
# IDEA: 2 SCANS with open/close counters — left->right, then right->left
# time = O(n), space = O(1)
class Solution(object):
def longestValidParentheses(self, s):
best = 0
# pass 1: left -> right; a surplus ')' can never be matched -> reset
opens = closes = 0
for c in s:
if c == '(':
opens += 1
else:
closes += 1
if opens == closes:
best = max(best, 2 * closes)
elif closes > opens:
opens = closes = 0
# pass 2: right -> left; catches "(()", where opens stays ahead in pass 1
opens = closes = 0
for c in reversed(s):
if c == '(':
opens += 1
else:
closes += 1
if opens == closes:
best = max(best, 2 * opens)
elif opens > closes:
opens = closes = 0
return best
陷阱
- 沒有放
-1:每段從索引0開始的區段("()")都會把堆疊 pop 空,永遠量不到。 - push 字元而不是索引:整個技巧就是
i - stack[-1],所以堆疊必須存位置。 - DP:漏了
dp[open_idx - 1]那一項,會把"()(())"算成 4 而不是 6。 - 計數掃描:只掃一個方向在
"(()"上會回傳 0;鏡像的那一趟是必要的。
單調堆疊的題目
單調堆疊題在 monotonic_stack.md 裡完整解出;核心形式是 stack § 模板 3。
| LC | 題目 | 完整解在哪裡 |
|---|---|---|
| 496 | Next Greater Element I | monotonic_stack § 2-3 |
| 739 | Daily Temperatures | stack § Template 3 — 錨點題 |
| 503 | Next Greater Element II | monotonic_stack § 2-5 |
| 901 | Online Stock Span | monotonic_stack § 2-6 |
| 907 | Sum of Subarray Minimums | monotonic_stack § 2-7 |
| 2104 | Sum of Subarray Ranges | monotonic_stack § 2-12 |
| 402 | Remove K Digits | monotonic_stack § 2-8 |
| 316 | Remove Duplicate Letters | monotonic_stack § 2-8-1 |
| 84 | Largest Rectangle in Histogram | monotonic_stack § 2-2 |
Min Stack 的題目
| LC | 題目 | 完整解在哪裡 |
|---|---|---|
| 155 | Min Stack | stack § 模板 4 — 錨點題,附只存最小值的省空間第二個堆疊 |
顯式堆疊的題目
| LC | 題目 | 完整解在哪裡 |
|---|---|---|
| 144 · 145 | Binary Tree Preorder / Postorder Traversal | stack § 模板 5 — 錨點題 |
| 232 | Implement Queue using Stacks | queue § 模板 5 |
| 173 | Binary Search Tree Iterator | bst_advanced § 模板 5b — 暫停式的中序走訪 |
| 341 | Flatten Nested List Iterator | iterator § 2-2 — 子元素倒序 push,在 hasNext() 裡展開 |
Add Two Numbers II — LC 445
關鍵 — 單向鏈結串列只能往前走,但加法要從最低位開始。把每個節點的值 push 進堆疊,就能不修改輸入地拿到倒序;再用「往前插入」建出答案,等於反轉第二次,所以結果是最高位在前。
歸約到:stack § 模板 5 — 堆疊存還沒做的工作,並以相反順序交回來,這裡是兩條串列的數字。
# python
# LC 445 - Add Two Numbers II
# IDEA: TWO STACKS give least-significant-first access without mutating the input
# time = O(n + m), space = O(n + m)
class Solution(object):
def addTwoNumbers(self, l1, l2):
s1, s2 = [], []
while l1: # push every digit: popping now yields least significant first
s1.append(l1.val)
l1 = l1.next
while l2:
s2.append(l2.val)
l2 = l2.next
carry = 0
head = None # the answer is built from its tail forwards
while s1 or s2 or carry: # a pending carry adds one more digit
total = carry
if s1:
total += s1.pop()
if s2:
total += s2.pop()
carry = total // 10
node = ListNode(total % 10)
node.next = head # prepend: the second reversal puts the digits back in order
head = node
return head
// java
// LC 445 - Add Two Numbers II
// IDEA: TWO STACKS give least-significant-first access without mutating the input
// time = O(n + m), space = O(n + m)
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
Deque<Integer> s1 = new ArrayDeque<>();
Deque<Integer> s2 = new ArrayDeque<>();
for (ListNode p = l1; p != null; p = p.next) {
s1.push(p.val);
}
for (ListNode p = l2; p != null; p = p.next) {
s2.push(p.val);
}
int carry = 0;
ListNode head = null; // built from the tail forwards
while (!s1.isEmpty() || !s2.isEmpty() || carry != 0) {
int sum = carry;
if (!s1.isEmpty()) {
sum += s1.pop();
}
if (!s2.isEmpty()) {
sum += s2.pop();
}
carry = sum / 10;
ListNode node = new ListNode(sum % 10);
node.next = head; // prepend: the second reversal restores the order
head = node;
}
return head;
}
陷阱
- 兩個堆疊都空了就停:
[9, 9] + [1]會掉最後的進位,回傳[0, 0]而不是[1, 0, 0]。還有進位時也要繼續迴圈。 - 把每條串列轉成整數再相加,是 add_x_sum § 2-7 的做法;它只在 Python 可行,因為 Python 的整數沒有上限 —— 到 100 位數時 Java 的
long就溢位了。 - 先把兩條串列反轉是 O(1) 額外空間,但會修改輸入,而 follow-up(「不要修改輸入串列」)正好排除它。
相似題 — LC 2(最低位在前:不需要堆疊)· LC 234(全部 push,再拿前端和 pop 出來的比)
作用域帳本的題目
| LC | 題目 | 完整解在哪裡 |
|---|---|---|
| 388 | Longest Absolute File Path | stack § Template 6 — 錨點題,外加「覆寫深度槽位」的變形 |
| 636 | Exclusive Time of Functions | stack § Template 6 — 第二個錨點題 |
| 71 | Simplify Path | string § Variation 6.1 — .. 會 pop 掉上一層目錄 |