樹狀資料結構 — 概念與模式

Trees & HeapsPriority 5 of 5 — Must know — expect it in almost every loopMust know 更新於 Sep 19, 2026
Section priorityPriority 5 of 5 — Must know — expect it in almost every loopMust knowPriority 4 of 5 — High value — a gap here costs you roundsHigh valuePriority 3 of 5 — Worth knowing — usually a variant of a must-know patternWorth knowingPriority 2 of 5 — Niche — read once, revisit only if a company is known to askNicheMarked on the sections that carry it — unmarked sections are background/reference.

範圍 — 樹的概念、樹的種類,以及走訪順序的策略 — 講的是為什麼選哪個,再加上那些不屬於單一模式模板的進階技巧(Morris 穿線、倍增法、換根)。模板本身放在 tree2.md另見從這份文件拆出去的深入內容tree_lca_distance.md — LCA、節點距離、父節點對照表與根到葉路徑模板;tree_codec.md — 子樹序列化與樹 ⟷ 字串的編解碼;tree_construction.md — 從走訪結果、字串與索引範圍建樹;tree_examples.md — 本頁教的模式所對應的 LC 實作檔案庫。 相鄰主題tree2.md — 每個模式一份編號好、可直接複製的模板;binary_tree.md — 二元樹 DFS 的狀態流向與結構性模板;bst.md — 有序的樹;tree_backtrack.md — 回程時要還原狀態的根→葉路徑題。

LeetCode 題目清單

時間複雜度

資料結構 搜尋 插入 刪除 最小/最大
樹(一般) O(n) O(n) O(n) O(n)

一般的樹(沒有順序保證)— 每個操作都可能走遍所有節點。平衡的樹會把這些降到 O(log n)。空間是儲存的 O(n) 加上遞迴堆疊的 O(h)。想看操作是 O(log n) 的有序樹,見 bst.md

總覽

是一種階層式資料結構,由節點以邊相連而成,有一個根節點且沒有環。樹是電腦科學裡組織資料的基本工具。

關鍵性質

  • 節點:存資料,並指向子節點
  • :最上層、沒有父節點的節點
  • 葉子:沒有子節點的節點
  • 高度:從根到最深葉子的距離
  • 深度:從根到某個特定節點的距離
  • 複雜度:見上方的 Time Complexity 表格

樹的陣列表示法

樹可以用陣列有效率地表示,完全二元樹尤其適合:

text
# Tree Structure
      1
     / \
    2   3
   / \  
  4   5

# Array Representation: [1, 2, 3, 4, 5]
# Index mapping:
# - Root at index 0
# - For node at index i:
#   - Left child at index 2*i + 1  
#   - Right child at index 2*i + 2
#   - Parent at index (i-1)/2

參考資料

0) 核心概念

0-1) 樹的種類

基本樹型

種類 說明 關鍵性質 使用情境
一般樹 節點可以有任意多個子節點 結構彈性大 檔案系統、組織圖
二元樹 每個節點 ≤ 2 個子節點 結構單純、適合遞迴 運算式樹、決策樹
完全二元樹 除了最後一層外每層都填滿 用陣列表示很有效率 堆積、優先佇列
完美二元樹 每一層都完全填滿 共 2^h - 1 個節點 理論分析
BST 左 < 根 < 右 的順序 搜尋/插入/刪除 O(log n) 搜尋操作、資料庫
堆積 父子之間有大小關係 取最小/最大很快 優先佇列、排序
字典樹 字串用的前綴樹 字串操作很有效率 自動補完、拼字檢查

0-2) 常見的樹模式 Priority 4 of 5 — High value — a gap here costs you rounds

九種一再出現的形狀。每一列都指出那個模式的程式碼放在唯一一個地方 — 在這一系列文件裡,沒有任何模式會被寫兩遍。

# 模式 核心想法 程式碼在 範例
1 路徑類 把累積值(總和、目前最大值、路徑)透過 DFS 參數往 tree_lca_distance.md — 根到葉路徑模板 LC 112, 113, 257, 437, 1448
2 子樹驗證 後序 — 先驗證兩個子節點,父節點才做決定 tree2 1.3) LC 98, 101, 110
3 高度 vs 深度 高度由下往上算(後序);深度由上往下帶(前序) 0-3) 由上而下 vs 由下而上 LC 104, 111, 543
4 建樹 一種走訪給出結構,另一種給出位置;或是在選定的根把索引範圍切開 tree_construction.md LC 105, 106, 654, 108
5 序列化 編碼 = 回傳字串的 DFS;解碼 = 消耗前綴的遞迴下降 tree_codec.md LC 297, 449, 606, 536
6 往父節點走 建一張父節點對照表,把樹當成無向圖,再往四面八方 BFS tree_lca_distance.md — 往父節點走的模式 LC 863, 742, 1740
7 節點路徑 把每棵子樹指紋化成 val,left,right(null 用 #),再對字串做雜湊 tree_codec.md LC 652, 572, 508
8 帶狀態的節點刪除 節點自己帶 isDeleted,父節點帶 isParentDeleted;父節點死掉而自己存活的節點,就變成森林的一個根 tree_examples.md — LC 1110 LC 1110, 1325, 669
9 求節點距離 前序 DFS 把 depth 往下帶,命中時再往上回傳;-1 當作找不到的哨兵值,因為 0 是合法答案 tree_lca_distance.md — 節點之間的距離 LC 1740, 863, 1123

模式 2 與 3 是所有樹遞迴的兩半,所以下面會完整展開;另外七個都只隔一頁,位置寫在表格裡。 參考(模式 2):Subtree Validation Video

0-3) 由上而下 vs 由下而上的 DFS — 解樹題的兩種策略 Priority 5 of 5 — Must know — expect it in almost every loop

參考:MaximumDepthOfBinaryTree.java

核心差別:

  • 由上而下:用參數把狀態從父節點往傳。答案在走訪過程中累積(前序位置)。
  • 由下而上:用回傳值把結果從子節點往收。答案在子樹解完之後才組出來(後序位置)。
text
Top-Down (Pre-order)                Bottom-Up (Post-order)
─────────────────────               ──────────────────────
      1  ← start here                    1  ← combine here
     / \  pass depth=1                  / \  return heights
    2   3  depth=2                     2   3  left=1, right=1
   / \     depth=3                    / \     left=2, right=0
  4   5  → update global max        4   5  → return max+1

模式 1:由上而下(把狀態往下傳,前序)

父節點把累積狀態(深度、路徑、目前最大值)交給子節點。通常會用全域變數輸出參數來收最終答案。

java
// LC 104 — Top-Down: pass depth down, update global max
// 3 variants: (a) void helper + global var, (b) void helper + depth param, (c) return depth param

// Variant A: void helper + global var (simplest top-down)
int maxDepth = 0;

public int maxDepth_topDown(TreeNode root) {
    dfs(root, 1);       // start at depth 1
    return maxDepth;
}

private void dfs(TreeNode root, int depth) {
    if (root == null) return;

    // Pre-order position: update answer with current state
    maxDepth = Math.max(maxDepth, depth);

    // Pass depth+1 DOWN to children
    dfs(root.left, depth + 1);
    dfs(root.right, depth + 1);
}

模式 2:由下而上(把結果往上收,後序)

每個節點先問子節點要結果,再把它們組合起來。回傳值負責把答案往上帶。不需要全域變數。

java
// LC 104 — Bottom-Up: children return their depth, parent adds 1
public int maxDepth_bottomUp(TreeNode root) {
    if (root == null) return 0;

    // Post-order: solve children FIRST
    int leftDepth  = maxDepth_bottomUp(root.left);
    int rightDepth = maxDepth_bottomUp(root.right);

    // Combine: take max of children, add 1 for current node
    return 1 + Math.max(leftDepth, rightDepth);
}

比較:

面向 由上而下 由下而上
方向 根 → 葉(前序) 葉 → 根(後序)
狀態怎麼傳 參數(深度、路徑、最大值) 回傳值
全域變數 常常需要 通常不用
輔助函式的回傳型別 常常是 void 回傳算出來的值
心智模型 「我目前為止知道什麼?」 「我的子節點回報了什麼?」
程式碼簡潔度 比較囉嗦(多帶參數) 比較精簡

什麼時候用哪一種:

text
Use TOP-DOWN when:
  → You need to pass parent/ancestor info to children
  → Path tracking: carry path, sum, or max-so-far downward
  → Early termination: can stop when condition met at a node
  → Examples: LC 112 (Path Sum), LC 129 (Sum Root to Leaf),
              LC 1448 (Count Good Nodes), LC 257 (Binary Tree Paths)

Use BOTTOM-UP when:
  → Answer depends on BOTH children's results
  → Need to compute subtree properties (height, size, balance)
  → Validation: check property holds for entire subtree
  → Examples: LC 104 (Max Depth), LC 110 (Balanced Tree),
              LC 543 (Diameter), LC 124 (Max Path Sum),
              LC 236 (LCA), LC 652 (Find Duplicate Subtrees),
              LC 968 (Binary Tree Cameras)

依策略分類的 LC 題目:

LC # 題目 由上而下 由下而上 備註
104 Maximum Depth Yes Yes 兩種都行;由下而上比較簡單
111 Minimum Depth Yes Yes 由下而上要防 null 子節點
110 Balanced Binary Tree - Yes 必須先檢查子樹高度
112 Path Sum Yes - 把剩餘的和往下帶
113 Path Sum II Yes - 由上而下 + 回溯
124 Max Path Sum - Yes 在每個節點把左右合起來
129 Sum Root to Leaf Numbers Yes - 把累積的數字往下帶
236 Lowest Common Ancestor - Yes 先在子樹裡找目標
257 Binary Tree Paths Yes - 把路徑字串往下帶
543 Diameter of Binary Tree - Yes 用全域變數追蹤 max(left+right)
968 Binary Tree Cameras - Yes 貪婪三狀態:0=未覆蓋、1=有相機、2=已覆蓋
1448 Count Good Nodes Yes - 把目前最大值往下帶

混合模式:由下而上 + 全域變數

有些題目用由下而上的回傳值算子樹資訊,但同時維護一個全域變數來追蹤跨子樹的答案(例如直徑、最大路徑和)。

java
// LC 543 — Diameter: bottom-up height + global max update
int diameter = 0;

public int diameterOfBinaryTree(TreeNode root) {
    height(root);
    return diameter;
}

private int height(TreeNode root) {
    if (root == null) return 0;

    int left  = height(root.left);   // bottom-up: get children's height
    int right = height(root.right);

    // Global update: diameter passes THROUGH this node
    diameter = Math.max(diameter, left + right);

    // Return value: height of subtree (for parent to use)
    return 1 + Math.max(left, right);
}

面試提示:

LC 104(Max Depth)是同時練兩種策略最好的題目。先寫由下而上(三行),再改寫成由上而下(全域變數 + void 輔助函式)。兩種都懂,整套樹題工具箱就開了。

0-4) 走訪順序的選擇策略 Priority 5 of 5 — Must know — expect it in almost every loop

text
When to use which traversal:

1. No specific root processing needed?
   → Any order works (preorder/inorder/postorder)

2. Need parent data for children?
   → Use PREORDER (root → left → right)

3. Need children data for parent?
   → Use POSTORDER (left → right → root)

4. Processing sorted data (BST)?
   → Use INORDER (left → root → right)

5. Level-by-level processing?
   → Use BFS/Level-order traversal

6. Need to move upward (to parent) or explore all directions?
   → Use MOVE PARENT pattern (Build parent map + BFS)

7. Need to compare or identify subtrees?
   → Use NODE PATH pattern (Subtree serialization with post-order)

收集葉子時該用前序還是後序(LC 872)

參考:LeafSimilarTrees.java

要收集葉節點時(例如 LC 872 Leaf-Similar Trees),任何先左後右的 DFS 順序都會得到同一串由左到右的葉子序列。不過實務上還是有差別:

前序(收集葉子時推薦):

java
private void getLeafSeq(TreeNode root, List<Integer> list) {
    if (root == null) return;

    // Check leaf BEFORE recursing into children
    if (root.left == null && root.right == null) {
        list.add(root.val);
        return;  // ← Early exit: skip 2 unnecessary null-child calls
    }

    getLeafSeq(root.left, list);
    getLeafSeq(root.right, list);
}

後序(也正確,但稍微浪費):

java
private void getLeafSeq(TreeNode root, List<Integer> list) {
    if (root == null) return;

    getLeafSeq(root.left, list);   // ← calls null, returns immediately
    getLeafSeq(root.right, list);  // ← calls null, returns immediately

    // Check leaf AFTER recursing (children were both null)
    if (root.left == null && root.right == null) {
        list.add(root.val);
    }
}

為什麼兩種結果一樣: 葉子序列只取決於「先左後右」的造訪順序,跟葉子檢查發生在什麼時候無關。葉子沒有子節點,所以後序對 null 的遞迴呼叫會在葉子檢查前立刻返回 — 葉子還是照同樣的由左到右順序被加進去。

為什麼比較推薦前序:

面向 前序 後序
葉子序列 左 → 右 左 → 右(一樣)
在葉子提早返回 可以(加完就 return 不行(已經先遞迴進 null 子節點了)
每個葉子多出的 null 呼叫 0 2
最適合 收集葉子、建路徑 求高度、子樹性質

面試時可以這樣答:

「我選前序,因為一旦判定是葉子就能立刻返回,省掉兩次對 null 子節點的多餘遞迴。任何先左後右的 DFS 都會得到同樣的葉子序列。」

其他「走訪順序會影響葉子/路徑收集」的相關題目:

LC # 題目 建議順序 為什麼
872 Leaf-Similar Trees 前序 在葉子提早返回
257 Binary Tree Paths 前序 由上而下建路徑
112 Path Sum 前序 把剩餘的和往下帶
104 Maximum Depth 後序 要先拿到子節點的高度
110 Balanced Binary Tree 後序 要驗證子樹高度

0-5) 走訪速查表(面試用) Priority 4 of 5 — High value — a gap here costs you rounds

靈感來自 LC 113 Path Sum II — 關鍵洞見:選哪種走訪,就決定了演算法的結構。

走訪 順序 核心用途 什麼時候選它
前序 根 → 左 → 右 由上而下建路徑 根到葉的路徑、把父節點資訊帶給子節點、DFS + 回溯
後序 左 → 右 → 根 由下而上算子樹結果 高度/深度、子樹總和、最大路徑、樹上 DP
中序 左 → 根 → 右 排序順序處理節點 BST 驗證、第 k 小、有序走訪
BFS 一層一層 逐層處理 最小深度、鋸齒走訪、右視圖、串接 next 指標

Interview Quick-Check Tips

Step 1 — What does the problem ask for?

Problem asks for… Use
All root-to-leaf paths / path with sum Pre-order DFS + backtracking
Count paths (any start/end) with target sum Pre-order DFS + prefix sum HashMap
Tree height / max depth Post-order DFS
Subtree property (sum, size, max) Post-order DFS
Identify / compare subtrees by structure Post-order DFS + serialize val,left,right + HashMap
Find duplicate subtrees Post-order DFS + subtree serialization + HashMap count
BST sorted order / kth smallest In-order DFS
Validate BST In-order DFS
Level-by-level / min depth BFS
Connect same-level nodes BFS

Interview Trick (from LC 113):

If the problem asks for “root → leaf path”, it is almost always pre-order DFS + backtracking.

Interview Trick (from LC 437):

If the path does NOT need to start/end at root/leaf and asks for count, use Pre-order DFS + Prefix Sum HashMap (the “2-sum on tree” pattern). The full template — why the map must be undone on the way back up, the two spellings of the base case, and the long overflow trap in Java — is Template 14 in prefix_sum_advanced.md.

Classic LC Problems by Traversal Type

Pre-order DFS + Backtracking (root → leaf path)

LC # Problem Key Idea
112 Path Sum Pre-order DFS, check leaf with remaining sum
113 Path Sum II Pre-order DFS + backtrack, collect all paths
257 Binary Tree Paths Pre-order DFS + backtrack, build string paths
437 Path Sum III Pre-order DFS + prefix sum HashMap, 2-sum trick: check (curSum-target) in map — template
129 Sum Root to Leaf Numbers Pre-order DFS, carry running number
404 Sum of Left Leaves Pre-order DFS, carry an isLeft flag down; add value only at a leaf reached as a left child

Post-order DFS (bottom-up subtree computation)

LC # Problem Key Idea
104 Maximum Depth of Binary Tree Post-order, return max(left, right) + 1
543 Diameter of Binary Tree Post-order, track max left+right at each node
124 Binary Tree Maximum Path Sum Post-order, track global max through root
110 Balanced Binary Tree Post-order, return height or -1 if unbalanced
572 Subtree of Another Tree Post-order serialization or recursive match
236 Lowest Common Ancestor Post-order, return node when both targets found
652 Find Duplicate Subtrees Post-order + serialize subtree → val,left,right + HashMap
968 Binary Tree Cameras Post-order greedy, 3 states: uncovered/camera/covered
563 Binary Tree Tilt Post-order, return subtree SUM upward while accumulating abs(leftSum - rightSum) into a global — classic “return one thing, collect another”

In-order DFS (BST / sorted order)

LC # Problem Key Idea
98 Validate Binary Search Tree In-order, check ascending order
230 Kth Smallest Element in BST In-order traversal, count to k
501 Find Mode in BST In-order, track current/prev with count
538 Convert BST to Greater Tree Reverse in-order (right → root → left)
700 Search in a Binary Search Tree In-order search leveraging BST property

BFS / Level-order

LC # Problem Key Idea
102 Binary Tree Level Order Traversal BFS with queue, collect each level
111 Minimum Depth of Binary Tree BFS, return level when first leaf found
116 Populating Next Right Pointers BFS level-order, connect siblings
199 Binary Tree Right Side View BFS, take last node of each level
103 Zigzag Level Order Traversal BFS + alternate direction per level
117 Populating Next Right Pointers II Level linking on a NON-perfect tree — dummy-head sweep, O(1) space (see Template 4-1)
637 Average of Levels in Binary Tree BFS, sum each level then divide by levelSize
987 Vertical Order Traversal Tag (col, row, val), sort col → row → val (see Template 4-2)

1) 樹的模板與演算法

1.1) 通用樹模板 Priority 5 of 5 — Must know — expect it in almost every loop

核心原則:樹題天生就是遞迴的 — 用子樹的解來解當前節點。

python
# Universal Tree Template
def solve_tree_problem(root, params):
    # Base case
    if not root:
        return base_case_value
    
    # Process current node (preorder position)
    process_current_node(root, params)
    
    # Recursively solve subtrees
    left_result = solve_tree_problem(root.left, updated_params)
    right_result = solve_tree_problem(root.right, updated_params)
    
    # Combine results (postorder position)
    result = combine_results(root, left_result, right_result)
    
    return result
java
// Java Universal Tree Template
public ResultType solveTreeProblem(TreeNode root, ParamType params) {
    // Base case
    if (root == null) {
        return defaultValue;
    }
    
    // Preorder: Process current node
    processCurrentNode(root, params);
    
    // Recursive calls
    ResultType leftResult = solveTreeProblem(root.left, updatedParams);
    ResultType rightResult = solveTreeProblem(root.right, updatedParams);
    
    // Postorder: Combine results
    ResultType result = combineResults(root.val, leftResult, rightResult);
    
    return result;
}

1.2) 模板選擇指南 Priority 4 of 5 — High value — a gap here costs you rounds

模式 模板 什麼時候用 範例題目
DFS 遞迴 標準遞迴 大部分樹題 LC 104, 110, 226
DFS 迭代 用堆疊 避開遞迴深度限制 LC 94, 144, 145
BFS 層序 用佇列 需要逐層處理 LC 102, 199, 515
分治法 由下而上遞迴 需要子樹的結果 LC 124, 543, 687
路徑追蹤 帶路徑狀態的 DFS 路徑相關題 LC 112, 257, 437
往父節點走 父節點對照表 + BFS 需要雙向探索 LC 863, 742, 1740
節點路徑 子樹序列化 子樹比對/偵測 LC 652, 572

1.3) 核心操作

1.3.1) 樹的走訪策略

兩大類作法:

  1. 深度優先搜尋(DFS) — 先往深處走,再往旁邊走

    • 前序:根 → 左 → 右(由上而下處理)
    • 中序:左 → 根 → 右(BST 上就是排序順序)
    • 後序:左 → 右 → 根(由下而上處理)
  2. 廣度優先搜尋(BFS) — 一層一層處理

    • 層序:先處理完深度 d 的所有節點,再處理 d+1

1.4) 走訪順序怎麼選

四種基本走訪的完整程式碼寫在 tree2.md — 那份文件是編號好的模板目錄,Python 與 Java 各一份,每個模式一份。 這裡要處理的是更前面的問題:到底該選哪一種。

答案取決於… 走訪 為什麼 模板
父節點,而且在子節點的結果出來之前就要 前序 — 根 → 左 → 右 狀態往流:一條路徑、一個深度、一個累積前綴 tree2 1.1) — LC 144
這棵樹是 BST,而你要的是排序順序 中序 — 左 → 根 → 右 BST 的中序就是排序後的序列,所以 LC 98 和 LC 230 各只要一行 tree2 1.2) — LC 94
兩個子節點,節點才能給出答案 後序 — 左 → 右 → 根 狀態往流:高度、總和、「這棵子樹合不合法」的判定 tree2 1.3) — LC 145
到根的距離,或每一層各自的答案 層序(BFS) 第一次碰到某節點走的一定是最短路徑,而一層就是佇列長度的一次快照 tree2 1.4) — LC 102
一樣看層,但方向要交替 BFS + 方向旗標 反轉那一列就好,不要去反轉走訪本身 tree2 1.5) — LC 103

一句話的判斷法:問自己*「這個節點不用聽子節點回報,就能給出答案嗎?」* 可以 → 前序。不行 → 後序。「我需要一次拿到整排」→ BFS。

1.4-1) 不需要佇列的走訪

有兩種技巧能拿到一般走訪的答案,卻不用付出它的空間代價。它們不屬於單一模式的模板,所以留在這裡而不是放進目錄:

  • O(1) 空間的逐層串接 — 節點本身已經有 next 指標時,那一層自己就能當佇列用。完整寫在 tree2 8.1) — LC 116 / LC 117
  • 帶座標標註的走訪 — 在任何走訪中帶著 (row, col),最後再排序;走訪順序就變得不重要了, 這也是為什麼在垂直走訪、俯視/仰視圖與 LC 662 那種寬度索引題上,DFS 與 BFS 可以互換。 tree2 8.3) — LC 987
  • Morris 走訪 — 見下方。

模板 5:Morris 走訪(O(1) 空間的樹走訪)

用穿線二元樹做 O(1) 空間的中序走訪

核心概念: Morris 走訪把每個節點空著的右指標當成暫時的「線」,接回它的中序後繼節點,於是不用遞迴堆疊就能做到 O(n) 時間、O(1) 空間的走訪。

python
# In-order Morris Traversal — O(n) time, O(1) space
def inorderMorris(root):
    result = []
    curr = root
    while curr:
        if not curr.left:
            result.append(curr.val)
            curr = curr.right
        else:
            # Find in-order predecessor
            pred = curr.left
            while pred.right and pred.right is not curr:
                pred = pred.right
            if not pred.right:          # Thread: set predecessor → curr
                pred.right = curr
                curr = curr.left
            else:                        # Unthread: restore tree
                pred.right = None
                result.append(curr.val)
                curr = curr.right
    return result
# Pre/post-order variants follow the same thread-manipulation pattern.
java
// Java — Morris In-Order (LC 94)
// time = O(N), space = O(1)
public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    TreeNode current = root;
    while (current != null) {
        if (current.left == null) {
            result.add(current.val);
            current = current.right;
        } else {
            TreeNode pred = current.left;
            while (pred.right != null && pred.right != current)
                pred = pred.right;
            if (pred.right == null) {   // first visit: create thread
                pred.right = current;
                current = current.left;
            } else {                    // second visit: unthread + process
                pred.right = null;
                result.add(current.val);
                current = current.right;
            }
        }
    }
    return result;
}

效能: O(n) 時間、O(1) 空間。面試官要求 O(1) 空間走訪時就用它。 前序變形:第一次造訪(建線的時候)就處理節點,而不是第二次。 後序: 需要反轉右脊 — 很複雜、幾乎不會考;還是用迭代堆疊比較實在。

走訪方法 時間 空間 會不會改動樹
遞迴 O(n) O(h) 不會
迭代堆疊 O(n) O(h) 不會
Morris O(n) O(1) 暫時會(之後還原)
Morris threading family — temporary thread vs. permanent rewire

Both the Morris traversal above and the O(1) flatten (LC 114) share the same core step: from the current node, find the rightmost node of its left subtree (the in-order predecessor) and use its empty right pointer to “thread” somewhere. They differ only in what they do with that thread:

Variant Thread points to Restored? Purpose Example
Morris traversal in-order successor (curr) ✅ yes (unthread on 2nd visit) Visit nodes O(1) space LC 94, 144
Morris rewire (flatten) original right subtree (curr.right) ❌ no (permanent) Restructure tree in-place LC 114
python
# Morris REWIRE pattern — permanent threading (LC 114 Flatten Binary Tree to Linked List)
# time = O(n), space = O(1)
def flatten(root):
    curr = root
    while curr:
        if curr.left:
            # find left subtree's rightmost node (in-order predecessor)
            rightmost = curr.left
            while rightmost.right:
                rightmost = rightmost.right
            # splice: predecessor.right -> original right subtree (PERMANENT, not restored)
            rightmost.right = curr.right
            curr.right = curr.left   # move left subtree to the right
            curr.left = None         # clear left
        curr = curr.right            # advance down the new right spine

Mental model: for each node with a left child, the left subtree is “inserted” between the node and its original right subtree, because the left subtree’s pre-order traversal must come immediately after the node and before the right subtree. The rightmost node of the left subtree is exactly where the right subtree should re-attach.

text
   curr                 curr
   /  \                    \
  L    R     ───►          L            (curr.right = curr.left)
   \                        \
    ...                     ...
      \                       \
     L_rightmost            L_rightmost
                              \
                               R         (L_rightmost.right = R)

The recursive counterpart — post-order where each call returns the tail of the subtree it flattened, so the parent knows where to splice — is worked through in tree_examples 16), including why the tail is checked right → left → node.

When to reach for this: any “in-place, O(1) space, restructure a tree along its right spine” problem. The while rightmost.right predecessor-finding step is the signature. Recognize it as the same machinery as Morris traversal — only the thread’s destination and whether you restore it change.


1.5) 樹節點的初始化

python
# Python TreeNode Class
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

# Create a simple tree
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
java
// Java TreeNode Class
public class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    
    TreeNode() {}
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

2) 依模式分類的題目

2.1) 題型分類與模板 Priority 4 of 5 — High value — a gap here costs you rounds

走訪類題目

題目 LC # 模式 模板 難度
Binary Tree Preorder Traversal 144 DFS 前序 前序模板 Easy
Binary Tree Inorder Traversal 94 DFS 中序 中序模板 Easy
Binary Tree Postorder Traversal 145 DFS 後序 後序模板 Easy
Binary Tree Level Order Traversal 102 BFS 層序 BFS 模板 Medium
Binary Tree Zigzag Level Order 103 BFS 交替方向 BFS + 方向 Medium

樹的性質類題目

題目 LC # 模式 模板 難度
Maximum Depth of Binary Tree 104 DFS 由下而上 後序求高度 Easy
Minimum Depth of Binary Tree 111 BFS/DFS BFS 提早停止 Easy
Balanced Binary Tree 110 DFS 檢查高度 高度驗證 Easy
Symmetric Tree 101 DFS 比對 鏡像驗證 Easy
Same Tree 100 DFS 比對 樹的比對 Easy

路徑類題目

題目 LC # 模式 模板 難度
Binary Tree Maximum Path Sum 124 DFS 追蹤路徑 全域最大值更新 Hard
Path Sum 112 DFS 路徑驗證 路徑累加 Easy
Path Sum II 113 DFS 收集路徑 路徑 + 回溯 Medium
Path Sum III 437 DFS 前綴和 路徑計數 Medium
Sum Root to Leaf Numbers 129 DFS 路徑計算 路徑數值組合 Medium
Count Good Nodes in Binary Tree 1448 DFS 路徑最大值 路徑狀態追蹤 Medium
Diameter of Binary Tree 543 DFS 路徑長度 最長路徑 Easy
Longest Univalue Path 687 DFS 路徑模式 同值路徑 Medium

距離與 LCA 類題目

題目 LC # 模式 模板 難度
Lowest Common Ancestor 236 DFS 後序 標準 LCA Medium
LCA of BST 235 BST 性質 數值比較 Easy
Distance in Binary Tree 1740 LCA + 距離 路徑距離 Medium
All Nodes Distance K 863 圖 + BFS 樹轉圖 Medium
Smallest Subtree w/ Deepest Nodes 865/1123 LCA + 深度比較 回傳 (node, dist) 的 DFS Medium

高度與深度類題目

題目 LC # 模式 模板 難度
Maximum Depth 104 DFS 由下而上 高度計算 Easy
Minimum Depth 111 BFS/DFS 到葉子的深度 Easy
Balanced Binary Tree 110 DFS 高度驗證 平衡檢查 Easy
Find Bottom Left Tree Value 513 BFS 層序 最深層最左節點 Medium

建樹類題目

題目 LC # 模式 模板 難度
Construct Binary Tree from Preorder and Inorder 105 分治 建樹 Medium
Construct Binary Tree from Inorder and Postorder 106 分治 建樹 Medium
Serialize and Deserialize Binary Tree 297 樹的編碼 字串轉換 Hard
Construct String from Binary Tree 606 DFS 組字串 字串建構 Easy

改動樹結構的題目

題目 LC # 模式 模板 難度
Invert Binary Tree 226 DFS 交換節點 翻轉樹 Easy
Flatten Binary Tree to Linked List 114 DFS 重接結構 攤平樹 Medium
Merge Two Binary Trees 617 DFS 合併 樹的合併 Easy
Delete Nodes And Return Forest 1110 DFS + 狀態追蹤 刪節點並形成森林 Medium

子樹比對類題目(Node Path 模式)

題目 LC # 模式 模板 難度
Find Duplicate Subtrees 652 Node Path 序列化 子樹雜湊 Medium
Subtree of Another Tree 572 Node Path 比對 子樹配對 Easy
Count Univalue Subtrees 250 Node Path 驗證 子樹性質檢查 Medium

2.2) 模式選擇指南

text
Problem Analysis Decision Tree:

1. Need to process all nodes?
   ├── Yes: Choose appropriate traversal (preorder/inorder/postorder/level-order)
   └── No: Continue

2. Need information from children for parent?
   ├── Yes: Use POSTORDER traversal
   └── No: Continue

3. Need information from parent for children?
   ├── Yes: Use PREORDER traversal
   └── No: Continue

4. Processing level by level?
   ├── Yes: Use BFS/Level-order traversal
   └── No: Continue

5. Need to move upward (to parent) or explore multi-directionally?
   ├── Yes: Use MOVE PARENT pattern (Build parent map + BFS)
   └── No: Continue

6. Need to compare or find duplicate subtrees?
   ├── Yes: Use NODE PATH pattern (Subtree serialization)
   └── No: Continue

7. Working with BST and need sorted order?
   ├── Yes: Use INORDER traversal
   └── No: Use any suitable approach

3) 總結與速查

3.1) 樹演算法複雜度總表

操作 平衡樹 非平衡樹 空間複雜度
搜尋 O(log n) O(n) O(h) 遞迴
插入 O(log n) O(n) O(h) 遞迴
刪除 O(log n) O(n) O(h) 遞迴
走訪 O(n) O(n) O(h) 遞迴
計算高度 O(n) O(n) O(h) 遞迴

3.2) 走訪速查

走訪 順序 使用情境 關鍵特徵
前序 根 → 左 → 右 複製樹、序列化 先處理父節點再處理子節點
中序 左 → 根 → 右 BST 的排序輸出 先左,再根,再右
後序 左 → 右 → 根 刪除樹、各種計算 先處理子節點再處理父節點
層序 一層一層 印出樹、最短路徑 用佇列,逐層處理

3.3) 解題模板

路徑追蹤模板

python
def solve_path_problem(root, target):
    def dfs(node, current_path, current_sum):
        if not node:
            return
        
        # Add current node to path
        current_path.append(node.val)
        current_sum += node.val
        
        # Check if we found target
        if not node.left and not node.right:  # Leaf node
            if current_sum == target:
                result.append(current_path[:])  # Add copy of path
        
        # Recurse to children
        dfs(node.left, current_path, current_sum)
        dfs(node.right, current_path, current_sum)
        
        # Backtrack
        current_path.pop()
    
    result = []
    dfs(root, [], 0)
    return result

3.4) 常見模式與技巧

高度 vs 深度模式

python
# Height (bottom-up, postorder)
def height(node):
    if not node:
        return 0
    return 1 + max(height(node.left), height(node.right))

# Depth (top-down, preorder)
def calculate_depth(node, depth=0):
    if not node:
        return
    
    node.depth = depth  # Assign depth to node
    calculate_depth(node.left, depth + 1)
    calculate_depth(node.right, depth + 1)

全域變數模式

python
class Solution:
    def __init__(self):
        self.max_sum = float('-inf')  # Global result
    
    def max_path_sum(self, root):
        def dfs(node):
            if not node:
                return 0
            
            left_max = max(0, dfs(node.left))   # Ignore negative paths
            right_max = max(0, dfs(node.right))
            
            # Update global maximum
            self.max_sum = max(self.max_sum, node.val + left_max + right_max)
            
            # Return maximum path through this node
            return node.val + max(left_max, right_max)
        
        dfs(root)
        return self.max_sum

3.5) 常見錯誤與提示 Priority 4 of 5 — High value — a gap here costs you rounds

🚫 常見錯誤:

  • 遞迴忘了寫終止條件
  • 走訪過程中錯誤地改動樹的結構
  • 沒有妥善處理 null 節點
  • 對題目選錯走訪順序
  • 遞迴太深導致 stack overflow(改用迭代寫法)

✅ 最佳實務:

  • 一律先檢查 null 節點
  • 用 helper 函式來多傳幾個參數
  • 樹很深時考慮改寫成迭代解
  • 驗證輸入並處理邊界情況
  • 變數名要有意義(left_result、right_result)
  • 用平衡與非平衡的樹分別測試

3.6) 面試提示

  1. 釐清題目:問清楚 null 輸入、樹的結構、輸出格式
  2. 先寫遞迴解:多數樹題都有很漂亮的遞迴解
  3. 想想迭代版本:當遞迴深度可能出問題時
  4. 拿例子走一遍:用小例子驗證邏輯
  5. 分析複雜度:一定要討論時間與空間複雜度
  6. 處理邊界情況:空樹、單一節點、極深的樹

3.7) 相關主題

  • 二元搜尋樹:順序性質讓操作變得有效率
  • 堆積:具有堆積性質的完全二元樹
  • 字典樹:字串操作用的前綴樹
  • 線段樹:處理區間查詢問題
  • 圖演算法:樹是圖的一種特例

進階樹技巧 — 倍增法、換根、Morris 走訪

倍增法(Binary Lifting)— 每次查詢 O(log n) 求 LCA Priority 3 of 5 — Worth knowing — usually a variant of a must-know pattern

python
import math

def build_binary_lifting(root, n):
    """Preprocess tree for O(log n) LCA queries."""
    LOG = max(1, int(math.log2(n)) + 1)
    parent = [[-1] * n for _ in range(LOG)]
    depth = [0] * n

    # BFS to set parent[0] and depth
    from collections import deque
    queue = deque([root.val])
    visited = {root.val}
    # ... (wire up parent[0][v] = direct parent of v)

    # Fill sparse table: parent[k][v] = 2^k-th ancestor of v
    for k in range(1, LOG):
        for v in range(n):
            if parent[k-1][v] != -1:
                parent[k][v] = parent[k-1][parent[k-1][v]]
    return parent, depth

def lca(u, v, parent, depth, LOG):
    # Bring u and v to the same depth
    if depth[u] < depth[v]: u, v = v, u
    diff = depth[u] - depth[v]
    for k in range(LOG):
        if (diff >> k) & 1:
            u = parent[k][u]
    if u == v: return u
    # Move both up until they meet
    for k in range(LOG - 1, -1, -1):
        if parent[k][u] != parent[k][v]:
            u = parent[k][u]; v = parent[k][v]
    return parent[0][u]

時間:預處理 O(n log n),每次 LCA 查詢 O(log n)。 適用:查詢量很大的 LC 236(LCA)、任兩點之間的路徑和。

換根 DP — 求出以每個節點為根時的答案

python
# LC 310 Minimum Height Trees — find roots minimizing tree height
# Equivalent: find centroid(s) of tree
def findMinHeightTrees(n, edges):
    if n == 1: return [0]
    from collections import defaultdict, deque
    graph = defaultdict(set)
    for u, v in edges:
        graph[u].add(v); graph[v].add(u)

    leaves = deque(i for i in range(n) if len(graph[i]) == 1)
    remaining = n
    while remaining > 2:
        remaining -= len(leaves)
        new_leaves = deque()
        while leaves:
            leaf = leaves.popleft()
            neighbor = next(iter(graph[leaf]))
            graph[neighbor].remove(leaf)
            if len(graph[neighbor]) == 1:
                new_leaves.append(neighbor)
        leaves = new_leaves
    return list(leaves)

樹的序列化/反序列化 — LC 297

前序 + null 標記的編解碼(Python 與 Java)、括號格式(LC 606/536)與深度前綴格式(LC 1028),見 tree_codec.md

Morris 走訪(O(1) 空間)— 精簡版參考

完整的中序 Morris 模板(Python + Java)見上面的 Template 5: Morris Traversal。核心想法:把每個節點空著的 right 指標穿線到它的中序後繼,第二次造訪時再解除穿線 — O(n) 時間、O(1) 空間。前序/後序的變體用的是同一套穿線手法。

面試提示 — 樹

訊號 模式
「直徑/最長路徑」 後序:回傳高度,同時追蹤最大直徑
「最近共同祖先」 遞迴:若 root 就是其中一個目標節點,回傳 root
「LCA 但查詢很多次」 倍增法(稀疏表)
「以每個節點為根時的答案」 換根 DP(兩次 DFS)
「序列化/反序列化樹」 前序 DFS 加 null 標記
「驗證 BST」 中序序列必須嚴格遞增
「用排序好的陣列建平衡 BST」 取中點遞迴
「O(1) 空間走訪」 Morris 穿線
「任兩點之間的路徑和」 後序:追蹤通過每個節點的最大路徑