The Analogy That Makes AVL Trees Click
A smart cinema manager would rebalance the seating every time someone new arrived — always keeping the middle seat occupied first, so no walk ever exceeds log₂(100) ≈ 7 rows. That manager's rebalancing rule is exactly what an AVL tree does.
Every time a node is inserted or deleted, the AVL tree checks whether any ancestor has become "lopsided" (height difference > 1) and fixes it immediately with a rotation. The result: every search, insert, and delete is guaranteed O(log n) — always.
An AVL tree (named after Adelson-Velsky and Landis, 1962) is a self-balancing binary search tree. It maintains the AVL property: for every node, the heights of its left and right subtrees differ by at most 1. The moment a violation is detected, the tree rotates nodes to restore balance before returning control.
A plain Binary Search Tree degrades to a linked list when data arrives in sorted order — giving O(n) operations. AVL trees pay a tiny overhead on inserts/deletes (at most two rotations) to guarantee O(log n) in the worst case, forever. No adversarial insertion sequence can break them.
Watch an AVL Tree Build Itself
The animation below inserts 10 → 20 → 30 → 15 → 25 → 5 → 1 one by one. Watch balance factors update after each insert and rotations fire automatically when the tree goes out of balance. Use STEP to advance manually or PLAY for auto-play.
BF = height(left subtree) − height(right subtree). BF 0 = perfectly symmetric. ±1 = allowed, no action. ±2 = triggers an immediate rotation. The BF is recalculated bottom-up after every insert/delete.
The Four Rotation Types — Animated Phase-by-Phase
When balance breaks, exactly one of four rotation patterns applies. Each animation loops through Before → Rotating → After, showing which nodes move and where.
Always check two BF values: the unbalanced node z and its
taller child y. Same-direction lean (++ or −−) → single rotation.
Opposite-direction lean (+− or −+) → double rotation. The signs determine the answer
deterministically — no guessing.
Python Implementation — Full AVL Tree
A complete, self-contained AVL tree in Python. Each block maps to a phase in the animations above.
class Node:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
self.height = 1 # leaf node starts at height 1
def get_height(node):
return node.height if node else 0 # None → 0
def get_balance(node):
return get_height(node.left) - get_height(node.right) if node else 0
def update_height(node):
node.height = 1 + max(get_height(node.left), get_height(node.right))
Computing height by traversing to every leaf costs O(n). By storing it in the node and
updating O(1) after each rotation, get_balance() runs in O(1) and every
insert/delete stays O(log n).
rotate_right(z) — z's left child becomes new root; z becomes its right child.rotate_left(z) — mirror of above; z's right child becomes new root.def rotate_right(z):
# z has BF=+2 (left-heavy). y = z's left child.
y = z.left
T2 = y.right # T2 moves to become z's new left subtree
y.right = z # y rises to take z's place
z.left = T2
update_height(z) # update LOWER node FIRST (z is now below y)
update_height(y)
return y # y is the new subtree root
def rotate_left(z):
# z has BF=-2 (right-heavy). y = z's right child.
y = z.right
T2 = y.left
y.left = z
z.right = T2
update_height(z)
update_height(y)
return y
def insert(node, val):
# ── 1. Standard BST insert ──────────────────────────────
if not node:
return Node(val)
if val < node.val: node.left = insert(node.left, val)
elif val > node.val: node.right = insert(node.right, val)
else: return node # duplicate — ignore
# ── 2. Update height on the way back up ─────────────────
update_height(node)
b = get_balance(node)
# ── 3. Four rotation cases ──────────────────────────────
# LL: left-left heavy → single right rotation
if b > 1 and val < node.left.val:
return rotate_right(node)
# RR: right-right heavy → single left rotation
if b < -1 and val > node.right.val:
return rotate_left(node)
# LR: left child is right-heavy → left then right
if b > 1 and val > node.left.val:
node.left = rotate_left(node.left)
return rotate_right(node)
# RL: right child is left-heavy → right then left
if b < -1 and val < node.right.val:
node.right = rotate_right(node.right)
return rotate_left(node)
return node # already balanced — no rotation needed
Python unwinds the recursion from the newly inserted leaf back to the root, running
update_height and the four balance checks at every ancestor.
The first unbalanced node rotates, and the stack continues upward — now with a
balanced subtree in place of the broken one.
def min_node(node):
while node.left: node = node.left
return node
def delete(node, val):
if not node: return node
# ── 1. BST delete ───────────────────────────────────────
if val < node.val: node.left = delete(node.left, val)
elif val > node.val: node.right = delete(node.right, val)
else:
if not node.left or not node.right:
node = node.left or node.right # leaf / single child
else:
s = min_node(node.right) # inorder successor
node.val = s.val
node.right = delete(node.right, s.val)
if not node: return node
# ── 2. Rebalance (same logic as insert) ─────────────────
update_height(node)
b = get_balance(node)
if b > 1:
if get_balance(node.left) >= 0: return rotate_right(node)
node.left = rotate_left(node.left); return rotate_right(node)
if b < -1:
if get_balance(node.right) <= 0: return rotate_left(node)
node.right = rotate_right(node.right); return rotate_left(node)
return node
root = None
for v in [10, 20, 30, 15, 25, 5, 1]:
root = insert(root, v)
def inorder(node, out=None):
if out is None: out=[]
if node:
inorder(node.left, out); out.append(node.val); inorder(node.right, out)
return out
print("In-order :", inorder(root))
print("Root :", root.val)
print("Height :", root.height)
print("Root BF :", get_balance(root))
root = delete(root, root.val) # delete the root node
print("After del:", inorder(root))
print("New root :", root.val)
print("Balanced :", abs(get_balance(root)) <= 1)
In-order traversal of any valid BST always yields a sorted sequence. If the output is sorted after every insert and delete, the BST property is intact. Combined with all |BF| ≤ 1, the full AVL property is proven.
Time & Space Complexity
| Operation | AVL Tree | Plain BST (worst) | Sorted Array | Guaranteed? |
|---|---|---|---|---|
| Search | O(log n) | O(n) | O(log n) | Always |
| Insert | O(log n) | O(n) | O(n) | Always |
| Delete | O(log n) | O(n) | O(n) | Always |
| Rotation cost | O(1) each | N/A | N/A | ≤ 2 per insert |
| Rotations per delete | O(log n) max | N/A | N/A | Unlike insert |
| Space | O(n) + 1 int/node | O(n) | O(n) contiguous | +1 int overhead |
| Sorted iteration | O(n) in-order | O(n) | O(n) | Always sorted |
| Tree height bound | ≤ 1.44 log₂(n+2) | n in worst case | N/A | Proven bound |
AVL trees are more strictly balanced — lookups are ~30% faster than Red-Black on average. But deletion can trigger O(log n) rotations, while Red-Black caps at 3. For write-heavy workloads (databases, OS schedulers) Red-Black is preferred. For read-heavy workloads (in-memory dictionaries, routers) AVL wins.
Where AVL Trees Are Used in Practice
AVL Tree vs Plain BST — Side-by-Side
╰─ 2
╰─ 3
╰─ 4
╰─ 5
┌──┴──┐
1 4
┌──┴──┐
3 5
Golden Rules — AVL Trees
rotate_right(z), call update_height(z) before
update_height(y). Reversing this produces wrong heights — a subtle silent bug
that corrupts every BF check upward.
insert() and delete() must return the (possibly rotated)
subtree root, and the caller must assign it:
node.left = insert(node.left, val).