BST — 進階模式與深入探討
範圍 — 第一輪可以先跳過的 BST 內容:O(h) 空間的延遲式迭代器、用中序下降點找出被破壞的 BST、順序統計(rank)查詢,以及有序陣列以外的各種建構變形 —— 每一項都附上什麼時候用
bst.md那張較簡單的卡就夠了。 另見:bst.md — 這裡每一節延伸的標準模板;bst_examples.md — BST 實戰題;binary_tree.md — 刪掉多個節點、回傳森林(LC 1110);segment_tree.md — 在陣列(而非樹)上做區間查詢;binary_indexed_tree.md — 不用節點樹的前綴和 rank。
LeetCode 題目清單
總覽
五種技巧,每一種都是 bst.md 的某個標準模板再加一樣東西:一個讓中序走訪可以暫停的顯式堆疊、
一個找出逆序的 prev 指標、一個回答 rank 的子樹大小欄位、一組從前序重建的區間上下界,以及一個嘗試
每一種根的迴圈。它們在面試裡多半以 follow-up 的形式出現(「如果資料一直在變呢?」、「能不能 O(1)
空間?」、「如果輸入是鏈結串列呢?」),很少是第一題。
先備知識
- bst § Template 5:迭代版中序走訪。 下面的模板 1 和 2 是讓它暫停、以及盯著它看。
- bst § Template 2:靠回傳子節點來插入。 模板 3 在上面多加一個計數器。
- bst § Template 4:往下帶的
(low, high)區間。模板 4 用它來建樹。 - bst § Template 6:有序區間的中間當根。 LC 109 和 LC 1382 歸約到它;模板 5 則改成每一個值都當一次根。
辨識
| 題目裡的訊號 | 限制條件提示 | 該用 | 不適用時 |
|---|---|---|---|
「iterator」、「next() / hasNext() 只用 O(h) 記憶體」 |
呼叫 ≤ 10⁵ 次,平均 O(1) | 模板 1 延遲式中序 | 只需要走一遍 → bst § Template 5 |
| 「有兩個節點被誤換了,把樹復原」 | follow-up:O(1) 空間 | 模板 2 中序下降點偵測 | 只問「合不合法?」→ bst § Template 4 |
| 「串流中的第 k 大」、「x 的 rank」、「頻繁插入下的第 k 小」 | ≤ 10⁴ 次 add,每次 O(log n) | 模板 3 附帶子樹大小的 BST | k 固定 → 大小為 k 的 min-heap 更短(heap.md) |
| 「用前序建出 BST」 | n ≤ 100,兩種都行 | 模板 4 在前序串流上用上下界 | 輸入是有序的 → bst § Template 6 |
| 「所有結構相異的 BST」、「有幾棵相異 BST」 | 列出 n ≤ 8,計數 n ≤ 19 | 模板 5 每個值都當一次根 | 只要一棵平衡樹 → bst § Template 6 |
| 「刪掉這些節點、回傳森林」 | 不是 BST 技巧 → binary_tree § Template 10 |
延遲式中序走訪
模板 1:延遲式中序迭代器 — LC 173 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
辨識 —— 「實作一個 BST 的 iterator」、「next() 回傳下一個最小值」、
「next() 與 hasNext() 平均 O(1) 時間、O(h) 記憶體」、「合併兩棵 BST」(LC 1305)。
關鍵 —— 堆疊裡放的恰好是「值還沒回傳、但左子樹已經處理完」的節點:也就是目前位置底下的左側脊線。 pop 出來的就是中序的下一個值;再把那個節點右子節點的左側脊線推進去,不變式就恢復了。
# python
# LC 173 - Binary Search Tree Iterator
# IDEA: keep the unvisited left spine on a stack; pop = next value, then push the right child's spine
# time = O(1) amortised per next(), space = O(h)
class BSTIterator:
def __init__(self, root):
self.stack = []
self._push_left(root)
def _push_left(self, node):
while node: # the smallest unvisited value ends up on top
self.stack.append(node)
node = node.left
def next(self):
node = self.stack.pop()
self._push_left(node.right) # its right subtree comes next in order
return node.val
def hasNext(self):
return len(self.stack) > 0
// java
// LC 173 - Binary Search Tree Iterator
// IDEA: unvisited left spine on a stack; pop = next value, then push the right child's spine
// time = O(1) amortised per next(), space = O(h)
class BSTIterator {
private final Deque<TreeNode> stack = new ArrayDeque<>();
public BSTIterator(TreeNode root) {
pushLeft(root);
}
private void pushLeft(TreeNode node) {
while (node != null) {
stack.push(node);
node = node.left;
}
}
public int next() {
TreeNode node = stack.pop();
pushLeft(node.right);
return node.val;
}
public boolean hasNext() {
return !stack.isEmpty();
}
}
複雜度
- 時間:每次
next()攤銷 O(1):每個節點被_push_left推入一次、被next彈出一次, 所以 n 次呼叫總共 O(n),雖然單次呼叫可能推入 h 個節點。 - 空間 O(h):堆疊裡永遠不超過一條從根到葉的脊線。
試跑 — [7,3,15,null,null,9,20]
init push 7, 3 stack [7, 3]
next() = 3 pop 3, no right child stack [7]
next() = 7 pop 7, push 15, 9 stack [15, 9]
next() = 9 pop 9 stack [15]
next() = 15 pop 15, push 20 stack [20]
next() = 20 pop 20 stack [] hasNext() = false
陷阱
- 在建構子裡把整棵樹攤平也會過測資,但那是 O(n) 記憶體,正是 follow-up 不允許的。
- 推入
node.right本身而不是它的左側脊線,試跑裡會先回傳 15 再回傳 9。
不適用時 —— 一次就要完整的排序結果,或只要第 k 個值: bst § Template 5 的一般中序走訪更短。
練習 —— LC 173 · LC 230 (pop k 次就停) · LC 1305 (兩個迭代器,合併) · LC 653 (一個正向、一個反向的迭代器在中間相遇)
All Elements in Two Binary Search Trees — LC 1305
關鍵 — 兩個延遲式迭代器就是兩條有序串流,所以答案就是 merge sort 的合併步驟:每次都從下一個值 比較小的那條串流取。額外記憶體是 O(h1 + h2),而不是兩個攤平串列的 O(m + n)。
歸約到:模板 1 —— 兩個並排。
# python
# LC 1305 - All Elements in Two Binary Search Trees
# IDEA: two left-spine stacks; pop from the one whose top is smaller, then push its right spine
# time = O(m + n), space = O(h1 + h2) excluding the output
def getAllElements(root1, root2):
stack1, stack2, result = [], [], []
def push_left(stack, node):
while node:
stack.append(node)
node = node.left
push_left(stack1, root1)
push_left(stack2, root2)
while stack1 or stack2:
# take from stack1 when stack2 is empty, or when both are live and stack1's top is smaller
if not stack2 or (stack1 and stack1[-1].val <= stack2[-1].val):
node = stack1.pop()
push_left(stack1, node.right)
else:
node = stack2.pop()
push_left(stack2, node.right)
result.append(node.val)
return result
陷阱
- 沒先確認兩個堆疊都非空就比較
stack1[-1].val <= stack2[-1].val:總有一棵樹先用完,[1,8]和[8,1]跑到一半就會丟出IndexError。
修復壞掉的 BST
模板 2:中序下降點偵測 — LC 99 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現
辨識 —— 「BST 中恰好有兩個節點被誤換了;在不改變結構的前提下復原這棵樹」、 「follow-up:O(1) 空間」。同一套走訪也能回答「任兩節點的最小差」(LC 530)和「眾數」(LC 501)。
關鍵 —— 合法 BST 的中序序列是嚴格遞增的,所以一次交換會以下降點的形式出現:
prev.val > node.val。相鄰的兩個值被交換會產生一個下降點;相隔較遠則產生兩個。
first 是第一個下降點的 prev,之後不再改變;second 在每個下降點都設成 node,所以最後停在
最後一個下降點。這一條規則同時涵蓋兩種情況。
# python
# LC 99 - Recover Binary Search Tree
# IDEA: in-order walk with a prev pointer; first = prev at the first drop, second = node at the last
# time = O(n), space = O(h)
def recoverTree(root):
first = second = prev = None
def inorder(node):
nonlocal first, second, prev
if not node:
return
inorder(node.left)
if prev and prev.val > node.val: # a drop in the sorted order
if first is None:
first = prev # the too-large value, from the first drop only
second = node # the too-small value, from the last drop
prev = node
inorder(node.right)
inorder(root)
first.val, second.val = second.val, first.val # swap values; the structure stays
// java
// LC 99 - Recover Binary Search Tree
// IDEA: in-order with prev; first is set once, second at every drop; then swap the values
// time = O(n), space = O(h)
private TreeNode first, second, prev;
public void recoverTree(TreeNode root) {
first = second = prev = null;
inorder(root);
int temp = first.val;
first.val = second.val;
second.val = temp;
}
private void inorder(TreeNode node) {
if (node == null) {
return;
}
inorder(node.left);
if (prev != null && prev.val > node.val) {
if (first == null) {
first = prev;
}
second = node;
}
prev = node;
inorder(node.right);
}
第二種解法,保留它是因為它是 O(1) 空間:同樣的下降點偵測,改用 Morris 走訪, 把每個節點的中序前驅串回它自己,而不用堆疊。
# python
# LC 99 - Recover Binary Search Tree, O(1) extra space
# IDEA: Morris in-order (thread the predecessor, unthread on the second visit) + the same drop rule
# time = O(n), space = O(1)
def recoverTree(root):
first = second = prev = None
cur = root
while cur:
if cur.left:
pred = cur.left
while pred.right and pred.right is not cur:
pred = pred.right
if not pred.right: # first visit: thread, then go left
pred.right = cur
cur = cur.left
continue
pred.right = None # second visit: unthread, then visit cur
if prev and prev.val > cur.val:
if first is None:
first = prev
second = cur
prev = cur
cur = cur.right
first.val, second.val = second.val, first.val
複雜度
- 時間 O(n):中序走訪每個節點一次。Morris 每條邊最多走三次,仍是 O(n)。
- 空間 O(h):
inorder的遞迴堆疊。Morris 只保留cur、pred和三個標記,O(1); 串線用的是本來就存在的 null 指標。
試跑 — 壞掉的樹的中序是 [1, 6, 3, 4, 5, 2, 7](2 和 6 被交換)
6 > 3 first drop -> first = 6, second = 3
5 > 2 second drop -> first stays 6, second = 2
swap 6 and 2 -> [1, 2, 3, 4, 5, 6, 7]
陷阱
- 每個下降點都設
first = prev:試跑最後會變成 5,把 5 和 2 交換後樹還是壞的。 - 在第一個下降點就停:
[1, 6, 3, 4, 5, 2, 7]會把 6 和 3 交換。 - 用
TreeNode(-2**31)當prev的哨兵:LC 99 允許真實節點有這個值。從None開始,比較前先擋掉。 - 交換兩個節點參考而不是它們的
.val,樹完全沒變。
不適用時 —— 只問這棵樹合不合法:把上下界往下帶, bst § Template 4。
練習 —— LC 99 · LC 530 (相鄰值的最小差) · LC 501 (相等相鄰值的連續長度) ·
LC 99 · LC 426 (把 prev 和 node 串成串列)
順序統計
模板 3:附帶子樹大小的順序統計 BST — LC 703 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
辨識 —— 「串流中的第 k 大元素」、「每次 add 後回傳第 k 大」、
「BST 經常被修改時的第 k 小」(LC 230 的 follow-up)、「有幾個值小於 x」——
任何你會用 TreeMap 或 SortedList 加上 rank 來回答的查詢。
關鍵 —— 每個節點存 count,也就是它子樹的大小,插入時沿路每個節點都加一。這樣 node.right
的 count 就告訴你有幾個值比 node.val 大,所以 rank 查詢像搜尋一樣只走一條路徑:k 落在右邊就往右,
輪到 node 就回傳它,否則往左,並把跳過的數量從 k 扣掉。
# python
# LC 703 - Kth Largest Element in a Stream
# IDEA: each node keeps its subtree size; kth largest descends using size(node.right)
# time = O(h) per add, space = O(n)
class CountNode:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
self.count = 1 # nodes in the subtree rooted here
class KthLargest:
def __init__(self, k, nums):
self.k = k
self.root = None
for v in nums:
self.root = self._insert(self.root, v)
def _insert(self, node, val):
if not node:
return CountNode(val)
node.count += 1 # the new value lands somewhere below
if val < node.val:
node.left = self._insert(node.left, val)
else:
node.right = self._insert(node.right, val) # duplicates go right
return node
def _size(self, node):
return node.count if node else 0
def add(self, val):
self.root = self._insert(self.root, val)
node, k = self.root, self.k
while True:
right_size = self._size(node.right) # values larger than node.val
if k <= right_size:
node = node.right
elif k == right_size + 1:
return node.val
else:
k -= right_size + 1 # skip the right subtree and node
node = node.left
// java
// LC 703 - Kth Largest Element in a Stream
// IDEA: subtree sizes on every node; rank-descend using the right subtree's size
// time = O(h) per add, space = O(n)
class KthLargest {
private static class Node {
int val;
int count = 1;
Node left, right;
Node(int val) { this.val = val; }
}
private final int k;
private Node root;
public KthLargest(int k, int[] nums) {
this.k = k;
for (int v : nums) {
root = insert(root, v);
}
}
private Node insert(Node node, int val) {
if (node == null) {
return new Node(val);
}
node.count++;
if (val < node.val) {
node.left = insert(node.left, val);
} else {
node.right = insert(node.right, val);
}
return node;
}
private int size(Node node) {
return node == null ? 0 : node.count;
}
public int add(int val) {
root = insert(root, val);
Node node = root;
int rank = k;
while (true) {
int rightSize = size(node.right);
if (rank <= rightSize) {
node = node.right;
} else if (rank == rightSize + 1) {
return node.val;
} else {
rank -= rightSize + 1;
node = node.left;
}
}
}
}
複雜度
- 時間:每次
addO(h):_insert和 rank 迴圈各走一條從根到葉的路徑。平衡時 O(log n), 串流依序到達時 O(n)。 - 空間 O(n):每個加入過的值一個節點。
試跑 — k = 3、nums = [4,5,8,2],接著 add(3)
tree (count): 4(5) -> left 2(2) -> right 3(1); right 5(2) -> right 8(1)
at 4: size(right) = 2, k = 3 == 2 + 1 -> answer 4
陷阱
- 只在新葉子上加
count:所有祖先的大小都會過時,LC 703 範例的第二次add回傳錯誤的 rank。 - 歪斜的最差情況是每次 O(n);要說出來,並點名平衡樹(
TreeMap、紅黑樹)是正式環境的解法。
不適用時 —— k 固定、而且永遠只問第 k 大:大小為 k 的 min-heap 每次 add 只要 O(log k), 五行寫完(heap.md)。先給那個,再把這個當成「如果 k 會變,或你還需要 rank(x)」的答案。
練習 —— LC 703 · LC 230 (follow-up) · LC 315 (計算右側較小的元素數) · LC 295 (中位數:k = n / 2 的查詢)
建構變形
模板 4:用上下界從前序建樹 — LC 1008
辨識 —— 「用前序走訪建出 BST」、「反序列化一棵以前序寫出的 BST」(LC 449)、 「驗證這個序列是不是某棵 BST 的前序」(LC 255)。
關鍵 —— 前序會先列出根,再列出它子樹裡的所有東西,所以把它當成只有一個游標的串流來讀。
每次呼叫擁有一個開區間 (low, high);下一個值落在區間內,它才是這棵子樹的根,否則它屬於某個祖先,
這次呼叫就回傳 None、不消耗它。每個值只被消耗一次。
# python
# LC 1008 - Construct Binary Search Tree from Preorder Traversal
# IDEA: one cursor over the preorder; a call builds a node only if the next value fits (low, high)
# time = O(n), space = O(h)
def bstFromPreorder(preorder):
index = 0
def build(low, high):
nonlocal index
if index == len(preorder) or not (low < preorder[index] < high):
return None # the next value belongs to an ancestor
node = TreeNode(preorder[index])
index += 1
node.left = build(low, node.val) # smaller values, until one is too big
node.right = build(node.val, high)
return node
return build(float("-inf"), float("inf"))
// java
// LC 1008 - Construct Binary Search Tree from Preorder Traversal
// IDEA: a shared cursor; build a node only while the next value fits the interval
// time = O(n), space = O(h)
private int index;
public TreeNode bstFromPreorder(int[] preorder) {
index = 0;
return build(preorder, Long.MIN_VALUE, Long.MAX_VALUE);
}
private TreeNode build(int[] preorder, long low, long high) {
if (index == preorder.length || preorder[index] <= low || preorder[index] >= high) {
return null;
}
TreeNode node = new TreeNode(preorder[index++]);
node.left = build(preorder, low, node.val);
node.right = build(preorder, node.val, high);
return node;
}
複雜度
- 時間 O(n):每個值恰好被一次
build呼叫消耗;其餘呼叫都在上下界檢查處以 O(1) 返回, 而每個節點最多只有兩次這種呼叫。 - 空間 O(h):遞迴堆疊。
試跑 — [8,5,1,7,10,12]
build(-inf, inf) takes 8
build(-inf, 8) takes 5
build(-inf, 5) takes 1; its children see 7, which is outside (-inf, 1) and (1, 5) -> None
build(5, 8) takes 7; its children see 10, outside (5, 7) and (7, 8) -> None
build(8, inf) takes 10; left sees 12, outside (8, 10) -> None; right takes 12
result [8,5,10,1,7,null,12]
陷阱
- 用 bst § Template 2 一個一個插入也行,
但遇到
[1,2,3,…]這種有序的前序就是 O(n²)。 - 在檢查上下界之前就推進游標,會吃掉一個屬於祖先的值。
不適用時 —— 輸入是有序的(中序):哪個值當根都行,取中間就得到平衡樹, bst § Template 6。
練習 —— LC 1008 · LC 449 (用這個當解碼器做序列化) · LC 255 (只驗證:同樣的上下界, 不建樹)
Convert Sorted List to Binary Search Tree — LC 109
關鍵 — 中間仍然是根,但串列沒有索引。快慢指標在 O(長度) 內找到
[left, right)的中間, 所以整個建構是 O(n log n)。
歸約到:bst § Template 6 —— 只是中間是用走的找到,而不是用索引。
# python
# LC 109 - Convert Sorted List to Binary Search Tree
# IDEA: slow/fast pointers find the middle of [left, right); it is the root, recurse on both halves
# time = O(n log n), space = O(log n)
def sortedListToBST(head):
def middle(left, right):
slow = fast = left
while fast is not right and fast.next is not right:
slow = slow.next
fast = fast.next.next
return slow
def convert(left, right): # builds the nodes in [left, right)
if left is right:
return None
mid = middle(left, right)
node = TreeNode(mid.val)
node.left = convert(left, mid)
node.right = convert(mid.next, right)
return node
return convert(head, None)
第二種解法,保留它是因為它是 O(n):依中序的順序建樹。先數節點數,再對索引區間遞迴; 左子樹先建,所以串列的開頭永遠是下一個要放的值。
# python
# LC 109 - in-order simulation
# IDEA: build(lo, hi) makes the left subtree first, then takes the list's current head as its root
# time = O(n), space = O(log n)
def sortedListToBST(head):
n = 0
node = head
while node:
n += 1
node = node.next
current = head
def build(lo, hi):
nonlocal current
if lo > hi:
return None
mid = (lo + hi) // 2
left = build(lo, mid - 1) # consumes exactly the values before mid
root = TreeNode(current.val)
current = current.next
root.left = left
root.right = build(mid + 1, hi)
return root
return build(0, n - 1)
陷阱
- 先把串列複製成陣列也是 O(n) 時間,可以接受;它花 O(n) 記憶體,這通常就是 follow-up 要問的。
Balance a Binary Search Tree — LC 1382 優先度 4/5 — 高價值 — 這裡有缺口就會掉關
關鍵 — 不要旋轉。任何 BST 都能用中序走訪攤平成有序值,而任何有序區間都能用「中間當根」 重建成平衡 BST。LC 1382 就是 LC 94 接上 LC 108。
歸約到:bst § Template 5,接著 bst § Template 6。
# python
# LC 1382 - Balance a Binary Search Tree
# IDEA: in-order into a sorted list, then rebuild with the middle of each range as its root
# time = O(n), space = O(n)
def balanceBST(root):
values = []
def inorder_walk(node):
if not node:
return
inorder_walk(node.left)
values.append(node.val)
inorder_walk(node.right)
def build(lo, hi):
if lo > hi:
return None
mid = (lo + hi) // 2
node = TreeNode(values[mid])
node.left = build(lo, mid - 1)
node.right = build(mid + 1, hi)
return node
inorder_walk(root)
return build(0, len(values) - 1)
陷阱
- 前序或後序都不是有序的,所以重建出來的樹不是 BST。
- LC 1382 允許 10⁴ 個節點排成一條歪斜鏈:遞迴的中序走訪會超過 Python 預設的 1000 層遞迴上限。 改用迭代走訪(bst § Template 5)。
- 重用舊節點而不新配置也可以,前提是
build對每個節點的兩個子節點都賦值;否則舊樹的過時指標會留下來。 - 原地、O(1) 空間的做法是 Day–Stout–Warren 演算法(先旋轉成一條藤,再壓縮)。面試很少見,說出名字就夠了。
模板 5:每個值都當一次根 — LC 95
辨識 —— 「生成所有存放 1..n 的結構相異 BST」、「有幾棵結構相異的 BST」(LC 96)、
「所有可能的滿二元樹」(LC 894)。列出時 n ≤ 8:答案本身就是指數級。
關鍵 —— 選 i 當根,就強制 lo..i-1 進左子樹、i+1..hi 進右子樹,所以以 i 為根的樹就是
每棵左子樹配上每棵右子樹。空區間必須回傳 [None](一棵空樹),不是 [],否則配對迴圈什麼都產生不出來。
# python
# LC 95 - Unique Binary Search Trees II
# IDEA: each value in [lo, hi] as the root; pair every left subtree with every right subtree
# time = O(n * Catalan(n)), space = O(n * Catalan(n)) for the output
def generateTrees(n):
def generate(lo, hi):
if lo > hi:
return [None] # one empty tree, so the pairing still runs
trees = []
for root_val in range(lo, hi + 1):
for left in generate(lo, root_val - 1):
for right in generate(root_val + 1, hi):
trees.append(TreeNode(root_val, left, right))
return trees
return generate(1, n) if n else []
// java
// LC 95 - Unique Binary Search Trees II
// IDEA: each value as the root; the cartesian product of the left and right subtree lists
// time = O(n * Catalan(n)), space = O(n * Catalan(n))
public List<TreeNode> generateTrees(int n) {
return n == 0 ? new ArrayList<>() : generate(1, n);
}
private List<TreeNode> generate(int lo, int hi) {
List<TreeNode> trees = new ArrayList<>();
if (lo > hi) {
trees.add(null);
return trees;
}
for (int rootVal = lo; rootVal <= hi; rootVal++) {
for (TreeNode left : generate(lo, rootVal - 1)) {
for (TreeNode right : generate(rootVal + 1, hi)) {
trees.add(new TreeNode(rootVal, left, right));
}
}
}
return trees;
}
複雜度
- 時間 O(n · Catalan(n)):會產出 Catalan(n) 棵樹(Catalan(8) = 1430),每棵要建 O(n) 個節點。 不同結果之間會共用子樹,這在 LC 95 是允許的。
- 空間 O(n · Catalan(n)):由輸出主導。
陷阱
- 空區間回傳
[]:每個葉子的配對迴圈都跑零次,n = 1一棵樹都回傳不了。 n = 0必須回傳[],不是[None]。
不適用時 —— 只要一棵平衡樹:取中間, bst § Template 6。只要數量: 見下面的 LC 96;把樹建出來毫無意義。
練習 —— LC 95 · LC 96 · LC 894 (滿二元樹:只有奇數大小) · LC 241 (同樣的切開再配對, 用在算式上)
Unique Binary Search Trees — LC 96
關鍵 — n 個值的數量只跟 n 有關:根
i讓左邊剩i - 1個值、右邊剩n - i個, 所以dp[n] = Σ dp[i-1] · dp[n-i]。這就是卡塔蘭數。
歸約到:模板 5 —— 只數不建。
# python
# LC 96 - Unique Binary Search Trees
# IDEA: dp[nodes] = sum over every root of dp[left size] * dp[right size]
# time = O(n^2), space = O(n)
def numTrees(n):
dp = [0] * (n + 1)
dp[0] = 1 # the empty tree
for nodes in range(1, n + 1):
for root in range(1, nodes + 1):
dp[nodes] += dp[root - 1] * dp[nodes - root]
return dp[n]
陷阱
- 把
dp[left] + dp[right]相加而不是相乘:每種左邊形狀都要配每種右邊形狀。n = 3必須得到 5。 dp[0] = 0會讓所有有一側為空的乘積都變成零。
帶著上下界去量測
Maximum Difference Between Node and Ancestor — LC 1026
關鍵 — 把路徑上的最小值與最大值往下帶,路徑結束時讀出
high − low;用不到 BST 的順序, 也用不到這份表單的任何技巧。
歸約到:bst § Template 4 —— 同樣往下帶兩個值,只是拿來擴張而不是檢查。它的完整解、陷阱 和與 LC 98 的對照,在 tree_examples.md § LC 1026。
不適用時
| 技巧 | 遇到這種情況,改用較簡單的卡… |
|---|---|
| 模板 1 延遲式迭代器 | 從頭到尾只走一次:bst § Template 5 的中序迴圈就是同一個堆疊,不必包成類別 |
| 模板 2 下降點偵測 | 只問「合不合法?」:bst § Template 4。只有被要求 O(1) 空間時才值得寫 Morris |
| 模板 3 附帶子樹大小的 BST | k 固定:大小為 k 的 heap。樹是靜態的:中序走到第 k 個節點,bst § Template 5 |
| 模板 4 前序加上下界 | 輸入是有序的,或任何合法 BST 都接受:中間當根,bst § Template 6 |
| 模板 5 每個值都當根 | 只要一棵樹,或只要數量:中間當根,或 LC 96 的 DP |
總結
| 如果 follow-up 是… | 該用 | 成本 |
|---|---|---|
| 「不要一次把整個走訪結果展開」(LC 173) | 模板 1 —— 只推入左側脊線 | O(h) 空間,每次 next() 攤銷 O(1) |
| 「有兩個節點被換了,修好它」(LC 99) | 模板 2 —— 中序 + prev,first 只寫一次 |
O(n) / O(h),或用 Morris 做到 O(1) |
| 「如果我們一直插入呢?」(LC 230 → 703) | 模板 3 —— 每個節點存子樹大小 | 每次插入與每次 rank 查詢都是 O(h) |
| 「輸入是前序傾印」(LC 1008、449) | 模板 4 —— 在串流上用上下界 | O(n) / O(h) |
| 「輸入是鏈結串列」(LC 109) | LC 109 —— 快慢指標找中間,或模擬中序 | O(n log n),或 O(n) |
| 「把這棵 BST 重新平衡」(LC 1382) | LC 1382 —— 先中序,再中間當根 | O(n) / O(n) |
| 「列出/數出所有 BST」(LC 95、96) | 模板 5 —— 每個值都當一次根 | Catalan(n) 棵樹;計數 O(n²) |
底下唯一的那個想法:BST 的中序走訪就是一條有序序列,而這裡的每個技巧,不是改變一次握住這條序列的多少、 就是盯著它找逆序,要不然就是在節點上多加一個欄位,讓一次下降就能回答關於這條序列的問題。