LC Patterns — Trees & BSTs

Previous: binary-search, two-pointers, sliding-window, 1d-dp, arrays-strings · Code: C++
Per-pattern template: What it is → Intuition → Approach → Challenges & pitfalls → LC problems (max 3) → Worked examples.

The Master Insight (read before the patterns)

A tree is a recursive structure, so almost every tree problem is answered by one question asked at a single node: "What do I need from my children, and what do I hand back to my parent?" Get that contract right and the recursion does the rest. Two directions of information cover nearly everything:

  • Bottom-up (returns): children report up, the node combines — heights, sums, "is it balanced," tree DP. Write down what f(node) returns in one precise sentence before coding — the exact discipline as the DP state sentence.
  • Top-down (parameters): the parent passes accumulated context down — the current path sum, the allowed value range, the max seen so far on the path.
  • Many hard problems need both at once: the value you return to the parent differs from the value you record globally (Pattern 9 — diameter, max path sum). Spotting that split is the single biggest tree-interview skill.

The BST master key: the BST rule (left < node < right, all the way down) gives you two superpowers — an inorder traversal visits the nodes in sorted order, and comparisons let you prune half the tree (O(h) search/insert/delete, the pointer-world twin of binary search). If a BST problem feels hard, ask: "what would I do to a sorted array, and how does inorder or pruning give me that here?"

Recognition routing: "level by level / nearest / minimum depth" → BFS (Pattern 6). "Any path / subtree property" → bottom-up recursion. "Path from the root" → top-down. "k-th / sorted / closest in a BST" → inorder or pruning. N-ary or graph-shaped input → same ideas, more children.

§0Conventions & Universal Pitfalls

struct TreeNode {
    int val;
    TreeNode *left, *right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
  1. Null is the base case — handle it first. if (!node) return ⟨identity⟩; opens almost every recursive tree function; the identity (0, true, INT_MIN, nullptr) is a design decision, not boilerplate.
  2. Nodes vs edges: "depth/diameter in nodes" vs "in edges" differ by one; LC 543 counts edges, 104 counts nodes. Decide the unit before coding and write it in a comment (the fencepost bug of this topic).
  3. Return value ≠ global answer: in diameter/max-path problems, f(node) returns the best downward arm while the answer (the best arch through any node) updates a captured variable. Mixing them is the #1 hard-tree bug.
  4. BST validation needs ranges, not parent-child checks: comparing each node only to its parent wrongly accepts invalid trees (a right-grandchild smaller than the grandparent). Pass (lo, hi) bounds down. And use long long bounds — test values include INT_MIN/INT_MAX.
  5. BFS level loops must snapshot the queue size: int sz = q.size(); for (int i = 0; i < sz; ++i) — reading q.size() live while pushing children merges levels together.
  6. Recursion depth: a skewed tree of 10⁴ nodes can overflow the stack — mention the iterative/explicit-stack conversion (and know Morris traversal by name for the O(1)-space follow-up; implement only if asked).
  7. Smart pointers/new in interviews: raw pointers are fine and expected for LC-style tree code; say so and move on.

1Bottom-Up Aggregates (height, balance, subtree sums)

What it is: Compute a property of every subtree from its children's answers: height = 1 + max(child heights); a tree is balanced when both subtrees are balanced and their heights differ by ≤ 1. One post-order pass, O(n).

Intuition: Post-order (children before self) is the order for bottom-up: by the time you reach a node, its children's answers already exist. The efficiency idiom: fold the check into the height computation — a naive "isBalanced calls height at every node" is O(n²); returning height and balance together (a sentinel -1 = "unbalanced somewhere below") makes it O(n). Combining several return values into one pass is the skill this pattern drills.

Approach:

int check(TreeNode* n) {                    // returns height, or -1 if unbalanced below
    if (!n) return 0;
    int l = check(n->left);  if (l == -1) return -1;
    int r = check(n->right); if (r == -1) return -1;
    if (abs(l - r) > 1) return -1;
    return 1 + max(l, r);
}
bool isBalanced(TreeNode* root) { return check(root) != -1; }

Challenges & pitfalls

the O(n²)-to-O(n) fusion (interviewers accept the naive version then ask "complexity? improve it" — arrive with the fused one); the sentinel must be outside the legal range (heights ≥ 0 ⇒ −1 is free); nodes-vs-edges (§0.2).

LC 104 Maximum DepthLC 110 Balanced Binary TreeLC 543 Diameter of Binary Tree
Example 1: Maximum Depth (LC 104) — tree with root 3, children 9 (a leaf) and 20, where 20 has children 15 and 7.
  • Compute heights bottom-up: leaves 9, 15, 7 all have height 1. Node 20 = 1 + max(1, 1) = 2. Root 3 = 1 + max(1, 2) = 3.
  • Max depth 3. ✓ Each node just added 1 to the taller child's height — one post-order pass.
Example 2: Balanced Binary Tree (LC 110) — same tree.
  • At 20: child heights 1 and 1, differ by 0 → balanced, report height 2. At root 3: child heights 1 (from 9) and 2 (from 20), differ by 1 → still balanced.
  • true. ✓ The fused version returns the height and short-circuits to -1 the instant any subtree is unbalanced, so it never re-walks — O(n).

2Top-Down Path State (root-to-leaf accumulation)

What it is: Carry state down from the root as parameters — the remaining target sum, the number built so far, the max value seen on this path — and evaluate at leaves (or at every node). "Does a root-to-leaf path sum to k?", "sum all root-to-leaf numbers," "count nodes ≥ every ancestor."

Intuition: Path-from-root state is exactly what call parameters model: each recursive call is one step along a path, and the call stack is the path. No data structure needed — the recursion shape mirrors the problem shape. Evaluate-at-leaf problems must define "leaf" precisely (!left && !right), not "null child" — checking at null double-counts through one-child nodes.

Approach:

int dfs(TreeNode* n, int cur) {             // 129: cur = number formed root..parent
    if (!n) return 0;
    cur = cur * 10 + n->val;
    if (!n->left && !n->right) return cur;  // true leaf: contribute the path's number
    return dfs(n->left, cur) + dfs(n->right, cur);
}

Challenges & pitfalls

leaf-vs-null evaluation (112's classic wrong answer on one-child nodes — trace [1,2], target 1); overflow when accumulating (129 fits int per constraints — say you checked); passing by value vs reference: path-vector variants (backtracking territory) need push/pop discipline, plain accumulators don't — know which you're in.

LC 112 Path SumLC 129 Sum Root to Leaf NumbersLC 1448 Count Good Nodes
Example 1: Path Sum (LC 112) — root 5; left 411 → (7, 2); right 8. Target 22.
  • Carry the remaining target down: start 22, subtract each node's value. Down the left spine: 22 − 5 = 17, − 4 = 13, − 11 = 2, then at leaf 2: 2 − 2 = 0true, a root-to-leaf path (5 → 4 → 11 → 2) sums to 22. ✓
  • The subtlety: only check "reached target" at a true leaf (!left && !right). Checking at a null child would wrongly accept paths through one-child nodes.

3Two-Tree Recursion (compare / mirror / merge)

What it is: Recurse on two nodes in lockstep: same-tree (equal here && left-left equal && right-right equal), symmetric (mirror: compare left-with-right and right-with-left), subtree-of (a same-tree check launched at every node), merge (combine two trees node-wise).

Intuition: The contract just takes two nodes: f(a, b). Null-handling does most of the work — both null = true/nullptr, one null = false/the-other. Symmetry's one insight: a tree is symmetric when its left subtree mirrors its right, and "mirrors" swaps children in the comparison (mirror(a->left, b->right) && mirror(a->right, b->left)).

Approach:

bool same(TreeNode* a, TreeNode* b) {
    if (!a || !b) return a == b;            // both null → true; one null → false
    return a->val == b->val && same(a->left, b->left) && same(a->right, b->right);
}

Challenges & pitfalls

the two-null idiom (a == b covers both cases — elegant, but explain it); 572's complexity: a same-check at every node is O(n·m) — the senior follow-up is serialize-both + substring search or subtree hashing (Pattern 11); value equality isn't structural equality — [1,2] vs [1,null,2].

LC 100 Same TreeLC 101 Symmetric TreeLC 572 Subtree of Another Tree
Example 1: Same Tree (LC 100)p = [1, 2, 3], q = [1, 2, 3].
  • Compare roots: 1 == 1 ✓. Recurse left pair (2 vs 2) ✓, right pair (3 vs 3) ✓. All leaves' children are null-null → true. Result true. ✓
  • Contrast p = [1, 2] (2 is a left child) vs q = [1, null, 2] (2 is a right child): comparing the left pair gives node-vs-null → false. Same values, different shape.

4Structural Modification (invert, merge, flatten)

What it is: Rebuild or rewire the tree itself: swap every node's children (invert), overlay two trees (merge), rewrite the tree into a linked list in preorder (flatten). The recursion returns the new subtree root, and the parent stitches it in.

Intuition: Treat "transform this tree" as "transform the children, then fix up this node" — post-order rewiring. The contract sentence matters doubly here because you're mutating: f(node) returns the root of the transformed subtree. Flatten's trick: recurse right first, thread each node's rewired left list between itself and its already-flattened right — or the O(1)-space version: for each node with a left child, splice the left subtree's rightmost node onto the right child.

Approach:

TreeNode* invertTree(TreeNode* root) {
    if (!root) return nullptr;
    TreeNode* l = invertTree(root->left);
    root->left = invertTree(root->right);
    root->right = l;
    return root;
}

Challenges & pitfalls

losing a subtree pointer mid-swap (save before you overwrite — the temp variable is not optional); flatten's ordering constraint (result must be preorder — verify on a 5-node example); mutating vs building-new (merging trees: reusing tree1's nodes is allowed by the problem — ask/state which you're doing).

LC 226 Invert Binary TreeLC 617 Merge Two Binary TreesLC 114 Flatten Binary Tree to Linked List
Example 1: Invert Binary Tree (LC 226) — root 4; left 2 → (1, 3); right 7 → (6, 9).
  • Post-order: at 2, swap children → 2 → (3, 1). At 7, swap → 7 → (9, 6). At root 4, swap the whole left/right → 4 → (7, 2).
  • Result: root 4; left 7 → (9, 6); right 2 → (3, 1). ✓ Saving one child in a temp before overwriting is what keeps the swap from clobbering itself.

5Construction from Traversals

What it is: Rebuild a unique binary tree from two traversal orders (preorder + inorder, or inorder + postorder), or build a balanced BST from a sorted array. The recurring shape: one traversal names the root, the other (inorder) splits the left subtree from the right.

Intuition: Preorder's first element is the root; find it in inorder — everything to its left is the left subtree, everything to its right is the right subtree; recurse on the matching slices. Sorted-array→BST is the same idea with the middle element as the root (balance for free). Fact to state: preorder + postorder alone is ambiguous (one-child nodes) — inorder is the disambiguator.

Approach:

unordered_map<int,int> pos;                  // inorder value → index (build once: O(1) splits)
TreeNode* build(vector<int>& pre, int& pi, int lo, int hi) {
    if (lo > hi) return nullptr;
    TreeNode* root = new TreeNode(pre[pi++]);
    int mid = pos[root->val];
    root->left  = build(pre, pi, lo, mid - 1);   // preorder consumes left-first —
    root->right = build(pre, pi, mid + 1, hi);   // order of these two calls is load-bearing
    return root;
}

Challenges & pitfalls

O(n²) from linear root-searches — the hashmap index is the expected fix; the shared preorder cursor (pi by reference) must advance left-then-right (swap the calls and it silently builds garbage — for postorder, walk the cursor backwards and build right-first); duplicate values break the position map (constraints usually forbid them — notice that aloud).

LC 105 Construct from Preorder & InorderLC 106 Construct from Inorder & PostorderLC 108 Sorted Array to BST
Example 1: Construct from Preorder & Inorder (LC 105)preorder = [3, 9, 20, 15, 7], inorder = [9, 3, 15, 20, 7].
  • Preorder's first element, 3, is the root. Find 3 in inorder (index 1): everything left ([9]) is the left subtree, everything right ([15, 20, 7]) is the right subtree.
  • Left subtree: next in preorder is 9, and its inorder slice is [9] → leaf 9.
  • Right subtree: next in preorder is 20; in the inorder slice [15, 20, 7], 20 sits in the middle → left child 15, right child 7.
  • Tree: root 3, left 9, right 20 → (15, 7). ✓ Preorder kept naming the next root; inorder kept splitting left from right.

6BFS / Level-Order Family

What it is: Process the tree level by level with a queue: collect levels, take each level's last node (right-side view), alternate direction (zigzag), connect next pointers within a level. Also the right tool whenever the ask is "nearest/shallowest" — BFS finds the minimum depth before DFS finishes exploring.

Intuition: The queue-size snapshot (§0.5) turns a plain BFS into a level-aware one: everything in the queue at the loop's start is exactly one level. Every level-order variant is that loop plus a different per-level action (record all / record last / reverse alternate levels / wire next pointers). Right-side view = each level's final node — a one-line change, not a new algorithm.

Approach:

vector<vector<int>> levelOrder(TreeNode* root) {
    vector<vector<int>> out;
    if (!root) return out;
    queue<TreeNode*> q; q.push(root);
    while (!q.empty()) {
        int sz = q.size(); out.emplace_back();
        for (int i = 0; i < sz; ++i) {
            TreeNode* n = q.front(); q.pop();
            out.back().push_back(n->val);
            if (n->left) q.push(n->left);
            if (n->right) q.push(n->right);
        }
    }
    return out;
}

Challenges & pitfalls

the size snapshot (§0.5); zigzag — reverse the collected level (simple) rather than juggling a deque live; DFS-with-depth can also produce level lists (record node at levels[depth]) — right-side view via DFS (visit right first, first node seen per depth wins) is a nice alternative to name; 116's O(1)-space version uses the already-wired parent level as the "queue" — the senior follow-up.

LC 102 Level Order TraversalLC 199 Right Side ViewLC 116 Populating Next Right Pointers
Example 1: Level Order Traversal (LC 102) — root 3, children 9 and 20, and 20 → (15, 7).
  • Queue starts [3]. Snapshot size 1 → pop 3, record [3], push its children 9, 20.
  • Snapshot size 2 → pop 9 and 20, record [9, 20], push 20's children 15, 7.
  • Snapshot size 2 → pop 15 and 7, record [15, 7].
  • Answer [[3], [9, 20], [15, 7]]. ✓ Snapshotting the size at the start of each round is what keeps the levels from bleeding into each other.
Example 2: Right Side View (LC 199) — same tree. Take the last node of each level → [3, 20, 7]. ✓ Same loop, one line changed.

7Lowest Common Ancestor

What it is: Find the deepest node that has both targets in its subtree. General tree: post-order — if the searches of the left and right subtrees each found one target, this node is the LCA. BST: no recursion insight needed — prune by value: both targets smaller → go left; both bigger → go right; they split (or one equals the node) → found.

Intuition: The general-tree recursion returns "the target found in this subtree, or the LCA if both were found": null+null = null, one-sided = pass it up, both sides non-null = I'm the split point. It works because the first node where the targets diverge is, by definition, their lowest common ancestor. The BST version is the pruning superpower: the LCA is the first node whose value falls between the targets on the root-down walk — O(h), no recursion required.

Approach:

TreeNode* lca(TreeNode* root, TreeNode* p, TreeNode* q) {
    if (!root || root == p || root == q) return root;
    TreeNode* l = lca(root->left, p, q);
    TreeNode* r = lca(root->right, p, q);
    if (l && r) return root;                 // targets split here → this is the LCA
    return l ? l : r;
}

Challenges & pitfalls

the standard version assumes both nodes exist (root == p returns immediately without checking q — the "what if q is absent?" follow-up needs a found-count variant; know it); compare values not pointers in the BST version; "LCA of deepest leaves" (865) fuses this with Pattern 1 — return (depth, lca) pairs.

LC 236 LCA of a Binary TreeLC 235 LCA of a BSTLC 865 Smallest Subtree with all the Deepest Nodes
Example 1: LCA of a BST (LC 235) — BST root 6; left 2 → (0, 4→(3,5)); right 8 → (7, 9). Find LCA of 2 and 8.
  • At 6: 2 < 6 and 8 > 6 — the targets straddle 66 is the LCA. ✓ One comparison, no recursion.
  • Find LCA of 2 and 4 instead: at 6, both < 6 → go left to 2. At 2, one target is 2 (and 4 is in its subtree) → LCA is 2.

8BST Invariant: Inorder & Range Machinery

What it is: Everything that exploits "inorder = sorted": validate a BST (the inorder sequence must be strictly increasing — or pass ranges down), k-th smallest (inorder, stop at the k-th visit), BST iterator (a paused inorder traversal: a stack of left-spines; next() pops and then pushes the popped node's right spine).

Intuition: The two interchangeable tools: range-passing (each node constrains its subtrees to (lo, hi) — top-down, Pattern 2 for BSTs) and inorder-walking (mentally flatten to sorted order and apply array thinking — k-th, prev/next, two-sum-in-BST). The iterator teaches the deeper skill: turning a recursion into an explicit stack you can pause — the left spine in the stack is exactly the recursion's pending frames, giving O(h) memory and O(1) amortized next().

Approach:

bool validate(TreeNode* n, long long lo, long long hi) {
    if (!n) return true;
    if (n->val <= lo || n->val >= hi) return false;
    return validate(n->left, lo, n->val) && validate(n->right, n->val, hi);
}
// call: validate(root, LLONG_MIN, LLONG_MAX)

Challenges & pitfalls

§0.4 in full (the parent-only comparison bug + long long bounds — 98 is the problem where both bite); strict vs non-strict inequality (duplicates policy — read the statement); the k-th smallest follow-up "the tree changes often" → augment nodes with left-subtree counts (order-statistics tree — describe, don't build); the iterator's amortized O(1) argument ("each node is pushed and popped once across all calls").

LC 98 Validate BSTLC 230 Kth Smallest in a BSTLC 173 BST Iterator
Example 1: Kth Smallest in a BST (LC 230) — BST root 3; left 1 (with right child 2); right 4. Find k = 1.
  • An inorder walk visits nodes in sorted order: 1, 2, 3, 4. Stop at the 1st → 1. ✓ (For k = 3 it would be 3.) No sorting — the BST already lays the values out in order under inorder traversal.
Example 2: Validate BST (LC 98) — root 5; left 1; right 4 → (3, 6).
  • Range-check top-down: 5 is fine in (−∞, +∞). Right subtree must be in (5, +∞), but node 3 (a descendant on the right) is < 5false. ✓ Comparing only against the immediate parent would have missed this — you need the inherited (lo, hi) bounds.

9Tree DP with a Global Answer (the return/record split)

What it is: The hard-tree archetype: max path sum (any node to any node), longest univalue path, house-robber-on-a-tree. At each node, the value you return to the parent (best single downward arm — the parent can only extend one arm) differs from the value you record globally (best arch joining both arms through this node).

Intuition: A path through a tree has a unique highest node — its apex. Enumerate paths by their apex: at each node, the best path apexed here = node + best-left-arm + best-right-arm; record it globally. But a parent extending through this node can use only ONE arm (paths can't fork) — so return node + max(arm). Clamp negative arms to 0 ("don't take a bad arm"). House Robber III is the same discipline with a pair return: (best if this node robbed, best if not).

Approach:

int best = INT_MIN;
int arm(TreeNode* n) {                       // best downward path STARTING at n (≥ its value)
    if (!n) return 0;
    int l = max(arm(n->left), 0);            // negative arm → drop it
    int r = max(arm(n->right), 0);
    best = max(best, n->val + l + r);        // arch apexed at n → record globally
    return n->val + max(l, r);               // parent may extend only one arm → return
}

Challenges & pitfalls

the return/record split (§0.3 — if your function returns the arch, the parent double-uses both arms: the canonical wrong answer); all-negative trees (that's why best seeds at INT_MIN and arms clamp at 0 — trace [-3]); the univalue variant returns arm-length conditional on matching the parent's value — the condition rides the return, not the record.

LC 124 Binary Tree Maximum Path SumLC 687 Longest Univalue PathLC 337 House Robber III
Example 1: Binary Tree Maximum Path Sum (LC 124) — root -10; left 9; right 20 → (15, 7).
  • Compute best downward arm at each node, and record the best arch:
    • 15 → arm 15. 7 → arm 7.
    • 20: arch through it = 20 + 15 + 7 = 42 → record 42. But it can only return one arm to its parent: 20 + max(15, 7) = 35.
    • 9 → arm 9. Root -10: arch = −10 + 9 + 35 = 34 (doesn't beat 42); returns −10 + max(9, 35) = 25.
  • Best path sum = 42 (the path 15 → 20 → 7). ✓ The recorded arch uses both arms; the returned value uses only one — mixing them is the classic bug.

10BST Surgery (insert, delete, trim)

What it is: Structural edits that keep the BST rule intact. Insert: walk down by comparison, attach at the null spot. Delete: the three-case classic — leaf (drop it), one child (splice the child up), two children (replace the value with the inorder successor — the leftmost node of the right subtree — then delete that node, which is guaranteed to be case 1 or 2). Trim: recursively discard subtrees wholly outside [lo, hi].

Intuition: All three are "recurse toward the target by comparison, and return the (possibly new) subtree root so the parent re-links" — the returning-root contract from Pattern 4 under the BST pruning from Pattern 8. Delete's two-child case works because the inorder successor is the smallest value greater than the node — promoting it keeps everything on the left smaller and everything on the right bigger. Trim's power move: if node->val < lo, then the entire left subtree is also < lo — return the trimmed right subtree and skip half the tree.

Approach:

TreeNode* deleteNode(TreeNode* root, int key) {
    if (!root) return nullptr;
    if (key < root->val)      root->left  = deleteNode(root->left, key);
    else if (key > root->val) root->right = deleteNode(root->right, key);
    else {
        if (!root->left)  return root->right;      // cases 1 & 2: splice
        if (!root->right) return root->left;
        TreeNode* s = root->right;                  // case 3: inorder successor
        while (s->left) s = s->left;
        root->val = s->val;
        root->right = deleteNode(root->right, s->val);  // delete successor (easy case)
    }
    return root;
}

Challenges & pitfalls

forgetting to assign the recursive result (root->left = deleteNode(...) — calling without assigning edits nothing: the signature bug of returning-root style); successor vs predecessor (both valid — pick one, be consistent); trim's whole-subtree discard needs the BST argument said aloud; degenerate-tree O(h)=O(n) worst case → name self-balancing trees (and that std::map is a red-black tree) as the production answer, don't implement one.

LC 450 Delete Node in a BSTLC 701 Insert into a BSTLC 669 Trim a BST
Example 1: Delete Node in a BST (LC 450) — BST root 5; left 3 → (2, 4); right 6 → (null, 7). Delete 3.
  • 3 < 5 → recurse left; found 3. It has two children (2 and 4) → case 3.
  • Inorder successor = leftmost node of 3's right subtree = 4. Copy 4 into the node (it becomes 4), then delete the original 4 from the right subtree (a leaf → easy case).
  • Result: root 5; left 4 → (2, null); right 6 → (null, 7). ✓ Promoting the successor keeps left-smaller / right-bigger intact.

11Serialization & Subtree Canonicalization

What it is: Encode a tree to a string and rebuild it exactly (297), or use serialization as a canonical key to find duplicate subtrees (652) — hash each subtree's serialization; same key = identical subtree. The tree-world version of the arrays-strings canonical-key pattern.

Intuition: Preorder with explicit null markers uniquely determines a binary tree — the nulls carry the structure that made preorder-alone ambiguous in Pattern 5. Deserialization is then one left-to-right pass with the same shared-cursor trick: read a token; if it's the null-marker return; else make the node and recurse left, then right. For duplicate detection, build keys bottom-up (val + "," + leftKey + "," + rightKey) so each subtree's key is O(1) to assemble from its children — and intern keys to ints (unordered_map<string,int>) to avoid O(n²) string growth on deep trees.

Approach:

void ser(TreeNode* n, string& out) {
    if (!n) { out += "#,"; return; }
    out += to_string(n->val) + ",";
    ser(n->left, out); ser(n->right, out);
}
TreeNode* deser(istringstream& in) {
    string tok; getline(in, tok, ',');
    if (tok == "#") return nullptr;
    TreeNode* n = new TreeNode(stoi(tok));
    n->left = deser(in); n->right = deser(in);   // same order as serialization — load-bearing
    return n;
}

Challenges & pitfalls

null markers and delimiters are both mandatory (negative values and multi-digit numbers break markerless/delimiterless schemes — "12,3" vs "1,23"); serialize and deserialize must agree on traversal order exactly; 652's key-growth trap (string keys concatenate to O(n²) total — the int-interning fix is the expected follow-up); BST-specific serialization (449) can skip null markers using bounds — a nice BST-invariant flex if asked.

LC 297 Serialize and Deserialize Binary TreeLC 652 Find Duplicate SubtreesLC 449 Serialize and Deserialize BST
Example 1: Serialize and Deserialize (LC 297) — root 1, children 2 and 3, and 3 → (4, 5).
  • Serialize with preorder + null markers (#): visit root, then left subtree, then right → "1,2,#,#,3,4,#,#,5,#,#,". The #s record where subtrees end, which is what makes the string unambiguous.
  • Deserialize reads tokens left to right: 1 (make root) → recurse left: 2 (make node) → # (null left) → # (null right) → back up, recurse right of 1: 3 → left 4 (#,#) → right 5 (#,#). Rebuilds the original tree exactly. ✓
  • For Find Duplicate Subtrees (LC 652), give each subtree the key val,leftKey,rightKey built bottom-up; two subtrees with the same key are identical, so a hashmap of keys finds the duplicates in one pass.

Summary Table — Recognition Cheat Sheet

#PatternDirectionTrigger phraseContract of f(node)
1Bottom-up aggregate"height", "balanced", "diameter"subtree property (fused, sentinel for early exit)
2Top-down path state"root-to-leaf", "path sum", "ancestors"accumulate via params, evaluate at true leaves
3Two-tree lockstep"same", "symmetric", "subtree of"f(a, b); two-null idiom
4Structural modification"invert", "merge", "flatten"returns new subtree root; parent stitches
5Build from traversals"construct from preorder/inorder"one order picks root, inorder splits; shared cursor
6BFS levelslevel"level order", "right side view", "nearest"queue + size snapshot
7LCA"lowest common ancestor"null/one/both-found trichotomy; BST: value pruning
8BST inorder/rangeboth"validate", "k-th smallest", "iterator"inorder = sorted; (lo, hi) ranges
9Tree DP, global answer↑ + global"max path sum", "any-to-any path"RETURN one arm, RECORD the arch
10BST surgery"insert/delete/trim BST"return possibly-new root; 3-case delete
11Serialization"serialize", "duplicate subtrees"preorder + null markers; canonical subtree keys
Study order: 1 → 2 → 3 (the recursion-contract trio) → 6 (BFS mechanics) → 8 (the BST master key) → 7 → 5 → 4 → 10 → 9 (the return/record split — the hard-interview pattern, drill 124 until automatic) → 11.
The interview sentence (per your communication playbook §1.3): "Let me define the recursion contract first: f(node) returns ⟨one sentence⟩, null returns ⟨identity⟩ — and for this problem the value I return to the parent differs from the answer I record globally, so I'll track ⟨best⟩ outside. That's one post-order pass, O(n) time, O(h) stack."