Trees — Binary Trees & BST
“Everything is just recursion on a node. Trust the function for the subtrees, combine at the root.”
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.
🧰 Tree Anatomy
Build the mental model before writing a single line of code:
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
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.
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:
// 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)
// 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.
🌟 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.
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.
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
🎮 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.
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); }
🏆 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.
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
leftGain + node.val + rightGain. Once you understand
Pattern 4, LC 124 is a 10-line solution.
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.
🆕 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.
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.
// 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
// 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.
// 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 }
💪 In-Lecture Practice Problems
📝 Assignment — Company Interview Problems
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.
int size = q.size() must come before the inner loop
[min, max] range
approach (not just parent comparison)
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.