Progress
🌍
Why does this topic matter?
Trees appear in roughly 25–30% of all FAANG on-site rounds. Google in particular is famous for tree problems. More importantly, trees are the first non-linear data structure you encounter — they force you to think recursively, which is the fundamental skill underlying graphs, dynamic programming, and almost everything in Phase 3. Once you truly understand trees, you understand recursion.

📚 Before We Start — The Recursive Leap of Faith

Every tree function follows one template: assume that solve(node.left) and solve(node.right) already return the correct answer for their subtrees. Your only job is to combine those two answers with the current node to produce the answer for the whole tree. This is called the recursive leap of faith — trust the function.

You have been doing this implicitly since Lecture 5 (Recursion). Every recursive call stack is a tree. Now you make it explicit with actual tree nodes and pointers.

🔗
How it connects: This lecture builds on Lecture 5 (Recursion) and Lecture 12 (Stacks & Queues). You will use a stack for iterative DFS and a queue for BFS level-order. Trees are the prerequisite for Lecture 16 (Heaps), Lecture 17 (Graphs), and the BST-based problems in Phase 3.

🧰 Tree Anatomy

Build the mental model before writing a single line of code:

Tree Vocabulary — Memorise These
          1          ← Root (no parent)
        /   \
       2     3       ← Internal nodes (have children)
      / \     \
     4   5     6    ← Leaf nodes (no children)

 Node 1: depth = 0, height = 2
 Node 2: depth = 1, height = 1
 Node 4: depth = 2, height = 0  (leaf)

 Tree height   = longest root-to-leaf path = 2
 Subtree of 2  = the entire tree rooted at node 2

 Full BT    : every node has 0 or 2 children
 Complete BT: all levels full except last (left-filled)
 Perfect BT : all internal nodes 2 children, all leaves same depth
 Balanced BT: |height(left) - height(right)| <= 1 at EVERY node
 BST        : left < root < right at EVERY node

🧮 The TreeNode Class

☕ Java · TreeNode
class TreeNode {
    int val;
    TreeNode left, right;
    TreeNode(int val) { this.val = val; }
}
// This is ALL you need. Every LeetCode tree problem uses this exact class.
// left and right are null by default — no child = null reference.
💡
The Recursive Leap of Faith: When solving any tree function, assume solve(node.left) and solve(node.right) already return the correct answer. Your job is only to combine those answers with the current node. Trust the recursion. This insight is the core of every single tree solution.

🎯 The 6 Core Patterns

# Pattern When to Use Core Tool Canonical Example
1 DFS Traversals Visit nodes in a specific order Recursion / Stack Inorder traversal, flatten BT to LL
2 BFS Level-Order Process one level at a time Queue Right side view, zigzag, avg of levels
3 Height Recursion Answer depends on height of subtrees Return int from DFS Max depth, balanced check, diameter
4 Global via Local Global answer updated inside DFS Instance variable Diameter, max path sum
5 BST Property Tree is a BST — exploit ordering Range check or inorder Validate BST, kth smallest
6 LCA Find where two nodes split Return node from DFS LCA of binary tree / BST

🟊 Pattern 1 — DFS Traversals (Pre / In / Post)

The foundational tree pattern. Everything else builds on top of this. The three orders all use the same recursive skeleton — only the position of the “process root” step changes:

Pattern Skeleton

Base case: if (node == null) return;
Preorder : process root → left → right
Inorder : left → process root → right
Postorder : left → right → process root

Pseudocode — Any DFS Traversal

function dfs(node):
  BASE: if node is null → return
  [PREORDER: process node here]
  dfs(node.left)
  [INORDER: process node here]
  dfs(node.right)
  [POSTORDER: process node here]

☕ Java · All Three DFS Orders
// Preorder: ROOT first — use when root context needed before children (clone, prefix)
void preorder(TreeNode n) {
    if (n == null) return;
    process(n.val);          // ROOT
    preorder(n.left);
    preorder(n.right);
}
// Inorder: Left-ROOT-Right — gives SORTED output for a BST
void inorder(TreeNode n) {
    if (n == null) return;
    inorder(n.left);
    process(n.val);          // ROOT
    inorder(n.right);
}
// Postorder: children before root — use when children results needed first (height, delete)
void postorder(TreeNode n) {
    if (n == null) return;
    postorder(n.left);
    postorder(n.right);
    process(n.val);          // ROOT
}

Iterative Traversals (No Recursion)

☕ Java · Iterative Preorder + Inorder
// Iterative Preorder — push RIGHT first, then LEFT (so left is processed first)
List<Integer> preorderIterative(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    if (root == null) return res;
    Deque<TreeNode> stack = new ArrayDeque<>();
    stack.push(root);
    while (!stack.isEmpty()) {
        TreeNode n = stack.pop();
        res.add(n.val);                     // visit ROOT immediately
        if (n.right != null) stack.push(n.right);  // right first → processed last
        if (n.left  != null) stack.push(n.left);
    }
    return res;
}

// Iterative Inorder — go left until null, pop & visit, then go right
List<Integer> inorderIterative(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    Deque<TreeNode> stack = new ArrayDeque<>();
    TreeNode curr = root;
    while (curr != null || !stack.isEmpty()) {
        while (curr != null) { stack.push(curr); curr = curr.left; }
        curr = stack.pop();
        res.add(curr.val);
        curr = curr.right;
    }
    return res;
}
// Time O(n), Space O(h). Key trick for preorder: push RIGHT before LEFT.
📊
Dry Run — All 3 orders on this tree:
1 / \ 2 3 / 4 Preorder (Root→L→R) : 1 2 4 3 ← use for: clone tree, serialize, prefix Inorder (L→Root→R) : 4 2 1 3 ← use for: BST sorted output, kth smallest Postorder (L→R→Root) : 4 2 3 1 ← use for: delete tree, height, evaluate expression Iterative inorder — stack state trace: curr=1 → push 1, go left curr=2 → push 2, go left curr=4 → push 4, go left curr=null → pop 4, visit 4, curr=4.right=null curr=null → pop 2, visit 2, curr=2.right=null curr=null → pop 1, visit 1, curr=1.right=3 curr=3 → push 3, go left curr=null → pop 3, visit 3, curr=3.right=null ✓ Result: [4, 2, 1, 3]

🌟 Pattern 2 — BFS Level-Order

Use a queue. Process the tree one level at a time. Any problem mentioning “by level,” “row,” or “right side view” is Pattern 2.

📌
The Critical Trick: Snapshot int size = q.size() before the inner loop. This locks in how many nodes are in the current level. As you add children mid-loop, the queue grows, but you only process size nodes per level.
☕ Java · BFS Level-Order Template
List<List<Integer>> levelOrder(TreeNode root) {
    List<List<Integer>> res = new ArrayList<>();
    if (root == null) return res;
    Queue<TreeNode> q = new LinkedList<>();
    q.offer(root);
    while (!q.isEmpty()) {
        int size = q.size();                    // ← snapshot current level
        List<Integer> level = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            TreeNode n = q.poll();
            level.add(n.val);
            if (n.left  != null) q.offer(n.left);
            if (n.right != null) q.offer(n.right);
        }
        res.add(level);
    }
    return res;
}
// Variants built on this template:
//   Right side view   → last element of each level
//   Zigzag traversal  → alternate direction per level (LinkedList.addFirst)
//   Average of levels → sum/size per level
📊
Dry Run — Level-Order on tree [1, 2, 3, 4]:
1 / \ 2 3 / 4 Queue=[1] size=1 → visit 1, enqueue 2,3 Level 0: [1] Queue=[2,3] size=2 → visit 2 (enqueue 4), visit 3 Level 1: [2,3] Queue=[4] size=1 → visit 4, no children Level 2: [4] Queue=[] done ✓ Result: [[1], [2,3], [4]] Right side view = last of each level = [1, 3, 4]

🎮 Pattern 3 — Height Recursion (Bottom-Up)

The function returns a number to its caller. Root combines left & right results. This is postorder DFS where each call computes something useful for its parent.

Pseudocode — Height Recursion

function height(node):
  BASE: if node is null → return 0
  leftH ← height(node.left)
  rightH ← height(node.right)
  return 1 + max(leftH, rightH)

Key: children compute FIRST (postorder). Parent uses both results.

☕ Java · Max Depth + Balanced Check
int maxDepth(TreeNode n) {
    if (n == null) return 0;
    int lh = maxDepth(n.left), rh = maxDepth(n.right);
    return 1 + Math.max(lh, rh);
}

// Balanced tree — use -1 as a sentinel for "unbalanced", O(n) single pass:
boolean isBalanced(TreeNode root) { return check(root) != -1; }
int check(TreeNode n) {
    if (n == null) return 0;
    int lh = check(n.left);   if (lh == -1) return -1;
    int rh = check(n.right);  if (rh == -1) return -1;
    if (Math.abs(lh - rh) > 1) return -1;
    return 1 + Math.max(lh, rh);
}
📚
Dry Run — maxDepth on [3, 9, 20, null, null, 15, 7]:
3 / \ 9 20 / \ 15 7 Call-stack unwind (children resolve before parent): maxDepth(9) → lh=0, rh=0 → 1+max(0,0) = 1 maxDepth(15) → lh=0, rh=0 → 1+max(0,0) = 1 maxDepth(7) → lh=0, rh=0 → 1+max(0,0) = 1 maxDepth(20) → lh=1, rh=1 → 1+max(1,1) = 2 maxDepth(3) → lh=1, rh=2 → 1+max(1,2) = 3

🏆 Pattern 4 — Global State via Local Traversal

The answer lives in a class-level variable updated during DFS. The function's return value is a helper value, not the final answer. This trips up beginners the most.

Pseudocode — Global State Pattern

global ans = initial_value

function dfs(node):
  BASE: if node is null → return 0 (or -INF)
  left ← dfs(node.left)   (helper value)
  right ← dfs(node.right)
  ans = max(ans, combine(left, right, node)) ← update global
  return max(left, right) + node ← return helper to parent

☕ Java · Diameter of Binary Tree (LC 543)
int maxD = 0;                      // global answer

int diameterOfBinaryTree(TreeNode root) {
    dfs(root);
    return maxD;
}
int dfs(TreeNode n) {               // returns HEIGHT, not diameter
    if (n == null) return 0;
    int lh = dfs(n.left), rh = dfs(n.right);
    maxD = Math.max(maxD, lh + rh);  // diameter through THIS node
    return 1 + Math.max(lh, rh);     // height returned to parent
}
// Diameter at a node = leftHeight + rightHeight (edges through it)
// We check EVERY node as a potential "peak" of the diameter path
💡
Same pattern solves Binary Tree Maximum Path Sum (LC 124). Return the max gain from one branch; update global with leftGain + node.val + rightGain. Once you understand Pattern 4, LC 124 is a 10-line solution.
☕ Java · Binary Tree Maximum Path Sum (LC 124) — Hard
int maxSum = Integer.MIN_VALUE;  // global: reset per problem instance

int maxPathSum(TreeNode root) {
    gain(root);
    return maxSum;
}
// gain(n) = max contribution this subtree can add to a path going UP to parent
int gain(TreeNode n) {
    if (n == null) return 0;
    int lGain = Math.max(0, gain(n.left));   // ignore negative branches
    int rGain = Math.max(0, gain(n.right));
    maxSum = Math.max(maxSum, lGain + n.val + rGain);  // path THROUGH this node
    return n.val + Math.max(lGain, rGain);  // can only go ONE direction up
}
// Why max(0, gain)? A negative subtree hurts the path — better to skip it.
// Why return only ONE direction? A path going up can't branch — parent picks one side.
📊
Dry Run — Diameter of [1,2,3,4,5] (answer=3):
1 / \ 2 3 / \ 4 5 ⚠ dfs() returns HEIGHT; global maxD stores DIAMETER Call-stack unwind (postorder): dfs(4) → lh=0,rh=0 → maxD=max(0,0+0)=0 → return 1 dfs(5) → lh=0,rh=0 → maxD=max(0,0+0)=0 → return 1 dfs(2) → lh=1,rh=1 → maxD=max(0,2) → return 2 dfs(3) → lh=0,rh=0 → maxD=max(2,0)=2 → return 1 dfs(1) → lh=2,rh=1 → maxD=max(2,3) → return 3 Answer: maxD=3 ✓ (path: 4→2→1→3)

🆕 Pattern 5 — The BST Property

For every node in a BST, all values in its left subtree are strictly less, and all values in its right subtree are strictly greater.

Pseudocode — BST Operations

Search: if val < node → go left; if val > node → go right; if equal → found
Insert: same as search until null, place new node there
Validate: pass (min, max) bounds; every node must satisfy min < val < max
Inorder: always produces sorted sequence ← key BST insight

⚠️
Classic Mistake — Wrong BST validation: Checking only node.left.val < node.val < node.right.val at each node is wrong. You must pass a valid range [min, max] down the tree. Going left narrows the max; going right narrows the min. Use Long to handle Integer.MIN_VALUE edge cases.
☕ Java · Validate BST + Kth Smallest
// Validate BST — range propagation:
boolean isValidBST(TreeNode root) { return check(root, Long.MIN_VALUE, Long.MAX_VALUE); }
boolean check(TreeNode n, long lo, long hi) {
    if (n == null) return true;
    if (n.val <= lo || n.val >= hi) return false;
    return check(n.left, lo, n.val) && check(n.right, n.val, hi);
}

// Kth Smallest — inorder = sorted, count as you go:
int k, result;
void kth(TreeNode n) {
    if (n == null) return;
    kth(n.left);
    if (--k == 0) { result = n.val; return; }
    kth(n.right);
}

BST Insert & Delete

☕ Java · BST Insert (LC 701) + Delete (LC 450)
// INSERT: navigate like search until null spot found, place node there
TreeNode insertIntoBST(TreeNode root, int val) {
    if (root == null) return new TreeNode(val);
    if (val < root.val) root.left  = insertIntoBST(root.left,  val);
    else               root.right = insertIntoBST(root.right, val);
    return root;  // return root so parent pointer stays valid
}

// DELETE: 3 cases
// Case 1: leaf node    → return null
// Case 2: one child    → return that child
// Case 3: two children → swap with inorder successor, delete successor below
TreeNode deleteNode(TreeNode root, int key) {
    if (root == null) return null;
    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  == null) return root.right;   // Cases 1 & 2
        if (root.right == null) return root.left;
        TreeNode succ = root.right;
        while (succ.left != null) succ = succ.left;      // leftmost = successor
        root.val = succ.val;                               // copy value up
        root.right = deleteNode(root.right, succ.val);   // delete old successor
    }
    return root;
}
// Time O(h): O(log n) balanced BST, O(n) worst case (skewed)

🌐 Pattern 6 — Lowest Common Ancestor (LCA)

The LCA of two nodes p and q is the deepest node that is an ancestor of both. It is the first node where they “split” in the tree.

Pseudocode — LCA (Binary Tree)

function lca(node, p, q):
  BASE: if node is null OR node is p OR node is q → return node
  left ← lca(node.left, p, q)
  right ← lca(node.right, p, q)
  if both left and right non-null → return node (split point!)
  return whichever is non-null

☕ Java · LCA Binary Tree (LC 236) + BST (LC 235)
// Binary Tree — works for any tree:
TreeNode lca(TreeNode n, TreeNode p, TreeNode q) {
    if (n == null || n == p || n == q) return n;
    TreeNode l = lca(n.left,  p, q);
    TreeNode r = lca(n.right, p, q);
    if (l != null && r != null) return n;  // split here → this IS the LCA
    return l != null ? l : r;
}

// BST — exploit ordering (simpler):
TreeNode lcaBST(TreeNode n, TreeNode p, TreeNode q) {
    if (p.val < n.val && q.val < n.val) return lcaBST(n.left,  p, q);
    if (p.val > n.val && q.val > n.val) return lcaBST(n.right, p, q);
    return n;  // split point → LCA
}
📌
Dry Run — LCA(root=3, p=5, q=1) on [3,5,1,6,2,0,8]:
3 / \ 5 1 / \ / \ 6 2 0 8 lca(3, p=5, q=1): left = lca(5, p=5, q=1) → node==p → return 5 ◄ found p right = lca(1, p=5, q=1) → node==q → return 1 ◄ found q both non-null → split point → return 3 ✓ Key: once both sides return non-null, the current node IS the LCA.

💪 In-Lecture Practice Problems

📚
6 problems, one per pattern. Solve each yourself before expanding the solution. Goal: within 30 seconds of reading any tree problem, identify the pattern.
Problem 01 · LC 94
Binary Tree Inorder Traversal
Easy Pattern 1: DFS Traversal Amazon · Microsoft · Google
Problem 02 · LC 104
Maximum Depth of Binary Tree
Easy Pattern 3: Height Recursion Amazon · Google
Problem 03 · LC 102
Binary Tree Level Order Traversal
Medium Pattern 2: BFS Level-Order Amazon · Microsoft · Google LC 102
Problem 04 · LC 543
Diameter of Binary Tree
Easy Pattern 4: Global via Local Google · Facebook LC 543
Problem 05 · LC 98
Validate Binary Search Tree
Medium Pattern 5: BST Property Amazon · Microsoft · Bloomberg LC 98
Problem 06 · LC 236
Lowest Common Ancestor of a Binary Tree
Medium Pattern 6: LCA Amazon · Google · Facebook LC 236

📝 Assignment — Company Interview Problems

📋
Topic 15 Assignment — 45 Problems (12 Easy · 23 Medium · 10 Hard)
All 6 tree patterns covered. Every problem includes the LeetCode link, pattern to apply, step-by-step hint, common mistakes, and companies that ask it most.

📄 Open Assignment.md →

✅ Lecture Completion Checklist

Check each item before advancing to Lecture 16 (Heaps & Priority Queues). Be honest — these are your interview readiness criteria.

I can write recursive preorder, inorder, and postorder from memory in under 60 seconds
I can write the iterative inorder using an explicit stack and explain every step
I can implement BFS level-order and explain why int size = q.size() must come before the inner loop
I can solve Max Depth, Balanced Tree, and Diameter using the height recursion pattern
I understand why Diameter uses a global variable while the DFS returns height
I can validate a BST using the [min, max] range approach (not just parent comparison)
I can explain why inorder traversal of a BST gives a sorted sequence
I can implement LCA for both BT (split-point logic) and BST (direction logic) from memory
I can identify the correct pattern within 30 seconds for any tree problem
I have completed all 45 assignment problems and can explain the pattern behind each one
You are ready for Topic 16: Heaps & Priority Queues
Heaps are built on arrays but think like complete binary trees. The heap property (parent ≤ children for min-heap) is a weaker version of the BST property — you only care about parent vs child, not left vs right ordering. The tree intuition you built here carries over directly.
← Topic 14: Matrix Problems Topic 15 of 30 — Phase 2 Topic 16: Heaps & PQ →