AVL Tree

An AVL tree (Adelson-Velsky & Landis, 1962 — the original self-balancing BST) keeps every node's subtree heights within 1 of each other. It is more rigidly balanced than a red-black tree, giving slightly faster lookups but slightly slower inserts/deletes.

Complexity

Average Worst Space
AccessSearchInsertionDeletion AccessSearchInsertionDeletion Worst
Θ(log n) Θ(log n) Θ(log n) Θ(log n) O(log n) O(log n) O(log n) O(log n) O(n)

How it works

Each node tracks its height. After a normal BST insert/delete, walk back up updating heights; at any node where balance factor (left height − right height) is −2 or +2, perform one of four rotations: LL, RR, LR, RL. Rebalancing fixes the imbalance in O(1), and at most O(log n) rotations happen per operation.

Python implementation

class Node:
    __slots__ = ("key", "left", "right", "h")
    def __init__(self, key):
        self.key, self.left, self.right, self.h = key, None, None, 1


def _h(n):  return n.h if n else 0
def _bf(n): return _h(n.left) - _h(n.right) if n else 0
def _upd(n): n.h = 1 + max(_h(n.left), _h(n.right))


def _rotate_right(y):
    x = y.left; y.left = x.right; x.right = y
    _upd(y); _upd(x); return x

def _rotate_left(x):
    y = x.right; x.right = y.left; y.left = x
    _upd(x); _upd(y); return y


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
    _upd(node)
    bf = _bf(node)
    if bf > 1 and key < node.left.key:    return _rotate_right(node)        # LL
    if bf < -1 and key > node.right.key:  return _rotate_left(node)         # RR
    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


root = None
for k in [10, 20, 30, 40, 50, 25]:
    root = insert(root, k)
print(root.key)    # 30 (well-balanced root)

Trade-offs

Step-by-Step Insertion Trace

Insert 25: RL Imbalance at 20, Fixed by Two Rotations 20 bf -2 10 40 30 50 25 new 30 20 40 10 25 50 Left: after inserting 10, 20, 30, 40, 50, 25 the root 20 has left height 1 and right height 3. Key 25 went right then left (RL): rotate_right(40), then rotate_left(20). Right: balanced, root 30, height 3.

Running the code above inserts 10, 20, 30, 40 and 50. Inserting 30 after 10 and 20 creates a right-right chain, so the tree rotates left at 10 and 20 becomes the root; inserting 50 triggers the same fix lower down, making 40 the parent of 30 and 50. The final insert of 25 descends 20, 40, 30 and attaches as the left child of 30. Walking back up, node 40 has balance factor +1 (fine), but node 20 now has a left subtree of height 1 and a right subtree of height 3, a balance factor of -2. Because 25 is smaller than 40, the path is right then left, the RL case: first rotate 40's subtree right so 30 rises, then rotate 20 left. Node 30 becomes the root and every node is back within the AVL invariant.

Rotations and Balance Factors

The Four Rebalancing Cases LLbf +2, left-left LRbf +2, left-right RRbf -2, right-right RLbf -2, right-left rotate_rightone rotation left then righttwo rotations rotate_leftone rotation right then lefttwo rotations Top row: the shape of the imbalance below the unbalanced node. Bottom row: the fix, applied once per insert.

The balance factor of a node is the height of its left subtree minus the height of its right subtree. AVL trees require it to stay in {-1, 0, +1} for every node. A single insertion can push it to +2 or -2 at nodes on the path from the new leaf to the root. A rotation is a local pointer rewrite of three nodes that preserves the in-order key sequence (so the tree is still a valid binary search tree) while shifting height from one side to the other. On insertion, one single or double rotation at the lowest unbalanced node restores balance for the whole tree; deletion may need rotations at several ancestors, up to O(log n).

The strict invariant bounds the height to roughly 1.44 log2(n + 2), compared with about 2 log2(n + 1) for red-black trees, which is why AVL lookups touch fewer nodes on average. The price is more frequent rebalancing on writes.

Lookup and Traversal

def contains(node, key):
    while node:
        if key == node.key:
            return True
        node = node.left if key < node.key else node.right
    return False

def inorder(node):
    if node:
        yield from inorder(node.left)
        yield node.key
        yield from inorder(node.right)

print(contains(root, 25), contains(root, 99))   # True False
print(list(inorder(root)))                       # [10, 20, 25, 30, 40, 50]
print(root.h)                                    # 3

Lookup is the plain BST search and needs no balance logic; the balancing work is entirely in the write path. An in-order traversal yields the keys sorted, which makes AVL trees suitable for ordered maps, range scans and rank queries (with an extra subtree-size field).

When to Use It

StructureLookupInsert/deleteNotes
AVL treeO(log n), shallowestO(log n), more rotationsread-heavy ordered maps
Red-black treeO(log n)O(log n), fewer rotationsstd::map, Java TreeMap
Skip listO(log n) expectedO(log n) expectedsimpler concurrency
Hash tableO(1) averageO(1) averageno ordering or range queries
B-treeO(log n), few disk readsO(log n)on-disk indexes

Pick an AVL tree when you need ordered keys in memory and reads clearly dominate writes. For mixed workloads most standard libraries choose red-black trees, and for data on disk B-trees win because they minimise page reads.

Pitfalls

← Back to Algorithms