DSA Trees
AVL Trees
You insert keys 1…n into a plain BST. The tree becomes a stick; every lookup is linear. Dual conflict: the search-tree ordering is correct; the height is not. AVL keeps a of at most 1 in absolute value.
After each update, walk ancestors and when |BF|=2, preserving the . Height stays Θ(log n) by a Fibonacci-style recurrence.
Algorithm journey
Ordinary BST insert/delete
Then climb parents updating height.
Detect |BF|=2 on the path.
LL / LR / RR / RL
One or two rotations repair locally.
Public API returns
Every node has |BF|≤1 again.
What actually happens
Naive idea: use an unbalanced BST. Breaks on sorted inserts: height n, Θ(n) search.
Better idea: store heights, enforce |BF|≤1 with rotations. Insert needs at most one single/double rebalance; delete may walk further up.
The improved design still fails if height conventions mix (null=0 vs −1), if you rotate without updating heights/parents, or if you need fewer rotations under heavy updates (then red-black may win constants).
Invariant
After every public operation, the structure is a BST and every node has balance factor in {-1, 0, +1}. Stored heights match 1 + max(child heights) under a single null convention.
How to choose AVL
1. Lookup-heavy ordered map? AVL's tighter balance helps.
2. Update-heavy library default? Red-black often rotates less.
3. Mostly static? Sorted array + binary search may beat both.
Quantitative check
Fibonacci bound: n ≥ F_{h} style growth implies height ≤ ~1.44 log₂(n+2). At n=10⁶, AVL height is tens of comparisons, not a million.
Ordering
BST invariant
Every subtree still owns a key interval.
Balance
|BF| ≤ 1
At every reachable node.
Worst height
Θ(log n)
Proved by a Fibonacci-like recurrence.
Fix one height convention and use it everywhere
These notes use height(null) = 0 and height(leaf) = 1. For a non-null node,height = 1 + max(height(left), height(right)). Define the balance factor as BF(v) = height(v.left) − height(v.right); positive means left-heavy and negative means right-heavy. The alternative null = −1, leaf = 0 convention is equally valid, but mixing conventions creates off-by-one bugs.
BST order
All keys respect the tree's explicit duplicate policy and ancestor bounds.
Short takeaway
AVL is a BST that keeps |balance factor| ≤ 1 via rotations, forcing Θ(log n) height. Pick a single height convention and update metadata after every rotation.
Exercises
- Insert 1…7 into an empty AVL. Which rotation fires first, and what is BF at the new root?
- Why does |BF|=2 with a zigzag child (LR) need two rotations?
- n=10⁶ sorted inserts: compare plain BST height vs AVL height bound ~1.44 log₂ n.
Primary sources
- Adelson-Velsky & Landis (1962): An algorithm for the organization of information.
- Cormen et al., CLRS: balanced search trees / rotation mechanics.
- Knuth, The Art of Computer Programming Vol. 3: balanced trees.
Stored height
Every node's height exactly matches 1 + max of its children's heights.
Short takeaway
AVL is a BST that keeps |balance factor| ≤ 1 via rotations, forcing Θ(log n) height. Pick a single height convention and update metadata after every rotation.
Exercises
- Insert 1…7 into an empty AVL. Which rotation fires first, and what is BF at the new root?
- Why does |BF|=2 with a zigzag child (LR) need two rotations?
- n=10⁶ sorted inserts: compare plain BST height vs AVL height bound ~1.44 log₂ n.
Primary sources
- Adelson-Velsky & Landis (1962): An algorithm for the organization of information.
- Cormen et al., CLRS: balanced search trees / rotation mechanics.
- Knuth, The Art of Computer Programming Vol. 3: balanced trees.
AVL balance
Every node has balance factor −1, 0, or +1 after a public operation.
Short takeaway
AVL is a BST that keeps |balance factor| ≤ 1 via rotations, forcing Θ(log n) height. Pick a single height convention and update metadata after every rotation.
Exercises
- Insert 1…7 into an empty AVL. Which rotation fires first, and what is BF at the new root?
- Why does |BF|=2 with a zigzag child (LR) need two rotations?
- n=10⁶ sorted inserts: compare plain BST height vs AVL height bound ~1.44 log₂ n.
Primary sources
- Adelson-Velsky & Landis (1962): An algorithm for the organization of information.
- Cormen et al., CLRS: balanced search trees / rotation mechanics.
- Knuth, The Art of Computer Programming Vol. 3: balanced trees.
Metadata
Parent, size, count, or aggregate fields agree with the rotated child links.
Short takeaway
AVL is a BST that keeps |balance factor| ≤ 1 via rotations, forcing Θ(log n) height. Pick a single height convention and update metadata after every rotation.
Exercises
- Insert 1…7 into an empty AVL. Which rotation fires first, and what is BF at the new root?
- Why does |BF|=2 with a zigzag child (LR) need two rotations?
- n=10⁶ sorted inserts: compare plain BST height vs AVL height bound ~1.44 log₂ n.
Primary sources
- Adelson-Velsky & Landis (1962): An algorithm for the organization of information.
- Cormen et al., CLRS: balanced search trees / rotation mechanics.
- Knuth, The Art of Computer Programming Vol. 3: balanced trees.
BF = +1
left subtree is one level taller
BF = 0
subtrees have equal height
BF = −1
right subtree is one level taller
Interactive: insert into an AVL tree
This original visualization shows rotations occurring during live insertion. Insert ascending or descending keys and compare the shape with a plain BST built from the same sequence.
Interactive: derive LL, RR, LR, and RL repairs
Switch among the four imbalance shapes. Track the balance factor, the subtree root returned by each rotation, the preserved inorder sequence, and why the demoted node's height must be updated before the promoted node's height.
AVL rotation laboratory
Rotations change shape, not inorder order
insert 30, 20, 10
one right rotation at 30
Step 1 of 3
Insertion creates an LL imbalance
BF = h(left) − h(right)Pointer change
Key 10 entered the left subtree of 20, which is itself the left child of 30. Node 30 now has BF = +2.
Ordering proof
10 < 20 < 30
Height update order
Heights were recomputed on the return path: 10, then 20, then 30.
Result
The first unbalanced ancestor is 30.
Keyboard: focus within the demo, then use ← →, Home, or End.
A rotation is an order-preserving local rewrite
Right rotation around y
x = y.leftPromote x, set x.right = y, and move the middle subtreeT₂ = x.right (before rewiring) to y.left. The key intervals remainT₁ < x < T₂ < y < T₃.
Left rotation around x
y = x.rightPromote y, set y.left = x, and move the middle subtreeT₂ = y.left (before rewiring) to x.right. This is the exact mirror operation.
Height-update dependency
After a right rotation, update demoted y first; promoted x now depends on the new height ofy. After a left rotation, update demoted x first, then promoted y. Reversing this order can leave a plausible but stale height that breaks a later rebalance decision.
Classify imbalance from subtree balance, not from names alone
| Case | Balance-factor test | Repair | Detail |
|---|---|---|---|
| LL | BF(z) = +2 and BF(z.left) ≥ 0 | rightRotate(z) | One right rotation; child BF can be 0 after deletion. |
| LR | BF(z) = +2 and BF(z.left) < 0 | z.left = leftRotate(z.left); rightRotate(z) | First straighten the zigzag, then rotate z. |
| RR | BF(z) = −2 and BF(z.right) ≤ 0 | leftRotate(z) | One left rotation; child BF can be 0 after deletion. |
| RL | BF(z) = −2 and BF(z.right) > 0 | z.right = rightRotate(z.right); leftRotate(z) | Mirror of LR. |
In insertion-only explanations, the newly inserted key can identify LL/RR/LR/RL. That shortcut is unsafe for deletion because the removed key is no longer on the heavy path. Inspect child balance factors instead.
Insertion: BST update, height update, rebalance
Descend
Use ordinary BST comparisons until a null child becomes the new leaf; this example rejects duplicate keys.
Unwind
For each ancestor, recompute height from already-correct child heights, then inspect its balance factor.
Repair
Return the root produced by zero, one, or two rotations and assign it to the parent's child link.
Stop rotations
After insertion, repairing the first unbalanced ancestor restores that subtree's prior height, so higher ancestors need no rotation.
Python
class Node:
def __init__(self, key):
self.key = key
self.left = None
self.right = None
self.height = 1
def height(node):
return 0 if node is None else node.height
def update(node):
node.height = 1 + max(height(node.left), height(node.right))
def balance(node):
return height(node.left) - height(node.right)
def rotate_right(y):
x = y.left
middle = x.right
x.right = y
y.left = middle
update(y) # demoted first
update(x)
return x
def rotate_left(x):
y = x.right
middle = y.left
y.left = x
x.right = middle
update(x) # demoted first
update(y)
return y
def rebalance(node):
update(node)
bf = balance(node)
if bf > 1:
if balance(node.left) < 0:
node.left = rotate_left(node.left)
return rotate_right(node)
if bf < -1:
if balance(node.right) > 0:
node.right = rotate_right(node.right)
return rotate_left(node)
return node
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 # reject duplicate
return rebalance(node)Java
final class AvlTree {
static final class Node {
int key, height = 1;
Node left, right;
Node(int key) { this.key = key; }
}
static int height(Node node) {
return node == null ? 0 : node.height;
}
static void update(Node node) {
node.height = 1 + Math.max(height(node.left), height(node.right));
}
static int balance(Node node) {
return height(node.left) - height(node.right);
}
static Node rotateRight(Node y) {
Node x = y.left;
Node middle = x.right;
x.right = y;
y.left = middle;
update(y); // demoted first
update(x);
return x;
}
static Node rotateLeft(Node x) {
Node y = x.right;
Node middle = y.left;
y.left = x;
x.right = middle;
update(x); // demoted first
update(y);
return y;
}
static Node rebalance(Node node) {
update(node);
int bf = balance(node);
if (bf > 1) {
if (balance(node.left) < 0) {
node.left = rotateLeft(node.left);
}
return rotateRight(node);
}
if (bf < -1) {
if (balance(node.right) > 0) {
node.right = rotateRight(node.right);
}
return rotateLeft(node);
}
return node;
}
static Node insert(Node node, int key) {
if (node == null) return new Node(key);
if (key < node.key) {
node.left = insert(node.left, key);
} else if (key > node.key) {
node.right = insert(node.right, key);
} else {
return node; // reject duplicate
}
return rebalance(node);
}
}Deletion can propagate imbalance to the root
First perform ordinary BST deletion. Removing a node can shorten a subtree. Even after one ancestor is rotated, the repaired subtree may remain one level shorter than before, so its parent can become unbalanced too. Continue recomputing and rebalancing on the entire return path. There can be O(log n) rebalanced ancestors, with at most a constant number of rotations at each.
Python
# Uses Node and rebalance from the insertion implementation.
def delete(node, key):
if node is None:
return None
if key < node.key:
node.left = delete(node.left, key)
elif key > node.key:
node.right = delete(node.right, key)
else:
if node.left is None:
return node.right
if node.right is None:
return node.left
successor = node.right
while successor.left is not None:
successor = successor.left
node.key = successor.key
node.right = delete(node.right, successor.key)
return rebalance(node) # every surviving ancestorJava
final class AvlDelete {
static final class Node {
int key, height = 1;
Node left, right;
Node(int key) { this.key = key; }
}
static int height(Node node) {
return node == null ? 0 : node.height;
}
static void update(Node node) {
node.height = 1 + Math.max(height(node.left), height(node.right));
}
static int balance(Node node) {
return height(node.left) - height(node.right);
}
static Node rotateRight(Node y) {
Node x = y.left;
Node middle = x.right;
x.right = y;
y.left = middle;
update(y);
update(x);
return x;
}
static Node rotateLeft(Node x) {
Node y = x.right;
Node middle = y.left;
y.left = x;
x.right = middle;
update(x);
update(y);
return y;
}
static Node rebalance(Node node) {
update(node);
int bf = balance(node);
if (bf > 1) {
if (balance(node.left) < 0) node.left = rotateLeft(node.left);
return rotateRight(node);
}
if (bf < -1) {
if (balance(node.right) > 0) node.right = rotateRight(node.right);
return rotateLeft(node);
}
return node;
}
static Node delete(Node node, int key) {
if (node == null) return null;
if (key < node.key) {
node.left = delete(node.left, key);
} else if (key > node.key) {
node.right = delete(node.right, key);
} else {
if (node.left == null) return node.right;
if (node.right == null) return node.left;
Node successor = node.right;
while (successor.left != null) {
successor = successor.left;
}
node.key = successor.key;
node.right = delete(node.right, successor.key);
}
return rebalance(node); // every surviving ancestor
}
}Insertion propagation
Heights may change all the way upward, but the first rotation (single or double) restores the affected subtree's pre-insertion height. At most one ancestor needs a structural rebalance.
Deletion propagation
A repaired subtree can still be shorter, so another ancestor may cross ±1. Continue to the root; choosing a case from child balance also handles a child BF of 0.
Why the height is logarithmic
Let N(h) be the fewest nodes in an AVL tree of height h under the leaf-height-1 convention. To make a height-h tree as sparse as possible while remaining balanced, one child has height h − 1 and the other h − 2:
This grows like the Fibonacci sequence, exponentially in h. Inverting the relationship gives h = O(log n). Since a binary tree can also require Ω(log n) height, AVL height is Θ(log n). A path operation and its recursive stack both use O(log n) space/time depth in the worst case; search itself can be iterative with Θ(1) auxiliary space.
Search
Θ(log n) worst
No mutation or rotation.
Insert
Θ(log n) worst
Path plus at most one single/double rebalance.
Delete
Θ(log n) worst
May rebalance multiple ancestors.
Audit all invariants in one postorder pass
A useful test helper returns facts upward rather than recomputing height repeatedly. It checks strict BST bounds, derives the actual height, compares it with the stored value, and rejects a balance factor outside −1…1.
Language-neutral pseudocode
audit(node, low, high):if node is null:return (valid=true, actualHeight=0, nodeCount=0)if not (low < node.key < high):return (valid=false, actualHeight=0, nodeCount=0)L = audit(node.left, low, node.key)if not L.valid:return LR = audit(node.right, node.key, high)if not R.valid:return Ractual = 1 + max(L.actualHeight, R.actualHeight)valid = node.height == actualand abs(L.actualHeight - R.actualHeight) <= 1return (valid, actual, 1 + L.nodeCount + R.nodeCount)
Run this after randomized insert/delete sequences in tests. Comparing inorder output with a trusted sorted set catches membership errors; the structural audit catches stale heights and balance violations that output alone cannot see.
Trade-offs: balance is not free
| Structure | Height / lookup | Update work | Typical fit |
|---|---|---|---|
| Plain BST | No guarantee; worst Θ(n) | No rotations | Simple or externally randomized/controlled workloads |
| AVL tree | Worst Θ(log n), tightly balanced | May rotate and update heights | Lookup-heavy ordered sets/maps with strict latency bounds |
| Red-black tree | Worst Θ(log n), looser balance | Often fewer rotations on updates | General ordered-library containers with frequent updates |
| Sorted array | Binary search Θ(log n) | Interior update Θ(n) | Mostly static data and cache-friendly scans |
AVL nodes need at least height or balance metadata. Rotations also require pointer writes and careful updates to parent links and augmentations. Tighter height can improve lookup path length, while red-black trees often trade slightly taller paths for less rebalancing in update-heavy workloads. Actual performance also depends on allocation and cache locality.
Pitfalls that corrupt the next operation
Mixing height conventions
Choose null/leaf base heights once; derive update, BF, and tests from that convention.
Updating the promoted node first
The promoted height depends on the demoted node after rewiring; update demoted, then promoted.
Dropping the middle subtree
Save T₂ before changing child pointers and attach it to the demoted pivot.
Ignoring the returned subtree root
Every insert, delete, and rotation can return a new root; assign it into the parent link and top-level root.
Using inserted/deleted key to classify deletion
After deletion, inspect the heavy child's balance factor; the removed key does not identify the shape.
Stopping after one deletion rotation
A deletion can shorten the repaired subtree, so keep rebalancing through every ancestor.
Undefined duplicate policy
Counts preserve strict structural order cleanly; side-placement policies may conflict with rotations.
Updating height but not other metadata
Rotations must also repair parent pointers, subtree sizes, sums, or application-specific aggregates.
Decision rule
Choose AVL when you need a dynamic ordered set/map with strict worst-case logarithmic paths and lookup latency matters enough to justify tighter balance and more intricate updates. Use a plain BST only when its shape is controlled or worst-case degradation is acceptable; consider red-black trees for update-heavy general containers, B/B+ trees for block storage, hash tables for unordered exact lookup, and sorted arrays for mostly static data.