AVL Trees
Imagine a phone-book application built on a plain binary search tree. It only ever inserts contacts in alphabetical order. Every new name attaches as the right child of the previous one. The tree degenerates into a linked list of depth . A lookup that should take microseconds now scans thousands of entries. The asymptotic promise of search silently collapses to . AVL trees were introduced by Georgy Adelson-Velsky and Evgenii Landis in 1962. They fix this by attaching a simple invariant to every ordinary BST. At each node, the heights of the left and right subtrees may differ by at most one. This "height-balance" condition is checked after every insertion and deletion. It is restored using local restructuring operations called rotations. The result is the first data structure ever proven to guarantee logarithmic height. This means logarithmic-time search, insertion, and deletion. It holds no matter what order the keys arrive in. The idea matters because unordered or adversarial input is the norm in real systems. This is true, not the exception. Sorted feeds, timestamped logs, and sequential IDs are all common sources of trouble. They produce the worst-case insertion order that cripples a naive BST. AVL trees were the historical proof-of-concept that self-balancing search trees are possible at all. They opened the door to the entire family of balanced trees. This includes Red-Black Trees, B-trees, and beyond. These now underlie databases, language runtimes, and operating-system kernels. This article gives the precise definition of the balance invariant. It covers the four rotation cases that restore it. It includes complete pseudocode for insertion and deletion. It gives a rigorous derivation of the height bound. This uses a Fibonacci-style recurrence. It also provides a fully worked insertion trace. Finally, it discusses where AVL trees are used. It notes where their stricter balancing gives way to the more relaxed but faster-to-update Red-Black trees.
- An AVL tree is a BST where every node's left and right subtree heights differ by at most 1 (the balance factor is in ).
- Insertions and deletions can violate this invariant at ancestors of the changed node; it is restored using rotations (single or double).
- There are exactly four imbalance shapes — Left-Left, Right-Right, Left-Right, Right-Left — each fixed by one or two rotations in time.
- The height-balance invariant forces : the minimum node count for height grows like Fibonacci numbers, giving .
- Search, insert, and delete are all worst case; insert needs at most one rebalancing rotation (pair), delete may need rotations up the path.
- AVL trees are more rigidly balanced than Red-Black Trees, giving faster lookups but slower updates — a classic read/write tradeoff.
Bounding the height-difference between sibling subtrees to at most 1, and restoring that bound with -time rotations after every update, forces the tree's height — and hence every operation's cost — to stay .
The idea
An AVL tree is a binary search tree augmented with one extra structural rule: for every node , define its balance factor
where the height of an empty subtree is defined as (or equivalently, treat a missing child as height and a leaf as height ). The AVL invariant requires
A tree satisfying this for all nodes is called height-balanced. Note this is strictly weaker than requiring the tree be a complete or perfect binary tree — an AVL tree can have subtrees whose sizes differ by a large amount, as long as their heights are close. This distinction is the whole reason AVL trees achieve logarithmic height with only local, -time repair operations, rather than needing global rebuilding. Because BST search cost is proportional to the depth of the node found, and depth is bounded by tree height, the entire performance story reduces to one question: how tall can a height-balanced tree with nodes possibly be? The answer — proven below — is , with a small constant close to . Every insertion or deletion is followed by a bottom-up walk that recomputes balance factors and applies a rotation — a local pointer-rearrangement that preserves the BST ordering property while reducing a subtree's height back to what it was before the update, or leaving it unchanged. Because a single rotation is , and at most ancestors must be checked, each update runs in time — matching the cost of the search that locates the update point.
How it works
Rotations: the repair mechanism
A rotation re-roots a locally unbalanced subtree without breaking the BST ordering invariant (left-subtree keys node key right-subtree keys). There are two elementary rotations, mirror images of each other:
- Right rotation at node with left child : becomes the new subtree root, becomes 's right child, and 's original right subtree () becomes 's new left subtree.
- Left rotation is the mirror: rotate around a node whose right child is the new root.
These are combined to fix four imbalance shapes that can arise after an insertion or deletion makes some node's balance factor reach :
| Case | Shape of imbalance | Fix |
|---|---|---|
| LL | left child's left subtree too tall | single right rotation at the unbalanced node |
| RR | right child's right subtree too tall | single left rotation at the unbalanced node |
| LR | left child's right subtree too tall | left rotation at the left child, then right rotation at the node |
| RL | right child's left subtree too tall | right rotation at the right child, then left rotation at the node |
Insertion
Insertion proceeds exactly as in an ordinary BST — walk down comparing keys, attach the new node as a leaf — and then walks back up the search path, recomputing heights and balance factors, and rotating at the first (deepest) node found unbalanced. ``` function INSERT(root, key): if root is null: return new Node(key, height = 0) if key < root.key: root.left = INSERT(root.left, key) else if key > root.key: root.right = INSERT(root.right, key) else: return root // duplicate key: no-op (policy choice) UPDATE_HEIGHT(root) return REBALANCE(root) function REBALANCE(node): balance = HEIGHT(node.left) - HEIGHT(node.right) if balance > 1: // left-heavy if HEIGHT(node.left.left) >= HEIGHT(node.left.right): return ROTATE_RIGHT(node) // LL case else: node.left = ROTATE_LEFT(node.left) // LR case return ROTATE_RIGHT(node) if balance < -1: // right-heavy if HEIGHT(node.right.right) >= HEIGHT(node.right.left): return ROTATE_LEFT(node) // RR case else: node.right = ROTATE_RIGHT(node.right) // RL case return ROTATE_LEFT(node) return node // already balanced function ROTATE_RIGHT(y): x = y.left T2 = x.right x.right = y y.left = T2 UPDATE_HEIGHT(y) UPDATE_HEIGHT(x) return x function ROTATE_LEFT(x): y = x.right T2 = y.left y.left = x x.right = T2 UPDATE_HEIGHT(x) UPDATE_HEIGHT(y) return y function HEIGHT(node): if node is null: return -1 return node.height function UPDATE_HEIGHT(node): node.height = 1 + max(HEIGHT(node.left), HEIGHT(node.right)) ```
A key fact used implicitly above: after a single insertion, at most one rotation (or one double rotation, which is still pointer work) is ever needed to restore the whole tree's balance. Once the first unbalanced ancestor on the path back to the root is fixed, the height of that subtree returns to its pre-insertion value, so no ancestor further up can have become unbalanced. This is why the `REBALANCE` call can `return` immediately after one fix at each level, and correctness only requires walking upward recomputing balance factors, stopping the rotation search once satisfied (the recursive pseudocode above still visits every ancestor to update heights, but performs at most one rotation total).
Deletion
Deletion also starts as standard BST deletion (splice out a leaf, or a node with one child, or replace a two-child node by its in-order predecessor/successor and delete that instead), then rebalances every ancestor on the path back to the root — because unlike insertion, a single deletion can require rotations at different levels. ``` function DELETE(root, key): if root is null: return null if key < root.key: root.left = DELETE(root.left, key) else if key > root.key: root.right = DELETE(root.right, key) else: if root.left is null: return root.right if root.right is null: return root.left successor = MIN_NODE(root.right) // in-order successor root.key = successor.key root.right = DELETE(root.right, successor.key) UPDATE_HEIGHT(root) return REBALANCE(root) // may trigger at every level ```
Complexity & correctness
Height bound — the central theorem. Let be the minimum number of nodes in an AVL tree of height . A tree of minimum size achieving height has a root whose two subtrees have heights and (the largest imbalance the invariant permits), and each of those subtrees is itself minimum-sized for its height. This gives the recurrence
This is Fibonacci-like. Substituting one can check it satisfies the Fibonacci recurrence exactly, so where is the -th Fibonacci number. Since grows as with ,
So the height of any -node AVL tree is — at most about 44% more than the information-theoretic minimum of a perfectly balanced tree. This is the whole payoff: search, insert, and delete all cost because each walks a single root-to-node path.
Why rotations preserve the BST property. A right rotation at (with left child ) only reassigns three pointers: becomes the new local root, becomes 's right child, and 's old right subtree becomes 's new left child. In-order traversal order is unchanged — check the key ranges: everything in 's left subtree is , everything in satisfies (true both before and after, since was 's right subtree, hence , and was part of 's left subtree, hence ), and everything in 's right subtree is . The new arrangement respects all these bounds, so the BST invariant is preserved. The mirror argument holds for left rotation.
Why rotations restore height-balance. Take the LL case: node has , and its left child has (this is guaranteed because the imbalance was just created by an insertion into 's left subtree). Before the insertion, suppose 's right subtree had height ; then had height , and (being itself balanced) 's two children and had heights in . A single right rotation at makes the new root with children (height or ) and the rebuilt (whose children are now and , both height or ) — a height-balanced configuration whose overall height equals 's height before the insertion. This is the crux of the one-rotation argument for insertion: the rotation restores not just balance but the exact pre-insertion height of the subtree rooted there, so no ancestor further up needs to change, and only one rotation (or one LR/RL double-rotation) is ever required per insertion.
Deletion is asymmetric. Removing a node can decrease a subtree's height even after rebalancing (unlike insertion, where the rotated subtree's height is restored exactly to its old value). That height decrease can propagate: an ancestor that was fine before may now have a imbalance, requiring another rotation, whose result may again shrink height and propagate further. In the worst case this cascades all the way to the root, so deletion may perform rotations — still total time, since each is work along an -length path, but structurally different from insertion's single-fix guarantee.
Complexity summary.
| Operation | Time | Space | Justification |
|---|---|---|---|
| Search | extra | bounded by height | |
| Insert | recursion (or iterative) | search path + at most one rotation | |
| Delete | recursion | search/splice path + up to rotations | |
| Build from sorted/unsorted items | insertions | ||
| In-order traversal | stack | visits each node once |
The tradeoff: AVL's tight balance () gives a smaller constant on lookup height than looser schemes, at the cost of more frequent/expensive rebalancing on updates — the exact motivation for the more relaxed invariant used by Red-Black Trees.
Worked example
Insert the keys into an initially empty AVL tree, one at a time.
| Step | Action | Resulting shape / imbalance | Fix applied |
|---|---|---|---|
| 1 | insert 10 | single node 10 | none |
| 2 | insert 20 | 10 (right: 20) — bf(10) = | none needed |
| 3 | insert 30 | 10→20→30 chain; bf(10) becomes (RR case) | left rotation at 10 → new root 20, children 10 and 30 |
| 4 | insert 25 | 25 attaches as left child of 30; bf(20)=, bf(30)= | none needed, tree balanced |
| 5 | insert 5 | 5 attaches as left child of 10; bf(20)=0 overall | none needed |
| 6 | insert 15 | 15 attaches as right child of 10; check bf(10): left=5(h0), right=15(h0) → bf=0; bf(20): left subtree height 1, right subtree(30,25) height 1 → bf=0 | none needed |
Final tree after all six insertions: ``` 20 / \ 10 30 / \ / 5 15 25 ``` Every node's balance factor is in (root: left height 1, right height 1, bf=0; node 10: bf=0; node 30: left height 0, no right, bf=+1). Height is 2 for nodes, comfortably within the bound . Now delete key 30. Standard BST deletion: 30 has one child (25), so splice it out, promoting 25 into 30's place. ``` 20 / \ 10 25 / \ 5 15 ``` Recompute balance factors bottom-up: node 25 (leaf) bf=0; node 10 bf=0 (heights 0,0); node 20: left height 1, right height 0 → bf = . Still within — no rotation needed here, illustrating that not every deletion triggers rebalancing, even though the algorithm always checks.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Forgetting to update heights before checking balance factor | Rotation logic reads stale height fields | Always call `UPDATE_HEIGHT` immediately after recursive insert/delete, before computing `bf` |
| Choosing the wrong rotation direction (single vs. double) | Confusing which grandchild is taller in the LR/RL cases | Explicitly compare `HEIGHT(node.left.left)` vs `HEIGHT(node.left.right)` (and mirror) rather than guessing from key values |
| Treating deletion like insertion (stopping after first rotation) | Insertion needs only one fix; deletion can cascade | Always continue rebalancing checks up to the root on delete |
| Off-by-one in height of empty subtree | Some implementations use height for null instead of , shifting all balance-factor arithmetic | Fix a single convention (this article uses height() ) and use it consistently everywhere |
| Ignoring duplicate keys | Naively inserting an equal key can create infinite loops or silently duplicate structure | Decide a policy up front: reject duplicates, count them, or route equals into a fixed side |
| Rebuilding the whole tree on every update instead of local rotation | Misunderstanding that rotations are , not | Only three pointers change per rotation; never traverse a rotated subtree to "fix" it |
Pioneers & history
Origins · 1 minGeorgy Maximovich Adelson-Velsky and how this was discovered
Adelson-Velsky worked at the Institute for Theoretical and Experimental Physics and later became a leading figure in Soviet computer science, contributing to chess-playing programs (his team's Kaissa won the first World Computer Chess Championship in 1974) and to foundational data-structure theory. With his student Evgenii Landis, he published the 1962 paper "An algorithm for the organization of information," introducing the first self-balancing binary search tree — now universally called the AVL tree in honor of both authors.
Where this matters
AVL trees are the canonical teaching example for self-balancing search trees, but their strict height-balance also makes them attractive whenever lookups vastly outnumber updates: because AVL height is provably closer to the optimal than a red-black tree's (whose height bound is ), AVL trees give faster worst-case search at the price of costlier rebalancing. This tradeoff shows up in:
- In-memory database indexes and caches that are built once (or rarely updated) but queried millions of times, where the tighter height bound directly reduces average query latency.
- Language and library implementations of ordered maps/sets in some ecosystems (certain STL-like libraries, and historically some Linux kernel data structures) choose AVL-style balancing for read-heavy internal tables.
- Educational and reference implementations of interval trees, order-statistics trees, and other augmented BSTs, where AVL's simpler, more local proof of correctness makes it a cleaner base to build augmentation logic on top of.
- Foreshadowing further structures: the same balance-and-rotate idea, relaxed to permit looser invariants for faster amortized updates, produces Red-Black Trees — the structure underlying most production ordered containers (C++'s `std::map`, Java's `TreeMap`, the Linux kernel's `rbtree`) — and generalizes further to weight-balanced trees, treaps, and B-trees used in on-disk indexes.
Ask the tutor: Can you show why a red-black tree's looser invariant still guarantees height, and compare its rotation cost to AVL's?
Why is the AVL invariant based on height rather than the number of nodes in each subtree?
Balancing by subtree size (equal node counts on both sides) is much harder to maintain with cheap local operations — a single insertion can shift the "ideal" split point for the whole path to the root, requiring expensive restructuring. Balancing by height is far more forgiving: two subtrees can differ wildly in node count yet still have nearly equal height, since a subtree can be sparse-but-tall or dense-but-short. AVL's insight is that height, not size, is what actually determines search cost, and height differences of at most 1 can be restored with a single rotation. This locality is what makes logarithmic-time rebalancing possible at all.
How does an AVL tree compare to a red-black tree in practice?
AVL trees maintain a tighter balance (), giving height at most about , versus red-black trees' looser bound of about . That means AVL trees have faster worst-case lookups. However, red-black trees need fewer rotations per update on average (insertion needs at most 2 rotations, deletion at most 3, both with more color-flip-only fast paths), making them cheaper for write-heavy workloads. Most general-purpose library maps (Java's `TreeMap`, C++'s `std::map`) use red-black trees as a better all-around default, while AVL trees are chosen when reads dominate.
Can an AVL tree become unbalanced without anyone noticing, if rotations are implemented incorrectly?
Yes — this is one of the most common bugs. If a buggy rebalancing routine misses a case or stops checking ancestors too early (especially in deletion, which can require rebalancing at every level), the tree can silently accumulate a taller-than-guaranteed shape. There's no runtime crash; operations simply get slower as grows, often only noticeable under production-scale load. This is why implementations should include an invariant-checking assertion (verify for all ) during testing.
Do AVL trees support order-statistics operations like "find the k-th smallest element"?
Not out of the box, but they are an excellent base for augmentation. By storing a subtree-size field at each node (updated alongside height during rotations and insertions/deletions, still per rotation), an AVL tree becomes an order-statistics tree supporting `select(k)` and `rank(x)` in time. The same augmentation technique generalizes to interval trees (storing max endpoint) and other range-query structures, since AVL rotations only touch nodes, so any per-node aggregate that can be recomputed in from a node's children survives rotation cheaply.
Why is the height bound derived using Fibonacci numbers?
The recurrence for the minimum node count at height , , is structurally identical to the Fibonacci recurrence because the AVL invariant permits the two subtrees of the "worst-case" (sparsest) balanced tree to differ in height by exactly 1 — mirroring how Fibonacci numbers are defined as sums of the two preceding terms. Solving this recurrence shows grows like where is the golden ratio, and inverting that relationship gives the height bound . It's a clean example of how a combinatorial recurrence directly yields an asymptotic bound.
What happens if I insert keys in already-sorted order into an AVL tree — does it still stay balanced?
Yes, and this is exactly the scenario AVL trees are designed to handle. In a plain BST, inserting in order produces a degenerate linked list of height . In an AVL tree, the same insertion sequence triggers a rotation roughly every other insertion (alternating LL/RR-style fixes), and the invariant guarantees the resulting height never exceeds about regardless of insertion order. You can verify this by tracing insertions of : after inserting 3, a left rotation at 1 already restores a shape resembling a balanced ternary structure, and the pattern continues self-correcting.
Is deletion in an AVL tree ever more expensive, asymptotically, than insertion?
Not asymptotically — both remain — but deletion has a worse constant factor in the number of rotations performed. Insertion provably needs at most one rotation (or one double rotation) because the fix restores the exact pre-insertion height of the affected subtree. Deletion can shrink a subtree's height even after rebalancing, so the imbalance can propagate and trigger a rotation at every level up to the root, in the worst case rotations. Since each rotation is still , the total work remains , just with a larger hidden constant than insertion.