Splay Tree
Key Points
- •A splay tree is a self-adjusting binary search tree that moves the most recently accessed node to the root with rotations.
- •The core splay operation uses three cases called zig, zig-zig, and zig-zag to restructure the tree around the accessed node.
- •Although a single operation can be linear in the worst case, the amortized time per search/insert/delete is O(log n).
- •Splay trees exploit locality of reference: recently or frequently accessed keys become faster to reach.
- •They naturally support split and join (merge) operations in O(log n) amortized time without extra random bits or colors.
- •Implicit-key splay trees can represent sequences and support range operations like reverse using lazy propagation.
- •They form the backbone of advanced structures like Link-Cut Trees for dynamic trees.
- •Splay trees are pointer-heavy; careful implementation of rotations, parent updates, and lazy flags is essential.
Prerequisites
- →Binary Search Trees — Understanding the BST property and how search/insert/delete work is essential before adding splay rotations.
- →Tree Rotations — Splay operations are sequences of rotations; you must be comfortable with left/right rotations and pointer updates.
- →Amortized Analysis and Potential Method — The O(log n) guarantee is amortized; the potential function explains why sequences of operations are efficient.
- →Pointer Manipulation in C++ — Implementing splay trees requires careful parent/child pointer maintenance and memory management.
- →Lazy Propagation in Trees — Implicit-key splay trees often use lazy flags (e.g., reverse) that must be pushed before rotations or traversals.
Detailed Explanation
Tap terms for definitions01Overview
Hook: Imagine a library where the most borrowed books always end up on the front shelf. Each time a book is borrowed, the librarian moves it closer to the front so that the next borrow will be faster. Concept: That is the spirit of a splay tree—a binary search tree (BST) that self-adjusts after every access so that recently touched nodes move near the root. It does this with a simple, unified restructuring step called splaying, composed of rotations (zig, zig-zig, zig-zag). Over time, this leads to excellent average performance. Example: If your program queries the same few keys repeatedly (like the current maximum, or a small group of hot items), a splay tree quickly arranges itself so those keys live near the root, making subsequent operations faster. Splay trees maintain the BST property (left subtree keys are smaller, right subtree keys are larger) but do not maintain strict balance like AVL or Red-Black Trees. Instead, they rely on amortized analysis: while a single operation may be slow, any sequence of m operations costs at most O(m log n) overall. This simplicity (no colors, no priorities) and strong theoretical guarantees make splay trees a powerful tool in competitive programming and as a core primitive in advanced data structures.
02Intuition & Analogies
Think of a long hallway of lockers (keys) ordered by number. If a student keeps opening locker 37, the school custodian eventually moves locker 37 closer to the entrance so the student spends less time walking. Each time the student opens a locker, it gets pulled toward the entrance. Over days, the pattern of frequent access shapes the hallway so the most used lockers are near the front. In a splay tree, the hallway is a search path from the root to a node, and the custodian’s move is a sequence of rotations called splaying that brings the accessed node to the root. There are three cases during the climb: zig (one rotation when the node’s parent is the root), zig-zig (two rotations in the same direction when the node and parent are both left or both right children), and zig-zag (two rotations in opposite directions when the node is a left child and its parent is a right child, or vice versa). These moves are local but have a global effect: deeply nested frequently used nodes bubble up and stay closer to the top. This behavior is great for workloads with locality: if you touch nearby keys repeatedly, the tree molds itself to your pattern, making those accesses cheaper. Just like a bookshelf that reorders itself based on what you grab most, a splay tree is always learning from your last move and setting you up for a faster next one.
03Formal Definition
04When to Use
Use a splay tree when:
- You need a general-purpose ordered set/map with excellent amortized O(log n) performance and very simple code (no color or height maintenance).
- Your workload exhibits temporal/spatial locality (e.g., repeatedly accessing a small set of keys or adjacent ranges). The tree adapts and keeps hot items near the root.
- You want split and join (merge) in O(log n) amortized time with minimal extra machinery. This is useful in problems that need to partition or concatenate ordered sets.
- You need an implicit-key tree (sequence) to support operations like kth, range reverse, cut-paste, and rotate with lazy propagation; splay trees are a classic option alongside treaps.
- You plan to implement Link-Cut Trees for dynamic tree queries; splay trees are the fundamental component for preferred path maintenance. Avoid splay trees when you need strict worst-case O(log n) per operation (e.g., real-time or latency-critical single operations) or when constant factors of rotations are an issue and your access pattern has no locality; in such cases, AVL/Red-Black/treaps or order-statistic trees might be preferable.
⚠️Common Mistakes
- Incorrect rotations and parent/child updates: Failing to fix all parent pointers, or forgetting to reattach the rotated subtree to the grandparent, causes subtle corruption. Always handle the three pointers (node, parent, grandparent) and update sizes after each rotation.
- Not pushing lazy flags before rotations (implicit splay): When using reverse/lazy tags for sequences, you must push pending flags down along the access path before rotating; otherwise, subtree structures and sizes become inconsistent.
- Forgetting to update subtree sizes/counts: If you track size or duplicate counts, ensure every structural change (link, cut, rotate, increment/decrement counts) is followed by a pull/update.
- Mishandling duplicates: Decide on a policy (store counts in nodes vs. allow equal keys to go left/right consistently). Mixing strategies leads to broken rank/kth queries.
- Splaying the wrong node after operations: After search, splay the found node or the last accessed node; after insert/erase, make sure the intended node becomes root to reap amortization and to make subsequent joins/splits trivial.
- Ignoring edge cases in split/join: join(left, right) requires all keys in left < all keys in right. Splay the maximum of left before attaching right, and be mindful of empty trees.
- Deep recursion: Prefer iterative splay/rotate to avoid stack overflows in adversarial cases. Also watch memory management in C++ to avoid leaks (consider pools or smart pointers in production).
Key Formulas
Splay Tree Potential
Explanation: The potential is the sum of logs of subtree sizes. It captures how 'unbalanced' the tree is and is used to prove amortized bounds.
Access Lemma (Unweighted Form)
Explanation: The amortized cost A(x) to splay node x is bounded by a constant times the change in potential. Since s(root) n and s(x) 1, this implies O( n) amortized time.
Weighted Access Lemma
Explanation: With node weights summing to W, the amortized access cost depends on the weight of x. Heavier nodes are cheaper to access on average.
Total Cost Over m Operations
Explanation: Any sequence of m operations on a splay tree with at most n nodes takes O(m log n) time. Occasional expensive operations are 'paid for' by cheap ones.
Rank Definition
Explanation: The rank of x is the count of elements strictly smaller than x. It can be computed by traversing and summing left subtree sizes and duplicate counts.
kth Smallest via Sizes
Explanation: To find the kth element, repeatedly compare k with the size of the left subtree plus the node’s count to decide to go left, return current, or go right.
Split Specification
Explanation: Splitting by a key x produces two BSTs with all keys in L strictly less than x and all keys in R at least x. Implemented by splaying the lowe(x).
Join Procedure
Explanation: To join, bring the maximum of L to the root so it has no right child, then attach R as its right subtree. This runs in amortized O(log n).
Complexity Analysis
Code Examples
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 struct Node { 5 int key; 6 int cnt; // number of duplicates stored at this node 7 int sz; // size of subtree = left.sz + right.sz + cnt 8 Node *p, *l, *r; 9 Node(int k): key(k), cnt(1), sz(1), p(nullptr), l(nullptr), r(nullptr) {} 10 }; 11 12 struct Splay { 13 Node* root = nullptr; 14 15 int size(Node* x) const { return x ? x->sz : 0; } 16 void pull(Node* x) { 17 if (!x) return; 18 x->sz = x->cnt + size(x->l) + size(x->r); 19 } 20 bool isLeft(Node* x) { return x->p && x->p->l == x; } 21 22 void connect(Node* x, Node* p, bool leftChild) { 23 if (x) x->p = p; 24 if (!p) return; 25 if (leftChild) p->l = x; else p->r = x; 26 } 27 28 void rotate(Node* x) { 29 Node* p = x->p; 30 Node* g = p ? p->p : nullptr; 31 if (!p) return; 32 bool xLeft = (x == p->l); 33 // Perform rotation 34 if (xLeft) { 35 connect(x->r, p, true); // p->l = x->r 36 connect(p, x, false); // x->r = p 37 } else { 38 connect(x->l, p, false); // p->r = x->l 39 connect(p, x, true); // x->l = p 40 } 41 // Attach x to grandparent 42 if (g) connect(x, g, p == g->l); else x->p = nullptr; 43 // Update sizes bottom-up 44 pull(p); 45 pull(x); 46 } 47 48 void splay(Node* x, Node* to = nullptr) { 49 if (!x) return; 50 while (x->p != to) { 51 Node* p = x->p; 52 Node* g = p->p; 53 if (g != to) { 54 bool zigzig = ( (x == p->l) == (p == g->l) ); 55 if (zigzig) rotate(p); else rotate(x); 56 } 57 rotate(x); 58 } 59 if (!to) root = x; 60 } 61 62 Node* lower_bound_node(int k) { 63 Node* cur = root; Node* ans = nullptr; Node* last = nullptr; 64 while (cur) { 65 last = cur; 66 if (k <= cur->key) { ans = cur; cur = cur->l; } 67 else cur = cur->r; 68 } 69 if (last) splay(last); 70 if (ans) splay(ans); 71 return ans; // may be null if all keys < k 72 } 73 74 bool contains(int k) { 75 Node* cur = root; Node* last = nullptr; 76 while (cur) { 77 last = cur; 78 if (k == cur->key) { splay(cur); return true; } 79 cur = (k < last->key) ? last->l : last->r; 80 } 81 if (last) splay(last); // splay last accessed to root 82 return false; 83 } 84 85 void insert(int k) { 86 if (!root) { root = new Node(k); return; } 87 Node* cur = root; Node* p = nullptr; 88 while (cur) { 89 p = cur; 90 if (k == cur->key) { cur->cnt++; pull(cur); splay(cur); return; } 91 cur = (k < cur->key) ? cur->l : cur->r; 92 } 93 Node* x = new Node(k); 94 if (k < p->key) p->l = x; else p->r = x; x->p = p; 95 splay(x); 96 } 97 98 // Join: assumes all keys in L < all keys in R 99 Node* join(Node* L, Node* R) { 100 if (!L) return R; if (!R) return L; 101 // Splay max(L) to root 102 Node* cur = L; 103 while (cur->r) cur = cur->r; 104 // Make cur parentless root of L 105 Splay tmp; tmp.root = L; tmp.splay(cur); 106 cur->r = R; if (R) R->p = cur; pull(cur); 107 return cur; 108 } 109 110 void erase(int k) { 111 if (!contains(k)) return; // splay brings k to root if exists 112 if (root->key == k && root->cnt > 1) { root->cnt--; pull(root); return; } 113 Node* L = root->l; Node* R = root->r; 114 if (L) L->p = nullptr; if (R) R->p = nullptr; 115 delete root; 116 root = join(L, R); 117 } 118 119 // rank: number of keys < k 120 int rank_of(int k) { 121 int res = 0; Node* cur = root; Node* last = nullptr; 122 while (cur) { 123 last = cur; 124 if (k <= cur->key) cur = cur->l; 125 else { res += size(cur->l) + cur->cnt; cur = cur->r; } 126 } 127 if (last) splay(last); 128 return res; 129 } 130 131 // kth: 1-indexed by multiplicity 132 int kth(int k) { 133 Node* cur = root; 134 if (!cur || k <= 0 || k > size(cur)) throw runtime_error("k out of bounds"); 135 while (cur) { 136 int leftSize = size(cur->l); 137 if (k <= leftSize) cur = cur->l; 138 else if (k <= leftSize + cur->cnt) { splay(cur); return cur->key; } 139 else { k -= leftSize + cur->cnt; cur = cur->r; } 140 } 141 throw runtime_error("unreachable"); 142 } 143 144 // predecessor of k (strictly less). Returns optional<int> 145 optional<int> predecessor(int k) { 146 lower_bound_node(k); // now root is lower_bound or last 147 Node* cur = root; 148 if (!cur) return nullopt; 149 if (cur->key < k) return cur->key; // lower_bound didn't exist; cur is max 150 // go to left subtree's max 151 Node* p = cur->l; 152 if (!p) return nullopt; 153 while (p->r) p = p->r; 154 splay(p); 155 return p->key; 156 } 157 158 // successor of k (strictly greater). Returns optional<int> 159 optional<int> successor(int k) { 160 Node* lb = lower_bound_node(k+1); // first >= k+1 161 if (!lb) return nullopt; // all < k+1 162 return lb->key; 163 } 164 165 void inorder(Node* x, vector<pair<int,int>>& out) const { 166 if (!x) return; 167 inorder(x->l, out); 168 out.push_back({x->key, x->cnt}); 169 inorder(x->r, out); 170 } 171 vector<pair<int,int>> to_vector() const { 172 vector<pair<int,int>> v; inorder(root, v); return v; 173 } 174 }; 175 176 int main() { 177 ios::sync_with_stdio(false); 178 cin.tie(nullptr); 179 180 Splay T; 181 // Demo operations 182 vector<int> a = {5, 3, 9, 3, 7, 1, 9, 9}; 183 for (int x : a) T.insert(x); 184 185 cout << "rank(7) = " << T.rank_of(7) << "\n"; // count < 7 186 cout << "kth(3) = " << T.kth(3) << "\n"; // 3rd smallest by multiplicity 187 188 auto pred = T.predecessor(6); if (pred) cout << "pred(6) = " << *pred << "\n"; else cout << "no pred\n"; 189 auto succ = T.successor(6); if (succ) cout << "succ(6) = " << *succ << "\n"; else cout << "no succ\n"; 190 191 T.erase(9); // remove one 9 192 T.erase(2); // no effect 193 194 auto v = T.to_vector(); 195 cout << "Inorder (val:cnt):"; 196 for (auto [k,c]: v) cout << ' ' << k << ':' << c; 197 cout << "\n"; 198 return 0; 199 } 200
This program implements a classic key-based splay tree with duplicate counts (multiset). It supports insert, erase, search (contains), rank (count of elements less than a key), kth (1-indexed by multiplicity), predecessor, successor, inorder traversal, and exposes split/join logic inside erase. Rotations, splaying, and subtree size maintenance provide O(log n) amortized operations.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 struct Node { 5 int key, sz, cnt; Node *p, *l, *r; 6 Node(int k): key(k), sz(1), cnt(1), p(nullptr), l(nullptr), r(nullptr) {} 7 }; 8 9 int size(Node* x){ return x ? x->sz : 0; } 10 void pull(Node* x){ if(x) x->sz = x->cnt + size(x->l) + size(x->r); } 11 12 bool isLeft(Node* x){ return x->p && x->p->l == x; } 13 void connect(Node* x, Node* p, bool left){ if(x) x->p = p; if(!p) return; if(left) p->l = x; else p->r = x; } 14 15 void rotate(Node*& root, Node* x){ 16 Node* p = x->p; if(!p) return; Node* g = p->p; 17 if (x == p->l) { connect(x->r, p, true); connect(p, x, false); } 18 else { connect(x->l, p, false); connect(p, x, true); } 19 if (g) connect(x, g, p==g->l); else x->p = nullptr, root = x; 20 pull(p); pull(x); 21 } 22 23 void splay(Node*& root, Node* x, Node* to=nullptr){ 24 if(!x) return; while (x->p != to){ Node* p = x->p; Node* g = p->p; if (g != to){ bool zigzig = ((x==p->l)==(p==g->l)); if(zigzig) rotate(root,p); else rotate(root,x);} rotate(root,x);} if(!to) root = x; } 25 26 Node* build_from_sorted(vector<int>& a, int l, int r){ 27 if(l>r) return nullptr; int m=(l+r)/2; Node* x=new Node(a[m]); x->cnt=1; x->l=build_from_sorted(a,l,m-1); if(x->l) x->l->p=x; x->r=build_from_sorted(a,m+1,r); if(x->r) x->r->p=x; pull(x); return x; } 28 29 Node* lower_bound_node(Node*& root, int k){ 30 Node* cur=root; Node* ans=nullptr; Node* last=nullptr; 31 while(cur){ last=cur; if(k<=cur->key){ ans=cur; cur=cur->l; } else cur=cur->r; } 32 if(last) splay(root,last); if(ans) splay(root,ans); return ans; 33 } 34 35 pair<Node*,Node*> split_by_key(Node* root, int key){ 36 if(!root) return {nullptr,nullptr}; 37 Node* lb = lower_bound_node(root, key); 38 if(!lb){ // all keys < key 39 return {root, nullptr}; 40 } 41 // lb is first >= key and at root 42 Node* L = lb->l; if(L) L->p=nullptr; lb->l=nullptr; pull(lb); 43 return {L, lb}; 44 } 45 46 Node* join(Node* L, Node* R){ 47 if(!L) return R; if(!R) return L; 48 // splay max(L) 49 Node* cur = L; while(cur->r) cur=cur->r; splay(L, cur); 50 cur->r = R; if(R) R->p = cur; pull(cur); return cur; 51 } 52 53 void inorder(Node* x, vector<int>& out){ if(!x) return; inorder(x->l,out); for(int i=0;i<x->cnt;i++) out.push_back(x->key); inorder(x->r,out);} 54 55 int main(){ 56 ios::sync_with_stdio(false); cin.tie(nullptr); 57 58 // Build a splay tree from sorted array for demo 59 vector<int> a = {1,2,3,5,7,9}; 60 Node* root = build_from_sorted(a, 0, (int)a.size()-1); 61 62 // Split by key 6: left contains <6, right contains >=6 63 auto [L, R] = split_by_key(root, 6); 64 65 vector<int> vL, vR; inorder(L, vL); inorder(R, vR); 66 cout << "Left:"; for(int x: vL) cout << ' ' << x; cout << "\n"; 67 cout << "Right:"; for(int x: vR) cout << ' ' << x; cout << "\n"; 68 69 // Join back 70 Node* J = join(L, R); 71 vector<int> vJ; inorder(J, vJ); cout << "Joined:"; for(int x: vJ) cout << ' ' << x; cout << "\n"; 72 return 0; 73 } 74
This program demonstrates splitting a splay tree by a key into two trees (keys < key, and keys ≥ key), then joining them back. The split is implemented by splaying the lower_bound(key) to the root and detaching its left subtree. Join splays the maximum of the left tree so that it has no right child, and then attaches the right tree. Both operations are amortized O(log n).
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 struct Node { 5 int val; // stored sequence value 6 int sz; // subtree size (number of nodes) 7 bool rev; // lazy reverse flag 8 Node *p, *l, *r; 9 Node(int v): val(v), sz(1), rev(false), p(nullptr), l(nullptr), r(nullptr) {} 10 }; 11 12 int size(Node* x){ return x ? x->sz : 0; } 13 14 void push(Node* x){ 15 if (!x || !x->rev) return; 16 x->rev = false; 17 swap(x->l, x->r); 18 if (x->l) x->l->rev ^= 1; 19 if (x->r) x->r->rev ^= 1; 20 } 21 22 void pull(Node* x){ if(x) x->sz = 1 + size(x->l) + size(x->r); } 23 24 void connect(Node* x, Node* p, bool left){ if(x) x->p = p; if(!p) return; if(left) p->l = x; else p->r = x; } 25 26 void rotate(Node*& root, Node* x){ 27 Node* p = x->p; if(!p) return; Node* g = p->p; 28 push(p); push(x); // ensure correctness under lazy flags 29 if (x == p->l) { connect(x->r, p, true); connect(p, x, false); } 30 else { connect(x->l, p, false); connect(p, x, true); } 31 if (g) connect(x, g, p==g->l); else x->p = nullptr, root = x; 32 pull(p); pull(x); 33 } 34 35 void splay(Node*& root, Node* x, Node* to=nullptr){ 36 if(!x) return; 37 // Push path from top to x before rotations 38 auto pushPath = [&](Node* a){ vector<Node*> st; Node* cur=a; while(cur){ st.push_back(cur); cur=cur->p; } reverse(st.begin(), st.end()); for(Node* t: st) push(t); }; 39 pushPath(x); 40 while (x->p != to){ 41 Node* p = x->p; Node* g = p->p; 42 if (g != to){ 43 // prepare for double rotation 44 push(g); push(p); push(x); 45 bool zigzig = ((x==p->l) == (p==g->l)); 46 if (zigzig) rotate(root, p); else rotate(root, x); 47 } 48 push(p); push(x); 49 rotate(root, x); 50 } 51 pull(x); 52 if(!to) root = x; 53 } 54 55 // kth: 1-indexed implicit position 56 Node* kth(Node*& root, int k){ 57 if (!root || k <= 0 || k > size(root)) return nullptr; 58 Node* cur = root; while (cur){ 59 push(cur); 60 int ls = size(cur->l); 61 if (k <= ls) cur = cur->l; 62 else if (k == ls + 1) { splay(root, cur); return cur; } 63 else { k -= ls + 1; cur = cur->r; } 64 } 65 return nullptr; 66 } 67 68 // Expose interval [l, r] as root->r->l between two sentinels 69 Node* expose(Node*& root, int l, int r){ 70 Node* left = kth(root, l-1); // sentinel at l-1 to root 71 splay(root, left); 72 Node* right = kth(root, r+1); // sentinel at r+1 to be root->r 73 splay(root, right, root); // splay under root so that right is root->r 74 return right->l; // middle subtree 75 } 76 77 void reverse_range(Node*& root, int l, int r){ if (l>r) return; Node* mid = expose(root, l, r); if(!mid) return; mid->rev ^= 1; } 78 79 void inorder_collect(Node* x, vector<int>& out){ if(!x) return; push(x); inorder_collect(x->l, out); out.push_back(x->val); inorder_collect(x->r, out); } 80 81 // Build a balanced tree from vector vals (without sentinels) 82 Node* build_balanced(const vector<int>& a, int l, int r){ if(l>r) return nullptr; int m=(l+r)/2; Node* x = new Node(a[m]); x->l = build_balanced(a, l, m-1); if(x->l) x->l->p = x; x->r = build_balanced(a, m+1, r); if(x->r) x->r->p = x; pull(x); return x; } 83 84 int main(){ 85 ios::sync_with_stdio(false); cin.tie(nullptr); 86 87 // Build initial sequence [1..8] with two sentinels at ends 88 vector<int> vals = {1,2,3,4,5,6,7,8}; 89 Node* root = nullptr; 90 Node* L = new Node(INT_MIN); // left sentinel 91 Node* R = new Node(INT_MAX); // right sentinel 92 Node* mid = build_balanced(vals, 0, (int)vals.size()-1); 93 // Link: L as root, mid as L->r, R as rightmost 94 root = L; root->r = mid; if (mid) mid->p = root; pull(root); 95 // Attach R 96 // splay to rightmost and attach R 97 Node* cur = root; while(cur->r) cur = cur->r; splay(root, cur); 98 root->r = R; R->p = root; pull(root); 99 100 // Reverse range [3, 6] 101 reverse_range(root, 3, 6); 102 103 // Collect sequence excluding sentinels 104 vector<int> out; inorder_collect(root, out); 105 // strip sentinels 106 vector<int> filtered; for(int x: out) if(x!=INT_MIN && x!=INT_MAX) filtered.push_back(x); 107 cout << "Sequence after reverse [3,6]:"; 108 for (int x: filtered) cout << ' ' << x; cout << '\n'; 109 110 // Another operation: reverse [2, 7] 111 reverse_range(root, 2, 7); 112 out.clear(); inorder_collect(root, out); filtered.clear(); 113 for(int x: out) if(x!=INT_MIN && x!=INT_MAX) filtered.push_back(x); 114 cout << "Sequence after reverse [2,7]:"; 115 for (int x: filtered) cout << ' ' << x; cout << '\n'; 116 117 return 0; 118 } 119
This program builds an implicit-key splay tree representing a sequence with two sentinels (left and right boundaries). It supports reversing any subarray [l, r] by exposing the interval with two splays and toggling a lazy reverse flag on the middle subtree. The push function ensures that pending reversals are applied before rotations or traversals.