Trees
15 questions, the largest section in the 150. Almost every one is the same recursion with a different combine step — so the work is learning to see which of four shapes a problem is.
The one skeleton
ReturnType solve(TreeNode node) {
if (node == null) return base; // what an empty tree means
ReturnType L = solve(node.left);
ReturnType R = solve(node.right);
return combine(node, L, R);
}Every question here is a choice of base, combine, and ReturnType. Get comfortable stating the contract — "this call returns X for the subtree below it" — and then trusting it for the children.
The four shapes
1. Information flows UP. The parent combines what its children return. (Q1, Q2, Q3, Q4, Q5, Q6, Q12, Q14)
2. Information flows DOWN. A node needs context from its ancestors, passed as a parameter. (Q10, Q11, Q13)
3. Return one thing, record another. The answer at a node isn't extendable upward, so return what the parent can use and accumulate the answer separately. (Q3, Q14 — the two that feel hardest)
4. Breadth-first by level. Snapshot q.size() before enqueuing children. (Q8, Q9)
The three traversal orders
Only inorder has a special property: on a BST it emits values in sorted order. That single fact drives Q11 and Q12.
The BST property is worth using
A BST comparison at a node tells you which subtree a value is in, which turns searching into navigating:
| Problem | Without the BST property | With it |
|---|---|---|
| LCA (Q7) | O(n) search | O(h) walk, O(1) space |
| Kth smallest (Q12) | O(n log n) sort | O(h + k), or O(h) with sizes |
| Validate (Q11) | — | inorder must be strictly increasing |
And the validation trap: the property constrains a node by its whole ancestry, not its parent.
The traps
| Trap | Symptom |
|---|---|
| Comparing only parent and child (Q11) | [5,4,6,null,null,3,7] wrongly accepted |
int bounds in Validate BST (Q11) | A lone Integer.MIN_VALUE node rejected |
| Checking balance only at the root (Q4) | [1,2,2,3,3,null,null,4,4] wrongly accepted |
| Walking the right spine (Q9) | Misses a rightmost node that sits in a left subtree |
Strict > instead of >= (Q10) | Ties undercounted |
best = 0 instead of MIN_VALUE (Q14) | All-negative tree returns 0 |
Forgetting int size = q.size() (Q8) | All levels merge into one |
| Serializing without null markers (Q6, Q15) | [1,2] and [1,null,2] become identical |
| Serializing without value delimiters (Q6) | [12] wrongly reports containing [2] |
| Recursing right-before-left (Q13, Q15) | Q13 throws; Q15 fails silently |
Sentinels — and the discipline they need
Three questions overload a return value to carry two facts:
| Question | Sentinel | Safe because |
|---|---|---|
| Balanced (Q4) | −1 means unbalanced | Heights are never negative |
| Validate BST (Q11) | Long.MIN/MAX bounds | Node values are int, so they can't tie |
| Serialize (Q15) | # means null | Values are integers, never # |
In every case the sentinel works only because it lies outside the data's range. Check that explicitly — it's exactly where the trick silently breaks when reused.
Verification
Every Java snippet in this section was compiled and cross-checked against an independent implementation — 6,000+ randomized cases, including Q3 and Q4 against their own O(n²) brute force, Q7 against the general LCA algorithm, Q12 against the sorted inorder for every k, Q14 against an all-pairs path-sum brute force, and Q15 round-tripped in both formats.
Six deliberately-broken variants confirm the traps are real rather than theoretical:
- the parent-only BST check accepts
[5,4,6,null,null,3,7] intbounds reject a valid loneInteger.MIN_VALUEnode- the right-spine view returns
[1,3]instead of[1,3,5]— and was wrong on 397 of 500 random trees - strict
>in Count Good Nodes returns 1 instead of 2 on[3,3] - seeding Max Path Sum's best at 0 returns 0 instead of −3 on
[-3] - serializing without delimiters makes
"12##"contain"2##"— a false positive