Trees, No Gaps — Traversal · Tree Recursion · Binary Search Trees Library
Show

PATTERN 3 — BINARY SEARCH TREES#

3.1 Pattern Breakdown#

A BST is a binary tree plus one promise: for every node, everything in the left subtree is smaller and everything in the right subtree is larger. The promise is worth exactly two things, and everything in this pattern is one of them.

  1. You may skip a subtree. Comparing once tells you an entire half is irrelevant — this is binary search with pointers instead of indices, and it is why search, insert, delete, range, and LCA are all O(h).
  2. Inorder is sorted. Any question about order statistics — k-th, successor, closest, minimum difference, mode — becomes a question about a sorted sequence you never have to materialise.

The failure mode that defines the pattern: the promise is about whole subtrees, not about parent–child pairs. A node can be larger than its parent and still violate the BST property. That single sentence is problem #72.

#Sub-variantWhat the ordering buysCost
ASearch on the orderingone comparison discards a subtreeO(h)
BValidation with an inherited range(low, high) narrows as you descendO(n)
CInorder is sortedorder statistics without sortingO(n), O(h) space
DSuccessor / predecessorthe descent remembers the last left turnO(h)
EInsert and deletestructural edit that preserves the promiseO(h)
FConstructiona sorted input already encodes the shapeO(n)
GRange queries and pruningwhole subtrees fall outside the rangeO(h + k)
HBST as an ordered containerTreeMap / TreeSet — floor, ceiling, headMapO(log n) per op

Sub-variants worth stating explicitly:

  • B and C are two different correct answers to the same question (validation), and knowing both is the point: the range version is top-down, the inorder version is bottom-up, and the inorder version generalises to "is this sequence sorted?" problems that the range version cannot express.
  • D is the one BST operation people cannot reconstruct under pressure, because the answer lives in a variable updated during the descent rather than at the node where you stop.
  • G — pruning — is where the ordering pays for itself twice: one comparison per node decides whether to recurse at all.
  • H is not really "trees" but it is where BSTs actually appear in production code, and it is the bridge to the ordered-multiset sliding windows in the companion document.

3.2 Problem Table#

A The ordering invariant and search#

Solved#ProblemDiffSub-variantWhy it's essential
69700. Search in a Binary Search TreeEasyAThe atom. Iterative in four lines, and it should be iterative — the recursion buys nothing here.
70235. Lowest Common Ancestor of a Binary Search TreeMediumAThe first place the ordering replaces an algorithm: no upward search, no post-processing. Descend while both targets are on the same side; the first node that splits them is the answer. Compare with #48 and notice how much machinery vanishes.
71270. Closest Binary Search Tree Value PROEasyATrack the best while descending. Free substitute: 2476. Closest Nodes Queries in a Binary Search Tree.

B Validation with an inherited range#

Solved#ProblemDiffSub-variantWhy it's essential
72⚠︎98. Validate Binary Search TreeMediumBThe single most instructive trap in trees. Comparing each node to its parent passes on trees that are not BSTs — the property constrains a node against every ancestor, not the nearest one. Pass (low, high) down as Long bounds, or validate by checking the inorder sequence is strictly increasing. Know both, and know why Integer.MIN_VALUE as a sentinel is a bug.
73255. Verify Preorder Sequence in Binary Search Tree PROMediumBThe same range logic against a stream instead of a tree. Free substitute: 1008, which builds the tree the same way.

C Inorder is sorted#

Solved#ProblemDiffSub-variantWhy it's essential
74230. Kth Smallest Element in a BSTMediumCCounting during inorder with an early exit. The follow-up — "the tree is modified often, optimise kthSmallest" — wants a subtree-size field on each node; have the answer ready.
75530. Minimum Absolute Difference in BSTEasyCThe minimum difference can only occur between inorder-adjacent nodes. Once you see that, the problem is a one-variable scan; without it, it looks like it needs every pair.
7699. Recover Binary Search TreeMediumC + §1.GTwo nodes swapped means one or two inversions in the inorder sequence; the first inversion's left element and the last inversion's right element are the culprits. The stated follow-up is O(1) space, which is exactly what Morris traversal is for.
77501. Find Mode in Binary Search TreeEasyCStreaming mode over a sorted sequence with O(1) extra space. Fiddly bookkeeping, no new idea.
78897. Increasing Order Search TreeEasyCInorder rewiring in place. Pleasant, rarely asked.

D Successor and predecessor#

Solved#ProblemDiffSub-variantWhy it's essential
79285. Inorder Successor in BST PROMediumDThe answer is not at the node you stop on — it is the last node from which you turned left. That is a one-variable descent, and almost nobody reconstructs it correctly the first time. Free substitute: 173. Binary Search Tree Iterator, which is the same idea with the state made explicit.
80510. Inorder Successor in BST II PROMediumDWith parent pointers and no root, the two cases split cleanly. Free substitute: reason it through on paper against #79.

E Insert and delete#

Solved#ProblemDiffSub-variantWhy it's essential
81701. Insert into a Binary Search TreeMediumEInsertion is always at a null leaf — no rebalancing, no case analysis. Return-the-subtree recursion makes rewiring automatic; learn that idiom here because #82 depends on it.
82450. Delete Node in a BSTMediumEThe only structurally hard BST operation. Zero children, one child, two children — and in the two-child case you replace the value with the inorder successor and then delete that node from the right subtree. Get this and E is closed.

F Construction#

Solved#ProblemDiffSub-variantWhy it's essential
83108. Convert Sorted Array to Binary Search TreeEasyFSorted array + "height-balanced" ⇒ the middle element is the root, by definition. Binary search written as a constructor.
841008. Construct Binary Search Tree from Preorder TraversalMediumFThe O(n) solution is the range trick from #72 running forward: consume values while they fit inside (low, high). This is the sub-variant B insight reused as a builder.
85109. Convert Sorted List to Binary Search TreeMediumFThe inorder-simulation solution (build left, then consume the head, then build right) is genuinely clever and worth one read.
8695. Unique Binary Search Trees IIMediumFCatalan recursion returning lists of trees. Fun, rarely asked.
8796. Unique Binary Search TreesMediumFPure DP; it is a counting problem wearing a tree costume.

G Range queries and pruning#

Solved#ProblemDiffSub-variantWhy it's essential
88938. Range Sum of BSTEasyGOne comparison per node decides whether to recurse at all. The whole value of the ordering, in eight lines.
89669. Trim a Binary Search TreeMediumGPruning that returns a replacement subtree. When a node is below low, its entire left subtree is too — so you return the trimmed right subtree, not null. That step is the trap.
90538. Convert BST to Greater TreeMediumG + CReverse inorder (right, node, left) with a running sum. Teaches that the inorder machine runs backwards for free, which is half of sub-variant D.

H BST as an ordered container#

Solved#ProblemDiffSub-variantWhy it's essential
91653. Two Sum IV — Input is a BSTEasyHTwo converging pointers over two BST iterators — the two-pointer pattern running on a tree. The naive hash-set answer is accepted; the iterator answer is the one that gets a follow-up nod.
92220. Contains Duplicate IIIHardHTreeSet.floor/ceiling inside a sliding window — the ordered-multiset window from the companion document. The clearest example of a BST used as a tool rather than a subject.
932476. Closest Nodes Queries in a Binary Search TreeMediumHFlatten to a sorted array, then lowerBound/upperBound per query. Explicitly ties §3.C to binary search on an array.

Extra Reps — BST (only if a gate fails)#

SolvedProblemTargets
1382. Balance a Binary Search TreeInorder to array, then problem 108 — composition of two solved things.
1305. All Elements in Two BSTsTwo iterators merged; sub-variant B of two pointers, on trees.
173. Binary Search Tree IteratorRe-rep if the paused-inorder state machine is not automatic.
333. Largest BST Subtree PROAugmented return carrying (min, max, size, isBst). Free substitute: 98 plus 104 composed.
426. Convert Binary Search Tree to Sorted Doubly Linked List PROInorder rewiring with a prev pointer. Free substitute: 897.
776. Split BST PRORecursive splitting that returns a pair of trees. Free substitute: 669.

3.3 Templates#

A Search and BST-LCA#

Binary search with pointers instead of indices. One comparison tells you an entire subtree is irrelevant, and that single fact is what makes search, insert, delete, range and LCA all O(h).

Mental model

“I compare once. Whatever I am looking for is either smaller than me or larger than me, so half the tree just stopped existing. Then I do it again.”

A BST is a binary tree plus one promise: for every node, everything in the left subtree is smaller and everything in the right subtree is larger. That promise is worth exactly two things — you may skip a subtree, and inorder is sorted — and every sub-variant here cashes in one of them.

LC 235 is worth contrasting with the general LCA of Pattern 2 G. On a BST there is no upward pass, no returned sentinels, and no proof about ancestors: you simply descend while both targets agree on the direction.

700 SEARCH compare once, discard a side, repeat O(h) iterative, because the recursion is pure tail-recursion and Java will not remove it 235 BST-LCA descend while BOTH targets go the SAME way both smaller -> go left both larger -> go right otherwise -> they split HERE, so this node is the answer compare with Pattern 2 G, where the same question needs a full postorder pass and a two-reading argument. The ordering removes all of it.

The first node that does not send both targets the same way is the split point, and therefore the answer. No recursion needed, no sentinel values, no proof obligation.

Recognition — reach for this when

  • The structure is a BST, and the question is where is x or where do these meet.
  • The expected cost is O(h) — the problem is testing whether you use the ordering.
  • A single comparison at each node can eliminate a whole side.
  • But not on a plain binary tree. Without the ordering promise, LCA needs Pattern 2's postorder argument.
Java700. Iterative, because the recursion is pure tail-recursion and Java will not remove it.6 lines
// 700. Iterative, because the recursion is pure tail-recursion and Java will not remove it.
TreeNode search(TreeNode root, int target) {
    TreeNode cur = root;
    while (cur != null && cur.val != target) cur = target < cur.val ? cur.left : cur.right;
    return cur;
}
Java235. BST LCA. Descend while both targets are strictly on the same side; the first node that12 lines
// 235. BST LCA. Descend while both targets are strictly on the same side; the first node that
// does NOT send them the same way is the split point — and therefore the answer.
// No upward pass, no returned sentinels, no proof about ancestors. Compare with §2.G.
TreeNode lcaBst(TreeNode root, TreeNode p, TreeNode q) {
    TreeNode cur = root;
    while (cur != null) {
        if (p.val < cur.val && q.val < cur.val)      cur = cur.left;
        else if (p.val > cur.val && q.val > cur.val) cur = cur.right;
        else return cur;
    }
    return null;
}
Why it works — what the ordering buys, and why LCA collapses4 steps

One promise, two consequences. This sub-variant is the first of them.

  1. 1

    The promise. For every node, the entire left subtree is smaller and the entire right subtree is larger. Note entire — it is a claim about subtrees, not about parent-child pairs.

  2. 2

    So one comparison discards half. If the target is smaller than the current node, it cannot be anywhere in the right subtree. That is a whole branch eliminated by a single test, which is exactly binary search.

  3. 3

    LCA needs no upward pass. If both targets are smaller than the current node, both live in the left subtree, so the meeting point is also down there. Same for larger. Descend while they agree.

  4. 4

    The first disagreement is the answer. When one target is smaller and the other larger — or one is the current node — the paths diverge here. This node is the lowest that contains both.

The thesis for every template in this pattern: every one of them is the same descent. Compare, discard a side, recurse or loop. If a BST solution of yours visits both children unconditionally, you have written a binary-tree solution and thrown the ordering away — which is sometimes right (see C) and usually wrong.

Write search iteratively. The recursion is pure tail recursion and Java will not eliminate it, so the loop is strictly better — O(1) space instead of O(h).

Using the general LCA on a BST is a wrong answer even when it is correct. It runs in O(n) and misses the entire point of the question, which is whether you noticed the ordering.

Walkthrough — LC 235 — LCA of 2 and 42 steps

Both targets are in the left subtree until they are not. Watch the descent stop at the first disagreement.

6 / \ 2 8 / \ 0 4 p = 2, q = 4 / \ 3 5
#At2 vs node4 vs nodeVerdict
16smallersmallerboth left -> descend left
22equallargerthey disagree -> node 2 is the answer

Answer 2 — reached in two comparisons on a seven-node tree. Node 2 is itself one of the targets and the other is beneath it, which on a plain binary tree needed the careful two-reading argument of Pattern 2 G. Here it falls out of the targets no longer agree on a direction, with no case analysis at all.

Key observations — what interviewers are listening for4 points
  • Name what the ordering bought, per problem. The gate asks exactly that. For search: half the tree per comparison. For LCA: the entire upward pass disappears.
  • Iterative search is the better answer. Tail recursion is not optimised in Java, so the loop saves O(h) stack for no cost in clarity.
  • The promise is about subtrees, not parents. Worth internalising here, because sub-variant B exists entirely because people forget it.
  • Reaching for Pattern 2's LCA on a BST is a tell. It signals you are treating the BST as a plain tree. Correct, O(n), and the wrong answer to the question being asked.
Common mistakes4 traps
  • Using the general LCA on a BST

    Symptom: correct but O(n), and it misses the point of the question.

    Prevention: If it is a BST, descend by comparison.

  • Writing search recursively

    Symptom: O(h) stack for a pure tail call Java will not eliminate.

    Prevention: Use a loop.

  • Visiting both children unconditionally

    Symptom: you have written a binary-tree solution and discarded the ordering.

    Prevention: One comparison must eliminate a side. If it does not, re-read the bounds.

  • Assuming parent-child ordering is the property

    Symptom: accepts trees that are not BSTs — see sub-variant B.

    Prevention: The promise covers the entire subtree, so it constrains every ancestor.

Key takeaway

  • Trigger: a BST, and the question is find or where do two paths meet.
  • The promise: whole left subtree smaller, whole right subtree larger.
  • Search: compare, discard a side, loop. O(h) time, O(1) space.
  • LCA: descend while both targets agree; the first disagreement is the answer.
  • Gate: iterative search and BST-LCA blind, stating what the ordering bought in each. See §5.3.

B Validation with an inherited range#

The BST property constrains a node against every ancestor, not just its parent. So carry the allowed range down with you, narrowing it at every step.

Mental model

“I arrive at this node already knowing the window it is allowed to occupy. Going left tightens the ceiling; going right tightens the floor. If the node is outside its window, the tree is not a BST.”

This is the failure mode that defines the pattern: a node can be larger than its parent and still violate the property, because it also has to respect a grandparent it never compares against.

There are two correct answers here, and knowing both is the point — the range version is top-down, the inorder version is bottom-up, and the inorder one generalises to is this sequence sorted? problems the range version cannot express.

THE TRAP -- valid against the parent, invalid against a grandparent: 5 / \ 1 4 4 < 5 ok, but 4 is in the RIGHT subtree of 5 / \ and 3 < 5 -- so this is NOT a BST 3 6 RANGE version (top-down): bounds narrow monotonically as you descend going LEFT tightens the HIGH bound going RIGHT tightens the LOW bound call: valid(root, Long.MIN_VALUE, Long.MAX_VALUE) INORDER version (bottom-up): inorder must be STRICTLY increasing no bounds arithmetic, and it generalises to "is this traversal sorted?"

Use long bounds, never Integer.MIN_VALUE sentinels — node values may legitimately be Integer.MIN_VALUE, and the sentinel then rejects a valid tree.

Recognition — reach for this when

  • Verify that a tree is a BST, or that some ordering constraint holds throughout.
  • A property that must hold against all ancestors, not just the immediate parent.
  • Or: a question phrased as is this traversal sorted?, which suits the inorder form.
  • But not if you only compare each node to its parent. That is the bug this sub-variant exists to prevent.
Java98. The BST property constrains a node against EVERY ancestor, not just its parent.10 lines
// 98. The BST property constrains a node against EVERY ancestor, not just its parent.
// The bounds narrow monotonically as you descend: going left tightens the high bound,
// going right tightens the low bound.
// Long bounds, NOT Integer.MIN_VALUE sentinels — node values may be Integer.MIN_VALUE.
boolean valid(TreeNode n, long low, long high) {
    if (n == null) return true;
    if (n.val <= low || n.val >= high) return false;
    return valid(n.left, low, n.val) && valid(n.right, n.val, high);
}
// call: valid(root, Long.MIN_VALUE, Long.MAX_VALUE)
JavaThe other correct answer: inorder must be STRICTLY increasing. Bottom-up instead of11 lines
// The other correct answer: inorder must be STRICTLY increasing. Bottom-up instead of
// top-down, no bounds arithmetic, and it generalises to "is this traversal sorted?".
TreeNode prev = null;

boolean validInorder(TreeNode n) {
    if (n == null) return true;
    if (!validInorder(n.left)) return false;
    if (prev != null && prev.val >= n.val) return false;
    prev = n;
    return validInorder(n.right);
}
Why it works — why parent comparison is not enough, and two ways to fix it4 steps

One counterexample, then the two standard repairs — which are worth knowing as a pair.

  1. 1

    The property is about subtrees. Everything in the right subtree of 5 must exceed 5 — not just 5's immediate right child. A node three levels down is still bound by that constraint.

  2. 2

    So parent comparison accepts invalid trees. In the diagram, 3 is a valid left child of 4, and 4 is a valid right child of 5 — yet 3 sits in 5's right subtree while being smaller than 5. Every local check passes.

  3. 3

    The range fix, top-down. Carry (low, high). Descending left replaces high with the node's value; descending right replaces low. The bounds narrow monotonically, so every ancestor's constraint is still enforced at the bottom.

  4. 4

    The inorder fix, bottom-up. A tree is a BST exactly when its inorder traversal is strictly increasing. Keep one prev node and compare. No arithmetic, and it extends to any is this sequence sorted question.

The sentence that defines the pattern's failure mode: the promise is about whole subtrees, not about parent-child pairs. A node can be larger than its parent and still violate the BST property.

Integer.MIN_VALUE sentinels are a bug, not a shortcut. A node may legitimately hold that value, and the comparison then fails on a perfectly valid tree. Use long bounds, or the nullable prev node of the inorder version.

State the duplicate policy before coding. LeetCode's BSTs are strict, so <= where < belongs silently accepts duplicates. It is a one-character difference and a real correctness decision.

Walkthrough — LC 98 — the tree that passes every local check4 steps

The counterexample from above. Every parent-child pair is fine; the inherited range catches it anyway.

5 / \ 1 4 / \ 3 6
#NodeInherited (low, high)Parent checkRange check
15(-inf, +inf)--ok
21(-inf, 5)1 < 5, okok
34(5, +inf)4 > ... parent is 5, so 4 < 5already fails: 4 > 5 is false
43(5, 4)3 < 4, looks finefails: 3 > 5 is false

Not a BST. Row 4 is the whole lesson: node 3 is a perfectly legal left child of 4, and a parent-only check waves it through. The inherited low = 5 — set three levels up, when the descent turned right at the root — is what catches it. Note also that row 3 already fails; the example is drawn so that both the immediate and the distant violation are visible.

Key observations — what interviewers are listening for4 points
  • Know both solutions, deliberately. The gate asks for LC 98 both ways. They are not redundant: the range form is top-down arithmetic, the inorder form is a bottom-up scan that generalises further.
  • The sentinel bug is worth naming. Node values may be Integer.MIN_VALUE is a concrete reason, not a stylistic preference. The gate asks you to explain it.
  • Bounds narrow monotonically. That monotonicity is what makes the top-down version correct — each step can only tighten, never loosen, so ancestor constraints survive to the leaves.
  • The inorder form has a wider reach. Anything phrased as is this traversal sorted becomes the same three lines with a different predicate.
Common mistakes4 traps
  • Validating against the parent only

    Symptom: accepts trees that are not BSTs.

    Prevention: The property is about ancestors, not parents. Inherit (low, high).

  • Integer.MIN_VALUE / MAX_VALUE as sentinels

    Symptom: fails on trees containing those exact values.

    Prevention: Use long bounds, or a nullable prev node in the inorder version.

  • <= instead of <

    Symptom: accepts duplicates.

    Prevention: LeetCode's BSTs are strict. State the duplicate policy before coding.

  • Comparing prev by value when it may be unset

    Symptom: a null dereference on the leftmost node.

    Prevention: prev starts null and the first comparison is skipped — that is what makes the nullable form work.

Key takeaway

  • Trigger: verify the BST property, or any all-ancestors ordering constraint.
  • The trap: parent comparison passes on trees that are not BSTs.
  • Range form: inherit (low, high); left tightens high, right tightens low.
  • Inorder form: the traversal must be strictly increasing; keep one prev.
  • Gate: LC 98 both ways, plus why Integer.MIN_VALUE sentinels are a bug. The foundation gate. See §5.3.

C Inorder is sorted#

The second thing the ordering buys: inorder is a sorted sequence. Every order-statistic question — k-th, closest, minimum gap, mode — becomes a scan over a sorted array you never have to build.

Mental model

“I am not walking a tree. I am walking a sorted list that happens to be stored as a tree, and one variable holding the previous element turns it into an ordinary scan.”

This is the one sub-variant where visiting both children unconditionally is correct — the whole point is to produce the full sorted order. Everywhere else in this pattern, that would mean you discarded the ordering.

One variable does most of the work: prev, the previously visited node. It converts inorder into a pairwise scan, which is what makes LC 530 and LC 99 short.

inorder over a BST == the sorted sequence, without materialising it 230 k-th smallest: count during inorder, STOP the moment you reach k without the early exit you have "sort the tree and index it", which is the wrong complexity story 530 minimum difference: the minimum gap is between INORDER-ADJACENT nodes only so one `prev` variable suffices -- never compare all pairs 99 recover a swapped BST -- look for INVERSIONS: first = the LEFT element of the FIRST inversion second = the RIGHT element of the LAST inversion (they coincide when the swapped nodes are adjacent)

Two inversions when the swapped nodes are far apart, one when they are adjacent. Tracking only the first inversion is the classic wrong answer.

Recognition — reach for this when

  • An order statistic — k-th smallest, closest value, minimum difference, mode.
  • A question about adjacency in sorted order.
  • Detecting that the sorted order has been disturbed, as in LC 99.
  • But not when a comparison could prune. If one side can be eliminated, use A or G and stay O(h).
Java230. Count during inorder, stop the moment you have k. The early exit is the point:10 lines
// 230. Count during inorder, stop the moment you have k. The early exit is the point:
// without it you have "sort the tree and index it", which is the wrong complexity story.
int count, answer;

void kth(TreeNode n, int k) {
    if (n == null || count >= k) return;
    kth(n.left, k);
    if (++count == k) { answer = n.val; return; }
    kth(n.right, k);
}
Java530 / 99. One variable — the previously visited node — turns inorder into a scan over a16 lines
// 530 / 99. One variable — the previously visited node — turns inorder into a scan over a
// sorted sequence. 530 minimises the gap; 99 looks for inversions:
//    first  = the LEFT element of the FIRST inversion
//    second = the RIGHT element of the LAST inversion   (they coincide when the swap is adjacent)
TreeNode prevNode = null, first = null, second = null;

void scan(TreeNode n) {
    if (n == null) return;
    scan(n.left);
    if (prevNode != null && prevNode.val > n.val) {
        if (first == null) first = prevNode;
        second = n;
    }
    prevNode = n;
    scan(n.right);
}
Why it works — what one prev variable buys4 steps

Three problems, one mechanism, and one complexity argument that has to be made explicitly.

  1. 1

    Inorder over a BST is sorted. Left subtree, node, right subtree — and everything left is smaller, everything right larger. The sequence comes out ascending by construction.

  2. 2

    So adjacency in the traversal is adjacency in sorted order. The minimum difference between any two values is therefore between inorder-adjacent nodes only. Comparing all pairs is O(n^2) for information a single scan already has.

  3. 3

    One prev node makes it a scan. Hold the previously visited node and compare on arrival. That is the whole machinery for LC 530 and LC 99.

  4. 4

    LC 99 needs two inversions, not one. If the swapped nodes are adjacent there is one inversion; if they are far apart there are two. Take first from the first inversion's left element and second from the last inversion's right element, and both cases are covered.

The sentence the gate asks for unprompted: the minimum difference is between inorder-adjacent nodes. Once that is said, LC 530 is a three-line scan and comparing all pairs stops being tempting.

LC 230's early exit is the point, not an optimisation. Stop as soon as the counter reaches k. Without it you have sort the tree and index it, which answers the question with the wrong complexity story — and the natural follow-up is the subtree-size augmentation, which makes repeated queries O(h).

Reverse inorder is a descending scan for free. Right, node, left — used by LC 538 to accumulate a running suffix sum without any extra structure.

Walkthrough — LC 99 — two inversions, far apart4 steps

The inorder sequence should be 1 2 3 4. Nodes 3 and 1 have been swapped, so the traversal reads 3 2 1 4 — two separate inversions.

inorder reads: 3 2 1 4 ^^^^^ first inversion (3 > 2) ^^^^^ last inversion (2 > 1)
#prevcurrentInversion?Record
1--3no (first node)--
232yes, 3 > 2first = 3 (the left element)
321yes, 2 > 1second = 1 (the right element, from the LAST inversion)
414no--

Swap 3 and 1 and the tree is repaired. Row 3 is why one inversion is not enough: had you stopped after the first, you would swap 3 and 2 and produce 2 3 1 4, which is still broken. When the swapped nodes are adjacent there is only one inversion, and first and second both come from it — which is why the same rule covers both cases.

Key observations — what interviewers are listening for4 points
  • This is the sub-variant where visiting both children is right. Everywhere else in this pattern that means you threw the ordering away. Here it is the point, and knowing the difference is what §3.1 is warning about.
  • prev is the whole technique. One nullable node variable turns a tree walk into a sorted-sequence scan. Recognising that collapses three problems into one idea.
  • Say the adjacency claim before writing LC 530. The minimum gap is between inorder-adjacent nodes. The gate asks for it unprompted, and it is what rules out the O(n^2) approach.
  • The early exit changes the complexity story. Without it, LC 230 is a full traversal. With it — and with the subtree-size follow-up — it is a genuinely different answer.
Common mistakes4 traps
  • Comparing all pairs for the minimum difference

    Symptom: O(n^2) for information a single inorder scan already contains.

    Prevention: In a BST the minimum gap is between inorder-adjacent nodes only.

  • kthSmallest without an early exit

    Symptom: a full O(n) traversal, and a bad follow-up conversation.

    Prevention: Stop as soon as the counter reaches k; mention the subtree-size augmentation.

  • Recovering a swapped BST by tracking one inversion

    Symptom: wrong whenever the swapped nodes are not adjacent.

    Prevention: first from the first inversion, second from the last.

  • Materialising the inorder list

    Symptom: O(n) extra space for a scan that needs one variable.

    Prevention: Hold prev and compare on arrival.

Key takeaway

  • Trigger: an order statistic — k-th, closest, minimum gap, mode.
  • The fact: inorder over a BST is the sorted sequence.
  • The tool: one prev node turns the walk into a pairwise scan.
  • LC 99: first from the first inversion, second from the last.
  • Gate: state the inorder-adjacency claim unprompted, and describe LC 99's two-inversion rule from memory. See §5.3.

D Successor and predecessor#

The successor is not at the node you stop on. It is the last node you turned left from — and that is why people cannot reconstruct this one under pressure.

Mental model

“As I descend, every time I go left I am passing a node that is bigger than my target. The deepest such node is the closest thing above the target, so I remember it. If the target turns out to have a right subtree, that subtree's minimum beats it.”

This is the one BST operation people cannot rebuild from memory, and the reason is structural: the answer lives in a variable updated during the descent, not at the node where the search stops.

Two cases, and the second is the one that catches people. If the node has a right subtree, the successor is that subtree's minimum. If it does not, the answer is the remembered left-turn.

descending towards the target: go RIGHT -> this node is SMALLER than the target, useless as a successor go LEFT -> this node is LARGER than the target -> REMEMBER IT the answer is the LAST node you turned left from -- the deepest ancestor still greater than the target BUT if the target has a RIGHT subtree, the successor is that subtree's MINIMUM instead (it is closer) the answer is in a VARIABLE, not at the node you stopped on

Reading the answer off the stopping node works only when that node happens to have a right child. Every other case needs the remembered turn.

Recognition — reach for this when

  • Next-larger or next-smaller in sorted order, given a node or a value.
  • Iterator-style traversal of a BST, where next() is exactly successor.
  • Closest value questions, which are a successor and predecessor comparison.
  • But not if you have the whole inorder sequence already. Then adjacency is direct and this machinery is unnecessary.
Java285. The successor is NOT at the node you stop on. It is the last node from which you11 lines
// 285. The successor is NOT at the node you stop on. It is the last node from which you
// turned LEFT — the deepest ancestor that is still greater than the target.
// If the node has a right subtree, the answer is that subtree's minimum instead.
TreeNode successor(TreeNode root, TreeNode p) {
    TreeNode best = null, cur = root;
    while (cur != null) {
        if (p.val < cur.val) { best = cur; cur = cur.left; }   // remember the left turn
        else                 { cur = cur.right; }              // equal or greater: go right
    }
    return best;
}
Why it works — why the answer is a remembered turn4 steps

Two cases. The first is easy and the second is the reason this sub-variant exists.

  1. 1

    Turning right tells you nothing useful. Going right means the current node is smaller than the target. A smaller node can never be the successor, so it is discarded.

  2. 2

    Turning left records a candidate. Going left means the current node is larger than the target. It is therefore an upper bound — and each subsequent left turn is a tighter one, so the last is the best.

  3. 3

    Case 1: the target has a right subtree. Then the successor is inside it — specifically its minimum, the leftmost node. That is closer to the target than any ancestor could be.

  4. 4

    Case 2: no right subtree. Nothing below the target is larger, so the answer must be above it — and it is exactly the deepest ancestor from which the descent turned left.

The sentence to have ready: the successor is the last node from which you turned left — the deepest ancestor still greater than the target. It is not the node you stopped on.

Predecessor is the exact mirror. Remember right turns instead, and if the target has a left subtree take that subtree's maximum.

LC 173's iterator is this operation repeated. A stack of not-yet-returned ancestors is the same remembered left turns idea, made persistent between calls — see Pattern 1 B.

Walkthrough — successor of 5, which has no right child4 steps

The target is a leaf, so the answer cannot be below it. Watch the remembered column.

8 / \ 4 12 / \ 2 6 / \ 5 7 target = 5
#At5 vs nodeTurnRemembered
185 < 8left8
245 > 4right8
365 < 6left6 <- tighter bound
45found, no right child--6

Successor is 6. The descent stopped at node 5, and 5 has no right child — so the answer had to come from the remembered variable. Notice how row 3 tightened the bound from 8 to 6: each left turn is a better upper bound than the last. Read the answer off the stopping node instead and you get nothing at all.

Key observations — what interviewers are listening for4 points
  • The answer is in a variable, and say so. The gate asks you to explain why it is the last left-turn rather than the stopping node. That framing is the whole sub-variant.
  • Hand-trace a target with no right child. The gate names this case specifically, because it is the one where the naive reading fails.
  • Each left turn tightens the bound. Which is why the last one is the answer and not the first. Worth stating — it explains why a single variable suffices.
  • Predecessor is not a separate skill. Mirror the turns and the subtree. Deriving it from successor rather than memorising it separately is the sign the idea has landed.
Common mistakes4 traps
  • Reading the successor off the stopping node

    Symptom: wrong answer whenever the node has no right child.

    Prevention: The answer is the last node you turned left from; track it in a variable.

  • Forgetting the right-subtree case

    Symptom: you return a distant ancestor when a much closer node exists below.

    Prevention: If the target has a right subtree, the successor is that subtree's minimum.

  • Remembering the first left turn instead of the last

    Symptom: an upper bound, but not the tightest one.

    Prevention: Overwrite on every left turn. The deepest one wins.

  • Materialising the inorder sequence to find it

    Symptom: O(n) time and space for an O(h) operation.

    Prevention: The descent already carries the information.

Key takeaway

  • Trigger: next-larger or next-smaller in sorted order; iterator next().
  • The rule: the answer is the last left turn, held in a variable.
  • Exception: if the target has a right subtree, take that subtree's minimum.
  • Predecessor: the exact mirror — remember right turns, take the left subtree's maximum.
  • Gate: successor blind, plus why the answer is the last left-turn rather than the stopping node. See §5.3.

E Insert and delete#

Structural edits that preserve the promise. Insertion is always at a null leaf; deletion has three cases, and only the third is hard.

Mental model

“I descend to where the node belongs, then hand the rewired subtree back to my caller. The parent reattaches whatever I return, so I never need a parent pointer and never special-case the root.”

The return the subtree idiom is what makes both operations short. Each call returns the new root of the subtree it was given, and the parent simply assigns it — so rewiring happens automatically at every level.

Deletion's two-child case is the one worth understanding rather than memorising, because the obvious pointer-splicing approach is wrong in a way that only shows up later.

701 INSERT always lands at a NULL leaf -- descend by comparison, attach 450 DELETE three cases, and the third is the only hard one: 0 children -> return null 1 child -> return that child 2 children -> copy the inorder SUCCESSOR's value into this node, then RECURSIVELY DELETE that successor from the right subtree the "return the subtree" idiom makes the PARENT rewire itself node.left = delete(node.left, key) node.right = delete(node.right, key) -> no parent pointer, no special case for the root

Recursing into the right subtree to remove the successor keeps every invariant intact. Splicing pointers by hand does not — it leaves a corrupt tree that fails on the next operation.

Recognition — reach for this when

  • Structural modification of a BST — insert, delete, or both.
  • The tree must remain a valid BST afterwards.
  • You want O(h) without maintaining parent pointers.
  • But not when the tree must stay balanced. That is an AVL or red-black rotation, a different subject.
Java701. Insertion is always at a null leaf. The "return the subtree" idiom makes the parent8 lines
// 701. Insertion is always at a null leaf. The "return the subtree" idiom makes the parent
// rewire itself, which is why no parent pointer and no special first-node case is needed.
TreeNode insert(TreeNode n, int v) {
    if (n == null) return new TreeNode(v);
    if (v < n.val) n.left  = insert(n.left, v);
    else           n.right = insert(n.right, v);
    return n;
}
Java450. Three cases, and the third is the only hard one:20 lines
// 450. Three cases, and the third is the only hard one:
//   0 children -> return null
//   1 child    -> return that child
//   2 children -> copy the inorder successor's value here, then DELETE THAT SUCCESSOR
//                 from the right subtree. Recursing into the right subtree keeps the
//                 invariant; splicing pointers by hand does not.
TreeNode delete(TreeNode n, int key) {
    if (n == null) return null;
    if (key < n.val)      n.left  = delete(n.left, key);
    else if (key > n.val) n.right = delete(n.right, key);
    else {
        if (n.left == null)  return n.right;
        if (n.right == null) return n.left;
        TreeNode s = n.right;
        while (s.left != null) s = s.left;      // inorder successor = leftmost of the right subtree
        n.val = s.val;
        n.right = delete(n.right, s.val);
    }
    return n;
}
Why it works — the three delete cases, and why the third recurses4 steps

Two easy cases and one that people get wrong in a specific, delayed-failure way.

  1. 1

    Insertion is always at a leaf. Descend by comparison until you fall off the tree. The null position you reach is exactly where the value belongs, so there is no restructuring to do.

  2. 2

    Delete, no children. Return null. The parent's assignment does the detaching for you.

  3. 3

    Delete, one child. Return that child. Everything in it already satisfies the ordering relative to the parent, because it satisfied it relative to the node being removed.

  4. 4

    Delete, two children. You cannot simply return one child — the other would be orphaned. So copy the inorder successor's value into this node (it is the smallest value larger than everything on the left, so the ordering still holds), then delete that successor from the right subtree recursively. The successor has at most one child, so that deletion is one of the easy cases.

Why the two-child case recurses instead of splicing: copy the successor's value, then recursively delete the successor. Recursing keeps the invariant at every level; splicing pointers by hand produces a corrupt tree that fails on the next operation rather than this one.

The successor always has at most one child. It is the leftmost node of the right subtree, so it has no left child by definition — which is why the recursive delete terminates in one of the easy cases and cannot recurse indefinitely.

The predecessor works equally well. Copying the largest value from the left subtree is symmetric and equally correct; pick one and be consistent.

Walkthrough — LC 450 — deleting a node with two children5 steps

Delete 5, which has both children. Watch the value get copied down and the successor removed from below.

5 / \ 3 6 / \ \ 2 4 7 delete 5
#StepDetail
1locate 5the root; it has two children, so case three applies
2find the inorder successorleftmost node of the right subtree -> 6
3copy the valuethe root becomes 6; the tree is momentarily invalid (two 6s)
4recursively delete 6from the right subtree — and that 6 has one child (7), so it is the easy case
5result6 at the root, right subtree is now just 7; ordering intact throughout

Final tree: root 6, left subtree 3(2,4), right child 7. Step 4 is the reason for recursion — the successor had a right child, and splicing pointers by hand would have dropped it. Because the successor is the leftmost node of the right subtree, it can never have a left child, so the recursive call always lands in the zero- or one-child case.

Key observations — what interviewers are listening for4 points
  • The return-the-subtree idiom is the reusable part. node.left = delete(node.left, key) makes the parent rewire itself. It removes parent pointers, root special-cases and most of the bug surface in one move.
  • Justify the recursive delete, do not just perform it. The gate asks specifically for that justification rather than a pointer splice.
  • Say why the successor is easy to remove. It is the leftmost node of the right subtree, so it has no left child. That is what guarantees termination.
  • Corruption here is delayed. A bad splice produces a tree that looks fine and fails on the next operation, which is what makes it worth getting right the first time.
Common mistakes4 traps
  • Deleting a two-child node by splicing pointers

    Symptom: corrupt tree on the next operation, not this one.

    Prevention: Copy the successor's value, then recursively delete the successor.

  • Taking the successor from the wrong side

    Symptom: the ordering breaks — you need the smallest value larger than the left subtree.

    Prevention: Inorder successor = leftmost node of the right subtree.

  • Maintaining parent pointers to rewire

    Symptom: extra state and a root special-case for no benefit.

    Prevention: Return the new subtree root and let the parent assign it.

  • Assuming insertion may need restructuring

    Symptom: unnecessary complexity.

    Prevention: Insertion is always at a null leaf. Descend, attach, done.

Key takeaway

  • Trigger: structurally modify a BST while preserving the ordering.
  • Insert: descend by comparison; the null position you reach is the right place.
  • Delete: 0 children -> null; 1 child -> that child; 2 children -> copy successor, recurse.
  • The idiom: return the new subtree root; the parent reattaches it.
  • Gate: LC 450 blind with all three cases, justifying the recursive delete over a pointer splice. See §5.3.

F Construction#

A sorted input already encodes the shape. For a balanced tree the middle element is the root — this is binary search with the recursion tree materialised as the answer.

Mental model

“I do not need to decide anything. The sorted order tells me which element belongs at the root, and the two halves are the same problem again.”

LC 108 is the clean case: sorted plus height-balanced means the middle element must be the root, and each half recurses identically.

LC 1008 is the interesting one, because it reuses sub-variant B's range trick — running forward over a preorder stream instead of validating an existing tree.

108 sorted array -> height-balanced BST the MIDDLE element is the root; recurse on each half this is binary search with the recursion tree kept as the answer O(n), and balanced by construction 1008 preorder -> BST, in O(n) the RANGE TRICK from sub-variant B, running FORWARD: consume values while they fit inside (low, high); return as soon as one does not inserting a sorted array one element at a time instead -> degenerates to a linked list, O(n^2)

Building by repeated insertion from sorted input is the classic wrong answer — every insert goes right, so the tree becomes a list and construction costs O(n^2).

Recognition — reach for this when

  • Build a BST from sorted input, or from a traversal of one.
  • The problem asks for height-balanced, which pins the root to the middle.
  • You are given a preorder stream and want O(n) rather than repeated insertion.
  • But not from unsorted input — there is no shape information to exploit, so you are back to inserting one at a time.
Java108. Sorted + height-balanced ⇒ the middle element is the root. This is binary search10 lines
// 108. Sorted + height-balanced ⇒ the middle element is the root. This is binary search
// with the recursion tree materialised as the answer.
TreeNode sortedToBst(int[] a, int lo, int hi) {
    if (lo > hi) return null;
    int mid = lo + (hi - lo) / 2;
    TreeNode n = new TreeNode(a[mid]);
    n.left  = sortedToBst(a, lo, mid - 1);
    n.right = sortedToBst(a, mid + 1, hi);
    return n;
}
Java1008. O(n): the range trick from 98, running forward over the preorder stream.11 lines
// 1008. O(n): the range trick from 98, running forward over the preorder stream.
// Consume values while they fit inside (low, high); return as soon as one does not.
int idx = 0;

TreeNode bstFromPreorder(int[] pre, long low, long high) {
    if (idx == pre.length || pre[idx] < low || pre[idx] > high) return null;
    TreeNode n = new TreeNode(pre[idx++]);
    n.left  = bstFromPreorder(pre, low, n.val);
    n.right = bstFromPreorder(pre, n.val, high);
    return n;
}
Why it works — why the middle, and how the range trick runs forward4 steps

Two constructions. The second is a genuinely nice reuse of an earlier sub-variant.

  1. 1

    Sorted plus balanced determines the root. For the tree to be height-balanced, the two subtrees must hold equal counts. The only element that splits a sorted array into equal halves is the middle one.

  2. 2

    Each half is the same problem. Everything before the middle is smaller (so it is the left subtree) and everything after is larger. Recurse identically, and the result is balanced by construction.

  3. 3

    LC 1008 turns validation into construction. In sub-variant B the range (low, high) checked each node. Here it decides whether the next value in the stream belongs in this subtree at all.

  4. 4

    Which gives O(n). Consume values while they fit inside the current bounds; return the moment one does not. Each value is examined a constant number of times, so the whole preorder stream is processed once.

The reuse worth naming: LC 1008 is the range trick from LC 98 running forward over the preorder stream — consume values while they fit inside (low, high), and return as soon as one does not.

Never build from sorted input by repeated insertion. Every value is larger than the last, so every insert descends right and the tree degenerates into a linked list — O(n^2) to build and O(n) per subsequent operation.

The cursor is shared, exactly as in Pattern 2 H and I. The left subtree consumes an unknown number of preorder values, so the right subtree must start after all of them.

Walkthrough — LC 108 — sorted array to balanced BST5 steps

[-10, -3, 0, 5, 9]. The middle element becomes the root at every level, so balance is automatic.

index 0 1 2 3 4 value -10 -3 0 5 9
#RangeMiddleBecomesRecurses on
1[0 .. 4]index 2 -> 0the root[0..1] and [3..4]
2[0 .. 1]index 0 -> -10root's left child[] and [1..1]
3[1 .. 1]index 1 -> -3right child of -10nothing
4[3 .. 4]index 3 -> 5root's right child[] and [4..4]
5[4 .. 4]index 4 -> 9right child of 5nothing

Height 3, perfectly balanced, in O(n). Compare against inserting -10, -3, 0, 5, 9 one at a time: each value exceeds everything already present, so every insert walks the full right spine and the result is a five-node chain built in O(n^2). The sorted order was carrying the shape information all along.

Key observations — what interviewers are listening for4 points
  • Sorted input is shape information, not just data. That reframing is what makes LC 108 obvious and repeated insertion visibly wrong.
  • Name the reuse in LC 1008. The LC 98 range trick, running forward. The gate asks for exactly that explanation, and it is what turns an O(n log n) insert-based solution into O(n).
  • Either middle works for even counts. Left or right of centre both give a valid height-balanced tree. Say so rather than agonising.
  • The degenerate case is worth stating unprompted. Inserting sorted data one at a time gives a linked list is a good instinct to demonstrate.
Common mistakes4 traps
  • Building from a sorted array by inserting one at a time

    Symptom: degenerates to a linked list, O(n^2).

    Prevention: Recurse on the middle element.

  • Rebuilding index ranges by copying subarrays

    Symptom: O(n log n) time and O(n log n) space for no benefit.

    Prevention: Pass (lo, hi) indices into the original array.

  • Copying the preorder cursor per frame in LC 1008

    Symptom: subtrees built from the wrong position in the stream.

    Prevention: The cursor is shared — a field or an int[1].

  • Assuming unsorted input can use this

    Symptom: the middle element is meaningless without an ordering.

    Prevention: Sort first, or accept repeated insertion.

Key takeaway

  • Trigger: build a BST from sorted input or from a preorder stream.
  • LC 108: the middle element is the root; recurse on each half. O(n), balanced by construction.
  • LC 1008: the LC 98 range trick running forward — consume while values fit (low, high).
  • Never: repeated insertion from sorted input — that is a linked list in O(n^2).
  • Gate: LC 108 blind, then explain LC 1008's O(n) solution as the LC 98 bound trick running forward. See §5.3.

G Range queries and pruning#

Where the ordering pays for itself twice: one comparison per node decides whether to recurse at all, so whole subtrees never get visited.

Mental model

“Before I look inside a subtree I ask whether it could possibly contain anything I want. If the answer is no, I do not descend — and that is not an optimisation, it is the algorithm.”

LC 938 is the clean case: sum the values in a range, and skip any subtree that lies entirely outside it. Cost is O(h + k) rather than O(n).

LC 669 is the one with a genuine trap, and it is the trap the gate asks you to explain with a concrete counterexample.

938 RANGE SUM one comparison decides whether to descend at all node.val < low -> the whole LEFT subtree is too small, skip it node.val > high -> the whole RIGHT subtree is too large, skip it 669 TRIM -- the trap: when a node is BELOW low, its entire LEFT subtree is below low too -> the replacement is the TRIMMED RIGHT SUBTREE, not null returning null here silently deletes valid nodes, and it is the standard wrong answer 538 reverse inorder -- right, node, left -- is a DESCENDING scan, for free

A node outside the range does not mean its whole subtree is outside. Half of it may still be in range, which is precisely why trim must return a subtree rather than null.

Recognition — reach for this when

  • A query restricted to a value range — sum, count, collect, trim.
  • The expected cost mentions k, the number of results, rather than n.
  • Whole subtrees can be excluded by a single comparison.
  • But not if you find yourself recursing into both children unconditionally. Then the bounds are not being used and the cost is O(n).
Java938. One comparison per node decides whether to descend at all.7 lines
// 938. One comparison per node decides whether to descend at all.
int rangeSum(TreeNode n, int lo, int hi) {
    if (n == null) return 0;
    if (n.val < lo) return rangeSum(n.right, lo, hi);   // whole left subtree is out of range
    if (n.val > hi) return rangeSum(n.left,  lo, hi);   // whole right subtree is out of range
    return n.val + rangeSum(n.left, lo, hi) + rangeSum(n.right, lo, hi);
}
Java669. When a node is below low, its entire LEFT subtree is below low too — so the11 lines
// 669. When a node is below `low`, its entire LEFT subtree is below `low` too — so the
// replacement is the trimmed RIGHT subtree, not null. Returning null here silently deletes
// valid nodes and is the standard wrong answer.
TreeNode trim(TreeNode n, int low, int high) {
    if (n == null) return null;
    if (n.val < low)  return trim(n.right, low, high);
    if (n.val > high) return trim(n.left,  low, high);
    n.left  = trim(n.left,  low, high);
    n.right = trim(n.right, low, high);
    return n;
}
Java538. Reverse inorder — right, node, left — is a descending scan, for free.10 lines
// 538. Reverse inorder — right, node, left — is a descending scan, for free.
int running = 0;

void greater(TreeNode n) {
    if (n == null) return;
    greater(n.right);
    running += n.val;
    n.val = running;
    greater(n.left);
}
Why it works — why trim returns a subtree, not null4 steps

Pruning is straightforward until LC 669, where the obvious move deletes valid data.

  1. 1

    Pruning on the sum. If node.val < low, everything in the left subtree is also below low, so it cannot contribute — recurse right only. Symmetrically for > high.

  2. 2

    Trim looks similar and is not. The temptation is to return null for any node outside the range, mirroring the pruning logic.

  3. 3

    But out-of-range does not mean the subtree is empty. If node.val < low, its left subtree is entirely below low and can go — but its right subtree may contain values inside the range.

  4. 4

    So return the trimmed surviving side. The replacement for an out-of-range node is the trimmed subtree on the side that could still hold valid values. Returning null throws those away silently.

The standard wrong answer, and why: when a node is below low, its entire left subtree is below low too — so the replacement is the trimmed right subtree, not null. Returning null here silently deletes valid nodes.

LC 538 gets a descending scan for free. Reverse inorder — right, node, left — visits values largest-first, so a running suffix sum needs no extra structure at all.

If both children are always visited, the bounds are not doing anything. That is the self-check for this sub-variant: one comparison should eliminate a side, and if it does not, re-read the bounds.

Walkthrough — LC 669 — trimming to [3, 4], the four-node counterexample4 steps

Node 1 is below the range. Return null for it and you delete nodes 2 and 3, which are perfectly valid.

3 / \ 1 4 \ 2 trim to [3, 4]
#AtIn range?Naive nullCorrect
13 (root)yeskeep, recurse both sidessame
21no, 1 < 3return null -> node 2 is lostreturn the trimmed right subtree
32no, 2 < 3--its right subtree is empty -> null here is correct
44yeskeepsame

Correct result: root 3 with right child 4 and no left child — because trimming node 1 returned its right subtree (node 2), which then trimmed to null in its own right. The naive version reaches the same answer here only by luck; add a node valued 3 under 2 and the naive version deletes it while the correct version keeps it. That is the counterexample the gate asks for.

Key observations — what interviewers are listening for4 points
  • The self-check is one sentence. Did a comparison eliminate a side? If both children are always visited, you are paying O(n) for a structure that offered O(h + k).
  • Trim is not pruning, despite looking like it. Pruning skips work; trimming rebuilds. Returning null conflates the two and loses data.
  • Have the counterexample ready. The gate asks for a concrete four-node tree. Abstract reasoning is not what it is testing.
  • Reverse inorder is a free descending scan. Worth remembering as a general trick, not just for LC 538.
Common mistakes4 traps
  • Recursing into both children in a range query

    Symptom: O(n) where O(h + k) was asked for.

    Prevention: One comparison must eliminate a side; if it does not, re-read the bounds.

  • trim returning null for an out-of-range node

    Symptom: silently deletes in-range descendants.

    Prevention: Return the trimmed subtree on the surviving side.

  • Trimming only one level

    Symptom: out-of-range nodes survive deeper in the tree.

    Prevention: The returned subtree must itself be trimmed — the recursion does this if you return its result.

  • Using forward inorder for a descending accumulation

    Symptom: you need the whole sequence before you can start.

    Prevention: Reverse inorder gives descending order directly.

Key takeaway

  • Trigger: a value-range query — sum, count, collect, trim.
  • Pruning: one comparison decides whether to descend; O(h + k).
  • LC 669: an out-of-range node is replaced by its trimmed surviving subtree, never null.
  • LC 538: reverse inorder is a descending scan, for free.
  • Gate: explain why LC 669 must return a subtree rather than null, with a concrete four-node counterexample. See §5.3.

H BST as an ordered container#

Where BSTs actually live in production code. TreeMap and TreeSet give you floor, ceiling, headMap and tailMap in O(log n) — and knowing that is the bridge out of trees as a topic.

Mental model

“I am not solving a tree problem. I need an ordered container with efficient neighbour queries, and a balanced BST is the data structure that provides it.”

This sub-variant is deliberately not really about trees. It is about recognising when the ordering operations are what you need, and reaching for the library rather than hand-rolling a tree.

It is also the bridge to the ordered-multiset sliding windows in the companion document — the same structure, used as a window aggregate rather than as a standalone container.

TreeMap / TreeSet -- O(log n) per operation floorKey(x) largest key <= x ceilingKey(x) smallest key >= x higherKey(x) strictly greater headMap / tailMap / subMap range views 653 Two Sum on a BST: two converging pointers over a FORWARD and a REVERSE BST iterator -- the sorted-array two-pointer template, with the array replaced by two O(h)-space cursors this is the same structure as the ordered-multiset sliding window in the companion document (§2.J there)

floor and ceiling are the operations a hash map cannot give you. The moment a problem needs nearest key, an ordered container is the answer.

Recognition — reach for this when

  • You need neighbour queries — floor, ceiling, nearest — not just membership.
  • A range view or ordered iteration over a changing collection.
  • The collection is mutated and queried in order, so sorting once is not enough.
  • But not for plain membership or counting. A hash map is O(1) and simpler; ordering costs you a log factor you are not using.
Java653. Two converging pointers over a forward and a reverse BST iterator — the sorted-array22 lines
// 653. Two converging pointers over a forward and a reverse BST iterator — the sorted-array
// two-pointer template with the array replaced by two O(h)-space cursors.
boolean findTarget(TreeNode root, int k) {
    Deque<TreeNode> lo = new ArrayDeque<>(), hi = new ArrayDeque<>();
    for (TreeNode n = root; n != null; n = n.left)  lo.push(n);
    for (TreeNode n = root; n != null; n = n.right) hi.push(n);
    TreeNode l = lo.peek(), r = hi.peek();
    while (l != null && r != null && l != r) {
        int s = l.val + r.val;
        if (s == k) return true;
        if (s < k) { l = advance(lo, true);  }      // smallest is too small: raise it
        else       { r = advance(hi, false); }      // largest is too large: lower it
    }
    return false;
}

TreeNode advance(Deque<TreeNode> st, boolean forward) {
    TreeNode n = st.pop();
    for (TreeNode c = forward ? n.right : n.left; c != null; c = forward ? c.left : c.right)
        st.push(c);
    return st.peek();
}
Why it works — when the ordering operations are the requirement4 steps

A short sub-variant, because the skill is recognition rather than implementation.

  1. 1

    Hash maps cannot answer neighbour queries. What is the largest key at most x? requires an ordering. A hash map has none, so the question costs a full scan.

  2. 2

    A balanced BST answers them in O(log n). floorKey, ceilingKey and the range views are exactly the descent from sub-variant A, with rebalancing handled for you.

  3. 3

    Mutation is what rules out sorting once. If the collection never changed you could sort an array and binary search it. An ordered container earns its keep precisely when inserts and deletes are interleaved with queries.

  4. 4

    LC 653 makes the connection explicit. Two converging pointers over a forward and a reverse BST iterator is the sorted-array two-pointer template, with the array replaced by two O(h)-space cursors. Same algorithm, different backing store.

The connection the gate asks you to name: LC 653 is converging two pointers — sub-variant A of Two Pointers — with the sorted array replaced by a forward and a reverse BST iterator. Solving it with a hash set works and misses the point.

The iterators are the ones from Pattern 1 B. A paused inorder walk gives the forward cursor; a paused reverse inorder gives the backward one. Each is O(h) space and amortized O(1) per step.

This is where the two documents meet. The ordered-multiset sliding window uses the same structure to maintain a window median — a BST used as a moving aggregate rather than a static container.

Walkthrough — LC 653 — two-sum on a BST with two iterators1 steps

Target 9. The forward iterator yields ascending values, the reverse one descending — exactly the two ends of a sorted array.

5 / \ 3 6 / \ \ 2 4 7 target = 9 forward iterator: 2 3 4 5 6 7 reverse iterator: 7 6 5 4 3 2
#lowhighsumvs 9Move
1279hitreturn true

Found immediately here, but the shape is the point: had the sum been too small the forward cursor would advance, and too large the reverse cursor would retreat — identical to converging pointers on a sorted array. The two iterators cost O(h) space each, versus O(n) for a hash set, and the discard argument is the one from Two Pointers A, unchanged.

Key observations — what interviewers are listening for4 points
  • Recognition is the whole skill here. Do I need neighbour queries on a changing collection? If yes, ordered container. If no, a hash map is simpler and faster.
  • Name the two-pointer correspondence. The gate asks you to solve LC 653 with two iterators and name the sub-variant it corresponds to. The naming is half the exercise.
  • Know the API, not just the concept. floorKey, ceilingKey, higherKey, subMap. Reaching for the right method is what makes this practical rather than theoretical.
  • This is the bridge out of the topic. Treating BSTs as a tool rather than a subject is exactly what the final gate is checking.
Common mistakes4 traps
  • Solving LC 653 with a hash set

    Symptom: correct, O(n) space, and it ignores the structure entirely.

    Prevention: Two iterators, O(h) space each. The gate asks for that version specifically.

  • Using an ordered container for plain membership

    Symptom: O(log n) where O(1) was available.

    Prevention: A hash map is simpler unless you need ordering.

  • Hand-rolling a balanced BST

    Symptom: rotation bugs, for functionality the standard library already provides.

    Prevention: TreeMap / TreeSet unless the problem explicitly forbids them.

  • Sorting once when the collection mutates

    Symptom: the sorted snapshot goes stale after the first insert.

    Prevention: An ordered container maintains the invariant across mutations.

Key takeaway

  • Trigger: neighbour queries — floor, ceiling, nearest — on a changing collection.
  • The tool: TreeMap / TreeSet, O(log n) per operation.
  • LC 653: converging two pointers over a forward and a reverse iterator, O(h) space.
  • The bridge: the same structure drives the ordered-multiset sliding window in the companion document.
  • Gate: solve LC 653 with two iterators and name the two-pointer sub-variant it corresponds to. See §5.3.

3.4 Failure Modes — Binary Search Trees#

#BugSymptomPrevention
1Validating against the parent onlyAccepts trees that are not BSTsThe property is about ancestors, not parents. Inherit (low, high).
2Integer.MIN_VALUE / MAX_VALUE as validation sentinelsFails on trees containing those exact valuesUse long bounds, or a nullable prev node in the inorder version.
3<= instead of < in validationAccepts duplicatesLeetCode's BSTs are strict. State the duplicate policy before coding.
4Recursing into both children in a range queryO(n) where O(h + k) was asked forOne comparison must eliminate a side; if it does not, re-read the bounds.
5trim returning null for an out-of-range nodeSilently deletes in-range descendantsReturn the trimmed subtree on the surviving side.
6Deleting a two-child node by splicing pointersCorrupt tree on the next operationCopy the successor's value, then recursively delete the successor.
7Successor read off the stopping nodeWrong answer whenever the node has no right childThe answer is the last node you turned left from; track it in a variable.
8kthSmallest without an early exitFull O(n) traversal, and a bad follow-up conversationStop as soon as the counter reaches k; mention the subtree-size augmentation.
9Comparing all pairs for the minimum differenceO(n²)In a BST the minimum gap is between inorder-adjacent nodes only.
10Recovering a swapped BST by tracking one inversionWrong when the swapped nodes are not adjacentfirst from the first inversion, second from the last.
11Using the general LCA on a BSTCorrect but O(n) and misses the point of the questionIf it is a BST, descend by comparison — §3.A.
12Building from a sorted array by inserting one at a timeDegenerates to a linked list, O(n²)Recurse on the middle element.