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.

What you’ll learn

  • 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 (bisectbisect, sortedcontainers.SortedListsortedcontainers.SortedList).

Why balance matters

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!
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.

AVL trees: strict height balance

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

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 2020’s right subtree (rooted at 3030) is taller than its left (1515), so 2020 becomes right-heavy. A left rotation promotes 2020’s right child (3030)… conceptually — in this small example the rotation re-centers the tree around 2020 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

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 NoneNone 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::mapstd::map / std::setstd::set, Java’s TreeMapTreeMap, and the Linux kernel’s scheduler.

The Python reality: no built-in balanced BST

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

  1. bisectbisect 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 nn.
  2. sortedcontainers.SortedListsortedcontainers.SortedList — a third-party package (allowed on most judges and installable via pippip) 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)
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)

Complexity at a glance

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
bisectbisect on a listO(logn)O(\log n)O(n)O(n)O(n)O(n)no dependency, fine for small nn
sortedcontainers.SortedListsortedcontainers.SortedListO(logn)O(\log n)O(logn)O(\log n) amortizedO(logn)O(\log n) amortizedclosest pure-Python analogue

Where this shows up in contests

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 SortedListSortedList (or bisectbisect, 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.

Practice — real LeetCode problems

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.

LC 110 — Balanced Binary Tree · Easy

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

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

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

Editorial

Balance must hold at every node, not just the root. The obvious solution calls a heightheight 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-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 == -1lh == -1 before even computing rhrh short-circuits whole subtrees once failure is known.

[1,2,2,3,3,null,null,4,4][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

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

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

Examples. [-10,-3,0,5,9][-10,-3,0,5,9] gives a balanced BST of height 3 · [1,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]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][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

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

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

Examples. A right-leaning chain [1,null,2,null,3,null,4][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][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.

Recap

  • 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 bisectbisect on a list for light use, or sortedcontainers.SortedListsortedcontainers.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.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did