van Emde Boas Trees
By the end of this lesson you will be able to explain why comparison-based priority queues can never beat Ω(lg n) per operation, but a structure built for a fixed integer universe {0, …, u−1} can search, insert and delete in only O(lg lg u) time — trace the direct-address bit vector, its binary tree of bits, the proto-vEB structure, and the full van Emde Boas tree step by step, and implement every one of vebMember, SUCCESSOR, PREDECESSOR, INSERT and DELETE in working Dart, verified against a sorted-set reference.
0. Why bother? Beating the comparison-sort lower bound
lg n comparisons in the worst case, the same reason binary search needs lg n guesses. But now imagine every parcel carries a locker number from 0 to u−1 and the shelves are built to exploit that numbering directly: a depot that shelves strictly by locker number can jump straight to shelf ⌊number / shelf-size⌋. Numbers let you do arithmetic tricks that pure comparisons never allow.The comparison-based priority queues you have met (binary heaps, red-black trees, Fibonacci heaps) and comparison sorting in general share a hard floor: any comparison-based priority queue must take Ω(lg n) time for at least one of INSERT or extractMin. Why? If both ran faster, you could sort n numbers in o(n lg n) time by doing n INSERTs followed by n extractMin calls — contradicting the Ω(n lg n) comparison-sorting lower bound, which is proved via the decision-tree argument.
Dart: the reduction as code — n INSERTs, then n extractMin calls sort n distinct keys. With comparisons only this loop costs Ω(n lg n); with a vEB tree as the queue it is O(n lg lg u) because the keys are used as addresses (tested in verify/c20.dart).
Input size → what’s feasible: n = 106 keys in u = 220 → a balanced BST needs about lg n = 20 comparisons per operation (2·107 steps in total), a vEB tree about 5 levels (5·106), but the dense vEB tree must pre-build 1,421,676 nodes; u = 232 → the dense tree would need 6.0·109 nodes (impossible), so use a BST or the reduced-space tree of Section 8.
But that lower bound only binds algorithms that treat keys as opaque, comparable blobs. The moment you're told keys are distinct integers drawn from a known, fixed range {0, 1, …, u−1} (exactly the assumption behind counting sort), you can use the key's own bit pattern as an address, the same trick this lesson pushes to its logical extreme. A van Emde Boas tree (vEB tree) supports MEMBER, INSERT, DELETE, MINIMUM, MAXIMUM, SUCCESSOR and PREDECESSOR — the full "dynamic set" toolkit — in O(lg lg u) worst-case time each. For u = 2^32, lg lg u = 5: a handful of steps versus the lg n a balanced tree would need.
u up front, its keys must be non-negative integers (or something that maps to them), no two elements may share a key (duplicates need a side counter or list per key), and — as you will see in Section 1 — its space cost is O(u), not O(n), unless you use the reduced-space variant from Section 8. It trades those restrictions for the same O(lg lg u) bound on every operation, including the ones like MINIMUM/MAXIMUM that a plain balanced BST already does in O(1)/O(lg n), and SUCCESSOR/PREDECESSOR which a balanced BST needs O(lg n) for.
Ω(lg n) per operation because of an information-theoretic argument over orderings. A vEB tree sidesteps that argument entirely by exploiting a known, bounded integer universe, reaching O(lg lg u) — this lesson builds up to that structure in three stages: a bit vector, a proto-vEB structure, then the real thing.1. Preliminary approaches
Direct-address bit vector. A single array A[0..u-1] of bits, A[x] = 1 iff x is in the set. INSERT/DELETE/MEMBER are all Θ(1) — one array access. But MINIMUM/MAXIMUM/SUCCESSOR/PREDECESSOR must scan, worst case Θ(u).
Dart (steps counts array reads; tested in verify/c20.dart):
Input size → what’s feasible: u ≤ 106 → a MINIMUM/SUCCESSOR scan is ≤ 106 reads, fine for a few queries; u = 216 with 106 queries → up to 6.5·1010 reads, far too slow → use a tree.
Binary tree of bits over that same vector: give every bit a parent whose value is the OR of its two children, all the way up to a single root — a segment tree recording, at every level, "does ANY leaf in my range have a member?" Height is lg u. MINIMUM follows the leftmost 1-path from the root down to a leaf; SUCCESSOR walks up until it can turn right into an unexplored subtree with a 1, then down the leftmost 1-path of that subtree. Every operation becomes O(lg u) — actually better than a red-black tree for MEMBER (which is O(1) here vs. a BST's O(lg n)), but no faster than a balanced BST for the rest.
Dart (level 0 holds the u leaves, the last level is the root; pos >> l is the ancestor of leaf pos at level l):
Input size → what’s feasible: u = 216 → every operation is ≤ 16 steps (2u = 131,071 cells), 106 queries ≈ 1.6·107 steps; u = 232 → still 32 steps but 233 = 8.6·109 cells, so memory is the limit.
A tree of degree √u, height 2. Split the universe into √u clusters of √u elements each, plus a √u-bit summary (one bit per cluster, OR of that cluster). INSERT is O(1) (set one bit in the cluster, one in the summary). MIN/MAX/SUCCESSOR/PREDECESSOR/DELETE cost O(√u) — worse asymptotically than the binary-tree-of-bits approach for those specific operations, but the KEY idea this seeds — shrinking the universe by a square root, recursively — is exactly the seed the rest of this lesson grows into a genuine O(lg lg u) structure.
Dart, plus a step counter for the worst MINIMUM (the only member is u−1): u = 16 → 16 / 4 / 8 steps, u = 256 → 256 / 8 / 32, u = 65536 → 65536 / 16 / 512 (bit vector / binary tree / √u tree), all asserted in verify/c20.dart:
Input size → what’s feasible: u = 216 → MINIMUM/SUCCESSOR ≤ 2√u = 512 steps (5·108 for 106 queries, borderline), INSERT stays O(1).
u^(1/k) for larger k has MORE levels (height k), and each level still costs work proportional to its own size — there is a genuine trade-off between fan-out and height that only gets resolved by recursing the SAME idea at every level, all the way down, which is precisely what Section 2 does next.2. The proto-vEB structure
high(x) = ⌊x / √u⌋ picks WHICH aisle/shelf/bin/slot; low(x) = x mod √u picks the position WITHIN it; index(x, y) = x·√u + y glues the two back into the original address.A proto-vEB(u) structure: if u = 2, it is just a 2-bit array A[0..1] (the base case). Otherwise it has a summary pointer to one proto-vEB(√u) structure (one bit per cluster, recording "is this cluster nonempty?"), and a cluster array of √u pointers, each to its own proto-vEB(√u) structure. Element x lives at position low(x) inside cluster[high(x)]. For u = 16: √16 = 4, so there are 4 clusters of size 4, plus a summary of size 4 — and since 4 itself is bigger than the base case, both the summary and every cluster recurse one more level down to size-2 leaves. (Section 3 will generalize to universes where lg u is odd by splitting √u into an upper root ↑√u and a lower root ↓√u; whenever lg u is even, as here, both simply equal √u.)
The recurrence that motivates this whole chapter: solving T(u) = T(√u) + O(1) by substituting m = lg u gives S(m) = S(m/2) + O(1), which the master method (case 2) solves as S(m) = O(lg m), i.e. T(u) = O(lg lg u). As you will see below, protoMember actually achieves exactly this recurrence — but the other three operations make MORE than one recursive call per level, and that single design flaw is what proto-vEB fails to fix (motivating the min/max fields added in Section 3).
Dart: the three recurrences as code, with m = lg u (a power of two here): T(m) = T(m/2) + 1 gives lg m + 1; T(m) = 2T(m/2) + 1 gives 2m − 1; T(m) = 2T(m/2) + m gives m(lg m + 1). All three closed forms are asserted for m = 1, 2, 4, …, 64:
protoMember(v, x)
Checks whether x is present by recursing into exactly the one cluster that could possibly contain it — one recursive call per level, achieving the target recurrence.
Input size → what’s feasible: u = 16 → 3 invocations; u = 216 → 5; even u = 232 → only 6 — but the structure holds Θ(u) nodes, so u ≤ 216 (92,007 nodes) is the practical cap.
Dart (keys are plain integers in [0, u); null is NIL; _high/_low/_index are the high/low/index helpers):
x is a key value in [0, u), not an array position): bool member(int x) { if (u == 2) return bits[x]; return cluster[high(x)].member(low(x)); } — see ProtoVEB.member in verify/c20.dart, tested against brute-force set membership on 30 random subsets of a 16-element universe.protoMinimum(v)
Finds the smallest member by asking the summary which cluster is smallest-indexed-and-nonempty, THEN asking that cluster for its own minimum — two recursive calls per level.
Input size → what’s feasible: u = 216 → 2·lg u − 1 = 31 invocations; u = 232 → 63, already worse than the bit tree’s lg u = 32 steps.
Dart (keys are plain integers in [0, u); null is NIL; _high/_low/_index are the high/low/index helpers):
lg lg u (since each level's own universe again halves in log-size) — total calls grow like 2^{lg lg u} = lg u, giving T(u) = 2T(√u) + O(1) = Θ(lg u) by the master method's case 1. That is no better than the O(lg u) binary-tree-of-bits from Section 1, even though proto-vEB looks more sophisticated!protoSuccessor(v, x)
Worst case makes up to three calls per level: search x's own cluster (SUCCESSOR), then the summary for the next nonempty cluster (SUCCESSOR), then that cluster's own MINIMUM (itself Θ(lg u)) — giving T(u) = 2T(√u) + Θ(lg u), which works out to Θ(lg u · lg lg u), the worst of the four proto-vEB operations.
Input size → what’s feasible: u = 216 → up to 57 invocations (measured, Θ(lg u · lg lg u)); the vEB version needs at most 5.
Dart (keys are plain integers in [0, u); null is NIL; _high/_low/_index are the high/low/index helpers):
protoInsert(v, x)
Always makes exactly two unconditional recursive calls per level (insert into the cluster, AND insert into the summary, every single time — there is no way to skip the summary insert, since proto-vEB has no O(1) way to check "is this cluster already nonempty?"). This costs Θ(lg u), the same doubling problem as MINIMUM.
Input size → what’s feasible: u = 216 → exactly 31 invocations per INSERT, always; n = 104 inserts → 3.1·105 calls fine, u = 220 → 39 per insert.
Dart (keys are plain integers in [0, u); null is NIL; _high/_low/_index are the high/low/index helpers):
min and max directly on every node, so most of those "extra" calls become O(1) field reads instead.3. The van Emde Boas tree — MINIMUM, MAXIMUM, MEMBER
min and max. Think of a company org chart where every department, no matter how deep, wears a big sticker on its door reading "our most junior member is ___, our most senior is ___" — you never have to walk inside to find those two facts. There's one deliberate twist: the person listed as min is NOT also filed inside any of the department's own sub-teams — they exist ONLY as the sticker, nowhere else. That twist is what makes INSERT/DELETE/EMPTY-checks all O(1) at the point they're needed, and it is also the source of a subtle extra case in PREDECESSOR (Section 4).To generalize beyond u = 2^{2^k}, we redefine the split using two square roots, written with an arrow above the radical. The upper square root ↑√u = 2^{⌈(lg u)/2⌉} (round the exponent UP) is the number of clusters, and the lower square root ↓√u = 2^{⌊(lg u)/2⌋} (round the exponent DOWN) is the size of each cluster. So high(x) = ⌊x / ↓√u⌋, low(x) = x mod ↓√u, index(x,y) = x·↓√u + y, and always ↑√u · ↓√u = u. For any power of two u this keeps the recursion well-defined: when lg u is even both roots equal √u (for u=16 both are 4, matching Section 2 exactly); when lg u is odd, ↑√u is twice ↓√u (for u=32: ↑√u = 8 clusters of size ↓√u = 4). A vEB(2) node has NO array at all — it is described entirely by min/max (empty: both NIL; one element: both equal that element; two elements: min=0, max=1). The explorer below shows the arithmetic on real bits.
Dart (the high/low/index helpers; upperSqrt is ↑√u and lowerSqrtOf is ↓√u; tested in verify/c20.dart):
The universal recurrence this lesson has been building toward: T(u) ≤ T(↑√u) + O(1). Substituting m = lg u: T(2^m) ≤ T(2^{⌈m/2⌉}) + O(1); since ⌈m/2⌉ ≤ 2m/3 for m ≥ 2, this becomes S(m) ≤ S(2m/3) + O(1), and the master method (case 2, since log_{3/2} 1 = 0) gives S(m) = O(lg m), i.e. T(u) = O(lg lg u). Every operation in this section and the next satisfies this exact recurrence by making at most one true recursive call per level.
Dart: the recurrence counted directly on u (tVeb), the inequality ⌈m/2⌉ ≤ 2m/3 behind the substitution (halvingBound), and the identity ↑√u · ↓√u = u with index(high(x), low(x)) = x (sqrtIdentity), asserted for every u = 21 … 240 (lg u ≤ 62 for tVeb):
vebMinimum(v) & vebMaximum(v)
Both are one-line field reads — O(1), no recursion at all, the first payoff of storing min/max directly.
Input size → what’s feasible: any u ≤ 262 → one field read, O(1); 108 calls ≈ 108 steps.
Dart (keys are plain integers in [0, u); null is NIL; _high/_low/_index are the high/low/index helpers):
vebMaximum(v)
The mirror image, return v.max, O(1). Unlike v.min, the max is also stored inside the cluster that holds it (unless max = min).
Input size → what’s feasible: any u ≤ 262 → one field read, O(1).
Dart (keys are plain integers in [0, u); null is NIL; _high/_low/_index are the high/low/index helpers):
vebMember(v, x)
Checks the min/max shortcut FIRST (O(1)); only if that misses AND the node isn't already a base case does it make its one recursive call.
Input size → what’s feasible: u = 216 → at most 5 invocations (measured worst case 5), so 107 queries ≈ 5·107 invocations; the dense tree itself is the cap (92,007 nodes).
Dart (keys are plain integers in [0, u); null is NIL; _high/_low/_index are the high/low/index helpers):
VEBTree.member in verify/c20.dart — bool member(int x) { if (x == min || x == max) return true; if (u == 2) return false; return cluster[high(x)].member(low(x)); } — tested against a SplayTreeSet<int> reference over hundreds of randomized operations at u = 16, 256 and 65536.vebMember would give the WRONG answer for x == v.min if line 1 didn't check it explicitly first — recursing straight into cluster[high(x)] would never find it, because it genuinely isn't there. Every operation in this lesson that touches a cluster has to remember this exception.4. vebSuccessor & PREDECESSOR
vebSuccessor(v, x)
Exactly one true recursive call happens per level — either into x's own cluster (if that cluster's max says there's room), or into the summary (to find the next cluster) — never both, and never the expensive MINIMUM-of-everything the proto-vEB version needed.
Input size → what’s feasible: u = 216 → at most 5 invocations per SUCCESSOR (measured 5) versus 57 for proto-vEB; 106 queries ≈ 5·106 steps.
Dart (keys are plain integers in [0, u); null is NIL; _high/_low/_index are the high/low/index helpers):
VEBTree.successor in verify/c20.dart is a direct translation — maxLow/offset reads from vebMaximum/MINIMUM are plain field accesses (t.max, t.min), never method calls that themselves recurse, which is exactly why they don't add to the recursion depth.vebPredecessor(v, x)
Symmetric to SUCCESSOR, with one genuinely extra case: if the summary search for an earlier nonempty cluster comes back empty, the predecessor might STILL be v.min itself — because, as Section 3's pitfall said, the minimum is invisible to every cluster and to the summary search alike.
Input size → what’s feasible: u = 216 → at most 5 invocations per PREDECESSOR (measured 5); 106 queries ≈ 5·106 steps.
Dart (keys are plain integers in [0, u); null is NIL; _high/_low/_index are the high/low/index helpers):
x does not depend on x being in the set (the same is true in a binary search tree). The pseudocode here has that property — it never assumes x is a member — and the custom-input players above let you verify it directly: try a query key that was never inserted.5. vebInsert
vebEmptyInsert(v, x)
The two-line helper both the empty-tree case of INSERT and the "seat someone as a cluster's very first member" case rely on: just set both stickers to the new value, O(1), no recursion.
Input size → what’s feasible: O(1) for every u: two field writes, no recursion.
Dart (keys are plain integers in [0, u); null is NIL; _high/_low/_index are the high/low/index helpers):
vebInsert(v, x)
Makes at most one true recursive call per level: EITHER into the summary (only when the target cluster was empty, paired with an O(1) vebEmptyInsert into that cluster) OR into the target cluster directly (when it already had members) — never both, matching T(u) ≤ T(↑√u) + O(1) exactly like SUCCESSOR did.
Input size → what’s feasible: u = 216 → at most 5 invocations per INSERT (measured 5); building n = 105 keys ≈ 5·105 calls.
Dart (keys are plain integers in [0, u); null is NIL; _high/_low/_index are the high/low/index helpers):
x is NOT already present. Violating it corrupts the structure: the tests in verify/c20.dart insert 2 twice, which stores a second copy of the minimum inside a cluster, and after one DELETE(2) the key is still reported as a member. Check MEMBER first if duplicates are possible, as the verify harness does (if (!veb.member(x)) veb.insert(x)).VEBTree.insert in verify/c20.dart mirrors the pseudocode almost line-for-line, including the min/max swap (final tmp = min!; min = x; x = tmp;) and the empty-cluster fast path.6. vebDelete
vebDelete looks like it might make two independent recursive calls (into a cluster, AND into the summary) — but the key insight is that these two calls are mutually exclusive in effect: the summary deletion only fires when the cluster-level deletion just emptied that entire cluster, and in THAT specific situation the cluster-level deletion was the O(1) singleton case (lines 1–4), so only one call is ever expensive. The worst-case analysis (not amortized) nets out to the same recurrence T(u) ≤ T(↑√u) + O(1) → O(lg lg u).
Input size → what’s feasible: u = 216 → at most 9 invocations per DELETE (measured; ≤ 2 per level: one cheap singleton call plus one recursive call); 106 deletes ≈ 9·106 steps.
Dart (keys are plain integers in [0, u); null is NIL; _high/_low/_index are the high/low/index helpers):
x was never really there in the first place. Always MEMBER-check before DELETE if you're not certain, exactly like verify/c20.dart's harness (if (veb.member(x)) veb.delete(x)).VEBTree.delete in verify/c20.dart. The subtlest line is reassigning x itself mid-procedure (line 11 of the pseudocode) when deleting the minimum — the Dart translation reassigns the local parameter x the same way, which is exactly why Dart's parameters need to be non-final here (unlike almost every other lesson on this site, which prefers final wherever possible).7. Why it's O(lg lg u): the recurrence, visually
u, u/2, u/4, …) would take 32 steps. Taking a square root each time (u, √u, √√u, …) reaches the same destination in only 5 steps — because repeatedly taking a square root is the same as repeatedly HALVING the EXPONENT of 2, and halving something 32 times is lg 32 = 5 steps.Formally: let m = lg u. One vEB recursive call shrinks u to roughly √u, which means it shrinks m to roughly m/2. So the number of levels before u bottoms out at the base case (u=2, i.e. m=1) is exactly the number of times you can halve m before reaching 1 — that's lg m = lg lg u. Since every operation in Sections 3–6 does O(1) work per level (plus at most one recursive call), the total cost is O(1) × (number of levels) = O(lg lg u).
Dart: levels(lgU) counts how many times m = lg u can be replaced by ⌈m/2⌉ before it reaches 1 (that is ⌈lg m⌉ = ⌈lg lg u⌉); ceilLog2 gives the balanced-BST comparison. Asserted: u = 232 → 5, u = 264 → 6, n = 109 → 30:
Now measure it instead of trusting the algebra. The players below run real instrumented copies of the procedures in your browser, count every invocation (the first call plus each recursive call) and compare with lg lg u. The custom player lets you pick your own u.
u = 2^32 (a 32-bit universe, 4 billion keys) gives lg lg u = lg 32 = 5. Even u = 2^64 only gives lg lg u = lg 64 = 6. Compare a balanced BST on n = 10^9 elements, needing lg n ≈ 30 comparisons — the vEB tree's advantage widens dramatically for large, dense integer universes, which is exactly the setting the lesson opened with (Section 0).lg u at every level — so the recursion bottoms out after only lg(lg u) = lg lg u levels, and O(1) work per level gives the whole O(lg lg u) bound.8. Going further: reduced-space vEB, y-fast tries
A plain vEB(u) tree takes Θ(u) space and Θ(u) time just to CREATE (recurrence P(u) = (√u+1)P(√u) + Θ(√u), solvable to P(u) = O(u); an empty RS-vEB tree, by contrast, is created in O(1)). That's fine when n (the number of elements actually stored) is close to u, wasteful when it isn't. The reduced-space vEB tree keeps a summary pointer that starts NIL and a sparse cluster map that only ever contains entries for clusters that have actually received an element — a lookup miss is simply treated as "empty," exactly like a real hash table. RSVEBTree in verify/c20.dart implements exactly this (lazy Map<int, RSVEBTree> instead of a dense array) and is verified to behave identically to the dense VEBTree while using dramatically fewer live nodes (in a seeded test with u = 65536 and 50 insertions, liveNodeCount() was at most about 3.1n, versus 92,007 nodes for a dense tree; proving the tight O(n) bound for insert-only use takes a finer argument).
Dart: the dense space recurrence P(u) = (√u + 1)·P(√u) + √u with P(2) = 1 (u = 2(2k)); it gives 1, 5, 29, 509, 131069 for u = 2, 4, 16, 256, 65536 and exactly 2u − 3:
Dart: the reduced-space tree (RSVEBTree) — clusters live in a Map and are created on first use; liveNodeCount() counts nodes that really exist:
Input size → what’s feasible: u = 232 with n = 3·104 keys → at most 12n ≈ 3.6·105 live nodes (measured bound) instead of 6.0·109 for a dense tree; u = 220 and n = 2000 scores → about 5,000 live nodes instead of 1,421,676.
y-fast tries (D. Willard's structure) reaches the same O(lg lg u) worst case for MEMBER/MIN/MAX/PRED/SUCC and O(lg lg u) AMORTIZED for INSERT/DELETE, using only O(n) space — by combining a perfect hash table of every element's binary PREFIXES (to binary-search over prefix lengths) with groups of lg u elements each kept in a small balanced BST, one "representative" per group filed in the hash table. It's a fundamentally different technique (hashing + binary search over prefix length, rather than recursive clustering) reaching the same asymptotic destination — covered as an Expert-tier interview question below.
Dart: the x-fast trie — every prefix of every key is hashed to the [min, max] below it, and SUCCESSOR/PREDECESSOR binary-search the prefix length with one hash probe per step (≤ ⌈lg(lg u + 1)⌉ = 5 probes for lg u = 16); the sorted list stands for the doubly-linked list of neighbours:
Dart: the y-fast grouping — groups of lg u consecutive keys, only each group’s maximum goes into the x-fast trie (n = 1976 keys, u = 216: 124 groups, 1,361 hashed prefixes instead of 12,410):
Input size → what’s feasible: u = 216, n = 2000 → x-fast: 12,410 hashed prefixes; y-fast: 1,361 (both for n = 1976 keys); u = 232 → each query is ≤ ⌈lg 33⌉ = 6 hash probes.
Quiz
Interview questions
Cheat sheet
| Structure / operation | Time | Space | Notes |
|---|---|---|---|
| Direct-address bit vector | MEMBER/INSERT/DELETE Θ(1); MIN/MAX/SUCC/PRED Θ(u) worst case | Θ(u) | Simplest baseline; no structure beyond the raw array |
| Binary tree of bits | All ops O(lg u); MEMBER O(1) | Θ(u) | Height-lg u OR-tree; better than a bit vector, no worse than a balanced BST for most ops |
| Degree-√u tree (height 2) | INSERT O(1); MIN/MAX/SUCC/PRED/DELETE O(√u) | Θ(u) | Seeds the recursive-clustering idea; not yet O(lg lg u) |
| protoMember | O(lg lg u) | Θ(u) | One recursive call/level — achieves the target recurrence |
| protoMinimum | Θ(lg u) | Θ(u) | Two recursive calls/level (summary then cluster) |
| protoSuccessor | Θ(lg u · lg lg u) | Θ(u) | Up to 3 calls/level, one of them itself Θ(lg u) |
| protoInsert | Θ(lg u) | Θ(u) | Always 2 unconditional recursive calls/level |
| vebMinimum / MAXIMUM | O(1) | — | Plain field reads, no recursion |
| vebMember | O(lg lg u) | — | Min/max shortcut checked first, then ≤ 1 recursive call |
| vebSuccessor / PREDECESSOR | O(lg lg u) | — | ≤ 1 recursive call/level; PREDECESSOR has one extra v.min fallback case |
| vebEmptyInsert | O(1) | — | Sets min=max=x; used by both empty-tree INSERT and freshly-seeded clusters |
| vebInsert | O(lg lg u) | — | ≤ 1 true recursive call/level (cluster XOR summary, never both) |
| vebDelete | O(lg lg u) | — | Cluster + summary recursive calls only co-occur when the cluster just emptied |
| Full vEB(u) tree (any op) | O(lg lg u) | Θ(u), and Θ(u) just to CREATE | Only worth it when n is not tiny relative to u, or you use the reduced-space variant |
| Reduced-space vEB (RS-vEB) | O(lg lg u) expected (amortized, hashing) | O(n) if nothing is ever deleted (finer argument); O(n lg lg u) is the easy bound | Lazy Map-based clusters instead of a dense array; O(1) to create empty |
| x-fast trie | MEMBER O(1) expected; MIN/MAX O(1); PRED/SUCC O(lg lg u); INSERT/DELETE O(lg u) | O(n lg u) | Perfect-hashes every element AND every binary prefix |
| y-fast trie | MEMBER/MIN/MAX/PRED/SUCC O(lg lg u) worst case; INSERT/DELETE O(lg lg u) amortized | O(n) | Groups of Θ(lg u) elements in small BSTs, cuts x-fast trie’s space to linear |
| Balanced BST (for comparison) | O(lg n) for every op | O(n) | Works for ANY comparable key, no universe-size assumption needed |