Binary Search Trees
A tree that keeps the binary-search rule alive — left < node < right — so every search and insert follows one root-to-leaf path instead of scanning all n items.
What you'll learn
- How the BST rule (left < node < right) makes search and insert follow a single path
- Why a balanced tree gives O(log n) but sorted inserts degrade it to O(n)
- How in-order traversal reads a BST in ascending order — a free sort
- What balancing means, and why AVL and red-black trees exist
Before you start
A binary search tree is the simplest structure that keeps data sorted at every moment — not a sorted array you rebuild, but a living rule you preserve with each insert and delete.
The rule is one sentence: at every node, all values in the left subtree are smaller, and all values in the right subtree are larger. From that single rule, everything else — fast search, sorted reading, quick min and max — falls out on its own.
Following the rule
Searching is just the binary-search game played on a tree. At each node you ask one question — is my target smaller or larger? — and step left or right accordingly, never looking at the other side. Let us search for 6 in this tree:
Both search and insert do the same walk: start at the root, compare, step down, until you either find the value or reach an empty spot where it belongs. So both cost about the height of the tree — one comparison per level.
class Node:
def __init__(self, val):
self.val = val
self.left = self.right = None
def insert(root, val):
if root is None:
return Node(val)
if val < root.val:
root.left = insert(root.left, val)
elif val > root.val:
root.right = insert(root.right, val)
return root
def inorder(root, out):
if root:
inorder(root.left, out)
out.append(root.val) # root in the middle → ascending order
inorder(root.right, out)
return out
root = None
for v in [8, 3, 10, 1, 6, 14]:
root = insert(root, v)
print("in-order:", inorder(root, []))
in-order: [1, 3, 6, 8, 10, 14]
That sorted output is no accident. Because every left subtree holds smaller values and every right subtree larger ones, walking left → root → right reads the tree in ascending order — a free O(n) sort once the values are in.
The height is everything
Each operation costs O(h), where h is the height — so the question that decides a BST’s whole performance is: how tall does it get?
- Balanced, where each node’s two sides stay roughly even: the height is about
log₂ n, so search, insert, and delete are O(log n). A million balanced nodes is only about 20 levels. - Degenerate: insert values in sorted order and every one is larger than the last, so each goes right, and the tree collapses into a right-leaning chain of height
n. Now a “search” walks all n nodes — no better than a linked list.
Deleting takes a little more care
Removing a node can break the rule for everything beneath it, so there are three cases. A leaf just goes. A node with one child is spliced out — its parent adopts the lone child. A node with two children is the tricky one: you find its in-order successor (the smallest value in its right subtree), copy that value up into the node, then delete the successor — which, being the leftmost of a subtree, is itself a leaf or one-child case. Each delete still follows one path down, so it stays O(h).
Keeping it balanced
The degeneration is exactly why self-balancing trees exist. They add one rule on top of the BST rule and fix any violation with a rotation — a local relink of a few pointers that lowers height without disturbing the ordering.
- An AVL tree keeps the two sides of every node within one level of each other, rebalancing eagerly — height stays very close to
log n. - A red-black tree uses a looser colouring rule, so it rotates less on average. It backs most standard-library ordered maps: Java’s
TreeMap, C++‘sstd::map.
You will rarely implement one — the libraries are excellent — but you need to recognise the problem they solve, so you reach for the right tool when sorted inserts threaten.
| Operation | Balanced (h ≈ log n) | Degenerate (h = n) |
|---|---|---|
| Search / Insert / Delete | O(log n) | O(n) |
| In-order traversal | O(n) | O(n) |
| Min / Max | O(log n) | O(n) |
Practice
Quick check
Practice this in an interview
All questionsBinary search halves the search space each iteration to find a target in O(log n). The tricky part is not the idea but the boundary conditions: closed vs. half-open intervals, how to update lo/hi, and when to use lo < hi vs. lo <= hi. One clean template eliminates all the classic bugs.
The maximum depth is 1 plus the larger of the left and right subtree depths. A single recursive call naturally expresses this: at every node, ask both children for their depth and take the max. The base case is None, which returns 0.
Use a queue (deque) to process nodes layer by layer. At each step, snapshot the current queue length to know exactly how many nodes belong to the current level, drain those, then enqueue their children. The result is a list of lists without any depth-tracking variable.
A B-tree index stores key values in a balanced tree of sorted nodes, allowing the engine to reach any value in O(log n) page reads instead of scanning every row. The optimizer skips the index when the estimated cost of random I/O exceeds a full-table scan, when a function wraps the indexed column, or when the query returns such a large fraction of rows that a sequential scan is cheaper.