Sparround

Binary Search Tree

The BST invariant: for every node — ALL values in the left subtree are smaller than the node, and ALL values in the right subtree are larger.

A frequent mistake lives here: the condition applies to the entire subtree, not just the direct children. Checking left.value < node.value is not enough — no value anywhere deep in the left subtree may exceed the node either. When writing a BST validator you must pass an allowed (min, max) range down to each node.

What does the invariant buy? Every comparison discards half the search space — exactly like binary search on a sorted array. The difference is that in a BST insertion and deletion are also O(log n), whereas placing an element in a sorted array costs O(n) because everything after it must shift.

OperationBalancedDegenerateSorted arrayHash map
SearchO(log n)O(n)O(log n)O(1) average
InsertO(log n)O(n)O(n)O(1) average
DeleteO(log n)O(n)O(n)O(1) average
Sorted outputO(n) — inorderO(n)O(n) — already sortedO(n log n) — must sort
Range queryO(log n + k)O(n)O(log n + k)O(n)

Search and insert follow the same logic: start at the root, compare the value with the node, go left if smaller and right if larger. Search stops on a match; insert creates a new node when it reaches a null slot. Both take as many steps as the tree's height — O(h).

Deletion splits into three cases, and the third is what interviewers ask about:

  • Leaf (0 children) — just set the parent's pointer to null.
  • 1 child — replace the node with its only child. The subtree moves up as a whole and the invariant holds.
  • 2 children — the hard case. Instead of removing the node, you replace its value with its inorder successor (the smallest value in the right subtree), then delete that successor from the right subtree. The successor has at most one child (it has no left child, being the smallest), so the second deletion is necessarily one of the easy cases and the recursion does not deepen.

The inorder predecessor (the largest value in the left subtree) works just as well — either choice preserves the invariant.

Why is the worst case O(n)? A BST's entire advantage rests on the assumption h ≈ log n. But a plain BST does not balance itself — its shape depends entirely on insertion order.

Insert sorted data (1, 2, 3, 4, 5...) and every new element goes right, turning the tree into a linked list: h = n and every operation is O(n). This is not a theoretical fear — real data often arrives sorted: IDs, timestamps, auto-increment keys, a sorted CSV. In other words, the most natural input is the BST's worst case.

Ways out:

  • A self-balancing tree — AVL or Red-Black. They rotate during insert/delete to keep the guarantee h = O(log n). That is the next topic.
  • Shuffle the data — randomising insertion order makes the expected height O(log n). Simple, but not a guarantee.
  • Use a ready-made structure — in real work you do not hand-write a BST: TreeMap in Kotlin, SplayTreeMap in Dart, std::map in C++.

The comparison with a hash map matters too: hashing gives O(1) but preserves no order. Order is the BST's reason to exist — sorted iteration, range queries, and operations like "the largest key below k" (floor/ceiling) are simply not available on a hash.

Interview tip. Two questions come up almost every time.

First: "How do you delete a node with two children in a BST?" This is the real test of the topic. A strong answer: "I do not physically remove the node — I replace its value with its inorder successor, the smallest value in the right subtree, then delete that successor from the right subtree. The successor cannot have a left child, so the second deletion is necessarily an easy 0- or 1-child case." Why the successor specifically — because it is the smallest value greater than the node, so putting it in that slot preserves the invariant.

Second: "What is a BST's worst case?" The answer: sorted input. When you say it, add a real example — auto-increment IDs or timestamps. Then give the fix: a self-balancing tree or a ready-made TreeMap.

The most common mistake: applying the BST invariant only to direct children. If your isValidBST does not pass a (min, max) range downward, the code passes simple tests and then fails on a node buried deep in the tree.

📚 Sources and documentation