AVL Trees Explained — Self-Balancing Trees That Never Get Lopsided

AVL Trees Explained
AVL Trees Explained — Self-Balancing Trees That Never Get Lopsided

Data Structures · Algorithms

Learn With Examples

A regular binary search tree can degrade into a linked list — making every search O(n) instead of O(log n). AVL trees fix this with automatic rotations that keep the tree perfectly balanced after every insert and delete. This guide walks you through the balance factor, all four rotation types, and lets you build and balance an AVL tree interactively in real time.

📖 20 min read 🌲 Live AVL tree builder 🔄 Rotation visualiser 💻 Python code ❓ Quiz included

Section 01

The Problem with Unbalanced BSTs

A Binary Search Tree (BST) is a beautiful data structure: every node’s left child is smaller, every right child is larger. Searching is O(log n) — you eliminate half the tree at each step. In theory.

In practice, the order you insert values determines the shape of the tree. Insert values in sorted order — 1, 2, 3, 4, 5 — and you get this:

That is not a tree — it is a linked list. Searching for 5 now requires visiting every single node. Your O(log n) search has degraded to O(n). The whole advantage of a BST is gone.

O(log n)
Balanced BST search — halves remaining nodes each step
O(n)
Degenerate BST search — visits every node in the worst case
1962
Year AVL trees were invented by Adelson-Velsky and Landis
±1
Maximum allowed balance factor in any AVL node

⚠️ The Real-World Risk

Any application that inserts data in sorted or near-sorted order — timestamps, auto-incrementing IDs, alphabetical names — is at risk of BST degeneration. This is why production databases and language runtimes use self-balancing trees, not plain BSTs.

Section 02

What is an AVL Tree?

An AVL tree (named after its inventors Adelson-Velsky and Landis) is a self-balancing binary search tree. It maintains all the BST properties — left child smaller, right child larger — but adds one additional constraint:

|height(left subtree) − height(right subtree)| ≤ 1
For every node in the tree — at every level

After every insertion or deletion, the AVL tree checks this constraint at every affected node. If any node becomes unbalanced, it performs one or two rotations — local restructuring operations — to restore balance. The tree never needs to be rebuilt from scratch.

📏

Height of a Node

The number of edges on the longest path from that node to a leaf. A leaf node has height 0. A null pointer has height −1.

⚖️

Balance Factor

BF = height(left) − height(right). In a valid AVL tree, every node’s BF is −1, 0, or +1. Any other value triggers a rotation.

🔄

Rotations

Local restructuring of 2–3 nodes that restores balance without violating the BST property. There are four types: LL, RR, LR, RL.

📐

Guaranteed Height

An AVL tree with n nodes has height at most 1.44 × log₂(n). A degenerate BST has height n. This gap is enormous for large n.

Section 03

The Balance Factor — The Key Metric

Every node in an AVL tree stores a balance factor (BF) — the difference between the height of its left subtree and the height of its right subtree.

Balance FactorMeaningAVL Valid?Action
BF = −2Right subtree is 2 tallerInvalidRotate left (RR or RL)
BF = −1Right subtree is 1 tallerValidNo action needed
BF = 0Both subtrees equal heightValid — perfectNo action needed
BF = +1Left subtree is 1 tallerValidNo action needed
BF = +2Left subtree is 2 tallerInvalidRotate right (LL or LR)

📌 Stored or Computed?

In most implementations the balance factor (or simply the height) is stored at each node and updated during insertions and deletions. This makes it O(1) to check — no need to traverse the subtree to measure height every time.

Python
class AVLNode:
    def __init__(self, key):
        self.key    = key
        self.left   = None
        self.right  = None
        self.height = 1   # new node starts at height 1

def get_height(node):
    return node.height if node else 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))

Section 04

The Four Rotations — How AVL Rebalances

When the balance factor at a node becomes ±2, the AVL tree performs a rotation. There are four types, each handling a different pattern of imbalance. They all share one beautiful property: they restore balance while preserving the BST ordering property.

CaseWhen it happensBF of unbalanced nodeBF of childFix
LL (Left-Left)Insertion in left subtree of left child+2≥ 0Single right rotation
RR (Right-Right)Insertion in right subtree of right child−2≤ 0Single left rotation
LR (Left-Right)Insertion in right subtree of left child+2< 0Left rotate child, then right rotate node
RL (Right-Left)Insertion in left subtree of right child−2> 0Right rotate child, then left rotate node

Right Rotation (LL case)

The unbalanced node z has BF = +2. Its left child y is taller. We rotate right around z — y takes z’s position, z becomes y’s right child.

Python — Right Rotation
def rotate_right(z):
    y = z.left          # y is the left child of z
    T3 = y.right        # T3 is the right subtree of y

    # Perform rotation
    y.right = z         # z becomes the right child of y
    z.left  = T3        # T3 moves to z's left (BST property preserved)

    # Update heights (z first, then y since y is now higher)
    update_height(z)
    update_height(y)

    return y             # y is the new root of this subtree

Left Rotation (RR case)

Python — Left Rotation
def rotate_left(z):
    y = z.right         # y is the right child of z
    T2 = y.left         # T2 is the left subtree of y

    # Perform rotation
    y.left  = z         # z becomes the left child of y
    z.right = T2        # T2 moves to z's right

    update_height(z)
    update_height(y)

    return y             # y is the new root

🔄 Double Rotations

LR and RL cases require two rotations. For LR: first rotate the left child leftward (turning LR into LL), then rotate the unbalanced node rightward. For RL: first rotate the right child rightward (turning RL into RR), then rotate the unbalanced node leftward. After a double rotation the tree is always balanced.

Section 05

Interactive Rotation Visualiser

Click any rotation to see the before and after states side by side. Notice how the BST ordering property is preserved — an in-order traversal of both trees gives the same sequence.

  Rotation Visualiser
Before (Unbalanced)
After (Balanced)
Click a rotation type above to see how it works.

Section 06

Live AVL Tree Builder

Insert and delete values to build your own AVL tree. The tree rebalances automatically after every operation. Balance factors are shown inside each node — green means balanced, red means a rotation just happened.

  AVL Tree Builder
Insert a value to begin building the AVL tree.

Section 07

Insertion Algorithm — Step by Step

AVL insertion combines standard BST insertion with a recursive rebalancing pass back up to the root.

1

Insert like a normal BST

Recursively traverse the tree comparing the new key with each node. Go left if smaller, right if larger. Insert at the correct null position.

2

Update heights on the way back up

As the recursion unwinds, update the height of each ancestor node: height = 1 + max(height(left), height(right)).

3

Check balance factor at each ancestor

Compute BF = height(left) − height(right) at each node. If |BF| ≤ 1, the node is balanced. Continue up the tree.

4

Identify the case and rotate if BF = ±2

Determine which of the four cases applies (LL, RR, LR, RL) by checking the balance factor of the child. Perform the appropriate single or double rotation.

5

Continue checking upward

After a rotation, continue checking balance factors up to the root. In practice at most one rotation is needed per insertion (but deletion may need O(log n) rotations).

Python — Full AVL Insert
def insert(node, key):
    # Step 1: Standard BST insertion
    if not node:
        return AVLNode(key)

    if key < node.key:
        node.left  = insert(node.left,  key)
    elif key > node.key:
        node.right = insert(node.right, key)
    else:
        return node  # duplicate key — ignore

    # Step 2: Update height
    update_height(node)

    # Step 3: Get balance factor
    bf = get_balance(node)

    # Step 4: Determine case and rotate
    # LL Case
    if bf > 1 and key < node.left.key:
        return rotate_right(node)

    # RR Case
    if bf < -1 and key > node.right.key:
        return rotate_left(node)

    # LR Case
    if bf > 1 and key > node.left.key:
        node.left = rotate_left(node.left)
        return rotate_right(node)

    # RL Case
    if bf < -1 and key < node.right.key:
        node.right = rotate_right(node.right)
        return rotate_left(node)

    return node  # node is balanced — no rotation needed

Section 08

Time and Space Complexity

OperationAverageWorst CaseWhy
SearchO(log n)O(log n)Height is always ≤ 1.44 log₂(n) — guaranteed
InsertO(log n)O(log n)BST insert + at most 2 rotations + height updates up to root
DeleteO(log n)O(log n)BST delete + up to O(log n) rotations on the path back to root
SpaceO(n)O(n)One node per element; each stores key, left, right, height

✅ The Guarantee that Matters

The key advantage of AVL trees over plain BSTs is that all operations are O(log n) in the worst case — guaranteed, regardless of insertion order. A plain BST can degrade to O(n). The cost is slightly higher constant factors due to rotation bookkeeping.

Section 09

AVL vs Red-Black Trees vs Plain BST

PropertyPlain BSTAVL TreeRed-Black Tree
Search worst caseO(n)O(log n)O(log n)
Insert worst caseO(n)O(log n)O(log n)
Rotations per insert0At most 2At most 2
Rotations per delete0O(log n)At most 3
Balance strictnessNoneStrict (BF ≤ 1)Loose (height ≤ 2 log n)
Search speedVariesFaster (shorter tree)Slightly slower
Insert/Delete speedVariesSlightly slowerFaster
Best use caseStatic dataRead-heavy workloadsWrite-heavy workloads
Used inSimple lookupsDatabases, compilersLinux kernel, Java TreeMap

💡 Which to Choose?

Choose AVL trees when your workload is read-heavy — the stricter balance means a shorter tree and faster lookups. Choose Red-Black trees when your workload is write-heavy — fewer rotations on deletion makes writes cheaper. In practice, most language standard libraries use Red-Black trees (Java’s TreeMap, C++ std::map) for their balanced write performance.

Section 10

Real-World Uses of AVL Trees

ApplicationHow AVL trees help
Database indexesMany database engines use AVL or similar balanced trees for in-memory indexes where read performance is critical. Guaranteed O(log n) lookup regardless of data insertion order.
Compilers (symbol tables)Compilers store identifiers (variable names, functions) in AVL trees for fast O(log n) lookup during compilation. GCC historically used AVL trees.
Memory allocatorsSome memory allocators track free memory blocks in AVL trees, enabling fast O(log n) search for a block of the right size.
Geometry / computational geometrySweep-line algorithms for intersections, polygon clipping, and spatial queries use balanced BSTs (including AVL) to maintain the event queue.
Network routing tablesFast prefix lookups for IP routing can be implemented with AVL trees in software routers, giving guaranteed lookup time per packet.
Python’s sortedcontainers libraryThe SortedList, SortedDict, and SortedSet in Python’s popular sortedcontainers library use B-tree variants inspired by AVL balancing.

Section 11

Complete Python Implementation

Python — Complete AVL Tree
class AVLNode:
    def __init__(self, key):
        self.key = key; self.left = self.right = None; self.height = 1

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

    def _rr(self, z):   # right rotation
        y = z.left; z.left = y.right; y.right = z
        self._upd(z); self._upd(y); return y

    def _lr(self, z):   # left rotation
        y = z.right; z.right = y.left; y.left = z
        self._upd(z); self._upd(y); return y

    def _balance(self, node, key):
        self._upd(node)
        bf = self._bf(node)
        if bf > 1:
            if key > node.left.key: node.left = self._lr(node.left)  # LR
            return self._rr(node)                                       # LL
        if bf < -1:
            if key < node.right.key: node.right = self._rr(node.right) # RL
            return self._lr(node)                                       # RR
        return node

    def insert(self, root, key):
        if not root: return AVLNode(key)
        if   key < root.key: root.left  = self.insert(root.left,  key)
        elif key > root.key: root.right = self.insert(root.right, key)
        else: return root
        return self._balance(root, key)

    def _min_node(self, n):
        while n.left: n = n.left
        return n

    def delete(self, root, key):
        if not root: return root
        if   key < root.key: root.left  = self.delete(root.left,  key)
        elif key > root.key: root.right = self.delete(root.right, key)
        else:
            if not root.left:  return root.right
            if not root.right: return root.left
            temp = self._min_node(root.right)
            root.key = temp.key
            root.right = self.delete(root.right, temp.key)
        return self._balance(root, root.key)

    def inorder(self, root):
        return (self.inorder(root.left) + [root.key] + self.inorder(root.right)) if root else []

# Usage
tree = AVLTree()
root = None
for v in [10, 20, 30, 40, 50, 25]:   # sorted order — would break plain BST
    root = tree.insert(root, v)

print("Inorder:", tree.inorder(root))    # → [10, 20, 25, 30, 40, 50]
print("Height:", root.height)             # → 3 (not 6 as it would be unbalanced)

root = tree.delete(root, 20)
print("After delete:", tree.inorder(root)) # → [10, 25, 30, 40, 50]

Section 12

Knowledge Quiz

Six questions to test your AVL tree understanding.

  AVL Trees Quiz
Question 1 of 6

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *