Core idea
- Divide and conquer on the universe, not on the item count; the structure is defined recursively and parametrized by universe size
u31:26. - A
VEB(u)contains√uchild structures each on a universe of size√u, plus one summary structure on a universe of size√u, plus the minimum stored separately32:19,32:58.
Fields of v = VEB(u)
v.cluster[0 … √u − 1]— an array of√uinstances ofVEB(√u); elements inside each are in0 … √u − 134:02,34:47.v.summary— also aVEB(√u)instance34:47.v.min,v.max— integers in0 … u − 135:22.
Splitting an item
- Write
xin binary and cut it into its left halfcand right halfi, sox = ⟨c, i⟩— effectively writingxin base√u, with bothc, i ∈ [0, √u − 1]36:03,36:58,37:36. candiare each extracted in constant time by shifting and masking with bitwise AND38:14,40:01.- Storage rule: if
x ∈ S, storeiinsidecluster[c]37:36. - Summary rule: when inserting into a cluster that was empty, also insert
cinto the summary — so the summary tracks which clusters are non-empty38:45.
graph TD
V["VEB(u)<br/>v.min, v.max stored explicitly"]
V --> S["v.summary : VEB(√u)<br/>which clusters are non-empty"]
V --> C0["v.cluster[0] : VEB(√u)"]
V --> C1["v.cluster[c] : VEB(√u)<br/>holds i where x = ⟨c,i⟩"]
V --> Cn["v.cluster[√u − 1] : VEB(√u)"]
Query: Predecessor(v, x = ⟨c, i⟩)
- If
x > v.max, returnv.max— everything in the structure is smaller thanx41:13. - Else if
v.cluster[c].min < x, the predecessor lives in my own cluster: recurse oniincluster[c]40:01,42:11. - Else (cluster empty, or
x ≤its min) the predecessor is in the largest non-empty cluster before me: setc' = Predecessor(v.summary, c)and returnv.cluster[c'].max— no recursion into that cluster needed40:52,43:28.
A student first proposed recursing into the found cluster; the lecturer's refinement is that storing min (and max) explicitly saves one recursive call 39:15, 39:37.
Insert: Insert(v, x = ⟨c, i⟩)
The minimum is treated as special — it lives only in v.min and is not also stored recursively in its cluster; think of v.min as a buffer 43:28, 44:29, 46:20.
- If
vis empty:v.min ← x; return44:54. - If
x < v.min: swapxwithv.min(recomputec, ifor the newx) so that the value being recursively inserted is never the minimum45:56,46:20. - If
v.cluster[c].minis null (cluster was empty):Insert(v.summary, c)46:44,47:03. Insert(v.cluster[c], i)47:51.
Deletion is left to the reader — conceptually not very different 47:51.
Time analysis
- Predecessor: at most one recursive call in every branch, so
T(u) = T(√u) + O(1), givingT(u) = O(log log u) = O(log w), sinceu = 2^w47:51,48:45,49:13. - Insert, naive reading: the summary insert doesn't return, so
T(u) ≤ 2·T(√u) + O(1), equivalentlyT(w) ≤ 2·T(w/2) + O(1)— square-rootinguis halvingw— which solves toO(w), i.e.log u. Too slow50:23,51:00. - Why that is pessimistic (student observation): if the cluster was empty, then the subsequent cluster insertion hits the "empty" base case and returns immediately in constant time. So the two recursive calls are never both deep — the
2becomes a151:38,51:58. - Hence insertion is also
O(log log u)52:30.