// 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.

cpp
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  T3

Inorder 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:

  1. z->left = x->right — T2 moves from x's right to z's left.
  2. x->right = z — z drops below x.
  3. update(z) then update(x) — z is now the lower node, so its height must be recomputed first.
  4. Return x as the new subtree root; the caller (recursion) reattaches it where z used to be.
cpp
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:

CasePath from zBF(z)BF of childRepair
LLleft, then left+2≥ 0one right rotation at z
RRright, then right−2≤ 0one left rotation at z
LRleft, then right+2< 0left-rotate the child, then right-rotate z
RLright, then left−2> 0right-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 109(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.

cpp
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 before update(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.

C++ ISO revisions, their publication identifiers, and what each one changes for the code on this page
RevisionPublished asWhat it changes for the code on this page
C++98ISO/IEC 14882:1998The first ISO C++, and the dialect most data-structures courses still teach from: templates, the STL containers, and raw pointers doing the work.
C++03ISO/IEC 14882:2003A defect-fix revision. Nothing on these pages depends on it, and nothing on these pages is broken by it.
C++11ISO/IEC 14882:2011Where nullptr, auto, range-based for, move semantics and the unordered containers arrive. Every listing here writes nullptr rather than NULL because of it.
C++14ISO/IEC 14882:2014A small revision: generic lambdas and std::make_unique. Used only where it makes ownership clearer.
C++17ISO/IEC 14882:2017What every listing on StuHub targets and was compiled against. If you build these files, build them with -std=c++17.
C++20ISO/IEC 14882:2020Concepts, ranges and std::midpoint. Flagged in the prose where it offers a shorter correct form, never assumed by the code.
C++23ISO/IEC 14882:2024Not 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.

Published 2026-08-09 · updated 2026-08-27 · DUOCODE TECHNOLOGY