代表性面试主题

编程面试:如何实现 van Emde Boas Tree 并支持前驱与后继查询?

编程题困难
Offer.cc 编辑团队发布 更新

题干

请实现支持插入、删除、成员查询、最小值、最大值、前驱和后继的 van Emde Boas Tree,并分析宇宙大小、时间复杂度和空间复杂度。

题目

给定一个固定的整数宇宙,键值范围为从 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);稀疏实现可减少常数,但不能自动消除理论上的宇宙依赖。

实现示例

以下伪代码使用 highlowindex 表示分解与合并,省略了内存池和参数检查。

text
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 通常更合适。

公开来源

同类题目

相关面试工具

用 Screenshot 处理算法题

截图题目后,按顺序看约束、解法、代码、边界条件和复杂度。

查看工具