Week 6: Binary Trees & BSTs
1. Overviewโ
Welcome to Week 6! It is time to leave linear data structures (arrays, strings, linked lists) behind and step into the multi-dimensional world of Hierarchical Data. This week focuses on Binary Trees and Binary Search Trees (BSTs).
Trees are the underlying architecture for databases (B-Trees), file systems, DOM rendering, and decision engines. You will learn the two primary ways to traverse hierarchical data: plunging deep into the leaves (DFS) and exploring layer by layer (BFS).
Goals for this week:
- Understand Tree terminology: Root, Leaf, Height, Depth, and Ancestor.
- Master Depth-First Search (Pre-order, In-order, Post-order) using Recursion.
- Master Breadth-First Search (Level-order) using a Queue.
- Understand the strict mathematical properties of a Binary Search Tree.
Knowledge You Need Before Startingโ
- Week 5 stack/queue mental models (LIFO and FIFO behavior).
- Basic recursion traceability from linked-list pointer practice.
- Comfort with class/object modeling for
TreeNode. - Confidence with null checks and branching edge cases.
2. Theory & Fundamentalsโ
2.1 Mental Model: Why Trees Existโ
Think of real-world hierarchies:
- Company org chart: CEO โ VPs โ Managers โ Employees
- File system: Root
/โhomeโuserโdocumentsโfile.txt - Decision tree: "Is it raining?" โ Yes โ "Bring umbrella" / No โ "Don't bring umbrella"
These relationships are naturally parent-child, not sequential. A tree captures this perfectly: one root, many branches, and leaves at the end.
Linear (Array): [A] โ [B] โ [C] โ [D]
โ can only go forward/backward
Hierarchical (Tree):
A (root)
/ \
B C
/ \
D E (leaves)
โ can branch, explore multiple paths
Why not just use an array of objects with parent IDs? You could, but then every operation (search, insert, delete) requires scanning the entire array. A tree gives you O(\log N) operations when balanced.
2.2 Tree Terminology (Critical for Interviews)โ
1 (root) โ depth 0, height 3
/ \
2 3 โ depth 1
/ \ \
4 5 6 โ depth 2
/
7 โ depth 3 (leaf), this is the deepest leaf
| Term | Definition | Example from tree above |
|---|---|---|
| Root | The topmost node with no parent | 1 |
| Leaf | A node with no children | 5, 3, 6, 7 |
| Parent | A node with children | 1 is parent of 2 and 3 |
| Child | Direct descendant of a node | 2 and 3 are children of 1 |
| Ancestor | Any node on the path from root to this node | For 7: ancestors are 4, 2, 1 |
| Descendant | Any node in the subtree rooted at this node | For 2: descendants are 4, 5, 7 |
| Sibling | Nodes with the same parent | 2 and 3 are siblings |
| Depth | Distance from root to this node | Depth of 4 is 2 |
| Height | Distance from this node to its deepest leaf | Height of 2 is 2, height of 1 is 3 |
| Level | All nodes at the same depth | Level 2 = [4, 5, 6] |
Critical formulas to memorize:
- Height of tree = depth of deepest leaf
- Number of nodes in a perfect binary tree of height
h=2^(h+1) - 1 - Maximum number of nodes at level
L=2^L - Height of a balanced binary tree with
Nnodes =O(\log N) - Height of a skewed binary tree with
Nnodes =O(N)(degenerate into a linked list)
2.3 Binary Trees: Structure and Propertiesโ
A Binary Tree means each node has at most 2 children (can have 0, 1, or 2).
Valid Binary Tree:
5
/ \
3 8
/
1
Still Valid (not full, but binary):
5
/
3
/
1 โ this is just a linked list, but still a "binary tree"
Types of Binary Trees:
| Type | Definition | Example |
|---|---|---|
| Full | Every node has 0 or 2 children (never just 1) | All internal nodes have both kids |
| Complete | All levels fully filled except possibly the last, which fills left-to-right | Heap structure |
| Perfect | All internal nodes have 2 children AND all leaves are at the same level | 2^(h+1) - 1 nodes |
| Balanced | Height is O(\log N) where N is number of nodes | AVL, Red-Black trees |
| Skewed | Every node has at most one child (essentially a linked list) | Worst-case BST |
2.4 Recursion: The Natural Way to Think About Treesโ
Why recursion dominates tree problems:
A tree is a recursive data structure. Every subtree is itself a tree. So tree problems naturally decompose into:
- Base case: What do I do at
nullor a leaf? - Recursive case: Solve for left child, solve for right child, combine the results.
height(tree) = ???
Recursive insight:
height(tree) = 1 + max(height(left_subtree), height(right_subtree))
Base case:
height(null) = 0
The Call Stack = Your Explorer's Backpack
When you write maxDepth(root.left), Java pushes a new stack frame. Think of it like:
- You're standing at node
5. - You tell your left-hand clone: "Go explore the left subtree, report back with its depth."
- While waiting, you tell your right-hand clone: "Go explore the right subtree."
- Once both return, you compute:
1 + max(leftDepth, rightDepth).
The call stack tracks all these "clones" waiting for their subtrees to report back.
Stack Overflow Risk in Java:
The JVM's default stack size is typically ~1MB, which allows about 5,000โ10,000 nested calls depending on local variable size. A deeply skewed tree with 100,000 nodes will crash with StackOverflowError.
// This will crash on a skewed tree with 100,000 nodes
public int countNodes(TreeNode root) {
if (root == null) return 0;
return 1 + countNodes(root.left) + countNodes(root.right);
}
Solution: For very deep trees, convert to iterative using an explicit Stack<TreeNode>.
2.5 DFS vs. BFS: When to Use Whichโ
Tree:
1
/ \
2 3
/ \
4 5
DFS Pre-order (Root โ Left โ Right): 1, 2, 4, 5, 3
DFS In-order (Left โ Root โ Right): 4, 2, 5, 1, 3
DFS Post-order(Left โ Right โ Root): 4, 5, 2, 3, 1
BFS Level-order: 1, 2, 3, 4, 5
| Question Type | Use DFS or BFS? | Why? |
|---|---|---|
| "Find the depth/height of the tree" | DFS (any order) | Natural recursion on subtrees |
| "Is this tree symmetric?" | DFS Pre-order | Mirror-check left vs right |
| "Print nodes level by level" | BFS | Need to group by level |
| "Find the shortest path to a leaf" | BFS | BFS finds shortest path in unweighted trees |
| "Right side view" (rightmost node per level) | BFS | Process level by level |
| "Validate BST" | DFS In-order | In-order gives sorted output |
| "Sum all root-to-leaf paths" | DFS Pre-order | Build path as you go down |
| "Find diameter (longest path)" | DFS Post-order | Need child results before computing parent |
| "Lowest common ancestor" | DFS Post-order | Bubble up found nodes |
| "Serialize/Deserialize tree" | DFS Pre-order | Need root first to rebuild |
Memory comparison:
| DFS (Recursive) | BFS (Queue) | |
|---|---|---|
| Space | O(H) call stack (H = height) | O(W) queue (W = max width) |
| Best case | Balanced tree: O(\log N) | Skewed tree: O(1) |
| Worst case | Skewed tree: O(N) | Perfect tree: O(N/2) = O(N) |
For a perfect binary tree, BFS uses more space because the last level has N/2 nodes all in the queue at once. For a skewed tree, DFS uses more space because the call stack is N deep.
2.6 The Three DFS Traversals: What They Meanโ
Pre-order (Root โ Left โ Right):
- "Process root before children"
- Use when: Creating a copy, serializing, printing tree structure
- Think: "I need to know the parent before I can process children"
void preorder(TreeNode node) {
if (node == null) return;
process(node); // โ Root first
preorder(node.left); // โ Then left
preorder(node.right); // โ Then right
}
In-order (Left โ Root โ Right):
- "Process root between children"
- Use when: BST problems requiring sorted order
- Think: "I'm sweeping left to right across the tree"
void inorder(TreeNode node) {
if (node == null) return;
inorder(node.left); // โ Left first
process(node); // โ Root in the middle
inorder(node.right); // โ Right last
}
Post-order (Left โ Right โ Root):
- "Process root after children"
- Use when: Calculating properties that depend on child results (height, diameter, deleting nodes)
- Think: "I can't know anything about this node until I've explored its children"
void postorder(TreeNode node) {
if (node == null) return;
postorder(node.left); // โ Left first
postorder(node.right); // โ Right second
process(node); // โ Root last (after getting child results)
}
2.7 Binary Search Trees: The Sorted Superpowerโ
A Binary Search Tree (BST) is a binary tree where:
- All values in the left subtree < current node
- All values in the right subtree > current node
- No duplicate values (standard convention)
- Both left and right subtrees are also BSTs (recursive property)
Valid BST:
8
/ \
3 10
/ \ \
1 6 14
/ \ /
4 7 13
WHY? At node 8:
- Left subtree [1,3,4,6,7] are ALL < 8 โ
- Right subtree [10,13,14] are ALL > 8 โ
Invalid BST:
8
/ \
3 10
/ \ \
1 6 14
/ \ /
4 9 13 โ 9 is in right subtree of 8 but < 8 โ
The Key Insight: In-order traversal of a BST gives sorted order
In-order of valid BST above: 1, 3, 4, 6, 7, 8, 10, 13, 14 โ Sorted! โ
Operations on a balanced BST:
| Operation | Time | Why |
|---|---|---|
| Search | O(\log N) | Eliminate half the tree each step |
| Insert | O(\log N) | Search for position, attach |
| Delete | O(\log N) | Find node, rearrange pointers |
| Find min/max | O(\log N) | Go all the way left/right |
| Find successor | O(\log N) | Smallest in right subtree |
Degenerate case (skewed tree): All operations degrade to O(N) โ the tree is just a linked list.
Skewed BST (worst case):
1
\
2
\
3
\
4 โ search for 4 takes O(N) steps, not O(log N)
Solution: Self-balancing trees like AVL or Red-Black trees guarantee O(\log N) height through rotations.
2.8 Common BST Validation Mistakeโ
WRONG approach โ only checking immediate children:
// โ This is INCORRECT โ fails on the example below
boolean isValidBST(TreeNode root) {
if (root == null) return true;
if (root.left != null && root.left.val >= root.val) return false;
if (root.right != null && root.right.val <= root.val) return false;
return isValidBST(root.left) && isValidBST(root.right);
}
This fails on:
10
/ \
5 15
/ \
6 20 โ 6 < 15 โ
but 6 < 10 โ (violates BST: right subtree must be > 10)
CORRECT approach โ passing valid range bounds:
Every node must fall within a range (min, max). As you go left, update max. As you go right, update min.
boolean isValidBST(TreeNode root) {
return validate(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
boolean validate(TreeNode node, long min, long max) {
if (node == null) return true;
if (node.val <= min || node.val >= max) return false;
return validate(node.left, min, node.val) && // left: shrink upper bound
validate(node.right, node.val, max); // right: shrink lower bound
}
3. Code Templates (Java)โ
The Standard 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;
}
}
Template 1: BFS / Level-Order Traversalโ
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) return result;
Queue<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size(); // Critical: lock in current level size
List<Integer> currentLevel = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode curr = queue.poll();
currentLevel.add(curr.val);
if (curr.left != null) queue.offer(curr.left);
if (curr.right != null) queue.offer(curr.right);
}
result.add(currentLevel);
}
return result;
}
Why levelSize matters: Without it, you'd mix nodes from different levels in the same iteration. By locking in the size, you process exactly one level per outer loop iteration.
Variant: Right Side View
// Return the rightmost node of each level
public List<Integer> rightSideView(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) return result;
Queue<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
TreeNode rightmost = null;
for (int i = 0; i < levelSize; i++) {
TreeNode curr = queue.poll();
rightmost = curr; // last node polled = rightmost
if (curr.left != null) queue.offer(curr.left);
if (curr.right != null) queue.offer(curr.right);
}
result.add(rightmost.val);
}
return result;
}
Template 2: DFS Pre-order (Recursive)โ
// Use for: copying, serializing, building paths
public void preorder(TreeNode root, List<Integer> result) {
if (root == null) return;
result.add(root.val); // Process root first
preorder(root.left, result);
preorder(root.right, result);
}
Template 3: DFS In-order (Recursive)โ
// Use for: BST problems requiring sorted order
public void inorder(TreeNode root, List<Integer> result) {
if (root == null) return;
inorder(root.left, result);
result.add(root.val); // Process root in the middle
inorder(root.right, result);
}
Template 4: DFS Post-order (Recursive)โ
// Use for: height, diameter, deletion (need child results first)
public int maxDepth(TreeNode root) {
if (root == null) return 0; // Base case
int leftDepth = maxDepth(root.left); // Get left result
int rightDepth = maxDepth(root.right); // Get right result
return 1 + Math.max(leftDepth, rightDepth); // Combine
}
Template 5: DFS Iterative (Using Stack)โ
// Pre-order iterative (useful for deep trees to avoid StackOverflowError)
public List<Integer> preorderIterative(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) return result;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode curr = stack.pop();
result.add(curr.val);
// Push right first so left is processed first (stack is LIFO)
if (curr.right != null) stack.push(curr.right);
if (curr.left != null) stack.push(curr.left);
}
return result;
}
Template 6: BST Searchโ
// Iterative (preferred for BST โ no call stack overhead)
public TreeNode searchBST(TreeNode root, int target) {
TreeNode curr = root;
while (curr != null) {
if (target == curr.val) return curr;
else if (target < curr.val) curr = curr.left;
else curr = curr.right;
}
return null;
}
Template 7: BST Insertโ
public 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;
}
Template 8: BST Validation (Range Bounds)โ
public boolean isValidBST(TreeNode root) {
return validate(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean validate(TreeNode node, long min, long max) {
if (node == null) return true;
if (node.val <= min || node.val >= max) return false;
return validate(node.left, min, node.val) &&
validate(node.right, node.val, max);
}
Template 9: Path Sum (Root to Leaf)โ
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) return false;
// Leaf node check
if (root.left == null && root.right == null) {
return root.val == targetSum;
}
int remaining = targetSum - root.val;
return hasPathSum(root.left, remaining) || hasPathSum(root.right, remaining);
}
Template 10: Lowest Common Ancestor (LCA)โ
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// If both sides found a target, current root is the LCA
if (left != null && right != null) return root;
// Otherwise return whichever side found something
return left != null ? left : right;
}
Template 11: Tree Diameter (Longest Path)โ
class Solution {
private int maxDiameter = 0;
public int diameterOfBinaryTree(TreeNode root) {
height(root);
return maxDiameter;
}
private int height(TreeNode node) {
if (node == null) return 0;
int leftHeight = height(node.left);
int rightHeight = height(node.right);
// Diameter through this node = left height + right height
maxDiameter = Math.max(maxDiameter, leftHeight + rightHeight);
return 1 + Math.max(leftHeight, rightHeight);
}
}
4. Problem-Solving Frameworkโ
Step 1 โ Clarify (2 minutes)โ
Ask these questions:
- Is this a binary tree or specifically a BST? (BST unlocks in-order sorted optimization)
- Can node values be negative? Zero? Duplicates?
- Can the tree be empty (
root == null)? - For path problems: Does the path have to start at root and end at a leaf?
- For BST: Are values unique?
- Memory constraints: Can I use
O(N)space or must it beO(H)?
Step 2 โ Draw the Treeโ
Always draw at least two test cases:
- A balanced tree
- A skewed tree (or other edge case: single node, empty, all left children, etc.)
Example: "Find max depth"
Balanced: Skewed:
1 1
/ \ \
2 3 2
/ \
4 3
Expected: 3 Expected: 3
Step 3 โ Identify Traversal Typeโ
| Signal | Traversal |
|---|---|
| "Level by level", "Shortest path", "Right side view" | BFS |
| "Sorted", "K-th smallest" (in BST) | DFS In-order |
| "Copy", "Serialize", "Path from root" | DFS Pre-order |
| "Height", "Diameter", "Validate", "Bottom-up calculation" | DFS Post-order |
| "Symmetric", "Mirror" | DFS Pre-order (with custom logic) |
Step 4 โ Write the Base Case Firstโ
The base case is the foundation of your recursion. Always write it before the recursive calls.
// Base case for most tree problems:
if (root == null) return /* appropriate value */;
// return 0 for height/count
// return null for finding a node
// return true for validation (empty trees are valid)
// return false for path sum (no path exists)
Step 5 โ Think "What Do I Need From My Children?"โ
This is the key to post-order thinking:
- Height: I need the height of left and right, then return
1 + max(left, right) - Balanced: I need the height of both sides and whether they're balanced. If heights differ by more than 1, return false.
- Diameter: I need the height of both sides. The diameter through this node is
leftHeight + rightHeight.
Step 6 โ Code and Testโ
Walk through your code manually with the drawn examples. Check:
- Does it handle
root == null? - Does it handle a single-node tree?
- Does it correctly combine left and right results?
5. Pattern Recognition Guideโ
Signal โ Pattern Mappingโ
| What the problem says | Pattern | Key technique |
|---|---|---|
| "Print/return nodes level by level" | BFS | Queue + levelSize loop |
| "Right side view", "Zigzag level order" | BFS | Track level, maybe reverse |
| "Shortest path to a leaf" | BFS | BFS finds shortest path |
| "Is tree symmetric?" | DFS Pre-order | Recursively compare left.left vs right.right |
| "Is tree balanced?" | DFS Post-order | Need child heights to check balance |
| "Find diameter" | DFS Post-order | Track max path through each node |
| "Validate BST" | DFS In-order or Range Bounds | In-order should be sorted |
| "K-th smallest in BST" | DFS In-order | Stop at K-th element |
| "Lowest common ancestor" | DFS Post-order | Bubble up found nodes |
| "Path sum", "All paths" | DFS Pre-order | Build path going down |
| "Serialize/Deserialize" | DFS Pre-order | Process root before children |
| "Invert tree" | DFS Pre-order | Swap children before recursing |
| "Delete node in BST" | DFS | 3 cases: no children, 1 child, 2 children |
| "Construct from traversals" | DFS | Use indices to split left/right |
Detailed Recognition Walkthroughsโ
Recognizing Post-Order (LC 543 โ Diameter)โ
When you see: "Find the longest path between any two nodes"
- "The longest path through a node = left height + right height."
- "But I can't compute this until I know the heights of my children."
- "I need to calculate height for every node and track the max diameter."
- Post-order: get left height, get right height, then compute diameter through current node.
Recognizing BFS (LC 102 โ Level Order)โ
When you see: "Return nodes grouped by level"
- "I need to process all nodes at depth 0, then depth 1, then depth 2, etc."
- "DFS doesn't naturally group by level โ it goes deep first."
- "BFS with a queue naturally processes level by level."
- Use the
levelSize = queue.size()trick to avoid mixing levels.
Recognizing BST In-Order (LC 230 โ K-th Smallest)โ
When you see: "Find the K-th smallest element in a BST"
- "BST in-order traversal gives sorted order."
- "Just do in-order and stop at the K-th element."
- Use a counter variable to track how many nodes processed.
Recognizing Range Validation (LC 98 โ Validate BST)โ
When you see: "Determine if a tree is a valid BST"
- "Checking only immediate children is not enough."
- "Every node must satisfy:
min < node.val < max." - "As I recurse left, the max shrinks to current node's value."
- "As I recurse right, the min grows to current node's value."
6. Common Mistakes & How to Avoid Themโ
Mistake 1: Forgetting the Null Checkโ
// โ NullPointerException when root is null
public int maxDepth(TreeNode root) {
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
// โ
Always check null first
public int maxDepth(TreeNode root) {
if (root == null) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
Mistake 2: Confusing Pre-order vs. Post-orderโ
// Problem: Find height
// โ WRONG โ pre-order processes root before getting child results
public int height(TreeNode root) {
if (root == null) return 0;
int h = 1; // trying to compute height before knowing child heights?
// How can we know the height without asking children first?
}
// โ
CORRECT โ post-order gets child results first
public int height(TreeNode root) {
if (root == null) return 0;
int left = height(root.left); // get left child's answer
int right = height(root.right); // get right child's answer
return 1 + Math.max(left, right); // then compute own answer
}
Mistake 3: Not Using the levelSize Trick in BFSโ
// โ WRONG โ mixes levels together
while (!queue.isEmpty()) {
TreeNode curr = queue.poll();
result.add(curr.val); // all nodes end up in one big list, not grouped by level
if (curr.left != null) queue.offer(curr.left);
if (curr.right != null) queue.offer(curr.right);
}
// โ
CORRECT โ lock in level size
while (!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> level = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode curr = queue.poll();
level.add(curr.val);
if (curr.left != null) queue.offer(curr.left);
if (curr.right != null) queue.offer(curr.right);
}
result.add(level); // each level is separate
}
Mistake 4: Only Checking Immediate Children in BST Validationโ
// โ WRONG โ fails when a right grandchild is < root
public boolean isValidBST(TreeNode root) {
if (root == null) return true;
if (root.left != null && root.left.val >= root.val) return false;
if (root.right != null && root.right.val <= root.val) return false;
return isValidBST(root.left) && isValidBST(root.right);
}
// โ
CORRECT โ pass min/max bounds down
public boolean isValidBST(TreeNode root) {
return validate(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean validate(TreeNode node, long min, long max) {
if (node == null) return true;
if (node.val <= min || node.val >= max) return false;
return validate(node.left, min, node.val) &&
validate(node.right, node.val, max);
}
Mistake 5: Forgetting to Handle Leaf Nodes in Path Problemsโ
// Problem: Path sum from root to leaf
// โ WRONG โ considers any node, not just leaves
public boolean hasPathSum(TreeNode root, int sum) {
if (root == null) return false;
if (root.val == sum) return true; // โ what if this is not a leaf?
return hasPathSum(root.left, sum - root.val) || ...
}
// โ
CORRECT โ check that it's a leaf
public boolean hasPathSum(TreeNode root, int sum) {
if (root == null) return false;
// Only check sum at a LEAF node
if (root.left == null && root.right == null) {
return root.val == sum;
}
int remaining = sum - root.val;
return hasPathSum(root.left, remaining) || hasPathSum(root.right, remaining);
}
Mistake 6: Modifying Tree Structure During Traversalโ
// โ DANGEROUS โ deleting nodes while recursing can lose references
public void deleteNodes(TreeNode root) {
if (root == null) return;
if (shouldDelete(root.left)) root.left = null; // might lose entire subtree!
deleteNodes(root.left);
// ...
}
// โ
SAFE โ process children first (post-order), then decide on current
public TreeNode deleteNodes(TreeNode root) {
if (root == null) return null;
root.left = deleteNodes(root.left); // clean left first
root.right = deleteNodes(root.right); // clean right first
if (shouldDelete(root)) return null; // then decide on current
return root;
}
7. Worked Examplesโ
Example 1: LeetCode 226 โ Invert Binary Treeโ
Thinking process:
- "Inverting means swapping left and right at every node."
- "I should do this top-down (pre-order) so the swap happens before recursing."
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) return null;
// Swap the children
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
// Recursively invert the subtrees
invertTree(root.left);
invertTree(root.right);
return root;
}
}
Dry run for tree:
Before: 1 After: 1
/ \ / \
2 3 3 2
Step 1: At node 1, swap 2 and 3 โ root.left=3, root.right=2
Step 2: Recurse on 3 (no children, return)
Step 3: Recurse on 2 (no children, return)
Result: โ
Time: O(N). Space: O(H) call stack.
Example 2: LeetCode 98 โ Validate Binary Search Treeโ
Thinking process:
- "Checking only immediate children doesn't work (see Section 2.8)."
- "Every node must satisfy
min < node.val < max." - "As I go left, max becomes node.val. As I go right, min becomes node.val."
class Solution {
public boolean isValidBST(TreeNode root) {
return validate(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
private boolean validate(TreeNode node, long min, long max) {
if (node == null) return true; // Empty trees are valid
if (node.val <= min || node.val >= max) return false;
// Left: upper bound shrinks to node.val
// Right: lower bound grows to node.val
return validate(node.left, min, node.val) &&
validate(node.right, node.val, max);
}
}
Dry run:
Tree: 8 Call stack:
/ \
3 10 validate(8, -โ, +โ)
/ \ \ 8 is in (-โ, +โ) โ
1 6 14 โโ validate(3, -โ, 8)
/ \ / โ 3 is in (-โ, 8) โ
4 7 13 โ โโ validate(1, -โ, 3) โ 1 in (-โ, 3) โ
โ โโ validate(6, 3, 8)
โ 6 is in (3, 8) โ
โ โโ validate(4, 3, 6) โ 4 in (3, 6) โ
โ โโ validate(7, 6, 8) โ 7 in (6, 8) โ
โโ validate(10, 8, +โ)
10 is in (8, +โ) โ
โโ validate(14, 10, +โ)
14 is in (10, +โ) โ
โโ validate(13, 10, 14) โ 13 in (10, 14) โ
Result: true โ
Time: O(N). Space: O(H).
Example 3: LeetCode 102 โ Binary Tree Level Order Traversalโ
Thinking process:
- "Need to group nodes by level โ BFS."
- "Use queue + lock in
levelSizeeach iteration."
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) return result;
Queue<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> currentLevel = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode curr = queue.poll();
currentLevel.add(curr.val);
if (curr.left != null) queue.offer(curr.left);
if (curr.right != null) queue.offer(curr.right);
}
result.add(currentLevel);
}
return result;
}
}
Dry run:
Tree: 3
/ \
9 20
/ \
15 7
Initial: queue = [3], result = []
Level 0: levelSize=1, poll(3), add 9,20 to queue โ currentLevel=[3] โ result=[[3]]
Level 1: levelSize=2, poll(9), poll(20), add 15,7 โ currentLevel=[9,20] โ result=[[3],[9,20]]
Level 2: levelSize=2, poll(15), poll(7) โ currentLevel=[15,7] โ result=[[3],[9,20],[15,7]]
Result: [[3],[9,20],[15,7]] โ
Time: O(N). Space: O(W) where W is max width.
Example 4: LeetCode 543 โ Diameter of Binary Treeโ
Thinking process:
- "Diameter through a node = left height + right height."
- "I need to compute height for every node โ post-order."
- "Track a global max diameter as I go."
class Solution {
private int maxDiameter = 0;
public int diameterOfBinaryTree(TreeNode root) {
height(root);
return maxDiameter;
}
private int height(TreeNode node) {
if (node == null) return 0;
int leftHeight = height(node.left);
int rightHeight = height(node.right);
// Diameter through this node
int diameterThroughNode = leftHeight + rightHeight;
maxDiameter = Math.max(maxDiameter, diameterThroughNode);
return 1 + Math.max(leftHeight, rightHeight);
}
}
Dry run:
Tree: 1
/ \
2 3
/ \
4 5
height(4): returns 1
height(5): returns 1
height(2): leftHeight=1, rightHeight=1, diameter=2, maxDiameter=2, returns 2
height(3): returns 1
height(1): leftHeight=2, rightHeight=1, diameter=3, maxDiameter=3, returns 3
Result: 3 โ
(path: 4 โ 2 โ 1 โ 3, length 3 edges)
Time: O(N). Space: O(H).
Example 5: LeetCode 236 โ Lowest Common Ancestor (LCA)โ
Thinking process:
- "If left subtree has one target and right has the other, current node is the LCA."
- "Use post-order: get results from children first, then decide at parent."
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null) return null;
if (root == p || root == q) return root; // found one of the targets
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// Both children returned a target โ current node is the LCA
if (left != null && right != null) return root;
// Only one side found something โ propagate it up
return left != null ? left : right;
}
}
Time: O(N). Space: O(H).
8. Decision Tree: Which Tree Pattern?โ
What does the problem need?
โโโ Group nodes by level?
โ โโโ YES โ BFS with levelSize loop
โ
โโโ Sorted order from BST?
โ โโโ YES โ DFS In-order
โ
โโโ Need child results before computing parent?
โ โโโ YES โ DFS Post-order
โ Examples: height, diameter, balanced, LCA
โ
โโโ Build something top-down (path, copy, serialize)?
โ โโโ YES โ DFS Pre-order
โ
โโโ Check structural property (symmetric, invert)?
โ โโโ YES โ DFS Pre-order with custom logic
โ
โโโ Validate BST?
โโโ Use range bounds OR in-order check for sorted
9. Complexity Cheat Sheetโ
| Operation | Time | Space |
|---|---|---|
| Traverse full tree (DFS/BFS) | O(N) | DFS: O(H), BFS: O(W) |
| Find height/depth | O(N) | O(H) call stack |
| Level order traversal | O(N) | O(W) queue (W = max width) |
| BST search (balanced) | O(\log N) | O(1) iterative, O(\log N) recursive |
| BST search (skewed) | O(N) | O(1) iterative, O(N) recursive |
| BST insert/delete (balanced) | O(\log N) | O(\log N) recursive |
| Validate BST | O(N) | O(H) |
| LCA | O(N) | O(H) |
| Serialize/Deserialize | O(N) | O(N) string, O(H) call stack |
| Count nodes in perfect tree | O(\log^2 N)* | O(\log N) |
*Count nodes in perfect tree: Check if left and right heights are equal. If yes, use formula 2^h - 1. If no, recurse.
10. 7-Day Practice Plan (21 Problems)โ
Day 1: DFS Fundamentals
- Invert Binary Tree (LC 226) โ Pre-order swap
- Maximum Depth of Binary Tree (LC 104) โ Post-order height
- Same Tree (LC 100) โ Pre-order structural check
Day 2: BFS / Level-Order Traversal 4. Binary Tree Level Order Traversal (LC 102) โ Classic BFS 5. Binary Tree Right Side View (LC 199) โ BFS variant 6. Binary Tree Zigzag Level Order Traversal (LC 103) โ BFS + reverse
Day 3: Tree Properties & Post-Order Processing 7. Diameter of Binary Tree (LC 543) โญ โ Global max tracking 8. Balanced Binary Tree (LC 110) โ Height + balance check 9. Symmetric Tree (LC 101) โ Pre-order mirror check
Day 4: Path Finding & Backtracking 10. Path Sum (LC 112) โ Root to leaf path 11. Path Sum II (LC 113) โ Collect all paths 12. Sum Root to Leaf Numbers (LC 129) โ Build number as you go
Day 5: BST Fundamentals 13. Search in a Binary Search Tree (LC 700) โ BST property 14. Insert into a Binary Search Tree (LC 701) โ Recursive insert 15. Lowest Common Ancestor of a BST (LC 235) โ BST-specific LCA
Day 6: Advanced BST & Validation 16. Validate Binary Search Tree (LC 98) โญ โ Range bounds 17. Kth Smallest Element in a BST (LC 230) โ In-order counting 18. Delete Node in a BST (LC 450) โ 3 cases (hard pointer work)
Day 7: Construction & Advanced 19. Construct Binary Tree from Preorder and Inorder (LC 105) โ Index splitting 20. Flatten Binary Tree to Linked List (LC 114) โ Pre-order flattening 21. Subtree of Another Tree (LC 572) โ Recursive structure check
11. Mock Interview Moduleโ
Problem: The Distributed Version Control Merge-Baseโ
Context: You are building the backend for a new internal Git-like version control system. Commits form a strictly branching Binary Tree (ignoring complex DAG merges). When a developer wants to merge two branches, the system must find the "merge-base" โ the most recent common ancestor commit.
Question: Write public CommitNode findMergeBase(CommitNode root, CommitNode branchA, CommitNode branchB) that returns the lowest common ancestor.
(Assume CommitNode = TreeNode. All node values are unique. Both branchA and branchB are guaranteed to exist.)
Step 1: Clarifying Questions & Expected Answersโ
| Candidate asks | Interviewer answers | Why it matters |
|---|---|---|
"Do nodes have a parent pointer?" | No, only left/right | Rules out bottom-up traversal |
| "Can a node be its own ancestor?" | Yes, if branchA is parent of branchB | Edge case handling |
| "Are both branches guaranteed to exist?" | Yes | No need for null checks on p/q |
| "Is the tree a BST?" | No, just a binary tree | Can't use BST optimization |
Step 2: Brute Force Solutionโ
// Time: O(N), Space: O(N) for path arrays
// Find path to branchA, find path to branchB, compare paths until they diverge
public CommitNode findMergeBase(CommitNode root, CommitNode a, CommitNode b) {
List<CommitNode> pathA = new ArrayList<>();
List<CommitNode> pathB = new ArrayList<>();
findPath(root, a, pathA);
findPath(root, b, pathB);
CommitNode lca = null;
for (int i = 0; i < Math.min(pathA.size(), pathB.size()); i++) {
if (pathA.get(i) == pathB.get(i)) lca = pathA.get(i);
else break;
}
return lca;
}
private boolean findPath(CommitNode node, CommitNode target, List<CommitNode> path) {
if (node == null) return false;
path.add(node);
if (node == target) return true;
if (findPath(node.left, target, path) || findPath(node.right, target, path)) {
return true;
}
path.remove(path.size() - 1); // backtrack
return false;
}
Explain: "This works but requires two full tree traversals and O(N) space for paths. Can we do it in one pass?"
Step 3: Optimized Solution (DFS Post-Order)โ
// Time: O(N), Space: O(H) call stack
public CommitNode findMergeBase(CommitNode root, CommitNode branchA, CommitNode branchB) {
// Base cases
if (root == null) return null;
if (root == branchA || root == branchB) return root; // found a target
// Recursively search left and right subtrees
CommitNode leftResult = findMergeBase(root.left, branchA, branchB);
CommitNode rightResult = findMergeBase(root.right, branchA, branchB);
// If both sides found a target, current node is the LCA
if (leftResult != null && rightResult != null) return root;
// Otherwise return whichever side found something
return leftResult != null ? leftResult : rightResult;
}
Walk through the logic:
Tree: 3
/ \
5 1
/ \ / \
6 2 0 8
/ \
7 4
Find LCA of 5 and 1:
Call stack (simplified):
lca(3, 5, 1):
leftResult = lca(5, 5, 1) โ returns 5 (found branchA)
rightResult = lca(1, 5, 1) โ returns 1 (found branchB)
Both non-null โ return 3 โ
Find LCA of 7 and 4:
lca(3, 7, 4):
leftResult = lca(5, 7, 4):
leftResult = lca(6, 7, 4) โ null
rightResult = lca(2, 7, 4):
leftResult = lca(7, 7, 4) โ returns 7
rightResult = lca(4, 7, 4) โ returns 4
Both non-null โ return 2 โ
return 2
return 2
rightResult = lca(1, 7, 4) โ null
return 2 โ
Time: O(N). Space: O(H) call stack.
Step 4: Follow-up Questionsโ
Q1: "What if this was a BST based on commit timestamps?"
Expected answer: "If it's a BST, I can optimize using the BST property:
- If both nodes are smaller than root, LCA is in left subtree.
- If both nodes are larger than root, LCA is in right subtree.
- Otherwise, current node is the LCA (split point).
This reduces average case from O(N) to O(\log N) in a balanced BST, and I can do it iteratively for O(1) space."
public CommitNode findMergeBaseBST(CommitNode root, CommitNode p, CommitNode q) {
CommitNode curr = root;
while (curr != null) {
if (p.val < curr.val && q.val < curr.val) {
curr = curr.left; // both in left subtree
} else if (p.val > curr.val && q.val > curr.val) {
curr = curr.right; // both in right subtree
} else {
return curr; // split point = LCA
}
}
return null;
}
Q2: "How would you handle a tree with millions of commits that doesn't fit in memory?"
Expected answer: "If the tree is on disk, I'd use a database index (B+ tree) on commit IDs. For LCA queries, I'd:
- Load only the path from root to branchA.
- Load only the path from root to branchB.
- Compare paths to find divergence point.
- Each path load is
O(\log N)disk I/O in a balanced tree.
Alternatively, store parent pointers in the database. Then:
- Traverse from branchA to root, mark all ancestors in a HashSet.
- Traverse from branchB to root, return first ancestor found in the set. This requires two index lookups but works for very deep trees."
12. Quick Reference: Tree Idioms in Javaโ
// โโ TREE NODE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
class TreeNode {
int val;
TreeNode left, right;
TreeNode(int val) { this.val = val; }
}
// โโ NULL SAFETY โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// ALWAYS null-check before accessing .left or .right
if (root == null) return /* base case */;
// โโ QUEUE SETUP FOR BFS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Queue<TreeNode> queue = new ArrayDeque<>(); // preferred over LinkedList
queue.offer(root); // add
TreeNode node = queue.poll(); // remove
// โโ LEVEL SIZE TRICK โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
while (!queue.isEmpty()) {
int levelSize = queue.size(); // lock in current level
for (int i = 0; i < levelSize; i++) {
TreeNode curr = queue.poll();
// process curr
if (curr.left != null) queue.offer(curr.left);
if (curr.right != null) queue.offer(curr.right);
}
}
// โโ LEAF NODE CHECK โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if (node.left == null && node.right == null) {
// node is a leaf
}
// โโ RECURSIVE TEMPLATE โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
int solve(TreeNode node) {
if (node == null) return /* base */;
int left = solve(node.left);
int right = solve(node.right);
return /* combine(left, right) */;
}
// โโ GLOBAL VARIABLE FOR TRACKING โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
class Solution {
private int maxDiameter = 0; // track across all recursion
public int diameterOfBinaryTree(TreeNode root) {
height(root);
return maxDiameter;
}
private int height(TreeNode node) {
// update maxDiameter in post-order
}
}
// โโ BST ITERATIVE SEARCH (NO RECURSION) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
TreeNode curr = root;
while (curr != null) {
if (target == curr.val) return curr;
else if (target < curr.val) curr = curr.left;
else curr = curr.right;
}
// โโ BUILDING A TREE FROM ARRAY (for testing) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Use level-order with null markers
TreeNode buildTree(Integer[] arr) {
if (arr == null || arr.length == 0) return null;
TreeNode root = new TreeNode(arr[0]);
Queue<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
int i = 1;
while (i < arr.length) {
TreeNode curr = queue.poll();
if (arr[i] != null) {
curr.left = new TreeNode(arr[i]);
queue.offer(curr.left);
}
i++;
if (i < arr.length && arr[i] != null) {
curr.right = new TreeNode(arr[i]);
queue.offer(curr.right);
}
i++;
}
return root;
}