Skip to content

Balanced Trees Overview

The BST from the last lesson promised O(logn)O(\log n) search and insert — but that promise has fine print: only if the tree is balanced. Insert already sorted data and a BST silently turns into a linked list.

  • Why a skewed BST loses its O(logn)O(\log n) guarantee entirely.
  • AVL trees: the height-balance rule and how rotations restore it.
  • Red-Black trees: coloring rules that give a looser, cheaper-to-maintain balance guarantee.
  • The practical reality: Python has no built-in balanced BST — what to reach for instead (bisect, sortedcontainers.SortedList).

A BST’s height determines its speed. Insert values in sorted order and every new node becomes the rightmost node’s only child — you get a chain, not a tree:

skewed_bst.py
class TreeNode:
    def __init__(self, val, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right
 
 
def bst_insert(root, val):
    if root is None:
        return TreeNode(val)
    if val < root.val:
        root.left = bst_insert(root.left, val)
    else:
        root.right = bst_insert(root.right, val)
    return root
 
 
def height(node):
    if node is None:
        return -1
    return 1 + max(height(node.left), height(node.right))
 
 
# Insert already-sorted data: worst case for a plain BST
root = None
for v in [1, 2, 3, 4, 5, 6, 7]:
    root = bst_insert(root, v)
 
print("nodes: 7, height:", height(root))   # height 6 -- a straight line, not a tree!
diagram Balanced (left) vs skewed (right) — same 7 values mermaid

Both trees hold the same 7 values. The balanced one answers “is 7 present?” in 3 comparisons; the skewed one needs 7 — it’s just a linked list wearing a tree costume. Self-balancing trees exist to prevent exactly this.

An AVL tree enforces a simple invariant at every node: the heights of its left and right subtrees can differ by at most 1.

height(left)height(right)1|\text{height(left)} - \text{height(right)}| \le 1

Whenever an insert or delete breaks that invariant, the tree fixes itself with a rotation — a local restructuring that restores balance in O(1)O(1) without changing the sorted (inorder) order of the values.

diagram Left rotation fixes a right-heavy imbalance mermaid

Before: node 20’s right subtree (rooted at 30) is taller than its left (15), so 20 becomes right-heavy. A left rotation promotes 20’s right child (30)… conceptually — in this small example the rotation re-centers the tree around 20 so both subtrees end up height-balanced again, without breaking the BST ordering. There are four rotation cases in total (left-left, right-right, left-right, right-left), but they all boil down to the same idea: re-parent a few pointers, keep the sort order, restore the height rule.

Because AVL rebalances aggressively, it guarantees O(logn)O(\log n) height at all times — the tightest balance guarantee of the common self-balancing trees, at the cost of more rotations on insert/delete-heavy workloads.

Red-Black trees: looser balance, cheaper to maintain

Section titled “Red-Black trees: looser balance, cheaper to maintain”

A Red-Black tree relaxes AVL’s strict rule and instead colors every node red or black, enforcing:

  1. The root is always black.
  2. A red node never has a red child (no two reds in a row on any path).
  3. Every path from a node to its descendant None leaves passes through the same number of black nodes.

These rules don’t force perfect height-balance like AVL does, but they guarantee the longest root-to-leaf path is never more than 2x the shortest one — still O(logn)O(\log n) height, just with fewer rotations needed on average. That trade-off (looser balance, cheaper maintenance) is why Red-Black trees back most language standard libraries: C++‘s std::map / std::set, Java’s TreeMap, and the Linux kernel’s scheduler.

The Python reality: no built-in balanced BST

Section titled “The Python reality: no built-in balanced BST”

Unlike C++ or Java, Python’s standard library ships no self-balancing tree. dict and set are hash tables (O(1)O(1) average, but no ordering). For competitive programming and interviews, you have two practical substitutes:

  1. bisect on a plain sorted list — no extra dependency, O(logn)O(\log n) search, O(n)O(n) insert (due to shifting), fine for small/medium n.
  2. sortedcontainers.SortedList — a third-party package (allowed on most judges and installable via pip) implemented as a list of small sorted blocks, giving O(logn)O(\log n) search and O(n)O(\sqrt{n})-ish amortized insert/delete in practice — close to a real balanced tree’s behavior, usable from pure Python.
ordered_structure_demo.py
import bisect
 
# Maintain an always-sorted list using bisect -- Python's go-to
# "ordered set" substitute when a full balanced tree is overkill.
ordered = []
for value in [50, 20, 70, 10, 60, 30]:
    bisect.insort(ordered, value)
    print(f"after inserting {value}:", ordered)
 
target = 30
idx = bisect.bisect_left(ordered, target)
found = idx < len(ordered) and ordered[idx] == target
print("30 present?", found, "at sorted index", idx)
 
# rank of a value == how many elements are strictly smaller -- O(log n) to find
rank = bisect.bisect_left(ordered, 60)
print("elements smaller than 60:", rank)
StructureSearchInsertDeleteNotes
Unbalanced BSTO(logn)O(\log n) avg, O(n)O(n) worstsamesamedegrades on sorted input
AVL treeO(logn)O(\log n)O(logn)O(\log n)O(logn)O(\log n)strictest balance, more rotations
Red-Black treeO(logn)O(\log n)O(logn)O(\log n)O(logn)O(\log n)looser balance, fewer rotations
bisect on a listO(logn)O(\log n)O(n)O(n)O(n)O(n)no dependency, fine for small n
sortedcontainers.SortedListO(logn)O(\log n)O(logn)O(\log n) amortizedO(logn)O(\log n) amortizedclosest pure-Python analogue

You rarely implement AVL/Red-Black rotations yourself in a contest — instead you recognize the need for an ordered, dynamically-updated set and reach for SortedList (or bisect, or a Fenwick tree / segment tree, covered later). Watch for phrasing like “maintain the k smallest values seen so far,” “process queries online,” or “find the number of elements less than x after each update” — all signals that a balanced-tree-like structure is the intended tool.

The degenerate case, made concrete. This is a valid BST built from sorted input:

treeA valid BST that is really a linked listh = n, so every operation is O(n)
12345
call stack
1
node1stack depth1
enterEnter 1. The recursive call is pushed onto the stack, which is now 1 frame deep.
1/16

In-order traversal still produces sorted output, so the BST invariant holds perfectly. The structure is simply useless: searching for 5 visits every node. Rotations exist to prevent exactly this shape, and this is the picture to have in mind when you say O(h) rather than O(log n).

Problems from the database that exercise this material. Progress is saved in this browser.

4 problems
4 easy0 medium0 hard

Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.

Each exercise is the actual LeetCode problem with its real method signature and LeetCode’s own examples as the test. Write the body, press Run, and match the expected output.

Problem. Return True if the tree is height-balanced — every node’s two subtrees differ in height by at most one.

Constraints. 0 <= number of nodes <= 5000.

Examples. [3,9,20,null,null,15,7] gives True · [1,2,2,3,3,null,null,4,4] gives False · [] gives True

Editorial

Balance must hold at every node, not just the root. The obvious solution calls a height helper at each node, recomputing the same subtree heights repeatedly — O(n2)O(n^2) on a skewed tree.

Threading the answer through a single traversal fixes it: the recursion returns the height when balanced, and -1 to mean “something below is already unbalanced”.

Time O(n)O(n). Space O(h)O(h).

The early propagation matters for more than speed: checking lh == -1 before even computing rh short-circuits whole subtrees once failure is known.

[1,2,2,3,3,null,null,4,4] is the failing case — the left subtree reaches depth 4 while the right stops at 2, so the root’s children differ by more than one.

This is the same “return one thing, signal another” idea as the split-brain trick in Tree DFS.

Follow-ups: “Return the height as well?” — it is already computed. “Balance an unbalanced BST (LC 1382)?” — below. “How do AVL trees maintain this invariant?” — rotations on insert and delete, keeping a balance factor per node. “Why O(n2)O(n^2) naively?” — be ready to describe the repeated height computations.

LC 108 — Convert Sorted Array to Binary Search Tree · Easy

Section titled “LC 108 — Convert Sorted Array to Binary Search Tree · Easy”

Problem. Given a sorted array of unique integers, build a height-balanced BST.

Constraints. 1 <= len(nums) <= 10^4, strictly increasing.

Examples. [-10,-3,0,5,9] gives a balanced BST of height 3 · [1,3] gives height 2

Editorial

Choosing the middle element as the root splits the remaining values into halves differing in size by at most one. Applying that recursively makes every subtree balanced, so no rotations are ever required.

Time O(n)O(n) — each element becomes a node exactly once. Space O(logn)O(\log n) recursion.

Passing bounds rather than slices is the detail worth getting right. Slicing nums[:mid] copies at every level, adding O(nlogn)O(n \log n) time and space for no benefit.

The resulting height is log2(n+1)\lceil \log_2(n+1) \rceil, which is minimal — [1..7] gives height 3, and 7 nodes cannot be arranged in fewer levels.

Because the in-order traversal of the result is the original array, this is the inverse of the observation that a BST’s in-order walk is sorted — see BST Patterns.

Follow-ups: “From a sorted linked list (LC 109)?” — no random access, so either copy to an array or use the in-order simulation that builds bottom-up while walking the list once. “Rebalance an existing BST (LC 1382)?” — next problem. “Why is the middle optimal?” — any other split makes one side larger and increases the height.

LC 1382 — Balance a Binary Search Tree · Medium

Section titled “LC 1382 — Balance a Binary Search Tree · Medium”

Problem. Given a BST, return a balanced BST containing the same values.

Constraints. 1 <= number of nodes <= 10^4, 1 <= Node.val <= 10^5, values unique.

Examples. A right-leaning chain [1,null,2,null,3,null,4] becomes a balanced tree of height 3

Editorial

The reduction is the whole solution: a BST’s in-order traversal is sorted, and building a balanced BST from a sorted array is LC 108. So flatten, then rebuild.

Time O(n)O(n) for both passes. Space O(n)O(n) for the value list.

The rotation-based alternative — the Day-Stout-Warren algorithm — achieves this in O(1)O(1) extra space by first flattening the tree into a right-leaning “vine” via rotations, then rotating it into balance. It is genuinely clever and worth naming, but nobody expects you to derive it live, and the flatten-and-rebuild answer is what interviewers want.

[1,null,2,null,3,null,4] is the worst input: a chain of height 4 becomes a balanced tree of height 3.

Follow-ups: “In O(1)O(1) extra space?” — name Day-Stout-Warren. “Keep it balanced under insertions?” — that is what AVL and red-black trees do, via rotations on every update; see Balanced Trees Overview above. “Why not just rotate?” — possible but far more code, and the reduction is obviously correct.

A single right rotation — the primitive every balanced tree is built from. Inserting 3, 2, 1 into an AVL tree:

after insertingshapebalance factor at rootaction
330none
23 with left child 2−1still within ±1
1321, left-leaning−2rotate right at 3
after rotation2 with children 1 and 30balanced

The rotation relinks three pointers and is O(1)O(1). Height drops from 2 to 1, and the BST invariant is preserved because 1 < 2 < 3 still holds in position.

There are four cases — left-left, right-right, left-right, right-left — and the two “zigzag” cases need two rotations. That is the whole of AVL insertion, and the level of detail worth carrying: know that rotations are O(1)O(1) and preserve ordering, rather than memorising the case analysis.

StructureBalance ruleBest for
AVLheights differ by ≤ 1read-heavy — stricter balance, faster lookups, more rotations on write
Red-blackcolour rules bound height at 2logn2\log nwrite-heavy — looser balance, fewer rotations. Used by most standard libraries
B-tree / B+treemany keys per nodedisks and databases — node size matches a page, minimising I/O
Treaprandom priorities give expected balancefar simpler to implement; O(logn)O(\log n) expected, not guaranteed
Skip listrandomised levelseasier concurrency; used by Redis sorted sets
  • Assuming a BST is balanced. Nothing enforces it. If the input order is adversarial or sorted, you have a linked list.
  • Reaching for a balanced tree when a hash map would do. If you never need ordering, range queries or a successor, a hash map is simpler and faster.
  • Trying to implement AVL rebalancing under time pressure. Four cases, easy to get wrong, and almost never what is being asked. State the approach and use a library.
  • Assuming sortedcontainers is available. It is third-party. On a platform that lacks it, bisect plus a list, or a heap, is the fallback.
  • Forgetting that a treap’s guarantee is only expected. Randomised balance is not worst-case balance, which matters if the input can be adversarial.
They askWhat they’re checkingThe answer
“Why not just use a BST?”Whether you know the failure modeSorted insertion gives h=nh = n and O(n)O(n) operations. Balancing converts that into an O(logn)O(\log n) guarantee
“AVL or red-black?”Trade-off reasoningAVL balances more strictly — faster reads, more rotations on write. Red-black is looser and cheaper to update, which is why libraries pick it
“When is a balanced tree better than a hash map?”JudgementWhen you need order: range queries, k-th element, successor and predecessor, or sorted iteration. A hash map has none of those
“Why do databases use B-trees rather than AVL?”Systems awarenessNode size matches a disk page, so one I/O fetches many keys. Height is tiny and I/O dominates, not comparisons
“Implement one”Whether you scope realisticallyDescribe rotations and the invariant, note that a full implementation is long and error-prone, and offer a treap as the simplest option that actually works
“What does Python give you?”Practical knowledgeNothing in the standard library. bisect on a list, or third-party sortedcontainers.SortedList
pch.quizTag Balanced trees — self-check
  1. What happens to a plain BST when values are inserted in sorted order?

    pch.quizShowAnswer

    B — It degenerates into a linked list, so h = n and every operation becomes O(n) — The BST invariant still holds perfectly; the structure is simply useless. This is the concrete reason to say O(h) rather than O(log n).

  2. AVL versus red-black: what is the trade-off?

    pch.quizShowAnswer

    B — AVL balances more strictly, giving faster lookups but more rotations on write; red-black is looser and cheaper to update, which is why libraries prefer it — Read-heavy favours AVL, write-heavy favours red-black. Naming the direction of the trade-off is enough; the rotation case analysis is not.

  3. When is a balanced tree preferable to a hash map?

    pch.quizShowAnswer

    B — When you need ORDER: range queries, kth element, successor/predecessor, or sorted iteration — A hash map wins on plain key lookup and loses everything order-related. If ordering is never needed, the tree is added complexity for nothing.

  4. Why do databases use B-trees rather than AVL trees?

    pch.quizShowAnswer

    B — A B-tree node matches a disk page, so one I/O fetches many keys — height stays tiny and I/O dominates, not comparisons — The right answer is about the cost model, not the algorithm. When a single disk read costs more than thousands of comparisons, you optimise for reads, not comparisons.

  • The problem — a plain BST has no balance guarantee, so sorted input gives h=nh = n and O(n)O(n) operations.
  • The fix — rotations on insert and delete, keeping h=O(logn)h = O(\log n). Rotations are O(1)O(1) and preserve the ordering invariant.
  • AVL vs red-black — strict balance and faster reads, versus looser balance and cheaper writes. Libraries pick red-black.
  • B-trees — many keys per node so a node fills a disk page. Databases, not interviews.
  • Tree over hash map when you need order — ranges, k-th, successor, sorted iteration.
  • In Python — nothing in the standard library. bisect on a list, or third-party sortedcontainers.
  • An unbalanced BST degrades to O(n)O(n) on adversarial (e.g. sorted) input — height is the whole game.
  • AVL trees enforce strict height balance via rotations after every insert/delete: fastest lookups, more rebalancing work.
  • Red-Black trees use coloring rules for a looser but still O(logn)O(\log n)-height guarantee: cheaper writes, standard in most language libraries.
  • Python has no built-in balanced BST — use bisect on a list for light use, or sortedcontainers.SortedList when you need real O(logn)O(\log n) ordered inserts/deletes.

Next: Tries — a tree specialized for strings, giving O(L)O(L) prefix operations regardless of how many words you’ve stored.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading