Local Algebra of Rotations & The Four AVL Rebalancing Cases
Define the height of a null subtree as $-1$ and a leaf as $0$, so $h(u) = 1 + \max(h(u.\text{left}), h(u.\text{right}))$. The balance factor is $\text{bf}(u) = h(u.\text{left}) - h(u.\text{right}) \in \{-1, 0, +1\}$. When an insertion or deletion pushes $\text{bf}(z)$ to $+2$ (left-heavy) or $-2$ (right-heavy) at the lowest unbalanced ancestor $z$, let $y$ be the taller child of $z$ and $x$ be the taller child of $y$:
- Left-Left (LL, Outer): $\text{bf}(z) = +2$ and $\text{bf}(y) \ge 0$. A single
rotateRight(z)restores balance and reduces the subtree height by $1$. - Right-Right (RR, Outer): $\text{bf}(z) = -2$ and $\text{bf}(y) \le 0$. A single
rotateLeft(z)restores balance. - Left-Right (LR, Inner Zig-Zag): $\text{bf}(z) = +2$ and $\text{bf}(y) = -1$. A single right rotation on $z$ fails because the heavy grandchild $x = y.\text{right}$ stays at the same depth! We must perform a double rotation:
rotateLeft(y)followed byrotateRight(z), lifting $x$ to the root of the subtree. - Right-Left (RL, Inner Zig-Zag): $\text{bf}(z) = -2$ and $\text{bf}(y) = +1$. Execute
rotateRight(y)followed byrotateLeft(z).
| AVL Operation | Time Complexity | Max Rotations (Worst-Case) | Height / Comparison Bound |
|---|---|---|---|
search(key) |
O(log N) | 0 | $\le 1.4404 \log_2(N + 2) - 0.328$ |
insert(key) |
O(log N) | ≤ 1 Single or 1 Double (2 rots) | Subtree height returns to pre-insert value! |
erase(key) |
O(log N) | O(log N) Rotations Up Spine | At most 1 rotation per ancestor level |
Fibonacci Tree Extremal Proof: Why $h < 1.4404 \log_2(N + 2)$
What is the minimum number of nodes $N_h$ required to construct a valid AVL tree of height $h$? To minimize nodes at height $h$, the root must have one subtree of height $h - 1$ and the other of height $h - 2$ (the maximum legal imbalance). By induction:
$$N_0 = 1, \quad N_1 = 2, \quad N_h = 1 + N_{h-1} + N_{h-2}$$
Adding $1$ to both sides yields $(N_h + 1) = (N_{h-1} + 1) + (N_{h-2} + 1)$, which is the exact Fibonacci recurrence! Since $N_0 + 1 = 2 = F_3$ and $N_1 + 1 = 3 = F_4$, we have the exact identity:
$$N \ge N_h = F_{h+3} - 1 > \frac{\phi^{h+3}}{\sqrt{5}} - 2 \implies h < \frac{1}{\log_2 \phi} \log_2(N + 2) - 0.3277 \approx 1.4404 \log_2(N + 2)$$
Crucial Insert vs. Delete Asymmetry: On AVL insert, the first unbalanced ancestor $z$ had height $k+1$ before insertion, grew to $k+2$, and after 1 single or double rotation shrinks back to $k+1$—so at most 1 rebalancing event ever occurs on insertion! On AVL delete, shrinking $z$ from $k+2$ to $k+1$ via rotation reduces the subtree's height as seen by $z$'s parent, which can cascade $O(\log N)$ rotations all the way to the root.
Trace how AVL LL/LR/RR/RL rotations and Sedgewick Left-Leaning Red-Black (LLRB) local transformations (rotateLeft, rotateRight, flipColors) preserve in-order sequence and subtree sizes while bounding tree height.
Invariant & State
Hardware Reality: Storing uint8_t height vs 2-Bit Balance Factor in Pointer Low Bits
Because $h < 1.44 \log_2(2^{64}) \approx 92$ for any tree that can ever fit in a 64-bit address space, storing a uint8_t height field requires only 1 byte. Better yet, because 64-bit heap allocations are at least 8-byte aligned, the lowest 3 bits of node->parent are always zero (0b000). Production C libraries (like libavl) pack the 2-bit balance factor $\text{bf} \in \{-1, 0, +1\}$ directly into the low 2 bits of the parent pointer, achieving zero bytes of structural alignment padding!
Implementation: Clean AVL Rotations & Rebalance Engine
// C++20: Self-Balancing AVL Tree Core with LL, RR, LR, RL Rebalancing
#include <algorithm>
#include <cstdint>
struct AVLNode {
int key;
int32_t height = 0; // Leaf has height 0; nullptr has height -1
AVLNode *left = nullptr, *right = nullptr;
explicit AVLNode(int k) : key(k) {}
};
inline int32_t h(const AVLNode* n) noexcept { return n ? n->height : -1; }
inline int32_t bf(const AVLNode* n) noexcept { return n ? (h(n->left) - h(n->right)) : 0; }
inline void pullHeight(AVLNode* n) noexcept { n->height = 1 + std::max(h(n->left), h(n->right)); }
AVLNode* rotateRight(AVLNode* z) noexcept {
AVLNode* y = z->left;
z->left = y->right;
y->right = z;
pullHeight(z); // Must update lower node z BEFORE new root y!
pullHeight(y);
return y;
}
AVLNode* rotateLeft(AVLNode* z) noexcept {
AVLNode* y = z->right;
z->right = y->left;
y->left = z;
pullHeight(z);
pullHeight(y);
return y;
}
AVLNode* rebalance(AVLNode* u) noexcept {
pullHeight(u);
int32_t balance = bf(u);
if (balance > 1) { // Left-heavy (+2)
if (bf(u->left) < 0) u->left = rotateLeft(u->left); // LR -> LL
return rotateRight(u);
}
if (balance < -1) { // Right-heavy (-2)
if (bf(u->right) > 0) u->right = rotateRight(u->right); // RL -> RR
return rotateLeft(u);
}
return u;
}
Failure Modes & Adversarial Pitfalls
- Updating Heights in the Wrong Order After Rotation: Inside
rotateRight(z)(which lifts $y = z.\text{left}$ above $z$), $z$ becomes the child of $y$. If you callpullHeight(y)beforepullHeight(z), $y$ computes its new height using $z$'s stale pre-rotation height, corrupting every ancestor's balance factor! Always recompute the demoted child $z$ first, then the promoted parent $y$. - Strict Equality Trap on Deletion Rebalancing (
bf(u->left) >= 0): Whenbf(u) == +2after a deletion in $u.\text{right}$,bf(u->left)can be $0$ (unlike insertion where it is strictly $+1$ or $-1$). Whenbf(u->left) == 0, a singlerotateRight(u)suffices; treating0as an LR double rotation performs unnecessary work.