🧩 Coding Interviews · ch.12 · trees ii: bsts & recursion patterns
🧩 Part III · Hierarchies · chapter 12 / 24

The tree that keeps
itself sorted

A binary search tree makes one promise — everything left is smaller, everything right is bigger — and that promise buys O(log n) search, a free sorted walk, and one-way answers to ancestor questions. This chapter cashes the bargain, then names the two shapes every tree recursion you'll ever write takes.

Chapter 11 gave you the traversals and promised that one throwaway fact — in-order on a BST comes out sorted — would solve three problems by itself. Time to collect. This chapter is half about the binary search tree's bargain, and half about something bigger: the two shapes that every tree recursion takes, which turn "write a recursive function" from improvisation into a fill-in-the-blanks form.

Canonical problems ahead: Validate Binary Search Tree Medium, Kth Smallest Element in a BST Medium, Lowest Common Ancestor of a BST Medium, Construct Binary Tree from Preorder and Inorder Traversal Medium, Subtree of Another Tree Easy, and a preview of Serialize and Deserialize Binary Tree Hard.

1One rule, three superpowers — the BST bargain

A binary search tree is a binary tree with one extra clause in its contract: at every node, everything in the left subtree is smaller, everything in the right subtree is bigger. Note the wording — everything in the subtree, not just the children. That distinction will earn a whole section, because it's where most wrong answers come from.

From that one clause, three superpowers fall out:

  • Search in O(h) — one comparison per level. Compare the target to the current node; the invariant tells you which half of the tree to throw away. It's Chapter 9's binary search where the array grew pointers: same "halve or die" energy, no indices.
  • Sorted output for free. An in-order walk (left, node, right) visits values in ascending order — the tree is a sorted list wearing branches. Chapter 11 planted this; Sections 3 and 4 harvest it.
  • One-way ancestor logic. Questions like "where does 26 belong?" or "what's the common ancestor of 26 and 32?" never require exploring both sides. The invariant points; you walk. Section 5 lives here.

Insert and delete are searches with a small edit at the end, so they're O(h) too. Everything in this chapter is either cashing one of these three checks or learning to write the recursion that does.

🎯
The tell: the problem says binary search tree, not just binary tree. That word is a contract, and the intended solution always cashes it. If your plan would work unchanged on any old tree, you're leaving the hint on the table — and probably a factor of n on the floor.

2Insert is a search that ends in an attach — and the shape you deserve

To insert a value, search for it. You won't find it (assume unique values — and ask your interviewer about duplicates; it's a free clarifying question from Chapter 2's script). The search falls off the tree at exactly one empty spot, and that spot is where the new node belongs. The number of comparisons is the depth of that spot — that's the whole cost story.

Here's the uncomfortable part: the tree's shape is decided by arrival order, not by you. Values arriving in friendly shuffled order build a bushy tree with height around log n. Values arriving already sorted build a tree where every node has one child — a linked list with delusions of grandeur, height n, every superpower revoked.

⚠️
The trap: BST does not mean balanced. Every O(log n) claim in this chapter is really O(h) — height — and h is only log n if the tree is bushy. Interviewers love the follow-up "what if the inserts arrive sorted?" The answer they want: h degrades to n, and real systems pay extra work per insert (AVL, red-black trees) to keep h logarithmic. You should know those exist and what they buy; nobody sane asks you to implement one in 45 minutes.

3The grandparent problem — validating with shrinking windows

Validate Binary Search Tree Medium hands you a tree and asks if the contract holds. The instinctive answer — check every node against its children, or every child against its parent — is the most famous wrong answer in the tree section of any question bank. It fails because the invariant is about ancestors, all of them: a node can be perfectly polite to its parent while betraying its grandparent.

Concretely: put 60 as the right child of 29, deep in the left subtree of 41. The parent check shrugs — 60 > 29, looks fine. But everything in 41's left subtree swore to stay below 41, and 60 didn't.

The fix is to make the promise explicit. Every node lives inside a window (lo, hi) assembled from its ancestors' values. The root's window is (−∞, +∞) — anything goes. Step left and the ceiling drops to the node's value; step right and the floor rises to it. A tree is a valid BST exactly when every node sits strictly inside its window. One pass, O(n) — every node checked once against a window it took O(1) to maintain.

Grow the tree below, then hit 💥 Corrupt: the widget plants exactly the grandparent-style lie above — a value that passes the parent-only check — and the window sweep walks the tree until the windows catch it.

Interactive · the BST grower Insert values, plant a lie, catch it with windows
seven in — insert more, or 💥 to plant a lie
nodes
0
height (levels)
0
windows checked
verdict
⚠️
The trap, again, because it's that common: comparing each node only to its parent. Say the magic sentence in the room — "every node must sit inside a window narrowed by all its ancestors" — and you've dodged the single most-harvested wrong answer in tree interviews. Bonus points for the edge case: if values can equal the node bounds (duplicates), ask which side duplicates go on before you pick < versus .

4The sorted walk pays out — kth smallest by countdown

Kth Smallest Element in a BST Medium sounds like it wants sorting, or a heap, or some clever order-statistics machinery. It wants none of that. The tree already is sorted — you just have to read it in the right order. Run an in-order walk with a counter initialized to k; decrement at every visit; when it hits zero, the node under your finger is the answer. Then — and this is the actual skill being tested — stop. Don't finish the traversal out of politeness.

That early exit makes the cost O(h + k) — walk down to the leftmost node, then k visits — instead of the O(n) of "collect everything into a list and index it". For k = 3 in a million-node tree, that's about twenty-three steps instead of a million. Same trick, mirrored, gives kth largest: run the walk right-node-left, so it comes out descending.

Drag k below and watch the countdown. The grey nodes are the point: they're the work you didn't do.

Interactive · the in-order countdown Kth smallest: decrement k at each visit, stop at zero
k = 4
in-order: —
k remaining
4
nodes visited
0
answer
work saved
🎯
The tell: "kth smallest", "kth largest", "in sorted order", "in-order successor" — inside a BST — means an in-order walk that stops early. No sorting, no heap; the tree did that work at insert time. (Same phrase over an unsorted array or a stream? That's Chapter 14's heap. The data structure in the problem statement is the referee.)

5Where the paths split — lowest common ancestor

Lowest Common Ancestor of a Binary Search Tree Medium: given two nodes p and q, find the deepest node that has both of them in its subtree. On a general tree this needs real recursion — search both sides, combine reports. On a BST it needs a hallway walk, because the invariant answers the only question that matters at each node: which way do we both go?

  • Both targets smaller than the current node → both live in the left subtree → so does their LCA. Step left.
  • Both bigger → step right.
  • They straddle the node — one smaller, one bigger — or one of them is the node → this is the last node the two paths share. You're standing on the answer.

No backtracking, no visited sets, not even recursion unless you feel like it — a while loop does fine. Cost: O(h) — one comparison per level, like everything the invariant touches — and O(1) space. The straddle case includes equality on purpose: if the walk lands on p, then p is an ancestor of q, and a node counts as its own ancestor in this problem (a clarification worth saying out loud).

Click any two nodes below, then watch the walk from the root find the split point.

Interactive · the LCA navigator Click two nodes; the walk from the root finds the split
p = 26 and q = 32 picked — find the split
p
26
q
32
steps from root
lca
🎯
The tell: "common ancestor" in a BST → compare both targets to the current node and walk one way. The first node where the targets split — or matches one of them — is the answer. If the tree isn't a BST, this door closes and you're writing the bottom-up version from the next section instead.

6Carry it down or send it up — the two recursion shapes

Step back from BSTs for a moment, because this section is the one you'll reuse in every tree problem for the rest of your career. Every tree recursion moves information in one of two directions, and identifying the direction before you type writes the function signature for you.

  • Top-down — info flows down as parameters. The node needs to know something only its ancestors know, so you carry it in the arguments. Validate BST is the poster child: the (lo, hi) window is ancestor knowledge, shrunk at every step. Same shape: path-so-far problems, depth-so-far, "count nodes greater than everything above them". The recursive call is go(child, updated_context); answers mostly ride along or accumulate.
  • Bottom-up — info flows up as return values. The node needs to know something only its subtrees know, so each call reports upward and the node combines the reports. Maximum Depth is the trivial case (1 + max(left, right)); Chapter 11's Diameter was this; balanced-tree checks return (height, ok) pairs; Subtree of Another Tree, next section, is this shape twice.

Some problems use both at once — kth smallest carries the countdown "down" while the traversal reports "found it" back up; a shared counter is top-down information in a trench coat. That's fine. The point isn't purity; it's that you decide the direction first, and then the base case, parameters, and return type stop being creative decisions.

💡
Before writing any tree function, answer two questions out loud: "What does this node need to know from above?" (those are your parameters) and "What must each subtree report back?" (that's your return type). Once both are answered, the recursion writes itself — base case on the empty tree, recurse on children, combine. This is also the on-ramp for later chapters: top-down with an undo step is Chapter 18's backtracking; bottom-up with a cache is Chapter 20's dynamic programming.

7Rebuilding from footprints — traversals as blueprints

Construct Binary Tree from Preorder and Inorder Traversal Medium looks like a puzzle and is actually a division of labor. Preorder tells you who the root is — it's literally the first element, that's what "root first" means. Inorder tells you where the root sits — find it in the inorder list, and everything to its left is the left subtree, everything to its right is the right subtree. Now you have two smaller who-and-where pairs. Recurse. It's a top-down shape: the parameters are the boundaries of the slice you're rebuilding.

The one performance beat interviewers listen for: don't scan the inorder list for the root at every level — that's O(n²) on a degenerate tree. Build a hash map from value to inorder index once (Chapter 4 says hello) and every lookup is O(1), making the whole rebuild O(n).

Two relatives round out the family:

  • Subtree of Another Tree Easy — pure bottom-up, twice: a same_tree(a, b) helper that compares two trees node-for-node, called at every node of the big tree. "Is the subtree rooted here identical to the target?" — if not, ask both children.
  • Serialize and Deserialize Binary Tree Hard, preview — flatten a tree to a string and back. The trick: a single preorder traversal pins down the whole tree if you keep the nulls. Write 41,20,11,#,#,29,… with # for empty, and deserializing is a recursion that eats tokens: take one, if it's # return empty, otherwise build the node and recurse for left then right.
💡
Why do you need two traversals to rebuild, but serialize needs only one? Because serialize keeps the nulls. Drop the nulls and one traversal is ambiguous — many trees share it — so you need a second traversal to disambiguate. Keep them and the shape is encoded in the string. That single sentence is a strong-hire answer to a common follow-up.

8Choosing the shape on a whiteboard — the skeleton

Here's the chapter folded into a decision you can make in the first ninety seconds. When a tree problem lands: first check whether the BST invariant can kill a subtree per step — search, LCA, insert, floor/ceiling, kth-with-early-exit all walk one path in O(h), and reaching for a full traversal there is leaving the discount on the table. If the invariant can't prune, you're visiting every node, and the only real decision left is the direction of information flow: down as parameters, or up as return values. Answer that, write the empty-tree base case, and the rest is transcription.

The pattern, as a whiteboard skeleton:

  1. 1Say the invariant precisely: everything left < node < everything right — whole subtrees, not just children. Ask about duplicates.
  2. 2Can the invariant discard a side per step? Search / LCA / insert / kth → walk one path, O(h), often just a while loop.
  3. 3Otherwise pick the recursion shape: ancestors know it → top-down (parameters); subtrees know it → bottom-up (return values).
  4. 4Base case first: the empty tree, returning the identity — True, 0, None — so leaves need no special casing.
  5. 5Top-down: shrink the context as you descend — windows narrow, k counts down, the path grows.
  6. 6Bottom-up: return a small tuple from each subtree and combine at the node; add fields to the tuple rather than re-traversing.
  7. 7State the cost out loud: full-tree shapes are O(n) time, O(h) stack; invariant walks are O(h) — and h = log n only if balanced.
  8. 8Test on the nasty four: empty tree, single node, sorted-insert chain, and values equal to a bound.

9Validate BST — the window travels down

The flagship, in both languages: Validate Binary Search Tree Medium, solved twice through two different doors. The first door is the chapter's top-down shape — carry the (lo, hi) window as parameters, shrink it at every turn, fail the moment a node steps outside. The second door is Section 4's fact wearing a different hat: a real BST's in-order walk is strictly increasing, so just take the walk and watch for a stumble.

One sharp edge worth narrating in the room: initial bounds. Python's float("-inf") is a clean sentinel; in typed languages, using the type's own MinValue/MaxValue as sentinels breaks when a node legitimately holds that value — which is why the Scala version carries Option[Int] bounds, where "no bound yet" is a first-class state instead of a magic number.

🎯
The tell: asked to verify a recursive property over a tree — valid BST, balanced, symmetric — and your check needs ancestor context? Top-down with shrinking parameters. Needs subtree summaries? Bottom-up with combined returns. Naming the shape before coding it is exactly the "names the pattern" dial from Chapter 1.