Splay Trees & Treaps (Randomized BST)
Splay tree self-adjusting zig/zig-zig/zig-zag rotations, amortized O(log n) potential method, and Treap dual heap-priority invariants.
Splay Trees and Treaps dispense with rigid deterministic balancing factors in favor of self-adjusting heuristics and randomized priorities. Splay trees exploit temporal locality to guarantee amortized performance with zero metadata overhead, while Treaps combine BST keys with heap priorities to provide expected search times and elegant split-and-merge array slicing.
1. Executive Summary & Learning Objectives
#Invented by Daniel Sleator and Robert Tarjan in 1985, the Splay Tree is a self-adjusting binary search tree that moves accessed nodes to the root via splay rotations without storing balance factors, heights, or color bits. Invented by Raimund Seidel and Cecilia Aragon in 1989, the Treap (Tree + Heap) merges binary search ordering on keys with max-heap ordering on randomly assigned priorities to guarantee a unique Cartesian tree with expected logarithmic height.
By the end of this chapter, you will be able to:
- Analyze the self-adjusting mechanics of Splay Trees and prove why rotating the grandparent first in the Zig-Zig configuration cuts tree depth in half.
- Formulate Tarjan's potential function and explain the Access Lemma establishing amortized bound per operation.
- Differentiate between standard Splay rotations: terminal Zig, homogeneous Zig-Zig, and heterogeneous Zig-Zag.
- Prove the Cartesian Uniqueness Theorem for Treaps and demonstrate why randomized priorities produce expected height.
- Implement the universal Treap primitives (
SplitandMerge) and apply Implicit Treaps to execute dynamic array range reversals.
2. Splay Tree Principles & Self-Adjusting Heuristics
#A Splay Tree stores no metadata per node beyond its key, left, right, and parent pointers. Whenever any key is accessed (searched, inserted, or deleted), the target node is splayed (rotated) through a sequence of local tree rotations until it becomes the new root of the tree.
The Temporal Locality Advantage
#In real-world workloads (e.g., caches, memory allocation page tables, network routers), memory access distributions follow the Pareto 80/20 rule: roughly 80% of operations access a working set of 20% of items. Splaying continually pulls accessed items toward the top of the tree, rendering frequent operations near .
The Three Splay Rotation Configurations
#Let be the active node being splayed upward, its parent, and its grandparent:
| Rotation Configuration | Structural Geometric Pattern | Surgical Action Order | Primary Effect on Path Depth |
|---|---|---|---|
| Zig (Terminal Case) | is the tree root (). is either left or right child. | Execute single RotateRight(P) or RotateLeft(P). | Moves into root position. Performed at most once per splay. |
| Zig-Zig (Homogeneous) | and are both left children or both right children (linear chain). | CRITICAL: Rotate grandparent first, then rotate parent ! | Cuts the depth of the entire path roughly in half, progressively rebalancing the tree. |
| Zig-Zag (Heterogeneous) | is right child of and is left child of (or vice versa). | Rotate parent first, then rotate grandparent (standard double rotation). | Moves up two levels; identical to AVL LR/RL double rotations. |
Why Rotate Grandparent First in Zig-Zig?
#If we naively rotated then (as in standard bottom-up single rotations), a degenerate linear path of length would simply be reversed into another linear path of length , doing nothing to compress path length. By rotating the grandparent first, all nodes along the path have their depths halved:
| Topological Element | Before Zig-Zig ( and both left children) | After Zig-Zig ( rotated right first, then rotated right) |
|---|---|---|
| Subtree Root | Node | Node |
| Left Child of Root | Node | Subtree (Left child of ) |
| Right Child of Root | Subtree | Node |
| Left Child of | Node | Subtree (Right child of ) |
| Right Child of | Subtree | Node |
| Subtrees of | Left: , Right: | Left: , Right: |
3. Tarjan Potential Function & Amortized Complexity
#While an individual splay operation in a skewed tree can take time, Robert Tarjan proved using the potential method that any sequence of operations on an -node splay tree takes at most time.
The Potential Function
#For any node in tree , let denote the size of the subtree rooted at (number of nodes). The rank of node is defined as:
The global potential function is the sum of ranks across all nodes in :
The Splay Access Lemma
#The amortized time to splay a node in a tree with root is:
Because and , the amortized cost per search, insertion, or deletion is strictly bounded by .
4. Complete Implementation: Splay Tree Operations
#export class SplayNode<T> {
key: T;
left: SplayNode<T> | null = null;
right: SplayNode<T> | null = null;
parent: SplayNode<T> | null = null;
constructor(key: T) {
this.key = key;
}
}
export class SplayTree<T> {
root: SplayNode<T> | null = null;
private rotateRight(p: SplayNode<T>): void {
const x = p.left!;
p.left = x.right;
if (x.right !== null) x.right.parent = p;
x.parent = p.parent;
if (p.parent === null) {
this.root = x;
} else if (p === p.parent.left) {
p.parent.left = x;
} else {
p.parent.right = x;
}
x.right = p;
p.parent = x;
}
private rotateLeft(p: SplayNode<T>): void {
const x = p.right!;
p.right = x.left;
if (x.left !== null) x.left.parent = p;
x.parent = p.parent;
if (p.parent === null) {
this.root = x;
} else if (p === p.parent.left) {
p.parent.left = x;
} else {
p.parent.right = x;
}
x.left = p;
p.parent = x;
}
public splay(x: SplayNode<T>): void {
while (x.parent !== null) {
const p = x.parent;
const g = p.parent;
if (g === null) {
// Case 1: Zig
if (x === p.left) this.rotateRight(p);
else this.rotateLeft(p);
} else if (x === p.left && p === g.left) {
// Case 2a: Zig-Zig (Rotate G first, then P!)
this.rotateRight(g);
this.rotateRight(p);
} else if (x === p.right && p === g.right) {
// Case 2b: Zig-Zig (Rotate G first, then P!)
this.rotateLeft(g);
this.rotateLeft(p);
} else if (x === p.right && p === g.left) {
// Case 3a: Zig-Zag
this.rotateLeft(p);
this.rotateRight(g);
} else {
// Case 3b: Zig-Zag
this.rotateRight(p);
this.rotateLeft(g);
}
}
}
public search(key: T): SplayNode<T> | null {
let curr = this.root;
let last: SplayNode<T> | null = null;
while (curr !== null) {
last = curr;
if (key < curr.key) curr = curr.left;
else if (key > curr.key) curr = curr.right;
else {
this.splay(curr);
return curr;
}
}
if (last !== null) this.splay(last);
return null;
}
}5. Treaps: Duality of Tree + Heap
#A Treap (Tree + Heap) assigns every item two distinct properties:
- Key : Maintains the symmetric Binary Search Tree Invariant ().
- Priority : Generated uniformly at random upon creation; maintains the Max-Heap Invariant ( and ).
Cartesian Uniqueness Theorem
#For any set of pairs with distinct keys and distinct priorities, there exists exactly one unique Treap structure.
Proof Sketch:
- The pair with the maximal priority must unconditionally serve as the Root of the tree to satisfy the Max-Heap property.
- By the BST property, all pairs with must fall into the left subtree, and all pairs with must fall into the right subtree.
- Applying this logic inductively down each partition defines a unique topological tree.
Because priorities are chosen uniformly at random, every key permutation is equally likely. Thus, the expected height of a Treap matches that of a randomly generated BST:
6. Core Treap Primitives: Split and Merge
#Instead of maintaining explicit balance factors and complex rotation cases, modern Treap implementations operate exclusively via two building blocks:
| Primitive | Preconditions | Input Arguments | Output Return Values | Purpose |
|---|---|---|---|---|
Split(T, val) | is a valid Treap. | Tree root , split threshold val. | Two valid Treaps where and . | Partitions a tree into two subtrees along key boundary. |
Merge(L, R) | Crucial: All keys in must be strictly less than all keys in (). | Left Treap root , Right Treap root . | A single merged Treap . | Joins two disjoint subtrees respecting both BST and Heap invariants. |
export class TreapNode<T> {
key: T;
priority: number;
left: TreapNode<T> | null = null;
right: TreapNode<T> | null = null;
constructor(key: T, priority: number = Math.random()) {
this.key = key;
this.priority = priority;
}
}
export function split<T>(
t: TreapNode<T> | null,
val: T
): [TreapNode<T> | null, TreapNode<T> | null] {
if (t === null) return [null, null];
if (t.key <= val) {
const [subL, subR] = split(t.right, val);
t.right = subL;
return [t, subR];
} else {
const [subL, subR] = split(t.left, val);
t.left = subR;
return [subL, t];
}
}
export function merge<T>(
l: TreapNode<T> | null,
r: TreapNode<T> | null
): TreapNode<T> | null {
if (l === null) return r;
if (r === null) return l;
if (l.priority > r.priority) {
l.right = merge(l.right, r);
return l;
} else {
r.left = merge(l, r.left);
return r;
}
}7. Implicit Treap: Dynamic Arrays with Range Reversals
#An Implicit Treap does not store explicit keys. Instead, the key of a node is implicitly determined by its 1-based index in the in-order traversal:
Each node tracks .
Range Reversal in
#To reverse the subarray :
split(T, r)into .split(T_1, l - 1)into .- now isolates exactly the subarray .
- Toggle a lazy boolean
reversedflag at 's root (which pushes down child pointer swaps on demand). - `merge(merge(T_{<l}, T_{\text{target}}), T_{>r})$ to restore the global tree.
8. Comparative Analysis: Splay Tree vs. Treap vs. AVL/Red-Black
#| Dimension | Splay Tree | Treap | AVL / Red-Black Tree |
|---|---|---|---|
| Balance Mechanism | Self-adjusting heuristics (Splaying) | Random priority heap ordering | Deterministic height/color invariants |
| Worst-Case Search | (Amortized ) | (Expected ) | Strictly guaranteed |
| Node Overhead | 0 extra bits (Only pointers) | 32-bit random priority integer | 1-bit color or 2-bit balance factor |
| Primary Strength | Working-set caching, Pareto 80/20 | Code simplicity, split/merge slicing | Mission-critical hard real-time latency |
| Concurrency Safety | Poor (read operations mutate tree structure) | Excellent (reads do not alter topology) | Excellent (read operations are pure) |
9. Common Traps, Edge Cases & Implementation Pitfalls
#- Splaying During Read Operations:
- In a Splay Tree, a
searchorcontainsquery must mutate the tree by splaying the found node (or last accessed node) to the root. Treating search as a read-only const operation destroys the amortized guarantee.
- In a Splay Tree, a
- Rotating the Wrong Node First in Zig-Zig:
- Rotating parent before grandparent in Zig-Zig fails to halve the path length, degrading sequential access to quadratic time.
- Treap Merge Precondition Violation:
- Calling
merge(L, R)when silently corrupts the Binary Search Tree invariant. Always verify or ensure that key ranges are strictly disjoint before merging.
- Calling
10. References & Academic Attribution
#- Sleator, D. D., & Tarjan, R. E. (1985). Self-adjusting binary search trees. Journal of the ACM (JACM), 32(3), 652–686.
- Aragon, C. R., & Seidel, R. (1989). Randomized search trees. 30th Annual Symposium on Foundations of Computer Science (SFCS), 540–545. IEEE.
- Tarjan, R. E. (1985). Amortized computational complexity. SIAM Journal on Algebraic Discrete Methods, 6(2), 306–318.