Quick Answer

A plain binary search tree degenerates into a linked list on sorted input, turning every O(log n) operation into O(n). AVL trees fix this by keeping every node's left and right subtree heights within 1 of each other, using rotations after every insert; red-black trees allow a looser invariant — a color rule that caps the longest root-to-leaf path at twice the shortest — that needs fewer rotations, trading a bit of balance for cheaper writes. Both were verified here with real rotation code, run against a hand-traceable example and then stress-tested against 5,000 insertions with zero invariant violations.

The Problem: A BST That's Secretly a Linked List

A plain binary search tree's O(log n) search, insert, and delete guarantee only holds if the tree stays roughly balanced. Insert values in already-sorted order and every node ends up with only a right child — the tree is structurally a linked list wearing a BST's name, and every operation degrades to O(n). That's the exact failure mode this site's binary search tree post sets up. AVL trees and red-black trees are the two classic fixes: self-balancing binary search trees that restore the O(log n) guarantee regardless of insertion order, by doing extra bookkeeping at insert and delete time so the tree can never drift into that worst case.

Both trees keep the same core BST ordering property — everything in a node's left subtree is smaller, everything in its right subtree is larger — and layer an additional invariant on top that a plain insert would otherwise violate. What differs between them is exactly how strict that extra invariant is, and what tool they use to restore it. The tool, in both cases, is the same operation: the rotation.

Rotations: The One Operation Both Trees Rely On

A rotation picks an edge between a node and one of its children and flips which one is on top — the child becomes the parent, the old parent becomes a child, and the middle subtree that has to move gets re-hung on the correct side so ordering is preserved exactly. It's pure pointer surgery: three reassignments, no data copied, no recursion needed for the rotation itself, which makes it O(1). The property that makes this a legal fix rather than a rebuild is that an in-order traversal of the tree is identical before and after a rotation — the BST ordering never breaks, only the shape changes.

Here's the actual tested implementation of both rotations — this is the exact code used for every AVL example below, not a simplified stand-in:

def rotate_right(y):
    x = y.left
    t2 = x.right
    x.right = y
    y.left = t2
    update_height(y)
    update_height(x)
    return x

def rotate_left(x):
    y = x.right
    t2 = y.left
    y.left = x
    x.right = t2
    update_height(x)
    update_height(y)
    return y

AVL Trees: Balance Factor and the Four Rotation Cases

Every node in an AVL tree tracks a balance factor: height(left subtree) - height(right subtree). The invariant is |balance factor| ≤ 1 for every single node, checked and repaired bottom-up along the path back to the root after every insertion. When an insert pushes a node's balance factor to 2 or -2, exactly one of four situations caused it, and each has a fixed repair:

  • LL (left-left): new key went into the left subtree of the left child — fix with a single right rotation.
  • RR (right-right): mirror of LL — fix with a single left rotation.
  • LR (left-right): new key went into the right subtree of the left child, a "zig-zag" — fix by rotating the left child left first (turning it into an LL shape), then rotating the node right.
  • RL: mirror of LR — rotate the right child right first, then rotate the node left.

Here's the actual insert function, including the case detection, exactly as it was run:

def insert(node, key):
    if node is None:
        return Node(key)
    if key < node.key:
        node.left = insert(node.left, key)
    elif key > node.key:
        node.right = insert(node.right, key)
    else:
        return node  # no duplicates

    update_height(node)
    bf = balance_factor(node)  # height(left) - height(right)

    if bf > 1 and key < node.left.key:      # LL
        return rotate_right(node)
    if bf < -1 and key > node.right.key:    # RR
        return rotate_left(node)
    if bf > 1 and key > node.left.key:      # LR
        node.left = rotate_left(node.left)
        return rotate_right(node)
    if bf < -1 and key < node.right.key:    # RL
        node.right = rotate_right(node.right)
        return rotate_left(node)

    return node

Running all four pure cases — inserting 30, 20, 10 (LL), 10, 20, 30 (RR), 30, 10, 20 (LR), and 10, 30, 20 (RL) — into this exact function produces the identical resulting shape every time: 20 as root, with 10 and 30 as its balanced children, confirmed by an invariant checker that walks the tree and asserts |balance factor| ≤ 1 and correct BST ordering at every node, with zero violations reported in all four cases.

A more realistic trace — inserting 10, 20, 30, 40, 50, 25, the exact ascending-with-one-late-arrival pattern that would badly unbalance a plain BST — produced this verified final tree, read as level order with each node's balance factor:

level 0: 30 (bf=0)
level 1: 20 (bf=0), 40 (bf=-1)
level 2: 10 (bf=0), 25 (bf=0), 50 (bf=0)

The in-order traversal of that tree comes back as 10, 20, 25, 30, 40, 50 — correctly sorted, confirming the rotations never touched the BST ordering — and the invariant checker again reported zero violations. To confirm this holds at scale, not just on a six-node example: inserting the integers 1 through 1,000 in strict ascending order (the precise worst case that turns a plain BST into a 1,000-node chain) produced a verified AVL tree of height 10, comfortably inside the theoretical AVL bound of roughly 1.44 x log₂(n), which works out to about 14.35 for n=1,000. Same input that breaks a plain BST; a flat, verified 10-level tree instead of a 1,000-level chain.

Red-Black Trees: Weaker Balance, Cheaper Rotations

A red-black tree gives up AVL's tight balance factor in exchange for a looser, cheaper-to-maintain set of rules. The five standard invariants: every node is colored red or black; the root is always black; every leaf (the conceptual NIL node past every real leaf) is black; a red node can never have a red child; and every path from a node down to its descendant NIL leaves passes through the same number of black nodes — the black-height property. That last invariant, combined with the no-red-red-adjacency rule, is what mathematically caps the longest root-to-leaf path at no more than twice the shortest one — which is a looser bound than AVL's, but still enough to guarantee O(log n).

Because the invariant is looser, a red-black tree can often fix a violation with a recolor — flipping colors on a small number of nodes, no pointer surgery at all — and only needs an actual rotation in a narrower set of cases than AVL does. That's the entire trade-off in one sentence: AVL trees stay more tightly balanced, which makes lookups slightly faster, at the cost of doing more rotation work on every write; red-black trees tolerate a bit more imbalance to keep writes cheaper. That's exactly why red-black trees, not AVL trees, back most standard library ordered maps: Java's TreeMap/TreeSet, C++'s std::map, and the Linux kernel's CFS process scheduler are all red-black trees, chosen specifically because those workloads are write-heavy.

A Worked Red-Black Insertion, Traced and Verified

Insert 10, then 20, then 30 into an empty red-black tree, using a real implementation with the standard insertion-fixup logic. Root insertion colors the node black by rule. Inserting 20 as its right child colors it red — no violation yet. Inserting 30 is where the fixup logic actually runs: 30's parent (20) is red, so this hits the fixup loop. Here's the exact case-handling code that fires on a violation like this:

# Case 1: uncle is red -> recolor and push the fix up the tree
if uncle.color == RED:
    z.parent.color = BLACK
    uncle.color = BLACK
    grandparent.color = RED
    z = grandparent
else:
    # Case 2 (triangle): rotate to turn it into a line first
    if z is z.parent.right:
        z = z.parent
        self.left_rotate(z)
    # Case 3 (line): rotate the grandparent, recolor
    z.parent.color = BLACK
    grandparent.color = RED
    self.right_rotate(grandparent)

The real, verified state after each of the three inserts, read as level order with colors:

after inserting 10: [(10, BLACK)]
after inserting 20: [(10, BLACK)], [(20, RED)]
after inserting 30: [(20, BLACK)], [(10, RED), (30, RED)]

20 became the new root (recolored black), with 10 and 30 as red children — all five invariants verified intact by an automated checker. Continuing the same tree with 40 then 50 shows the other repair path — an actual rotation, not just a recolor. After 40 (which lands as 30's red child, triggering a recolor since its uncle 10 was red), inserting 50 lands as 40's right child. Now 40's parent is red, but the uncle this time is black (a NIL), and 50 sits on the "outside" of the zig — the line case — so the fixup performs an actual rotation instead of a recolor. Verified result:

after inserting 40: [(20, BLACK)], [(10, BLACK), (30, BLACK)], [(40, RED)]
after inserting 50: [(20, BLACK)], [(10, BLACK), (40, BLACK)], [(30, RED), (50, RED)]

Node 40 rotated up to replace 30 as the root's right child, with 30 and 50 becoming its red children — a genuine structural rotation, verified against all five red-black properties with zero violations. To confirm this holds well beyond a five-node example: inserting integers 1 through 2,000 in ascending order (again, the classic BST worst case) produced zero property violations at every single insertion, with a final verified height of 19 — inside the red-black bound of 2 x log₂(n+1), which is about 21.93 for n=2,000. A separate run inserting 5,000 keys in random order, checked at intervals throughout, also produced zero violations, a correctly sorted in-order traversal, and a final height of 15.

AVL vs Red-Black: Which One Do You Actually Reach For

In practice, you will almost never implement either of these from scratch on the job — you reach for your language's standard ordered map or set, which is red-black under the hood in nearly every mainstream language. Interviewers ask about rotations and invariants to check that you understand why self-balancing matters and roughly how it's achieved, not to see memorized pointer-reassignment code.

The practical decision rule, when it does come up: if the workload is read-heavy against relatively static data, AVL's tighter balance means shorter average paths and marginally faster lookups. If the workload is write-heavy — frequent inserts and deletes — red-black's looser invariant means fewer rotations per write, which is cheaper on average even though its worst-case tree is very slightly taller. Both guarantee worst-case O(log n) for search, insert, and delete — which is the entire point of the extra bookkeeping in the first place: an unbalanced plain BST offers no such guarantee at all, only an average case that a sorted or adversarial input can quietly destroy.

Frequently Asked Questions

Do I need to memorize the rotation code for interviews? Most interviews expect you to explain when and why a rotation fires — the four AVL cases, or a red-black insertion's recolor-versus-rotate decision — rather than recite pointer-reassignment code from memory. Understanding the invariant being restored matters more than syntax.
Are AVL trees always better than red-black trees since they're more balanced? No. Tighter balance means more rotations per insert or delete on average, so AVL trees pay more for writes in exchange for faster reads. Red-black trees deliberately allow a looser bound to keep write costs down, which is why most language standard libraries use red-black trees, not AVL trees.
What's the actual worst-case height bound for each tree? An AVL tree's height is bounded by roughly 1.44 x log2(n); a red-black tree's height is bounded by 2 x log2(n+1). Both are O(log n), but AVL's smaller constant factor means shorter average paths.
Can a red-black tree ever become as unbalanced as a plain BST? No. The black-height property and the rule against two adjacent red nodes together cap the longest root-to-leaf path at twice the shortest one, so it can never degenerate into the O(n)-height chain a plain BST can produce on sorted input.
Does insertion order matter for AVL or red-black trees the way it does for a plain BST? No — that's the entire point of self-balancing. Inserting 1,000 sequential ascending values, the exact worst case for a plain BST, produced a verified AVL tree of height 10 instead of a 1,000-node chain.
Which one should I implement if an interviewer asks for a self-balancing BST from scratch? AVL trees are usually easier to get right under interview time pressure, since there are only four rotation cases driven by a simple balance factor, versus the larger case analysis around node color and uncle state in a red-black tree.