Module 18 · BST & Ordered Structures

Balance & Why It Matters

Concept~11 min

The promise, and how it breaks

The previous lesson ended on a threat: every BST operation is O(h), and h — the height — is only small if the tree's shape cooperates. This lesson is about what happens when it doesn't, what "balanced" precisely means, and the mechanism real libraries use to guarantee it.

Start with the failure, concretely — the same 1, 2, 3, 4, 5 chain the last lesson closed on. Every insert turned right because every value was larger than everything already there, so the "tree" collapsed into a straight line instead of branching. Contrast that with a bushy tree of the same keys:

balanced1234567height ≈ log nskewed12345height = n

Its height is n, not log n. A search for 5 visits all 5 nodes. This is a degenerate BST, and it is not an exotic edge case — it's what you get from sorted input, which is extremely common (data loaded from an ordered file, timestamps arriving in order, an already-sorted array inserted one element at a time). The BST has silently become a linked list with extra pointer overhead: O(n) search, O(n) insert, O(n) delete. Everything the invariant promised is still true — it's just worthless, because h = n.

What "balanced" means

Informally, a tree is balanced when it's "bushy" rather than "stringy." Precisely, a family of trees is balanced when it guarantees

height h = O(log n)

for n nodes. That bound is the whole point, because it's what turns the honest O(h) cost of every operation into O(log n).

Why is O(log n) the best you can hope for, and why is it achievable? A binary tree of height h has at most 2^(h+1) − 1 nodes: level 0 holds at most 1 node, level 1 holds at most 2, and in general level k holds at most 2^k, since each level's nodes are children of the level above and every node has at most 2 children. A tree of height h has levels 0 through h, so its node count is at most 2⁰ + 2¹ + ⋯ + 2ʰ. That's a geometric series, and it telescopes: each term is one short of double the next, so the whole sum is one short of double the last term — 2⁰ + 2¹ + ⋯ + 2ʰ = 2ʰ⁺¹ − 1. Turn that around: if n ≤ 2ʰ⁺¹ − 1, then 2ʰ⁺¹ ≥ n + 1, so h + 1 ≥ log₂(n + 1), giving h ≥ log₂(n + 1) − 1 — so no binary tree can be shorter than about log₂ n. A perfectly full tree hits that floor exactly. "Balanced" means staying within a constant factor of that unavoidable floor, forever, no matter the insertion order. The degenerate chain above is the opposite extreme: it puts one node per level, spending n levels on n nodes.

The catch: we can't just hope for balance, because as we saw, adversarial (or merely sorted) input destroys it. We need a mechanism that actively restores balance as the tree changes — and it has to do so without breaking the BST invariant, or search stops working. That mechanism is rotation.

Rotation: the one primitive

Picture a hanging decorative mobile, the kind with crossbars and strings that balances objects on either side. When one side gets too heavy and the whole thing tilts, you don't take anything off — you just slide the pivot point, letting the heavy side's nearest piece rise up to become the new attachment point while everything below it resettles underneath. Nothing is added or removed, no piece changes its position relative to its neighbors on the same string — only which crossbar holds which piece up changes, and that's enough to level the whole structure out. A rotation on a tree is the same move: a local reshuffling of who-holds-up-whom that changes height without touching what's actually there.

A rotation is a local, O(1) restructuring that changes a subtree's height while preserving its inorder order — and therefore the BST invariant. It rewires exactly three pointers. Here is a right rotation around a node P, promoting its left child Q:

text
        P                    Q
       / \                  / \
      Q   C     ───►       A   P
     / \                      / \
    A   B                    B   C

Look at what moved: Q rises to the top, P descends to become Q's right child, and B — which was Q's right subtree — is handed to P as its new left child. Everything else stays put. Only three links change (P.left, Q.right, and the parent's pointer to the subtree root).

Make it concrete: let P = 5, Q = 3, C = 8, A = 1, B = 4 (so the before-tree is 5 with left child 3 — itself holding 1 and 4 — and right child 8). Right-rotating around 5 promotes 3 to the top: the after-tree is 3 with left child 1 and right child 5, and 5 now holds 4 (formerly Q's right child B) as its left child and 8 as its right child. Read the inorder sequence of both: before, 1, 3, 4, 5, 8; after, 1, 3, 4, 5, 8 — identical, confirmed by walking both trees. Only the shape changed.

Why does this preserve the invariant in general? The cleanest way to see it is through inorder order, which the last lesson proved is the sorted sequence of values. Read off the inorder sequence of the before tree: A, Q, B, P, C (left subtree of Q, then Q, then B, then P, then C). Now read off the after tree: A, Q, B, P, C — identical. Rotation does not change which values come before which in the inorder walk; it only changes the shape (who is whose parent). Since the invariant is equivalent to "inorder is sorted" (a BST is exactly a tree whose inorder walk is sorted), and rotation leaves the inorder walk untouched, a rotation of a valid BST is always a valid BST. What it does change is height: in the picture, if the left side was too tall, promoting Q and demoting P shortens the left path and lengthens the right — trading height from the heavy side to the light side, exactly like sliding the mobile's pivot. A left rotation is the exact mirror image, promoting the right child.

That is the entire mechanical toolkit. Every self-balancing BST is "plain BST insert/delete, followed by a sequence of rotations that restore the height guarantee." The families differ only in when they rotate and how strict a balance they insist on.

Two classic approaches (conceptual)

You will almost never implement one of these by hand, but you must know the two shapes, because the ordered maps and sets you use every day are built from them.

AVL trees enforce strict balance: at every node, the heights of the left and right subtrees differ by at most 1. This is stored as a small per-node "balance factor" and checked on the way back up after every insert and delete; whenever a node goes out of range, one or two rotations fix it. The payoff is a very tight height bound (about 1.44 log₂ n worst case — that constant comes from the skinniest possible AVL tree at each height, a family that turns out to grow exactly like the Fibonacci sequence), so lookups are as fast as a BST can offer. The cost: because the balance condition is strict, insertions and deletions rotate often — more restructuring work per update.

Red-black trees enforce a looser balance using a coloring scheme (each node red or black, with rules that no red node has a red child and every root-to-leaf path crosses the same number of black nodes). These rules only guarantee that the longest root-to-leaf path is at most twice the shortest — a weaker bound than AVL (height up to about 2 log₂ n), so lookups are very slightly slower in the worst case. In exchange, updates need fewer rotations on average, which makes insert/delete-heavy workloads cheaper. This trade — a little lookup speed for a lot less restructuring — is why red-black trees are the default in most production ordered containers: C++'s std::map and std::set, Java's TreeMap and TreeSet are red-black trees. AVL tends to win where reads vastly dominate writes.

Both guarantee h = O(log n), and therefore restore all the core operations to O(log n) — the point of the whole exercise. The difference between them is a knob on the read-versus-write trade-off, not a difference in what they promise asymptotically.

Complexity

OperationCostWhy
unbalanced BST, worst caseO(n)sorted input degenerates the tree into a height-n chain; h = n
balanced BST (AVL / red-black), all core opsO(log n)the balance rule guarantees h = O(log n), and every core operation is O(h)
a single rotationO(1)it rewires a fixed number (three) of pointers regardless of tree size
rebalancing after one insert/deleteO(log n)at most O(h) = O(log n) rotations along the path back to the root — often O(1) amortized for red-black

Where this lands

The lower bound h ≥ log₂ n says a balanced BST is asymptotically optimal for a comparison-based ordered structure — you cannot search faster than O(log n) this way. One problem ahead leans directly on this lesson: Convert Sorted Array to BST builds a guaranteed- balanced tree in one shot by always choosing the middle element as the root (contrast it with the degenerate chain above — same values, opposite outcome, entirely because of construction order). More broadly, this lesson is the reason every O(h) operation from the previous lesson is worth trusting at all: without a balance guarantee, "O(h)" is honest but silent about whether h is log n or n.

Check yourself

3 questions

01

Inserting the already-sorted sequence 1,2,3,4,5 into a plain BST produces a height-5 chain. What is the underlying reason?

02

Why does a rotation preserve the BST invariant?

03

C++'s std::map and Java's TreeMap use red-black trees rather than AVL trees. What trade-off does that reflect?