Binary Trees & BST
Fourteen drills on the structure where recursion stops being a trick and becomes the only sane way to think. Every problem here is answered by deciding what each node needs from its children, what it needs from its ancestors, or which level it lives on โ post-order DFS, top-down DFS, or BFS. Each drill comes with a traced tree diagram, complete Java code and the classic pitfalls.
What a Binary Tree Really Is
A binary tree is a linked structure where each node has a value and two child references, left and right, either of which may be null. The top node is the root; nodes with no children are leaves. Every node is itself the root of a smaller binary tree โ its subtree โ and that self-similarity is why almost every tree algorithm is a recursive function that calls itself on node.left and node.right and combines the results.
A binary search tree (BST) adds an ordering invariant: every value in a node's left subtree is smaller than the node, every value in its right subtree is larger. That invariant makes search, insert and delete O(h), where h is the height โ O(log n) when balanced, O(n) when the tree degenerates into a chain. It also means an inorder traversal of a BST visits the values in sorted order, a fact several drills exploit.
| Term | Meaning | Why it matters in drills |
|---|---|---|
| Depth of a node | edges from the root to the node | BFS levels, zigzag direction |
| Height of a node | nodes (or edges) on the longest path down to a leaf | max depth, balanced check, diameter |
| h (tree height) | O(log n) balanced, O(n) skewed | recursion stack = O(h) extra space |
| Leaf | both children null | path-sum must stop only at leaves |
| Full / complete / perfect | shape constraints | affect width of BFS queue (up to n/2) |
Every Java program below also contains a small build(Integer... vals) helper that constructs a tree from LeetCode's level-order notation (null = missing child), so each main() reproduces the example exactly.
How to Think Recursively About Trees
left and right โ the recursive call is the induction step.node == nullqueue.size() at the start of each level.Four Templates for Every Tree Problem
Four templates solve all fourteen drills. The recursive ones are three to six lines each; what changes from problem to problem is only what you return and what you pass down.
- What does the function return for
null? (This is your base case.) - What does each node need from its children, and what does it pass down?
- Is the answer at the root the final answer, or do I need a global / field for "best seen anywhere"?
Inorder & Preorder Traversal
root of a binary tree, return the inorder traversal of its values (left subtree, node, right subtree). Follow-up: do it iteratively.left children (those are the calls that have not returned yet); when you can go no further left, pop โ that node's left subtree is finished, so visit it and switch to its right subtree.
Recursion is O(n) time and O(h) stack โ and it is the right answer unless the interviewer asks for iterative (to avoid stack overflow on skewed trees with 105 nodes). The explicit-stack version has the same bounds but uses heap memory. Morris traversal achieves O(1) extra space by temporarily threading right pointers back to ancestors โ worth naming as a follow-up, rarely required.
| Approach | Time | Space | Notes |
|---|---|---|---|
| Recursive | O(n) | O(h) stack | simplest |
| Iterative with stack | O(n) | O(h) | no recursion depth limit |
| Morris traversal | O(n) | O(1) | temporarily modifies the tree |
- Outer loop must be
while (curr != null || !stack.isEmpty())โ checking only the stack stops before exploring the root's right subtree. - Use
ArrayDeque, not the legacy synchronisedStackclass. - For a BST, inorder gives sorted order โ the basis of "kth smallest" and an alternative BST validation.
root of a binary tree, return the preorder traversal of its values (node, left subtree, right subtree).As with inorder, recursion is O(n)/O(h) and perfectly acceptable; the iterative form avoids recursion-depth limits. Preorder is the order used to serialise a tree (copy it, send it over the network) because the root comes first โ Drill 63 rebuilds a tree from exactly this sequence.
| Approach | Time | Space | Notes |
|---|---|---|---|
| Recursive | O(n) | O(h) stack | |
| Iterative with stack | O(n) | O(h) | right pushed before left |
- Pushing left before right yields node, right, left โ a mirrored preorder.
- Pushing
nullchildren and then dereferencing them โ check before pushing. - Reversing "node, right, left" gives postorder (left, right, node) โ a common trick for iterative postorder.
Maximum Depth & Same Tree
root of a binary tree, return its maximum depth: the number of nodes along the longest path from the root down to the farthest leaf.There is no meaningful brute force โ any correct solution must look at every node, so O(n) is optimal. The choice is DFS (O(h) stack, one line) versus BFS that counts levels (O(w) queue, where w can be n/2 for a complete tree). For a skewed tree of 105 nodes, BFS avoids StackOverflowError.
| Approach | Time | Space | Notes |
|---|---|---|---|
| Recursive DFS | O(n) | O(h) | one line |
| BFS level count | O(n) | O(w) | no recursion |
- Returning -1 for null switches the definition to "edges" โ the LeetCode answer counts nodes.
- Minimum depth (LC 111) is NOT
1 + min(L, R): a node with one null child is not a leaf, so take the non-null side.
p and q, return true if they are structurally identical and every corresponding node has the same value.Serialising both trees (with null markers) and comparing the strings works in O(n) but allocates O(n) memory and is error-prone without the markers ([1,2] and [1,null,2] serialise identically without them). Paired recursion stops at the first mismatch: O(min(n, m)) time, O(h) stack.
| Call | p node | q node | Result |
|---|---|---|---|
| isSame(1, 1) | 1 | 1 | values equal โ check children |
| isSame(2, 2) | 2 | 2 | equal; both children null โ true |
| isSame(3, 3) | 3 | 3 | equal; both children null โ true |
| back at root | true && true && true โ true |
| Approach | Time | Space | Notes |
|---|---|---|---|
| Serialise both, compare strings | O(n+m) | O(n+m) | needs null markers |
| Paired recursion | O(min(n,m)) | O(h) | short-circuits |
- Checking only values, not structure: forgetting the "exactly one null" case.
- Comparing traversal lists without null markers โ different shapes can give identical sequences.
- Using
p == q(reference equality) on non-null nodes: two separate trees never share nodes.
Symmetric Tree
root of a binary tree, check whether it is a mirror of itself (symmetric around its centre).One brute-force idea: build a mirrored copy of the tree, then run Same Tree against the original โ O(n) time but O(n) extra nodes. Another: level-order with null markers and check each level is a palindrome โ O(n) time, O(w) space. Mirror recursion compares the pairs directly: O(n) time, O(h) stack, no allocation. An iterative version pushes node pairs onto a queue in the same crossed order.
| Approach | Time | Space | Notes |
|---|---|---|---|
| Mirror copy + Same Tree | O(n) | O(n) | allocates a second tree |
| Level palindromes (BFS) | O(n) | O(w) | needs null markers |
| Mirror recursion | O(n) | O(h) | no allocation |
- Recursing on (left.left, right.left) โ that tests "same", not "mirror".
- Checking only that each level's VALUES read as a palindrome while ignoring nulls: [1,2,2,null,3,null,3] has levels [1],[2,2],[3,3] yet is not symmetric.
- Calling
isMirror(root, root)is correct but does every comparison twice;isMirror(root.left, root.right)halves the work.
Balanced Binary Tree
-1 meaning "unbalanced somewhere below" โ and the check becomes a single post-order pass.
The top-down version calls height() on both children at every node and then recurses: correct, but heights are recomputed for each ancestor, giving O(n log n) on balanced trees and O(n2) on skewed ones. The bottom-up version computes each height once and short-circuits on the first failure: O(n) time, O(h) stack.
| Approach | Time | Space | Notes |
|---|---|---|---|
| Top-down: height() at every node | O(n log n) โ O(nยฒ) | O(h) | recomputes heights |
| Bottom-up with -1 sentinel | O(n) | O(h) | each node once |
- Checking balance only at the root: [1,2,2,3,null,null,3,4,null,null,4] has equal root subtree heights but is unbalanced deeper down.
- Forgetting to propagate -1 from a child โ a later
Math.maxsilently swallows it. - "Balanced" here is the AVL definition (difference โค 1 at every node), not "complete" or "perfect".
Path Sum
root of a binary tree and an integer targetSum, return true if there is a root-to-leaf path whose values add up to targetSum.A brute force collects every root-to-leaf path into a list and sums each โ O(nยทh) time and memory for the path copies. Passing the remainder as a parameter makes each node O(1) work: O(n) time, O(h) stack, and the || stops as soon as one valid path is found.
| Approach | Time | Space | Notes |
|---|---|---|---|
| Enumerate all paths, sum each | O(nยทh) | O(nยทh) | copies paths |
| Top-down remainder | O(n) | O(h) | short-circuits on success |
- Checking
remaining == 0at a null child instead of at a leaf: for [1,2] and target 1 that wrongly returns true via the missing right child. - Pruning when
remaining < 0โ node values may be negative. - Empty tree with targetSum 0 must return false: there is no root-to-leaf path at all.
- Follow-ups: Path Sum II (return all paths โ backtracking with a list), Path Sum III (any downward path โ prefix sums + HashMap).
Diameter of Binary Tree
left + right as a candidate for the global maximum. The function returns height (what the parent can extend) but records diameter (what is complete here).
Calling height() separately at every node to compute left + right is O(n2) on skewed trees. Merging the two jobs into one post-order pass computes each height exactly once: O(n) time, O(h) stack.
| Approach | Time | Space | Notes |
|---|---|---|---|
| height() called at every node | O(nยฒ) worst | O(h) | recomputes |
| Single post-order pass | O(n) | O(h) | height returned, diameter recorded |
- Returning
height(root.left) + height(root.right)only at the root โ the longest path may bend lower down, entirely inside one subtree. - Mixing up nodes and edges: diameter counts edges, so with height counted in nodes, left + right is already the edge count.
- A static field must be reset before each call (as in the solution), or repeated calls leak the previous answer.
Level Order & Zigzag Traversal
root of a binary tree, return the level order traversal of its values: left to right, level by level, one list per level.size = queue.size() and poll that many.
DFS can also produce levels by passing depth and appending to result.get(depth) โ O(n) time, O(h) stack, and a handy alternative. BFS is the idiomatic answer: O(n) time, O(w) queue where w is the maximum width (up to n/2 in the last level of a complete tree).
| Approach | Time | Space | Notes |
|---|---|---|---|
| DFS with depth index | O(n) | O(h) | result.get(depth).add(val) |
| BFS + level-size snapshot | O(n) | O(w) | idiomatic |
- Using
queue.size()directly in the for-loop condition โ it changes as children are added, mixing levels. - Offering null children and then calling
node.valon them. - Using
Stack/pushinstead of a queue gives a DFS order, not BFS. - Same template solves Right Side View (last of each level), Average of Levels, and Minimum Depth (first leaf found).
LinkedList makes addFirst O(1).
The simplest correct approach is Drill 59 plus Collections.reverse(level) on odd levels โ still O(n) overall, since each value is reversed once. Writing into a deque avoids the extra pass. Trying to alternate the order in which children are enqueued is the classic wrong turn: it scrambles the next level.
| Approach | Time | Space | Notes |
|---|---|---|---|
| BFS + Collections.reverse on odd levels | O(n) | O(w) | fine, one extra pass per level |
| BFS + addFirst / addLast | O(n) | O(w) | no reversal |
| Two stacks | O(n) | O(w) | works, more bookkeeping |
- Swapping the order children are enqueued on alternate levels โ the level after next comes out in the wrong order.
- Using
ArrayList.add(0, x)for front insertion: O(level size) per insert, O(wยฒ) per level. - Off-by-one on direction: level 0 (the root) is left-to-right.
Lowest Common Ancestor
p and q that both exist in it, return their lowest common ancestor: the deepest node that has both p and q as descendants (a node counts as a descendant of itself).root == p handles the "one is the ancestor of the other" case for free.
The two-path approach records the root-to-p and root-to-q paths (DFS with backtracking) and returns the last common node โ O(n) time but O(n) for the paths and two traversals. A parent-pointer map plus an ancestor set also works in O(n) space. The single post-order pass is O(n) time, O(h) stack, and is the standard interview answer. For a BST (LC 235) you can do better: walk down from the root, O(h), using the ordering.
| Approach | Time | Space | Notes |
|---|---|---|---|
| Store both root-to-node paths | O(n) | O(n) | two traversals |
| Parent map + ancestor set | O(n) | O(n) | iterative |
| Single post-order DFS | O(n) | O(h) | standard answer |
- Comparing by value instead of reference when values may repeat.
- Continuing to search below p after finding it โ unnecessary, since if q is below p the answer is p anyway.
- This version assumes both nodes exist. If one might be missing, you must count how many were actually found before trusting the result.
Validate Binary Search Tree
root of a binary tree, determine if it is a valid BST: every node's left subtree contains only values strictly less than the node, its right subtree only values strictly greater, and both subtrees are BSTs themselves.Brute force: for every node, scan its entire left subtree for the max and right subtree for the min โ O(n2) on skewed trees. The window method is O(n) time, O(h) stack. Equivalent alternative: an inorder traversal must be strictly increasing โ keep the previous value and compare (also O(n), and it reuses Drill 51).
| Approach | Time | Space | Notes |
|---|---|---|---|
| Max/min of each subtree per node | O(nยฒ) worst | O(h) | |
| Top-down (low, high) window | O(n) | O(h) | long bounds |
| Inorder must be strictly increasing | O(n) | O(h) | track previous value |
- Only comparing a node with its direct children โ fails on [5,4,6,null,null,3,7].
- Using
intbounds initialised toInteger.MIN_VALUE/MAX_VALUE: a node holding exactly that value is then rejected. Uselongor nullableIntegerbounds. - Using
<=/>=loosely: duplicates are invalid in this problem, so the checks must be strict.
Construct Tree from Preorder & Inorder
preorder and inorder of the same tree (all values unique), construct and return the binary tree.The direct approach scans inorder for the root on every call and copies sub-arrays with Arrays.copyOfRange โ O(n2) time on skewed trees and O(n2) memory in copies. A HashMap from value to inorder index makes the lookup O(1), and passing index bounds instead of copies removes the allocation: O(n) time, O(n) space for the map plus O(h) stack.
| Approach | Time | Space | Notes |
|---|---|---|---|
| Linear search + array copies | O(nยฒ) | O(nยฒ) | copyOfRange each call |
| Index map + bounds + shared preIdx | O(n) | O(n) | map + O(h) stack |
- Building the right subtree before the left while consuming preorder sequentially โ the indices go out of sync.
- Forgetting to reset the shared
preIdxbetween calls when it is a static field. - The algorithm requires unique values; with duplicates the inorder split is ambiguous.
- Variant LC 106 (inorder + postorder): consume postorder from the END and build the RIGHT subtree first.
Flatten Binary Tree to Linked List
right pointer as next, set every left to null, and the order must match a preorder traversal. Follow-up: O(1) extra space.The easy way collects nodes in preorder into a list and relinks them โ O(n) time and O(n) space. The reverse-preorder recursion (right, left, then node, with a prev pointer) is elegant and O(n) time but O(h) stack. The Morris-style splice is O(n) time โ each "find rightmost" walk covers edges that are never walked again โ and O(1) space.
| curr | has left? | pred (rightmost of left) | Rewire | Chain so far |
|---|---|---|---|---|
| 1 | yes (2) | 4 | 4.right = 5; 1.right = 2; 1.left = null | 1 โ 2 โฆ |
| 2 | yes (3) | 3 | 3.right = 4; 2.right = 3; 2.left = null | 1 โ 2 โ 3 โ 4 โ 5 โ 6 |
| 3, 4, 5, 6 | no | โ | just move right | done |
| Approach | Time | Space | Notes |
|---|---|---|---|
| Preorder into a list, relink | O(n) | O(n) | |
| Reverse preorder recursion | O(n) | O(h) | right, left, node + prev |
| Morris-style splice | O(n) | O(1) | follow-up answer |
- Forgetting
curr.left = nullโ the result still has left children and fails the check. - Moving the left subtree to the right before saving the old right subtree โ it becomes unreachable.
- Recursive version: visiting left before right with a
prevpointer overwritesnode.rightbefore it is explored.
All Fourteen Drills at a Glance
Every drill on this page is O(n) time โ each node is visited a constant number of times. The space column is where solutions differ: O(h) for recursion (which is O(n) on a skewed tree), O(w) for BFS queues, and O(1) only when you are willing to temporarily rewire pointers (Morris). When an interviewer asks "can you do better on space?", that table is the answer.
| # | Drill | Pattern | Time | Space | Difficulty |
|---|---|---|---|---|---|
| 51 | Inorder Traversal | DFS + stack | O(n) | O(h) | Easy |
| 52 | Preorder Traversal | DFS + stack | O(n) | O(h) | Easy |
| 53 | Maximum Depth | post-order DFS | O(n) | O(h) | Easy |
| 54 | Same Tree | paired recursion | O(n) | O(h) | Easy |
| 55 | Symmetric Tree | mirror recursion | O(n) | O(h) | Easy |
| 56 | Balanced Binary Tree | post-order + sentinel | O(n) | O(h) | Easy |
| 57 | Path Sum | top-down DFS | O(n) | O(h) | Easy |
| 58 | Diameter of Binary Tree | post-order + global | O(n) | O(h) | Easy |
| 59 | Level Order Traversal | BFS | O(n) | O(w) | Medium |
| 60 | Zigzag Level Order | BFS + deque | O(n) | O(w) | Medium |
| 61 | Lowest Common Ancestor | post-order DFS | O(n) | O(h) | Medium |
| 62 | Validate BST | top-down bounds | O(n) | O(h) | Medium |
| 63 | Construct from Pre + In | divide & conquer + map | O(n) | O(n) | Medium |
| 64 | Flatten to Linked List | Morris splice | O(n) | O(1) | Medium |
- Start from the null base case, then decide whether information flows up (return values) or down (parameters).
- Post-order for height-like answers: depth (53), balanced (56), diameter (58), LCA (61).
- Top-down state for ancestor context: path sum remainder (57), BST windows (62).
- Paired recursion for comparing shapes: same tree (54), symmetric (55).
- BFS with a frozen level size for anything per-level (59, 60).
- Return one value, record another when the global answer differs from what the parent needs (58), or encode failure as a sentinel (56).
- Preorder is the serialisation order โ it rebuilds trees (63) and defines the flattened order (64).
- Extend the templates: Binary Tree Right Side View (BFS), Path Sum II / III (backtracking, prefix sums), Kth Smallest in a BST (inorder), Serialize and Deserialize Binary Tree (preorder + null markers), Binary Tree Maximum Path Sum (Drill 58 with values instead of edges).
- Theory refreshers: Binary Tree, Binary Search Tree, DFS and BFS.
- Full single-problem write-ups: LC 94, LC 104, LC 100, LC 101, LC 102, LC 236, LC 98, LC 105; more in the LeetCode trees and BST sets.
- Next page: 07 Grids & Matrices โ the same DFS/BFS templates, applied to a 2-D grid where each cell has four neighbours.