1. Introduction & Motivation
AVL trees and Red-Black trees keep $O(\log n)$ height by enforcing an explicit invariant on every insert and delete: a height difference bound, or a color-and-black-height rule. Enforcing that invariant requires bookkeeping — heights, balance factors, or colors — that must be updated and checked at every node on the path back to the root.
This lesson covers three data structures that reach the same $O(\log n)$-ish behavior a different way: they use randomness or self-adjustment instead of an explicit structural invariant.
- Treap — a BST where each node also carries a random priority, and the tree simultaneously maintains BST-order on keys and heap-order on priorities. Randomness alone keeps the expected height logarithmic, with no rebalancing logic at all beyond a few rotations.
- Splay Tree — a BST with no balance invariant whatsoever. Instead, every access moves the touched node all the way to the root via a sequence of rotations. No single access is guaranteed fast, but a long sequence of accesses is provably efficient — this is amortized, not worst-case, efficiency.
- Cartesian Tree — built directly from an array so that it is simultaneously a min-heap (or max-heap) on values and its inorder traversal recovers the array. It needs no rotations at construction time at all, and it connects binary trees to a completely different problem: range-minimum queries.
All three are still binary search trees underneath. What differs is how they stay shallow enough to be useful.
2. Prerequisites
This lesson deliberately does not re-derive two things you should already have from earlier parts of this series, and links to them instead of repeating them:
- BST basics and tree rotation mechanics — read Part 10: BST & Balanced Trees, specifically the AVL Rotations section. Its
left_rotate/right_rotateprimitives are the exact same local pointer-surgery operations that Treaps and Splay Trees use — only the trigger for calling them changes (a broken heap-order property here, instead of a broken height-balance factor there). - The heap-order property — read Part 11: Heaps, Sorting & Hashing, specifically the Heap Fundamentals section. A Treap layers BST-order (on keys) and heap-order (on priorities) onto the same set of nodes; a Cartesian Tree layers BST-order (on array position) and heap-order (on array value) onto the same nodes. If heap-order feels shaky, that section is the place to firm it up first.
You should also be comfortable with recursive BST insert/search and basic array indexing before continuing.
3. Treap: Core Insight
Put differently: instead of an algorithm deciding when to rotate by inspecting heights or colors, the random priorities decide the shape in advance, and rotations are only ever used to restore heap-order after a BST-order-only insert or delete disturbs it — reusing the same rotation primitives from Part 10.
4. Treap: Rotation-Based Insert/Delete
Insert is a normal BST insert (walk down by key) followed by bubbling the new node up with rotations wherever it violates heap-order with its parent — the new node keeps rotating upward until its priority is no longer greater than its parent's. Delete does the reverse: instead of splicing an internal node out directly (which is awkward once it has two children), rotate the node downward — always rotating up whichever child has the higher priority — until it becomes a leaf, then drop it in $O(1)$.
5. Treap: Split and Merge
Because heap-order is enforced by independent random numbers rather than a global shape rule, two Treap operations become almost trivially simple: split(treap, key) — partition one Treap into two Treaps, all keys < key and all keys ≥ key — and merge(left, right) — combine two Treaps, given that every key in left is less than every key in right, into one. Both are $O(\log n)$ expected and recurse on exactly one child at each step.
Why this makes Treap uniquely composable: AVL and Red-Black invariants (height balance, or black-height/color rules) are properties of the entire tree's shape, so splicing two balanced trees together at an arbitrary seam can violate the invariant in ways that need careful, multi-case rebalancing along that seam — it is possible (a "join" algorithm exists for balanced BSTs), but it is genuinely more machinery. A Treap's merge only ever needs to compare the two roots' priorities and recurse; because the priorities are random, the result is a valid, well-balanced Treap automatically. No case analysis, no rebalancing pass.
6. Treap: Implicit Treap
split(root, k) partitions "the first $k$ elements" from "the rest" using sizes instead of key comparisons. This is the classic "array as a balanced tree" technique, and it turns insert-at-index, delete-at-index, and range reversal into $O(\log n)$ operations — none of which a plain array supports better than $O(n)$.The only new piece of machinery is lazy propagation: to reverse a range in $O(\log n)$ rather than $O(\text{range length})$, mark a node's whole subtree as reversed with a single flag, and only actually swap its two children (pushing the flag one level down) the next time that node is visited.
Range rotation (cyclic shift) builds on the same primitive: rotating a range left by $k$ is exactly the classic "reverse the whole range, then reverse each of the two pieces" trick, so it costs three calls to reverse_range — still $O(\log n)$.
7. Splay Tree: Core Insight
left_rotate/right_rotate primitives from AVL Rotations; what's new is only the pattern in which they're chained together (see the next section) and the fact that they run on every access, not just after a height violation.The payoff: whatever node you just looked up is now at the root, so looking it up again is $O(1)$. A single splay can still cost $O(n)$ in the worst case (a long chain), but no single access can be catastrophically expensive often — and that trade-off is exactly what the amortized analysis in section 9 makes precise.
8. Splay Tree: Splaying Cases
Splaying node x repeats one of three moves, based on x's position relative to its parent and grandparent, until x is the root:
- Zig — the parent is already the root. One rotation (left or right, whichever makes
xtake the parent's place) finishes the splay. - Zig-zig —
xand its parent are both left children (or both right children). Rotate the grandparent first, then the parent — same direction twice. - Zig-zag —
xis a left child and its parent is a right child (or vice versa). Rotate the parent, then the grandparent — opposite directions.
Trace: start from the degenerate chain $4 \to 3 \to 2 \to 1$ (each node is the left child of the previous one — the worst possible BST shape), and splay(1):
| Step | x, parent, grandparent | Case | Rotations applied | New local root |
|---|---|---|---|---|
| 1 | x=1, parent=2, grand=3 | Zig-zig (LL) | rotate_right(3), then rotate_right(2) | 1 (now a child of 4) |
| 2 | x=1, parent=4, grand=none | Zig | rotate_right(4) | 1 (whole-tree root) |
After both steps, node 1 is the root with 1.right = 4, 4.left = 2, and 2.right = 3 — still a valid BST (inorder is unchanged: 1, 2, 3, 4), just reshaped around the newly accessed node.
9. Splay Tree: Working-Set / Amortized Analysis
A splay tree has no invariant to point to as proof of balance — a single splay really can take $O(n)$ time on an unlucky chain, exactly as the trace above hints at (splaying the deepest node of a chain does not instantly produce a perfectly balanced tree). What Sleator and Tarjan proved is an amortized guarantee: over any sequence of $m$ operations on an $n$-node splay tree, the total cost is $O(m \log n + n \log n)$ — an average of $O(\log n)$ per operation, even though individual operations can be expensive.
The proof uses a potential function $\Phi$ — a single number summarizing "how much rebalancing potential is stored in the tree's current shape," defined (conceptually) as $\Phi(T) = \sum_{v} \log(\text{size of the subtree rooted at } v)$. Splaying an unbalanced node does real rotation work, but it also lowers the potential by roughly the same amount, so the sum of (real work) + (change in potential) — the amortized cost charged to that operation — comes out to $O(\log n)$. This is the same aggregate-style argument used for dynamic array doubling in Algorithm Analysis, just with a cleverer potential function; the full derivation (the "access lemma") is beyond this lesson's scope.
10. Cartesian Tree
The construction keeps a stack of node indices with increasing values from bottom to top (a monotonic stack — the same maintain-and-pop mechanic covered in depth in Monotonic Stacks, Queues & Histogram Problems; this lesson only applies it, it doesn't re-derive the technique). For each new element, pop everything larger off the stack — the last node popped becomes the new element's left child (it was the largest thing smaller elements couldn't beat) — then attach the new element as the right child of whatever remains on top of the stack (or make it the new root if the stack emptied).
The RMQ connection: because the Cartesian tree recursively places the minimum of a range at the root of that range's subtree, the lowest common ancestor (LCA) of positions $i$ and $j$ in the tree is exactly the position of the minimum value in $arr[i..j]$. So range-minimum query = LCA once the Cartesian tree is built — a classic reduction (used, for example, in the $O(n)$-preprocessing / $O(1)$-query Bender–Farach-Colton RMQ algorithm).
# Illustrative only (not O(1) per query — a real RMQ structure adds an LCA index):
def parent_map(node, parent, out):
out[id(node)] = parent
if node.left:
parent_map(node.left, node, out)
if node.right:
parent_map(node.right, node, out)
def path_to_root(node, parents):
path = []
while node is not None:
path.append(id(node))
node = parents.get(id(node))
return path
def lca(u, v, root):
parents = {}
parent_map(root, None, parents)
ancestors_of_u = set(path_to_root(u, parents))
node = v
while id(node) not in ancestors_of_u:
node = parents[id(node)]
return node
i, j = 3, 8 # query range: arr[3..8]
result = lca(nodes[i], nodes[j], root)
print(f"min(arr[{i}..{j}]) =", result.value) # 1 — matches min(arr[3:9])
The Treap connection: a Cartesian tree is a Treap — the BST key is the element's position in the array (so inorder traversal recovers the array), and the heap priority is the element's actual value instead of an independent random number. When the array's values arrive in an effectively random order, the resulting tree has the same expected $O(\log n)$ height as a Treap; feed it an already-sorted (or adversarially ordered) array, though, and there is no randomness left to rely on — the tree degenerates to a chain, height $O(n)$.
11. Complexity Analysis
- Treap: $O(\log n)$ expected time for search, insert, delete, split, and merge. Worst case is $O(n)$ (an adversarial priority assignment), but that is exponentially unlikely with independent random priorities. Space is $O(n)$.
- Splay Tree: $O(\log n)$ amortized time per operation over any sequence of operations; a single operation's worst case is $O(n)$. No extra per-node bookkeeping (no heights, no colors, no priorities) beyond child/parent pointers.
- Cartesian Tree: $O(n)$ one-time construction from an array via the monotonic-stack method. Height is $O(\log n)$ on average for randomly-ordered input (equivalent to a Treap), but can degrade to $O(n)$ for adversarial input such as an already-sorted array, since nothing here is randomized unless the input itself is.
12. Applications & Alternatives
Where each structure earns its keep
- Treap / Implicit Treap: competitive-programming "rope"-style array structures (fast insert/delete/reverse anywhere in a sequence), mergeable ordered sets, persistent data structures (splitting/merging without in-place mutation).
- Splay Tree: caches and working-set-sensitive workloads, the classic Unix
splay(3)library, LRU-like access patterns, and as the base structure for link-cut trees in advanced graph algorithms. - Cartesian Tree: the RMQ <=> LCA reduction used inside $O(n)$/$O(1)$ range-minimum-query algorithms; also a backbone for treap-style "merge sort tree" and suffix-array-adjacent techniques.
- When to prefer AVL/Red-Black instead: when you need a hard worst-case guarantee on every single operation (real-time systems, adversarial inputs) rather than an expected or amortized one.
13. Exercises & Mastery
Easy
- Implement
search(root, key)for a Treap (plain BST search — priorities don't affect where a key lives, only the tree's shape).
Medium
- Using an Implicit Treap, reverse an arbitrary range
[l, r]of an array in $O(\log n)$, then verify the result against a naive $O(n)$ array reversal on several random ranges.
Hard
- Build a Cartesian tree from an array, then answer a range-minimum query by computing the LCA of the two endpoint nodes. Verify against a naive $O(n)$-per-query linear scan.
Review rhythm: tomorrow, redraw the Treap heap-order-plus-BST-order picture and the three splay cases from memory; next week, implement all three structures' core operation (Treap insert, Splay Tree splay, Cartesian tree build) without notes; several weeks later, explain to someone who knows AVL trees why none of these three needs a height or color field.
14. Summary Cheat Sheet
- Treap = BST-order on keys + heap-order on random priorities; balance comes from randomness, not an explicit rule.
- Treap insert/delete reuse the same rotation primitives as AVL, just triggered by a broken heap-order property instead of a broken height balance.
- Treap split/merge are what AVL/Red-Black trees can't do cheaply without extra machinery — that's Treap's signature advantage.
- Implicit Treap swaps the key for subtree size/rank, turning the tree into an $O(\log n)$ array (insert/delete/reverse anywhere).
- Splay Tree has zero balance invariant; every access splays the touched node to the root via zig / zig-zig / zig-zag.
- Splay Tree's guarantee is $O(\log n)$ amortized (via a potential-function argument), not worst-case per operation.
- Working-set intuition: recently-touched nodes in a Splay Tree stay cheap to re-access.
- Cartesian Tree = min/max-heap on array values + inorder recovers array order; built in $O(n)$ via a monotonic stack.
- Cartesian Tree's LCA = RMQ; a Cartesian Tree is literally a Treap with array values standing in for random priorities.