BST — 進階模式與深入探討

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

範圍 — 第一輪可以先跳過的 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
# 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
// 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]

text
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
# 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
# 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
// 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
# 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 被交換)

text
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
# 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
// 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;
            }
        }
    }
}

複雜度

  • 時間:每次 add O(h):_insert 和 rank 迴圈各走一條從根到葉的路徑。平衡時 O(log n), 串流依序到達時 O(n)。
  • 空間 O(n):每個加入過的值一個節點。

試跑 — k = 3、nums = [4,5,8,2],接著 add(3)

text
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
# 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
// 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]

text
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
# 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
# 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
# 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
# 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
// 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
# 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 的中序走訪就是一條有序序列,而這裡的每個技巧,不是改變一次握住這條序列的多少、 就是盯著它找逆序,要不然就是在節點上多加一個欄位,讓一次下降就能回答關於這條序列的問題。