题目
给定一个固定的整数宇宙,键值范围为从 0 到 U-1。请实现 van Emde Boas Tree,支持插入、删除、成员查询、最小值、最大值、前驱和后继。说明高低位分解、summary 结构、空簇处理,以及为什么操作时间是 O(log log U) 而不是 O(log U)。
面试官考察点
- 能否明确 vEB Tree 依赖固定整数宇宙和位级运算,不能直接替代任意对象键的平衡树。
- 能否正确计算高位簇编号和低位偏移,并让 summary 记录非空簇。
- 能否处理空树、单元素、边界键、删除最小值和删除后清理空簇。
- 能否同时说明理论复杂度与 O(U) 空间成本,给出 y-fast trie、排序数组或普通平衡树的替代条件。
参考答案
设宇宙大小 U 为 2 的幂,令位宽为 w。每个 vEB 节点负责一个大小为 u 的子宇宙,把键拆成高位 cluster 和低位 offset。常用递归定义是高半部分与低半部分各占约一半位宽,因此一个节点拥有约 sqrt(u) 个簇,每个簇又是一个大小为 sqrt(u) 的 vEB,另有一个大小为 sqrt(u) 的 summary 记录哪些簇非空。
节点额外维护 min 和 max,避免每次查询都递归到叶子。插入第一个元素时只设置 min 与 max;后续插入先把较小键放到 min,再把原 min 插入对应簇。删除时要处理删除 min 或 max 后从 summary 找到下一个非空簇,并在簇变空时从 summary 移除。
递归深度满足 T(u)=T(sqrt(u))+O(1)。连续开平方后,宇宙规模的指数每层减半,因此深度为 O(log log U)。空间方面,朴素布局会为每个节点预留簇指针和 summary,整体为 O(U);稀疏实现可减少常数,但不能自动消除理论上的宇宙依赖。
实现示例
以下伪代码使用 high、low 和 index 表示分解与合并,省略了内存池和参数检查。
high(x, bits) = x >> ceil(bits / 2)
low(x, bits) = x & ((1 << floor(bits / 2)) - 1)
index(h, l, bits) = (h << floor(bits / 2)) | l
insert(v, x):
if v.min is empty:
v.min = x; v.max = x; return
if x < v.min:
swap(x, v.min)
if v.bits > 1:
h = high(x, v.bits); l = low(x, v.bits)
if v.cluster[h].min is empty:
insert(v.summary, h)
insert(v.cluster[h], l)
if x > v.max:
v.max = x
successor(v, x):
if v.min is empty or x >= v.max: return empty
if v.bits == 1:
return v.max if v.max > x else empty
if x < v.min: return v.min
h = high(x, v.bits); l = low(x, v.bits)
c = v.cluster[h]
if c is not empty and l < c.max:
return index(h, successor(c, l), v.bits)
next_h = successor(v.summary, h)
if next_h is empty: return empty
return index(next_h, v.cluster[next_h].min, v.bits)真实删除实现需要维护与插入对称的空簇规则。叶节点通常直接用一个小位图或两个值表示,避免递归创建无意义的对象。实现时应先固定位宽计算,再写随机操作测试验证前驱和后继与有序集合结果一致。
常见误区
- 把 U 当成元素数量 n,声称所有操作都是 O(log log n)。复杂度参数是宇宙大小 U。
- 忽略 U 不是 2 的幂时的位宽与簇大小取整,导致 high、low 和 index 不互逆。
- 只实现成员查询和最小值,未处理删除空簇后 summary 的清理。
- 认为 vEB 一定比红黑树更快,忽略 O(U) 空间、缓存局部性和真实键分布。
- 让 summary 也使用同样的递归结构,却没有明确它的宇宙边界和空值表示。
复杂度取舍
在键是机器字整数、宇宙范围已知且查询集中在前驱后继时,vEB 的 O(log log U) 理论上很有吸引力。若 U 接近 2 的字宽而实际元素很少,朴素空间会浪费;可以改用 x-fast 或 y-fast trie,把空间降到与 n 更相关的规模,但会引入哈希、随机性或更复杂的实现。
普通平衡树提供 O(log n) 操作、O(n) 空间和更简单的迭代器语义。排序数组适合静态集合和批量查询。面试中应根据键域、更新比例、内存预算和可维护性选择,而不是只报出最快的渐进式复杂度。
先测试 U 为 2、4、16 和非幂次宇宙的边界转换。再生成随机插入、删除和查询序列,与语言内置有序集合逐步对照 min、max、member、predecessor 和 successor。额外覆盖重复插入、删除不存在键、删除最后一个键,以及连续删除 min 或 max 的路径。
参考资料
- MIT OpenCourseWare 的 van Emde Boas Trees 讲义:递归分簇、summary 和操作推导。
- Carnegie Mellon Graduate Algorithms Lecture 7:O(log log U) 递归分析与实现细节。
- Springer 的 predecessor search 综述:原始 van Emde Boas 工作与前驱问题背景。
追问
为什么需要 summary?
当前簇没有更大元素时,必须快速找到下一个非空簇。summary 把“哪些簇非空”压缩成另一个前驱后继问题,避免线性扫描所有簇。
min 和 max 为什么可以不放进簇?
把 min 和 max 单独保存能让空树和单元素操作成为常数时间,并减少递归层数。插入时把较小值交换到 min,删除时从 summary 找到新的极值再恢复不变量。
U 不是 2 的幂时怎么办?
可以向上取整到覆盖所有键的 2 的幂宇宙,并拒绝超出原始范围的输入;也可以实现带取整的簇划分,但必须证明 high、low、index 的互逆关系与复杂度。
如何把空间从 O(U) 降下来?
使用稀疏簇、x-fast trie 或 y-fast trie。选择时要说明哈希冲突、随机性、迭代器语义和常数开销,而不是只比较大 O。
什么场景不该使用 vEB?
键域巨大且稀疏、宇宙边界无法固定、需要通用比较器或必须提供成熟迭代器时,平衡树或 B-tree 通常更合适。