DSA DSA In Depth
0%
lesson splay-treeamortized-analysisself-adjusting

Splay Trees: Zig-Zig / Zig-Zag Rotations, Amortized O(log N) Potential Proof & Static Optimality

7 min read Module 3.2: Self-Balancing, Augmented, Disk-Backed & Persistent Trees Updated Oct 23, 2026
📄 Cheat Sheet
MIT 6.851 Lecture 5: Splay Trees, Amortized Potential Analysis & Dynamic Optimality
MIT OpenCourseWare (Erik Demaine) MIT 6.851 Lecture 5: Splay Trees, Amortized Potential Analysis & Dynamic Optimality Watch on YouTube ↗

Erik Demaine proves the amortized O(log N) access bound via the sum-of-log-subtree-sizes potential function Φ(T) = Σ log |T_x|, derives Static Optimality, and introduces the Dynamic Optimality Conjecture.

0:00 / --:--

The Three Splaying Steps: Zig, Zig-Zag, and Zig-Zig

To splay node $x$ to the root, we repeat two-level steps according to the geometry of $x$, its parent $p$, and its grandparent $g$ until $x$ is the root:

  1. Zig (Terminal Single Step, $p$ is the root): Rotate the edge $(p, x)$ once so $x$ becomes the new root. Occurs at most once at the very end of splay(x).
  2. Zig-Zag ($x$ and $p$ lean in opposite directions—e.g., $p = g.\text{left}$ and $x = p.\text{right}$): Rotate $x$ above $p$, then rotate $x$ above $g$ (identical to an AVL LR/RL double rotation).
  3. Zig-Zig ($x$ and $p$ lean in the SAME direction—e.g., $p = g.\text{left}$ and $x = p.\text{left}$): Rotate $p$ above $g$ FIRST, and then rotate $x$ above $p$! Notice how this differs from Move-to-Root (which would rotate $x$ above $p$ first and then $x$ above $g$). By rotating $g$ first, $g$ is pushed down two levels, folding the long path in half!
Splay Tree Property / Theorem Bound (for $M$ Accesses on $N$ Nodes) Physical Meaning
Balance Theorem O((M + N) log N) Matches worst-case AVL / Red-Black over any sequence
Static Optimality Theorem O(M + ∑ q_i log(M / q_i)) Matches optimal static Huffman/Knuth BST (Shannon entropy!)
Working-Set Theorem O(N log N + ∑ log(t_k + 1)) Accessing the $t$-th most recently used item takes $O(\log t)$!
Sequential Access (Tarjan 1985) O(N) Total (O(1) per item!) Splaying keys $1, 2, \dots, N$ in sorted order takes linear time

Sleator–Tarjan Access Lemma Proof: Why Zig-Zig Releases Potential

Define the subtree size $s(u) = |\text{subtree}(u)|$, the node rank $r(u) = \lfloor \log_2 s(u) \rfloor$, and the tree potential $\Phi(T) = \sum_{u \in T} r(u)$. We prove that the amortized cost of a Zig-Zig or Zig-Zag step lifting $x$ above $p$ and $g$ is bounded by:

$$\hat{c}_{\text{step}} = 2 + r'(x) + r'(p) + r'(g) - r(x) - r(p) - r(g) \le 3(r'(x) - r(x))$$

where $r$ and $r'$ denote ranks before and after the double rotation. Observe that after the step, $x$ roots the exact subtree previously rooted at $g$, so $r'(x) = r(g)$, and clearly $r(p) \ge r(x)$ and $r'(p) \le r'(x)$. Thus the left side simplifies to $2 + r'(p) + r'(g) - r(x) - r(p) \le 2 + r'(x) + r'(g) - 2r(x)$. Now comes the magic of Zig-Zig: before the step, $x$'s subtree and $g$'s post-rotation subtree are completely disjoint inside $g$'s original subtree! Therefore:

$$s(x) + s'(g) \le s(g) = s'(x)$$

By the concavity of $\log_2$: for any positive $a + b \le c$, $\log_2 a + \log_2 b \le 2 \log_2(c/2) = 2 \log_2 c - 2$. Applying this with $a = s(x), b = s'(g), c = s'(x)$ gives $r(x) + r'(g) \le 2r'(x) - 2$, so the $+2$ actual rotation cost cancels the $-2$ from concavity:

$$\hat{c}_{\text{step}} \le 2 + r'(x) + (2r'(x) - 2 - r(x)) - 2r(x) = 3(r'(x) - r(x))!$$

Telescoping $\sum 3(r'(x) - r(x))$ across all steps from $x$'s initial rank $r_0(x)$ up to the root rank $r(\text{root}) = \log_2 N$ (plus $+1$ for the final Zig) proves the Access Lemma: $\text{Amortized Cost}(\text{splay}(x)) \le 3(\log_2 N - \log_2 s(x)) + 1 = O(\log N)$!

Randomized Treap (split / merge) & Splay Tree Zig-Zig Animator
Expected / Amortized O(log N)

Inspect how a Treap simultaneously satisfies BST order on keys and Max-Heap order on random priorities via two primitives — split(T, k) and merge(L, R) — and how Splay Tree zig-zig rotations halve path depth.

Step 1 / 9
Custom Input:
Timeline

Invariant & State

Treap Root = (key=20, prio=95): BST invariant on keys (10 < 20 < 25 < 30 < 40) ∧ Max-Heap on priorities (95 > 81 > 60).
🌳 Live 2D Hierarchical Tree Topology — Randomized Treap (split / merge) & Splay Tree Zig-Zig Animator Visited/Active Pivot/Root Resolved
10 (p… [0] 20 (p… [1] 25 (p… [2] 30 (p… [3] 40 (p… [4]
[0]10 (p=42)
[1]20 (p=95)
[2]25 (p=60)
[3]30 (p=81)
[4]40 (p=35)
Primitive
split(T, k=25)
Recursion Depth
1 (O(log N))
L Keys (≤ 25)
Building...
R Keys (> 25)
Building...
[Step 1] Start split(root=20, k=25): Since root.key (20) ≤ 25, root 20 and its entire left subtree [10] belong to Left Output Tree L.
1. Initial Left-Left Spine (g → p → x): g p x 2. Sleator-Tarjan Zig-Zig (Rotate g FIRST, then p): x p g Disjoint Subtrees: s(x) + s'(g) ≤ s'(x) ⇒ Halves path depth! 3. Broken Move-to-Root (Rotate p first): x g p g stays above p ⇒ O(N) amortized!
flowchart TD Loop["while x.parent != null"] --> CheckG{"x.parent.parent == null ?"} CheckG -- "Yes (Zig)" --> Zig["rotate(x) // 1 single rotation"] CheckG -- "No" --> SameDir{"(p == g.L) == (x == p.L) ?"} SameDir -- "Yes (Zig-Zig)" --> ZZ["rotate(p) // Rotate parent with g FIRST! rotate(x) // Then rotate x with p"] SameDir -- "No (Zig-Zag)" --> ZZag["rotate(x) // Rotate x with p first rotate(x) // Then rotate x with g"]

Hardware Reality: Splay Trees as Self-Organizing L1 Cache & Link-Cut Tree Auxiliary Paths

Because Splay Trees satisfy the Working-Set Theorem (re-accessing one of the $k$ most recently touched keys takes $O(\log k)$ comparisons), they act like a hardware LRU cache embedded inside a search tree, making them popular in network packet classifiers and GCC's libiberty (splay-tree.c). Even more importantly, because splay(x) requires zero balance fields and splits/joins paths at the root, Sleator and Tarjan built Link-Cut Trees (supporting $O(\log N)$ dynamic forest connectivity and path aggregates) using Splay Trees as the auxiliary heavy-path representation!

Implementation: Sleator-Tarjan Bottom-Up Splay Engine

// C++20: Sleator-Tarjan Bottom-Up Splay Tree (Zero Balance Metadata, O(log N) Amortized)
struct SplayNode {
    int key;
    SplayNode *ch[2] = {nullptr, nullptr}, *p = nullptr;
    explicit SplayNode(int k) : key(k) {}
};

// Returns 0 if x is a left child, 1 if x is a right child
inline int dir(const SplayNode* x) noexcept {
    return x->p->ch[1] == x;
}

// Rotates x one step up above its parent p
void rotate(SplayNode* x) noexcept {
    SplayNode* p = x->p;
    SplayNode* g = p->p;
    int d = dir(x);
    p->ch[d] = x->ch[d ^ 1];
    if (x->ch[d ^ 1]) x->ch[d ^ 1]->p = p;
    x->ch[d ^ 1] = p;
    p->p = x;
    x->p = g;
    if (g) g->ch[g->ch[1] == p] = x;
}

// Splays x to the root using Zig, Zig-Zig (rotate p first!), and Zig-Zag
SplayNode* splay(SplayNode* x) noexcept {
    if (!x) return nullptr;
    while (x->p != nullptr) {
        SplayNode* p = x->p;
        SplayNode* g = p->p;
        if (g == nullptr) {
            rotate(x);                          // Zig (p is root)
        } else if (dir(p) == dir(x)) {
            rotate(p);                          // Zig-Zig: rotate p with g FIRST!
            rotate(x);
        } else {
            rotate(x);                          // Zig-Zag: rotate x twice
            rotate(x);
        }
    }
    return x;
}

Failure Modes & Adversarial Pitfalls

  • Searching for a Non-Existent Key Without Splaying the Last Visited Node: If find(key) hits a null pointer after walking down a long $O(N)$ chain and returns false without splaying the last non-null node visited before falling off the tree, an attacker can repeatedly query a missing key in $\Theta(N)$ time forever without paying down the tree's potential! Always splay the last visited node even on an unsuccessful search.
  • Multi-Threaded Read Contention: Because even a read-only find(key) mutates the tree via splay(x) all the way to the root, concurrent readers cannot share a Splay Tree without an exclusive mutex lock.
Check yourself Question 1 of 4
Concept Check Zig-Zig vs Double Rotation on Collinear Nodes recall

Suppose node $x$ is the left child of $p$, and $p$ is the left child of $g$ (a left-left collinear configuration). How does Tarjan and Sleator's **Zig-Zig** splay step differ from naive bottom-up single rotations?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Why can a Splay Tree suffer catastrophic throughput degradation compared to a Red-Black tree or B-tree in this multi-threaded read-heavy scenario?

Key Takeaways

Splay Trees store zero balance metadata and move every accessed node x to the root via splay(x) in O(log N) amortized time.
When x and parent p lean in the same direction (Zig-Zig), rotating p with grandparent g FIRST and then x with p ensures s(x) + s'(g) <= s'(x), halving path depth and enabling the 3(r'(x) - r(x)) potential bound.
Unsuccessful searches MUST splay the last visited non-null node to the root, otherwise repeated misses on a deep branch take Θ(N) time without reducing tree potential.
By the Static Optimality and Working-Set Theorems, Splay Trees automatically match optimal static BSTs and access recently used keys in O(log k) amortized time.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue