// STUHUB · C++ DATA STRUCTURES
AVL Tree Rotations: Balance Factors, LL/RR/LR/RL, Traced Step by Step
Balance factors, the four imbalance cases, and every rotation traced pointer by pointer on one fixed tree — plus the exam traps: updating heights before checking balance, and the double rotation that must be read as two singles.
Introduction
An AVL tree is a binary search tree that keeps itself balanced with one number per node — the balance factor — and one local repair tool: the rotation. Every insertion and deletion updates heights, finds the lowest node whose balance factor has left ±2, and applies one of four named cases (LL, RR, LR, RL). This note defines the invariant, traces each case on fixed examples, and shows the C++ that makes the repair mechanical — including the two traps that cost the most marks.
The invariant: balance factor within ±1
For each node, define height (longest path to a leaf, leaf height = 1, empty subtree = 0) and
balance factor BF(node) = height(left) − height(right).
The AVL property is BF ∈ {−1, 0, +1} for every node. That local rule is what bounds the whole tree's height at 1.44 · log₂(n+2) — i.e., O(log n) — which is the entire point: search, insert and delete stay logarithmic guaranteed, unlike a plain BST that can degenerate into a linked list.
Two facts to hold onto before the mechanics: BF is computed from heights, so heights must be correct before anything is judged; and an imbalance is always repaired at the lowest offending node first — fixing there usually fixes everything above.
struct Node {
int key;
Node* left = nullptr;
Node* right = nullptr;
int height = 1; // leaf: 1 (nullptr treated as 0)
};
int h(Node* n) { return n ? n->height : 0; }
void update(Node* n) { n->height = 1 + std::max(h(n->left), h(n->right)); }
int bf(Node* n) { return h(n->left) - h(n->right); }Rotations: the single rotation, traced
A rotation is a three-pointer rewrite that swaps a parent with one of its children while preserving BST order. The right rotation at node z lifts z's left child x above z:
z x
/ \ / \
x T3 ==> T1 z
/ \ / \
T1 T2 T2 T3Inorder sequence T1 < x < T2 < z < T3 is identical before and after — that is the property that makes rotations safe. The left rotation is the mirror image. Trace the pointers once, slowly:
z->left = x->right— T2 moves from x's right to z's left.x->right = z— z drops below x.update(z)thenupdate(x)— z is now the lower node, so its height must be recomputed first.- Return x as the new subtree root; the caller (recursion) reattaches it where z used to be.
Node* rotateRight(Node* z) {
Node* x = z->left;
z->left = x->right; // T2 moves to z
x->right = z; // z drops below x
update(z); // lower node FIRST
update(x);
return x;
}
Node* rotateLeft(Node* z) { // mirror image
Node* x = z->right;
z->right = x->left;
x->left = z;
update(z);
update(x);
return x;
}The four cases: LL, RR, LR, RL
After an insert, walk back up the recursion updating heights. At the first node z with |BF| = 2, look at the direction of the two steps from z toward the newly inserted node:
| Case | Path from z | BF(z) | BF of child | Repair |
|---|---|---|---|---|
| LL | left, then left | +2 | ≥ 0 | one right rotation at z |
| RR | right, then right | −2 | ≤ 0 | one left rotation at z |
| LR | left, then right | +2 | < 0 | left-rotate the child, then right-rotate z |
| RL | right, then left | −2 | > 0 | right-rotate the child, then left-rotate z |
LL (the straight line). Insert 5 into 10 ← 8 ← 6: tree 10(8(6)). BF(10) = +2, path left-left. One right rotation at 10 → 8(6, 10). Balanced.
LR (the zig). Insert 9 into 10(8(_, 9)): path from 10 is left (to 8), then right (to 9). A single right rotation at 10 would give 9(8(_, _), 10) — wait: rotating right with x = 8 makes 8 the root and 9 its right-right chain, leaving BF(8) = −2 still unbalanced. The correct repair: left-rotate at 8 first (9 lifts above 8: 10(9(8)) — an LL shape), then right-rotate at 10 → 9(8, 10). Balanced.
RR and RL are the exact mirrors. The mnemonic to trust under pressure: a zig needs two rotations, a straight line needs one; the middle value of the three ends up as the subtree root.
The repair, in code
Insertion is BST descent, then the recursive return path does all the AVL work — update height, judge BF, repair:
After the four cases, BF(z) is back in range, and — the insertion theorem — one repair at the lowest offender restores the whole tree's height to what it was before the insert, so at most one rotation (single or double) happens per insertion. Deletion is crueler: a repair can shorten a subtree, re-exposing an ancestor, so deletion can cascade O(log n) rotations on the way back up — a classic distinguisher question.
Node* rebalance(Node* z) {
update(z);
if (bf(z) == 2) { // left-heavy
if (bf(z->left) < 0) // LR: zig
z->left = rotateLeft(z->left);
return rotateRight(z); // LL or LR's second half
}
if (bf(z) == -2) { // right-heavy
if (bf(z->right) > 0) // RL: zig
z->right = rotateRight(z->right);
return rotateLeft(z); // RR or RL's second half
}
return z; // still balanced
}
Node* insert(Node* n, int key) {
if (!n) return new Node{key};
if (key < n->key) n->left = insert(n->left, key);
else if (key > n->key) n->right = insert(n->right, key);
else return n; // duplicate: ignore
return rebalance(n);
}The mistakes that cost marks
- Judging BF before updating heights. The freshly changed subtree's height is stale until
update(z)runs; checking BF on stale heights skips the repair entirely. Order is: update, then judge. - Updating the rotation's heights in the wrong order. After lifting x above z,
update(z)must run beforeupdate(x)— x's new height depends on z's corrected one. Reversed, every height above is wrong by one. - Reading LR/RL as a single new rotation. Double rotations are two standard rotations with a specific order; writing one bespoke pointer shuffle loses the inorder argument and usually the marks.
- Choosing the case from BF(z) alone. LL and LR both give BF(z) = +2; the child's BF is what separates them. Same for −2 (RR vs RL).
- Forgetting the deletion cascade. "At most one rotation per operation" is true for insertion only; deletion may rotate at every level back up to the root.
Exercises with solutions
Work through each question before opening the solution below it.
Exercise 1
Insert the keys 30, 20, 10 into an empty AVL tree, showing the tree and every BF after each insertion. Repair where needed.
Solution
Insert 30: 30 alone. All BF = 0. Insert 20: 30(20, _). BF(30) = 1, BF(20) = 0. Balanced — nothing to do. Insert 10: 30(20(10, _), _). Heights: 10 → 1, 20 → 2, 30 → 3. BF(30) = h(20) − 0 = 2, BF(20) = 1. Offender: 30, path to new node is left (to 20) then left (to 10) — case LL. One right rotation at 30: 20's right (empty) moves to 30's left; 20 takes the root. Result 20(10, 30). Heights: 10 → 1, 30 → 1, 20 → 2. BF(20) = 0. Balanced.
Exercise 2
Insert 40, 20, 60, 50 into an empty AVL tree, showing only the state after the insertion that triggers a repair, the case name, and the repair.
Solution
After 40, 20, 60: 40(20, 60), all BF = 0. Insert 50: 50 goes right of 40? No — 50 > 40 and < 60, so it lands as 60's left child: 40(20, 60(50, _)). Heights: 50 → 1, 60 → 2, 20 → 1, 40 → 3. BF(60) = 1, BF(40) = 1 − 2 = −1. Everything is within ±1 — no repair fires; the tree is already valid. (This is the deliberate trap: an insertion near the root often needs nothing. If the key had instead created BF = −2 with a left-leaning right child — e.g. inserting 30 under 40(_, 60) — that would be RL: right-rotate at 60, then left-rotate at 40.)
Exercise 3
An AVL insert of key k triggers a case-LR imbalance at node z. Explain why a single right rotation at z does not restore balance, and name the two rotations that do.
Solution
In LR, z is left-heavy (BF = +2) but z's left child is itself right-heavy (BF < 0): shape z(L(_, R)). A right rotation at z lifts the left child L to the root — but L's right subtree is the tall one, so after the rotation that subtree hangs off L's right while z becomes L's right child; L ends up right-heavy with BF ≤ −2. The imbalance moved, it did not disappear. The repair is two rotations: left-rotate at L first (the RL-shaped inner pair lifts L's right child above L, turning the whole thing into an LL shape with a straight left line), then right-rotate at z. The middle key of {z's subtree root, L, L's right child} becomes the new subtree root with z and L as its children.
Which C++ standard does this page assume?
Standard C++17. C++ began as C with Classes in 1979 and first shipped commercially in 1985; what a compiler flag actually selects today is one of the ISO revisions below, and the committee has published a new one every 3 years since 2011. Build these listings with -std=c++17 and they compile as written.
| Revision | Published as | What it changes for the code on this page |
|---|---|---|
| C++98 | ISO/IEC 14882:1998 | The first ISO C++, and the dialect most data-structures courses still teach from: templates, the STL containers, and raw pointers doing the work. |
| C++03 | ISO/IEC 14882:2003 | A defect-fix revision. Nothing on these pages depends on it, and nothing on these pages is broken by it. |
| C++11 | ISO/IEC 14882:2011 | Where nullptr, auto, range-based for, move semantics and the unordered containers arrive. Every listing here writes nullptr rather than NULL because of it. |
| C++14 | ISO/IEC 14882:2014 | A small revision: generic lambdas and std::make_unique. Used only where it makes ownership clearer. |
| C++17 | ISO/IEC 14882:2017 | What every listing on StuHub targets and was compiled against. If you build these files, build them with -std=c++17. |
| C++20 | ISO/IEC 14882:2020 | Concepts, ranges and std::midpoint. Flagged in the prose where it offers a shorter correct form, never assumed by the code. |
| C++23 | ISO/IEC 14882:2024 | Not used here. Named so you can tell whether a snippet you found elsewhere will compile on a lab machine that predates it. |
Common questions
What does this page cover?
Balance factors, the four imbalance cases, and every rotation traced pointer by pointer on one fixed tree — plus the exam traps: updating heights before checking balance, and the double rotation that must be read as two singles.
How long does this page take to work through?
About 6 minutes of reading at 200 words per minute, plus 3 questions with worked solutions at the foot of the page. Reading it end to end is the slow way; the intended use is to find the section you are stuck on, then do the questions for that section with the solutions covered.
Which C++ standard do these examples target?
Standard C++17 — ISO/IEC 14882:2017. Every listing was compiled with -std=c++17 and -Wall -Wextra before publication, and the linked-structure examples were also run under AddressSanitizer. Where C++20 offers a shorter correct form, such as std::midpoint, the prose says so instead of quietly using it.
Is StuHub free, and do I need an account?
It is free and there is nothing to sign in to. No login, account or payment is required to read any of the 20 topics — StuHub is published by DUOCODE TECHNOLOGY alongside APRide, and the ride board's accounts have nothing to do with it.
Can I paste this code into my assignment?
Treat it as a reference, not as an answer key. StuHub is educational material only, it is not coursework and it is not affiliated with or endorsed by any institution, so your own submission rules decide what you may reuse. Every listing was compiled and run before publication, and you should still compile and test anything you take.
Why is everything written in C++ rather than pseudocode?
Because most of the mistakes worth catching are C++ mistakes, not algorithm mistakes: a lost pointer, a destructor that never runs, an index that underflows because it was unsigned. Pseudocode hides exactly the layer where a data-structures assignment is actually failed.
Where should I check what the standard library really guarantees?
cppreference for the day-to-day answer, and the WG21 working drafts when the exact wording matters — both are linked below. Compiler documentation settles the rest: a warning you cannot explain is usually the compiler being right.
Does StuHub replace my lecture notes?
No. It is written to sit beside them: your course decides what is examinable, in what notation, and with which library restrictions. Where this page and your module handbook disagree about scope, the handbook wins.
Is StuHub connected to Asia Pacific University?
No. It is an independent reference published by DUOCODE TECHNOLOGY, not affiliated with or endorsed by Asia Pacific University or any other institution. It was written for APU students because that is who asked for it, and it is open to anyone.
Where can I check this against the language itself?
Nothing on this page outranks the standard or the library reference. When this page and one of these disagree, they are right and we want to know.
- cppreference — the C++ standard library referenceThe fastest correct answer for what a container or algorithm actually guarantees.
- isocpp.org — the Standard C++ FoundationThe FAQ and the core guidelines, written by the people who define the language.
- ISO/IEC JTC1/SC22/WG21 — the C++ standards committeeWorking drafts of the standard itself, free to read, when the wording is the question.
- GCC online documentationWhat -Wall, -Wextra and the sanitizers used on these listings actually check.
- Microsoft Learn — C++ language documentationThe MSVC view, for readers whose lab machines build with Visual Studio.
Published 2026-08-09 · updated 2026-08-27 · DUOCODE TECHNOLOGY