Tree — Concepts & Patterns

Trees & HeapsPriority 5 of 5 — Must know — expect it in almost every loopMust know Updated Nov 12, 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.

Scope — Tree concepts, tree types and traversal-order strategy — the why and which, plus the advanced techniques that are worked here (Morris threading, binary lifting, and re-rooting DP, the home of LC 834). The per-pattern binary-tree templates live in tree2.md. See also — deep dives split out of this file: tree_lca_distance.md — LCA, node distance, parent maps and root-to-leaf path templates; tree_codec.md — subtree serialization and tree ⟷ string codecs; tree_construction.md — building a tree from traversals, strings and index ranges; tree_examples.md — the worked LC archive for the patterns taught here. Neighbouring sheets: tree2.md — one numbered, copy-paste template per pattern; binary_tree.md — binary-tree DFS state-flow and structural templates; bst.md — ordered trees; tree_backtrack.md — root→leaf path problems that undo state on the way back up.

LeetCode Problem Lists

Time Complexity

Data structure Search Insert Delete Min/Max
Tree (general) O(n) O(n) O(n) O(n)

General tree (no ordering guarantee) — every operation may visit all nodes. A balanced tree drops these to O(log n). Space is O(n) for storage plus O(h) for the recursion stack. For an ordered tree with O(log n) operations see bst.md.

Overview

Tree is a hierarchical data structure consisting of nodes connected by edges, with one root node and no cycles. Trees are fundamental in computer science for organizing data efficiently.

Key Properties

  • Nodes: Elements that store data and references to children
  • Root: The topmost node with no parent
  • Leaves: Nodes with no children
  • Height: Distance from root to deepest leaf
  • Depth: Distance from root to a specific node
  • Complexity: see the Time Complexity table above

Tree Array Representation

Trees can be efficiently represented using arrays, especially for complete binary trees:

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

References

Recognition

Most tree problems route out of this sheet: it owns the traversal choice and three techniques no per-pattern template covers. Constraint cues come from the constraint ruler.

Signal in the statement Constraint cue Reach for Not this if
“the answer for every node as the root”, “sum of distances from each node” n ≤ 3 · 10⁴ → O(n), not n DFS runs Template 3 re-rooting DP one best root (“minimum height trees”, LC 310) → leaf trimming, topology_sorting § Template 7
“k-th ancestor”, many LCA queries on a tree that never changes n, q ≤ 5 · 10⁴ → O(log n) per query Template 2 binary lifting a single LCA query → one O(n) DFS, tree_lca_distance.md
“O(1) extra space”, “without recursion or a stack”, “flatten in place” a follow-up, rarely the first ask Template 1 Morris threading the tree may not be modified even briefly (shared, read-only) → iterative stack, binary_tree § Template 2
a path, a depth or a running max the parent must hand down nodes ≤ 10⁵ → one O(n) pass 0-3) top-down the node needs both children’s answers → bottom-up
“height”, “balanced”, “diameter” — needs both children first nodes ≤ 10⁴ 0-3) bottom-up, then binary_tree.md the value comes from above (depth) → top-down
“level by level”, “right side view”, “minimum depth” nodes ≤ 10⁴ BFS, binary_tree § Template 2
“root-to-leaf path”, “lowest common ancestor”, “distance between nodes” nodes ≤ 10⁵ tree_lca_distance.md the tree is a BST → walk by value, bst.md
“valid BST”, “k-th smallest” nodes ≤ 10⁴ in-order, bst.md an unordered tree → no sorted order to lean on
“serialize / deserialize”, “duplicate subtrees” nodes ≤ 10⁴ tree_codec.md

0) Core Concepts

0-1) Tree Types Classification

Basic Tree Types

Type Description Key Properties Use Cases
General Tree Node with any number of children Flexible structure File systems, org charts
Binary Tree Each node has ≤ 2 children Simple structure, recursive Expression trees, decision trees
Complete Binary Tree All levels filled except possibly last Efficient array representation Heaps, priority queues
Perfect Binary Tree All levels completely filled 2^h - 1 nodes Theoretical analysis
BST Left < Root < Right ordering O(log n) search/insert/delete Search operations, databases
Heap Parent-child ordering property Fast min/max extraction Priority queues, sorting
Trie Prefix tree for strings Efficient string operations Auto-complete, spell check

0-2) Common Tree Patterns Priority 4 of 5 — High value — a gap here costs you rounds

The nine recurring shapes. Each row names the one place the code for that pattern lives — no pattern is written out twice in this family of sheets.

# Pattern Core idea Code lives in Examples
1 Path-Based carry the accumulated value (sum, max-so-far, path) DOWN through DFS parameters tree_lca_distance.md — root-to-leaf path templates LC 112, 113, 257, 437, 1448
2 Subtree Validation post-order — validate both children before deciding for the parent tree2 1.3) LC 98, 101, 110
3 Height vs Depth height is computed bottom-up (post-order); depth is carried top-down (pre-order) 0-3) Top-Down vs Bottom-Up LC 104, 111, 543
4 Tree Construction one traversal gives the structure, the other gives the position; or split an index range at the chosen root tree_construction.md LC 105, 106, 654, 108
5 Serialization encode = DFS that returns a string; decode = recursive descent that consumes a prefix tree_codec.md LC 297, 449, 606, 536
6 Move Parent build a parent map, treat the tree as an undirected graph, then BFS in all directions tree_lca_distance.md — move-parent pattern LC 863, 742, 1740
7 Node Path fingerprint each subtree as val,left,right with # for null, then hash the string tree_codec.md LC 652, 572, 508
8 Node Deletion with State carry isDeleted for the node and isParentDeleted for its parent; a survivor whose parent died becomes a forest root tree_examples.md — LC 1110 LC 1110, 1325, 669
9 Get Node Distance pre-order DFS carrying depth DOWN, returning it UP on a hit; -1 is the not-found sentinel because 0 is a valid answer tree_lca_distance.md — distance between nodes LC 1740, 863, 1123

Patterns 2 and 3 are the two halves of every tree recursion, so they are spelled out below; the other seven are one hop away in the sheet named in the table.

0-3) Top-Down vs Bottom-Up DFS — Two Strategies for Tree Problems Priority 5 of 5 — Must know — expect it in almost every loop

Reference: MaximumDepthOfBinaryTree.java

Core Distinction:

  • Top-down: Pass state down from parent to children via parameters. The answer accumulates during traversal (pre-order position).
  • Bottom-up: Collect results up from children to parent via return values. The answer is built after subtrees are solved (post-order position).
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

Pattern 1: Top-Down (pass state down, pre-order)

The parent passes accumulated state (depth, path, max-so-far) to children. Typically uses a global variable or output parameter to collect the final answer.

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);
}

Pattern 2: Bottom-Up (collect results up, post-order)

Each node asks its children for their results, then combines them. The return value carries the answer upward. No global variable needed.

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);
}

Comparison:

Aspect Top-Down Bottom-Up
Direction Root → Leaves (pre-order) Leaves → Root (post-order)
State passing Via parameters (depth, path, max) Via return values
Global variable Often needed Usually not needed
Return type of helper Often void Returns computed value
Mental model “What do I know so far?” “What did my children find?”
Code simplicity More verbose (extra params) More concise

When to Use Which:

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 Problems by Strategy:

LC # Problem Top-Down Bottom-Up Notes
104 Maximum Depth Yes Yes Both work; bottom-up is simpler
111 Minimum Depth Yes Yes Bottom-up needs null-child guard
110 Balanced Binary Tree - Yes Must check subtree heights first
112 Path Sum Yes - Carry remaining sum downward
113 Path Sum II Yes - Top-down + backtracking
124 Max Path Sum - Yes Combine left+right at each node
129 Sum Root to Leaf Numbers Yes - Carry running number downward
236 Lowest Common Ancestor - Yes Find targets in subtrees first
257 Binary Tree Paths Yes - Carry path string downward
543 Diameter of Binary Tree - Yes Track max(left+right) globally
968 Binary Tree Cameras - Yes Greedy 3-state: 0=uncovered, 1=camera, 2=covered
1448 Count Good Nodes Yes - Carry max-so-far downward

Hybrid Pattern: Bottom-Up + Global Variable

Some problems use bottom-up return values to compute subtree info, but also maintain a global variable to track a cross-subtree answer (e.g., diameter, max path sum).

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);
}

Interview Tip:

LC 104 (Max Depth) is the best problem to practice both strategies. Start with bottom-up (3 lines), then rewrite as top-down (global var + void helper). Understanding both unlocks the full tree problem toolkit.

0-4) Traversal Order Selection Strategy 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)

Pre-order vs Post-order for Leaf Collection (LC 872)

Reference: LeafSimilarTrees.java

When collecting leaf nodes (e.g., LC 872 Leaf-Similar Trees), any DFS order that visits left before right produces the same left-to-right leaf sequence. However, there are practical differences:

Pre-order (recommended for leaf collection):

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);
}

Post-order (also correct, but slightly wasteful):

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);
    }
}

Why both produce the same result: The leaf sequence depends only on left-before-right visitation order, NOT on when the leaf check happens. Since a leaf has no children, post-order’s recursive calls to null return immediately before the leaf check — the leaf is still added in the same left-to-right order.

Why pre-order is preferred:

Aspect Pre-order Post-order
Leaf sequence Left → Right Left → Right (same)
Early exit at leaf Yes (return after adding) No (already recursed into null children)
Unnecessary null calls per leaf 0 2
Best for Leaf collection, path building Height, subtree properties

Interview answer:

“I chose pre-order because it allows an immediate return once a leaf is identified, avoiding two redundant recursive calls to null children. Any DFS that visits left before right produces the same leaf sequence.”

Related problems where traversal order matters for leaf/path collection:

LC # Problem Recommended Order Why
872 Leaf-Similar Trees Pre-order Early exit at leaf
257 Binary Tree Paths Pre-order Build path top-down
112 Path Sum Pre-order Carry remaining sum down
104 Maximum Depth Post-order Need children’s height first
110 Balanced Binary Tree Post-order Validate subtree heights

0-5) Traversal Quick-Reference Table (Interview Cheat Sheet) Priority 4 of 5 — High value — a gap here costs you rounds

Inspired by LC 113 Path Sum II — key insight: the traversal choice determines the algorithm structure.

Traversal Order Core Use Case When to Choose
Pre-order root → left → right Build path top-down root-to-leaf paths, carry parent info to children, DFS + backtrack
Post-order left → right → root Compute subtree results bottom-up height/depth, subtree sum, max path, DP on trees
In-order left → root → right Process nodes in sorted order BST validation, kth smallest, sorted traversal
BFS level by level Level-by-level processing min depth, zigzag, right side view, connect next pointer

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) Tree Templates & Algorithms

1.2) Template Selection Guide Priority 4 of 5 — High value — a gap here costs you rounds

Pattern Template When to Use Example Problems
DFS Recursive Standard recursion Most tree problems LC 104, 110, 226
DFS Iterative Stack-based Avoid recursion depth limits LC 94, 144, 145
BFS Level-order Queue-based Level processing needed LC 102, 199, 515
Divide & Conquer Bottom-up recursion Need subtree results LC 124, 543, 687
Path Tracking DFS with path state Path-related problems LC 112, 257, 437
Move Parent Parent map + BFS Bidirectional exploration LC 863, 742, 1740
Node Path Subtree serialization Subtree comparison/detection LC 652, 572

1.3) Core Operations

1.3.1) Tree Traversal Strategies

Two Main Approaches:

  1. Depth-First Search (DFS) - Go deep before going wide

    • Preorder: Root → Left → Right (top-down processing)
    • Inorder: Left → Root → Right (sorted order for BST)
    • Postorder: Left → Right → Root (bottom-up processing)
  2. Breadth-First Search (BFS) - Process level by level

    • Level-order: Process all nodes at depth d before depth d+1

1.4) Choosing a Traversal

The four basic traversals are written out in tree2.md — that sheet is the numbered template catalogue, in Python and Java, one per pattern. What belongs here is the question that comes first: which one.

The answer depends on… Traversal Because Template
the parent, before the children are known Pre-order — root → left → right state flows down: a path, a depth, a running prefix tree2 1.1) — LC 144
the tree being a BST, and you want sorted order In-order — left → root → right in-order on a BST is the sorted sequence, which is why LC 98 and LC 230 are one line each binary_tree § Template 1 — LC 94
both children, before the node can answer Post-order — left → right → root state flows up: a height, a sum, a “is this subtree valid” verdict tree2 1.3) — LC 145
distance from the root, or a per-level answer Level-order (BFS) the first time you meet a node is via a shortest path, and a level is a queue-length snapshot binary_tree § Template 2 — LC 102
the level, but alternating direction BFS + direction flag reverse the row, do not reverse the traversal tree2 1.5) — LC 103

The one-line test: ask “can this node answer without hearing from its children?” Yes → pre-order. No → post-order. “I need the whole row at once” → BFS.

1.4-1) Traversals that do not need the queue

Two techniques get an ordinary traversal’s answer without its space cost. They are not per-pattern templates, so they stay here rather than in the catalogue:

  • O(1)-space level linking — when the node already has a next pointer, the level itself can act as the queue. Written out at binary_tree § Template 8 — LC 116 / LC 117.
  • Coordinate-annotated traversal — carry (row, col) through any traversal and sort at the end; the traversal order stops mattering, which is what makes DFS and BFS interchangeable for vertical order, top/bottom view, and LC 662-style width indexing. tree2 8.3) — LC 987.
  • Morris traversal — below.

Template 1: Morris In-order Traversal — LC 94

Recognise — an in-order (or pre-order) traversal with the follow-up “can you do it in O(1) extra space?” or “without recursion and without a stack”. The time bound stays O(n); only the O(h) stack goes. Nodes ≤ 10⁴, so this is about the follow-up, not about speed.

Key point — a node with a left child is visited twice. The first time, the rightmost node of its left subtree (its in-order predecessor) has right == None, so point it back at the node and go left. The second time, that thread is how you got back, so cut it and emit the node. Every thread made is unmade, so the tree ends exactly as it started.

python
# python
# LC 94 - Binary Tree Inorder Traversal
# IDEA: thread each node's in-order predecessor back to it; follow the thread up instead of popping a stack
# time = O(n), space = O(1)   output list not counted
def inorderTraversal(root):
    result = []
    curr = root
    while curr:
        if curr.left is None:
            result.append(curr.val)          # nothing on the left: emit, go right
            curr = curr.right
            continue
        pred = curr.left                     # find the in-order predecessor
        while pred.right is not None and pred.right is not curr:
            pred = pred.right
        if pred.right is None:               # first visit: make the thread, go left
            pred.right = curr
            curr = curr.left
        else:                                # second visit: we came back up the thread
            pred.right = None                # restore the tree
            result.append(curr.val)
            curr = curr.right
    return result
java
// java
// LC 94 - Binary Tree Inorder Traversal
// IDEA: thread each node's in-order predecessor back to it; follow the thread up instead of popping a stack
// time = O(n), space = O(1)   output list not counted
public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    TreeNode curr = root;
    while (curr != null) {
        if (curr.left == null) {
            result.add(curr.val);            // nothing on the left: emit, go right
            curr = curr.right;
            continue;
        }
        TreeNode pred = curr.left;           // find the in-order predecessor
        while (pred.right != null && pred.right != curr) {
            pred = pred.right;
        }
        if (pred.right == null) {            // first visit: make the thread, go left
            pred.right = curr;
            curr = curr.left;
        } else {                             // second visit: we came back up the thread
            pred.right = null;               // restore the tree
            result.add(curr.val);
            curr = curr.right;
        }
    }
    return result;
}

Complexity

  • Time O(n): the inner while pred.right ... looks nested, but it walks each left subtree’s right spine exactly twice (once to thread, once to unthread), and the spines of different nodes do not share edges, so all the walks together cover each edge at most twice.
  • Space O(1): curr and pred are the only state. The threads live in right pointers that were None anyway, which is why there is no stack.
Traversal Method Time Space Modifies Tree
Recursive O(n) O(h) No
Iterative Stack O(n) O(h) No
Morris O(n) O(1) Temporarily (restored)

Dry run

text
tree   2          curr  pred   action                       out
      / \         2     1      1.right = 2, go left         []
     1   3        1     -      no left: emit, go right (2)  [1]
                  2     1      1.right is 2: cut, emit       [1, 2]
                  3     -      no left: emit, go right       [1, 2, 3]

Traps

  • while pred.right: without is not curr follows the thread back into curr and loops for ever on any node with a left child: [2, 1, 3] hangs.
  • Emitting on the first visit gives pre-order, not in-order: [2, 1, 3] returns [2, 1, 3] instead of [1, 2, 3]. That is the whole pre-order variant (LC 144), so say which one you are writing.
  • Returning early (say, at the k-th smallest) leaves threads in place. LC 230 with k = 1 on [2, 1, 3] leaves 1.right == 2, a cycle the caller can loop on. Finish the walk, or unthread before returning.
  • Post-order needs the right spines reversed. It is rarely asked; use the iterative stack.

Not this if — the tree is shared or read-only while you walk it (another thread reads it, or the interviewer says “do not modify the input”). Morris writes to the tree, even if only briefly. Use the iterative stack, binary_tree § Template 2.

Drill — LC 94 · LC 144 (emit on the first visit) · LC 114 (rewire, below) · LC 99 (recover a BST in O(1) space: the two out-of-order nodes show up in the Morris order)

Morris threading family — temporary thread vs. permanent rewire

Both the Morris traversal in Template 1 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
# python
# LC 114 - Flatten Binary Tree to Linked List
# IDEA: Morris REWIRE - splice the right subtree onto the left subtree's rightmost node, permanently
# 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) Tree Node Initialization

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) Problems by Pattern Classification

2.1) Problem Categories & Templates Priority 4 of 5 — High value — a gap here costs you rounds

Tree Traversal Problems

Problem LC # Pattern Template Difficulty
Binary Tree Preorder Traversal 144 DFS Preorder Preorder Template Easy
Binary Tree Inorder Traversal 94 DFS Inorder Inorder Template Easy
Binary Tree Postorder Traversal 145 DFS Postorder Postorder Template Easy
Binary Tree Level Order Traversal 102 BFS Level-order BFS Template Medium
Binary Tree Zigzag Level Order 103 BFS with alternating BFS + Direction Medium

Tree Property Problems

Problem LC # Pattern Template Difficulty
Maximum Depth of Binary Tree 104 DFS Bottom-up Postorder Height Easy
Minimum Depth of Binary Tree 111 BFS/DFS BFS Early Stop Easy
Balanced Binary Tree 110 DFS Height Check Height Validation Easy
Symmetric Tree 101 DFS Comparison Mirror Validation Easy
Same Tree 100 DFS Comparison Tree Comparison Easy

Path-Based Problems

Problem LC # Pattern Template Difficulty
Binary Tree Maximum Path Sum 124 DFS Path Tracking Global Max Update Hard
Path Sum 112 DFS Path Validation Path Accumulation Easy
Path Sum II 113 DFS Path Collection Path + Backtrack Medium
Path Sum III 437 DFS Prefix Sum Path Count Tracking Medium
Sum Root to Leaf Numbers 129 DFS Path Calculation Path Value Building Medium
Count Good Nodes in Binary Tree 1448 DFS Path Max Path State Tracking Medium
Diameter of Binary Tree 543 DFS Path Length Longest Path Easy
Longest Univalue Path 687 DFS Path Pattern Same Value Path Medium

Distance and LCA Problems

Problem LC # Pattern Template Difficulty
Lowest Common Ancestor 236 DFS Post-order LCA Standard Medium
LCA of BST 235 BST Property Value Comparison Easy
Distance in Binary Tree 1740 LCA + Distance Path Distance Medium
All Nodes Distance K 863 Graph + BFS Tree to Graph Medium
Smallest Subtree w/ Deepest Nodes 865/1123 LCA + Depth Comparison Result(node, dist) DFS Medium

Height and Depth Problems

Problem LC # Pattern Template Difficulty
Maximum Depth 104 DFS Bottom-up Height Calculation Easy
Minimum Depth 111 BFS/DFS Depth to Leaf Easy
Balanced Binary Tree 110 DFS Height Validation Balance Check Easy
Find Bottom Left Tree Value 513 BFS Level-order Leftmost at Depth Medium

Tree Construction Problems

Problem LC # Pattern Template Difficulty
Construct Binary Tree from Preorder and Inorder 105 Divide & Conquer Tree Building Medium
Construct Binary Tree from Inorder and Postorder 106 Divide & Conquer Tree Building Medium
Serialize and Deserialize Binary Tree 297 Tree Encoding String Conversion Hard
Construct String from Binary Tree 606 DFS String Building String Construction Easy

Tree Modification Problems

Problem LC # Pattern Template Difficulty
Invert Binary Tree 226 DFS Node Swapping Tree Inversion Easy
Flatten Binary Tree to Linked List 114 DFS Restructuring Tree Flattening Medium
Merge Two Binary Trees 617 DFS Combination Tree Merging Easy
Delete Nodes And Return Forest 1110 DFS + State Tracking Tree Deletion + Forest Formation Medium

Subtree Comparison Problems (Node Path Pattern)

Problem LC # Pattern Template Difficulty
Find Duplicate Subtrees 652 Node Path Serialization Subtree Hashing Medium
Subtree of Another Tree 572 Node Path Comparison Subtree Matching Easy
Count Univalue Subtrees 250 Node Path Validation Subtree Property Check Medium

2.2) Pattern Selection Guide

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

Advanced Tree Techniques — Binary Lifting and Re-rooting

Two techniques that answer many questions about one fixed tree faster than one DFS per question. Both are rarely the first thing an L3 interviewer asks; both are the standard answer when the follow-up is “now do it for every node” or “now answer 10⁵ queries”.

Template 2: Binary Lifting — LC 1483 Priority 3 of 5 — Worth knowing — usually a variant of a must-know pattern

Recognise — “the k-th ancestor of a node”, or LCA / path questions asked many times on a tree that does not change. n, q ≤ 5 · 10⁴, so O(n) per query (walking up one parent at a time) is 2.5 · 10⁹ steps; O(log n) per query is what fits.

Key point — up[j][v] is the 2^j-th ancestor of v, and up[j][v] = up[j-1][up[j-1][v]] because 2^j = 2^(j-1) + 2^(j-1). Any k is a sum of distinct powers of two, so jumping once per set bit of k lands exactly k levels up.

python
# python
# LC 1483 - Kth Ancestor of a Tree Node
# IDEA: up[j][v] = the 2^j-th ancestor of v; any k is a sum of powers of two, so jump once per set bit
# time = O(n log n) build, O(log n) per query; space = O(n log n)
from collections import deque

class TreeAncestor:
    def __init__(self, n, parent):
        self.LOG = max(1, n.bit_length())
        self.up = [parent[:]]                    # up[0][v] = direct parent, -1 above the root
        for j in range(1, self.LOG):
            prev = self.up[j - 1]
            row = [-1] * n
            for v in range(n):
                mid = prev[v]
                if mid != -1:
                    row[v] = prev[mid]           # 2^j = 2^(j-1) + 2^(j-1)
            self.up.append(row)
        # depth of every node, for lca(); parent[i] is not guaranteed < i, so BFS from the root
        children = [[] for _ in range(n)]
        root = 0
        for v, p in enumerate(parent):
            if p == -1:
                root = v
            else:
                children[p].append(v)
        self.depth = [0] * n
        queue = deque([root])
        while queue:
            u = queue.popleft()
            for c in children[u]:
                self.depth[c] = self.depth[u] + 1
                queue.append(c)

    def getKthAncestor(self, node, k):
        j = 0
        while k and node != -1:
            if k & 1:
                node = self.up[j][node] if j < self.LOG else -1
            k >>= 1
            j += 1
        return node

    def lca(self, u, v):
        if self.depth[u] < self.depth[v]:
            u, v = v, u
        u = self.getKthAncestor(u, self.depth[u] - self.depth[v])   # same depth first
        if u == v:
            return u
        for j in range(self.LOG - 1, -1, -1):    # largest jumps first, never onto a common node
            if self.up[j][u] != self.up[j][v]:
                u = self.up[j][u]
                v = self.up[j][v]
        return self.up[0][u]
java
// java
// LC 1483 - Kth Ancestor of a Tree Node
// IDEA: up[j][v] = the 2^j-th ancestor of v; any k is a sum of powers of two, so jump once per set bit
// time = O(n log n) build, O(log n) per query; space = O(n log n)
class TreeAncestor {
    private final int LOG;
    private final int[][] up;

    public TreeAncestor(int n, int[] parent) {
        LOG = Math.max(1, 32 - Integer.numberOfLeadingZeros(n));   // bit length of n
        up = new int[LOG][n];
        up[0] = parent.clone();                  // up[0][v] = direct parent, -1 above the root
        for (int j = 1; j < LOG; j++) {
            for (int v = 0; v < n; v++) {
                int mid = up[j - 1][v];
                up[j][v] = (mid == -1) ? -1 : up[j - 1][mid];   // 2^j = 2^(j-1) + 2^(j-1)
            }
        }
    }

    public int getKthAncestor(int node, int k) {
        for (int j = 0; k > 0 && node != -1; j++, k >>= 1) {
            if ((k & 1) == 1) {
                node = (j < LOG) ? up[j][node] : -1;
            }
        }
        return node;
    }
}

Complexity

  • Time O(n log n) to build, O(log n) per query: the double loop for j … for v … fills LOG × n cells, one O(1) lookup each. A query jumps once per bit of k, at most LOG times; lca does one such jump to equalise depths and one top-down sweep over j.
  • Space O(n log n): the up table itself, LOG rows of n.

Dry run

text
parent = [-1, 0, 0, 1, 1, 2, 2]          getKthAncestor(6, 3):  k = 3 = 0b11
        0                                 bit 0 set: node = up[0][6] = 2
       / \                                bit 1 set: node = up[1][2] = -1  (2's grandparent)
      1   2          up[1] = [-1,-1,-1,0,0,0,0]
     / \ / \                              -> -1: node 6 has only 2 ancestors
    3  4 5  6

Traps

  • Sizing the table by log2(n) rounded down misses the top bit: with n = 5, k = 4 on the chain [-1, 0, 1, 2, 3] needs up[2], which int(log2(5)) = 2 rows do not have. Use the bit length of n.
  • Not stopping at -1: indexing up[j][-1] in Python silently reads the last node’s ancestor and returns a wrong node instead of -1. Guard every jump.
  • In lca, sweeping j upward (small jumps first) can overshoot the meeting point and is wrong; sweep from the largest jump down, and only jump when the two ancestors differ.
  • parent[i] < i is not promised, so depth cannot be filled in index order; BFS from the root.

Not this if — there is only one query. A single O(n) DFS answers one LCA (tree_lca_distance.md) and building the table costs more than it saves.

Drill — LC 1483 · LC 236 (the same LCA, asked q times) · LC 2836 (jump k steps along a functional graph) · LC 2846 (per-query path counts: lift a count table with the ancestors)

Template 3: Re-rooting DP — LC 834

Recognise — “return an array where answer[i] is … when the tree is rooted at i” or “the sum of distances from each node to all others”. n ≤ 3 · 10⁴, so one DFS per root (O(n²), 9 · 10⁸) is too slow; two passes in total is the target.

Key point — rooting at 0 gives size[v] (nodes under v) and ans[0]. Moving the root across one edge u → v changes every distance by exactly ±1: the size[v] nodes on v’s side get 1 closer, the other n − size[v] get 1 farther. So ans[v] = ans[u] − size[v] + (n − size[v]), an O(1) update per edge.

python
# python
# IDEA: two passes; the second moves the root across each edge and adjusts the answer by subtree size
# time = O(n), space = O(n)
# pass 1 (children first) roots the tree at 0: size[u] = nodes under u, ans[0] = sum of depths.
# pass 2 (parents first) moves the root across edge u -> v: the size[v] nodes under v get 1 closer,
# the other n - size[v] get 1 farther, so ans[v] = ans[u] - size[v] + (n - size[v]).
# LC 834 - Sum of Distances in Tree
def sumOfDistancesInTree(n, edges):
    graph = [[] for _ in range(n)]
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)

    # one iterative DFS from 0 records a parents-before-children order (no recursion limit)
    order, parent = [], [-1] * n
    seen = [False] * n
    seen[0] = True
    stack = [0]
    while stack:
        u = stack.pop()
        order.append(u)
        for v in graph[u]:
            if not seen[v]:
                seen[v] = True
                parent[v] = u
                stack.append(v)

    size = [1] * n
    ans = [0] * n
    for u in reversed(order):                    # pass 1: children before parents
        for v in graph[u]:
            if v != parent[u]:
                size[u] += size[v]
                ans[u] += ans[v] + size[v]       # every node under v is 1 deeper from u

    for u in order:                              # pass 2: parents before children
        for v in graph[u]:
            if v != parent[u]:
                ans[v] = ans[u] - size[v] + (n - size[v])
    return ans
java
// java
// IDEA: two passes; the second re-roots across each edge and adjusts the answer by subtree size
// time = O(n), space = O(n)
// pass 1 roots the tree at 0 (size[], ans[0]); pass 2: ans[v] = ans[u] - size[v] + (n - size[v])
// LC 834 - Sum of Distances in Tree
public int[] sumOfDistancesInTree(int n, int[][] edges) {
    List<List<Integer>> graph = new ArrayList<>();
    for (int i = 0; i < n; i++) {
        graph.add(new ArrayList<>());
    }
    for (int[] e : edges) {
        graph.get(e[0]).add(e[1]);
        graph.get(e[1]).add(e[0]);
    }

    // iterative DFS from 0: a parents-before-children order, no recursion depth limit
    int[] order = new int[n];
    int[] parent = new int[n];
    Arrays.fill(parent, -1);
    boolean[] seen = new boolean[n];
    Deque<Integer> stack = new ArrayDeque<>();
    stack.push(0);
    seen[0] = true;
    int idx = 0;
    while (!stack.isEmpty()) {
        int u = stack.pop();
        order[idx++] = u;
        for (int v : graph.get(u)) {
            if (!seen[v]) {
                seen[v] = true;
                parent[v] = u;
                stack.push(v);
            }
        }
    }

    int[] size = new int[n];
    int[] ans = new int[n];
    Arrays.fill(size, 1);
    for (int i = n - 1; i >= 0; i--) {           // pass 1: children before parents
        int u = order[i];
        for (int v : graph.get(u)) {
            if (v != parent[u]) {
                size[u] += size[v];
                ans[u] += ans[v] + size[v];      // every node under v is 1 deeper from u
            }
        }
    }
    for (int i = 0; i < n; i++) {                // pass 2: parents before children
        int u = order[i];
        for (int v : graph.get(u)) {
            if (v != parent[u]) {
                ans[v] = ans[u] - size[v] + (n - size[v]);
            }
        }
    }
    return ans;
}

Complexity

  • Time O(n): the DFS and both passes visit each node once and each edge twice (once from each end, filtered by v != parent[u]). The re-root step is one O(1) formula per edge.
  • Space O(n): graph, order, parent, size and ans are each O(n); the explicit stack replaces the recursion stack, which on a 3 · 10⁴-node path would overflow Python’s limit.

Dry run

text
edges 0-1, 0-2, 2-3, 2-4, 2-5        pass 1 (root 0): size = [6,1,4,1,1,1], ans[0] = 1+1+2+2+2 = 8
      0                              pass 2:
     / \                               ans[1] = 8 - 1 + (6 - 1) = 12
    1   2                              ans[2] = 8 - 4 + (6 - 4) = 6
      / | \                             ans[3] = 6 - 1 + (6 - 1) = 10   (same for 4, 5)
     3  4  5                         -> [8, 12, 6, 10, 10, 10]

Traps

  • Running pass 2 in the same order as pass 1 (children first) reads ans[u] before it has been re-rooted: ans[3] would be computed from the pass-1 ans[2] = 3, giving 7 instead of 10. Pass 1 is children-first, pass 2 is parents-first.
  • Only ans[0] is a full answer after pass 1; every other ans[u] is a subtree sum. Reading ans[2] there gives 3, not 6.
  • Recursive DFS on a path of 3 · 10⁴ nodes hits Python’s recursion limit (1000 by default); record the order iteratively, as above.
  • An undirected edge list has no parent/child direction, so every neighbour loop needs the v != parent[u] filter, or the parent is counted as a child.

Not this if — the question wants one best root, not a value for every root. “Minimum height trees” (LC 310) peels leaves layer by layer until one or two centres remain: topology_sorting § Template 7.

Drill — LC 834 · LC 2581 (count the roots that satisfy ≥ k guesses: re-root a guess count) · LC 2858 (edge reversals: re-root a count of edges pointing the wrong way) · LC 3241 (time to mark all nodes: re-root the top two child depths)

Tree Serialization / Deserialization

LC 297 lives in tree_codec.md: the pre-order + null-marker codec (Python and Java), the parenthesis format (LC 606 / 536) and the depth-prefix format (LC 1028).

Drill Path

Stage Problems (easy → hard) What it adds
Core ⭐ LC 104 ok ⭐ · LC 102 ok ⭐ · LC 297 again ⭐ the traversal choice once each: bottom-up and top-down, level-order, pre-order with null markers
Practice LC 872 ok · LC 94 ok · LC 114 no verdict · LC 1483 no verdict pre-order leaf collection; Morris threading and rewiring; binary lifting
Stretch LC 99 again · LC 834 no verdict · LC 2581 no verdict Morris on a broken BST; re-rooting DP, then re-rooting a count

3) Summary & Quick Reference

3.1) Tree Algorithm Complexity Summary

Operation Balanced Tree Unbalanced Tree Space Complexity
Search O(log n) O(n) O(h) recursion
Insert O(log n) O(n) O(h) recursion
Delete O(log n) O(n) O(h) recursion
Traversal O(n) O(n) O(h) recursion
Height Calculation O(n) O(n) O(h) recursion

3.2) Traversal Quick Reference

Traversal Order Use Case Key Characteristics
Preorder Root → Left → Right Tree copying, serialization Process parent before children
Inorder Left → Root → Right BST sorted output Process left, then root, then right
Postorder Left → Right → Root Tree deletion, calculations Process children before parent
Level-order Level by level Tree printing, shortest path Use queue, process by levels

3.3) Problem-Solving Templates

The root-to-leaf path template (pre-order, append, recurse, pop) is a pattern card in tree_lca_distance.md, which owns every path problem in this family.

3.4) Common Patterns & Tricks

Height vs Depth Pattern

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)

Global Variable Pattern

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) Common Mistakes & Tips Priority 4 of 5 — High value — a gap here costs you rounds

🚫 Common Mistakes:

  • Forgetting base cases in recursion
  • Modifying tree structure incorrectly during traversal
  • Not handling null nodes properly
  • Using wrong traversal order for the problem
  • Stack overflow due to deep recursion (use iterative approach)

✅ Best Practices:

  • Always check for null nodes first
  • Use helper functions to pass additional parameters
  • Consider iterative solutions for very deep trees
  • Validate input and handle edge cases
  • Use meaningful variable names (left_result, right_result)
  • Test with balanced and unbalanced trees

3.6) Interview Tips

  1. Clarify the Problem: Ask about null inputs, tree structure, expected output format
  2. Start with Recursive Solution: Most tree problems have elegant recursive solutions
  3. Consider Iterative Alternative: If recursion depth might be an issue
  4. Trace Through Examples: Walk through small examples to verify logic
  5. Analyze Complexity: Always discuss time and space complexity
  6. Handle Edge Cases: Empty tree, single node, very deep trees
  • Binary Search Trees: Ordering property enables efficient operations
  • Heaps: Complete binary trees with heap property
  • Tries: Prefix trees for string operations
  • Segment Trees: For range query problems
  • Graph Algorithms: Trees are special cases of graphs