目录
  1. 1. 一、引言:超越比较排序模型的优先队列
    1. 1.1. 1.1 比较模型的下界
    2. 1.2. 1.2 核心思想
  2. 2. 二、数据结构与原理
    1. 2.1. 2.1 基本结构图
    2. 2.2. 2.2 min 和 max 的特殊存储
  3. 3. 三、vEB 树的 Java 实现
    1. 3.1. 3.1 核心类定义
    2. 3.2. 3.2 核心操作实现
      1. 3.2.1. Member (包含判断) — $O(\log \log u)$
      2. 3.2.2. Successor — $O(\log \log u)$
      3. 3.2.3. Insert — $O(\log \log u)$
      4. 3.2.4. Delete — $O(\log \log u)$
    3. 3.3. 3.3 完整实现总结
  4. 4. 四、复杂度汇总与推导
    1. 4.1. 4.1 递推关系
    2. 4.2. 4.2 空间复杂度
    3. 4.3. 4.3 与其他优先队列的对比
  5. 5. 五、vEB 树的设计哲学与局限
    1. 5.1. 5.1 设计哲学
    2. 5.2. 5.2 实际局限
  6. 6. 六、vEB 树的递归结构可视化
  7. 7. 七、面试常见追问
    1. 7.1. 问题一:vEB 树的 $O(\log \log u)$ 与二叉堆的 $O(\log n)$,在什么规模下 vEB 会更快?
    2. 7.2. 问题二:为什么 vEB 树空间必须是 $O(u)$?
    3. 7.3. 问题三:vEB 树支持 decreaseKey 吗?
    4. 7.4. 问题四:vEB 树如何处理宇宙大小不是 2 的幂的情况?
    5. 7.5. 问题五:实现 vEB 树时最容易犯的 bug 是什么?
  8. 8. 八、总结
【数据结构与算法体系】van Emde Boas 树

一、引言:超越比较排序模型的优先队列

1.1 比较模型的下界

所有基于元素间比较的优先队列(二叉堆、二项堆、斐波那契堆等)的 insertextractMindecreaseKey 等操作,都存在 $\Omega(\log n)$ 的信息论下界。

但如果我们能够放弃”比较”的范式呢?如果键值来自一个有限且已知的宇宙 ${0, 1, \ldots, u-1}$($u$ 为宇宙大小),能否超越 $\log n$ 的屏障?

van Emde Boas 树(vEB 树)给出了肯定的答案——所有操作 **$O(\log \log u)$**。

1.2 核心思想

vEB 树使用递归分块策略:将大小为 $u$ 的宇宙递归地划分为 $\sqrt{u}$ 个大小为 $\sqrt{u}$ 的簇(Cluster)。每个簇自己也是一个 vEB 树。同时维护一个摘要(Summary)——指示哪些簇非空。

这种”递归平方根”的策略将树的高度从 $\log_2 u$ 降低到了 $\log_\gamma u$(其中 $\gamma = \sqrt{u}$,$\log_{\sqrt{u}} u = 2$),但实际上每个节点有两层结构(簇 + 摘要),最终效果是 $O(\log \log u)$。

vEB 树的关键数据:
- u: 宇宙大小(必须是 2 的幂或至少可表示为某个整数的平方)
- min: 当前集合中的最小值(不存储在簇中!)
- max: 当前集合中的最大值
- summary: vEB 树(大小 √u),记录哪些簇非空
- cluster[i]: 第 i 个簇,每个簇是一个 vEB 树(大小 √u)

二、数据结构与原理

2.1 基本结构图

假设 $u = 16$(宇宙 ${0, 1, \ldots, 15}$):

┌──────────────────────┐
│ vEB(16) │
│ min = 2 │
│ max = 14 │
│ │
│ summary: vEB(4) │ ← 记录哪些簇非空
│ ┌────┬────┬────┬───┐│
│ │v(0)│v(1)│v(2)│v(3)││
│ └────┴────┴────┴───┘│
│ │
│ clusters: │
│ [0]: vEB(4) ← 簇0: {0..3} │
│ [1]: vEB(4) ← 簇1: {4..7} │
│ [2]: vEB(4) ← 簇2: {8..11} │
│ [3]: vEB(4) ← 簇3: {12..15} │
└──────────────────────┘

数据元素 x 的位置确定:

  • high(x) = $\lfloor x / \sqrt{u} \rfloor$ —— 所属簇的索引
  • low(x) = $x \bmod \sqrt{u}$ —— 在簇内的位置
  • index(high, low) = $high \cdot \sqrt{u} + low$ —— 从簇和内部位置恢复原始值

2.2 minmax 的特殊存储

关键优化:vEB 树的 min存储在子簇中。这避免了递归到底层时的无限递归问题,也是很多操作能够达到 $O(\log \log u)$ 的关键。

例如,插入元素 5 到一个空 vEB(16) 中:

  • min = 5(直接设置,不进入簇)
  • 如果再插入 7:min = 5 不变,7 被存入对应的簇

extractMin 时,当前 min 是直接可用的。取出后,如果 min 之后还有元素,需要从子簇中取出下一个最小元素来填补 min


三、vEB 树的 Java 实现

3.1 核心类定义

class VanEmdeBoasTree {
private int u; // 宇宙大小
private int min; // 当前集合的最小值
private int max; // 当前集合的最大值
private VanEmdeBoasTree summary; // 摘要
private VanEmdeBoasTree[] cluster; // 簇数组

private static final int NIL = -1; // 表示"空"

// 计算高位和低位
private int high(int x) { return x / lowerSqrt(); }
private int low(int x) { return x % lowerSqrt(); }
private int index(int high, int low) { return high * lowerSqrt() + low; }

// 上取整平方根和下取整平方根
private int upperSqrt() { return (int) Math.pow(2, Math.ceil(Math.log(u) / Math.log(2) / 2.0)); }
private int lowerSqrt() { return u / upperSqrt(); }

public VanEmdeBoasTree(int universeSize) {
u = universeSize;
min = NIL;
max = NIL;

if (u > 2) {
int upper = upperSqrt();
int lower = lowerSqrt();
summary = new VanEmdeBoasTree(upper);
cluster = new VanEmdeBoasTree[upper];
for (int i = 0; i < upper; i++) {
cluster[i] = new VanEmdeBoasTree(lower);
}
}
// u == 2 是 base case:只有 summary/cluster,只需维护 min/max
}
}

3.2 核心操作实现

Member (包含判断) — $O(\log \log u)$

public boolean member(int x) {
if (x < 0 || x >= u) return false;
if (x == min || x == max) return true;
if (u == 2) return false; // base case: 只有 0 和 1
return cluster[high(x)].member(low(x));
}

Successor — $O(\log \log u)$

public int successor(int x) {
if (u == 2) {
// base case: u=2, 只有 0 和 1
if (x == 0 && max == 1) return 1;
return NIL;
}
// 情形 1: x < min → min 就是后继
if (min != NIL && x < min) return min;

int h = high(x), l = low(x);
int maxLow = cluster[h].max;

// 情形 2: x 在其簇内的后继存在
if (maxLow != NIL && l < maxLow) {
return index(h, cluster[h].successor(l));
}

// 情形 3: 需要在后续的簇中寻找
int succCluster = summary.successor(h);
if (succCluster == NIL) return NIL;
return index(succCluster, cluster[succCluster].min);
}

逻辑展开

  1. 如果 $x < \text{min}$,则 min 就是 $x$ 的后继
  2. 否则,在 $x$ 所属的簇 cluster[high(x)] 内寻找后继
  3. 如果簇内没有更大的元素,在摘要树中寻找下一个非空簇
  4. 下一个非空簇的最小元素即为答案

复杂度分析:每次递归最多只沿一条路径下降(或经过 summary 做一次递归调用),递归深度 $\leq 2$(因为先进入子簇,可能再进入 Summary 再进入下一个子簇)。每次 call 的宇宙大小从 $u$ 变为 $\sqrt{u}$。设 $T(u)$ 为宇宙大小 $u$ 上的 successor 时间:

$$T(u) = T(\sqrt{u}) + O(1)$$

令 $k = \log u$:$T(2^k) = T(2^{k/2}) + O(1)$。展开得 $T(2^k) = O(\log k) = O(\log \log u)$。

Insert — $O(\log \log u)$

public void insert(int x) {
if (min == NIL) {
// 树为空:直接设置 min 和 max
min = max = x;
return;
}
if (x < min) {
// 交换:新元素成为 min,旧 min 成为被插入的元素
int tmp = min;
min = x;
x = tmp;
}

if (u > 2) {
int h = high(x);
// 如果簇为空,需要在 summary 中标记该簇非空
if (cluster[h].min == NIL) {
summary.insert(h);
cluster[h].min = cluster[h].max = low(x);
} else {
cluster[h].insert(low(x));
}
}

if (x > max) {
max = x;
}
}

关键设计:当 $x < \text{min}$ 时,我们交换 $x$ 和 min——旧 min 现在需要被真正插入到子簇中,而新 min 占用特殊位置。这保证了 min 始终是最小元素且不进入递归结构,避免了无限递归。

Delete — $O(\log \log u)$

public void delete(int x) {
if (min == max) {
// 只有一个元素
min = max = NIL;
return;
}
if (u == 2) {
// base case: 只有 {0,1} 两个可能值
min = (x == 0) ? 1 : 0;
max = min;
return;
}

if (x == min) {
// 删除 min:找到下一个最小的元素作为新 min
int firstCluster = summary.min;
int newMin = index(firstCluster, cluster[firstCluster].min);
min = newMin;
x = newMin; // 现在删除 newMin 在簇中的副本
}

int h = high(x);
cluster[h].delete(low(x));

// 如果该簇变空,从 summary 中移除
if (cluster[h].min == NIL) {
summary.delete(h);
}

// 更新 max
if (x == max) {
int summaryMax = summary.max;
if (summaryMax == NIL) {
max = min;
} else {
max = index(summaryMax, cluster[summaryMax].max);
}
}
}

3.3 完整实现总结

class VanEmdeBoasTree {
private int u, min, max;
private VanEmdeBoasTree summary;
private VanEmdeBoasTree[] cluster;
private static final int NIL = -1;

public VanEmdeBoasTree(int u) {
this.u = u;
this.min = NIL;
this.max = NIL;
if (u > 2) {
int upper = (int) Math.pow(2, Math.ceil(Math.log(u) / Math.log(2) / 2.0));
int lower = u / upper;
summary = new VanEmdeBoasTree(upper);
cluster = new VanEmdeBoasTree[upper];
for (int i = 0; i < upper; i++) cluster[i] = new VanEmdeBoasTree(lower);
}
}

private int high(int x) { return x / (u / upperSqrt()); }
private int low(int x) { return x % (u / upperSqrt()); }
private int index(int h, int l) { return h * (u / upperSqrt()) + l; }
private int upperSqrt() { return (int) Math.pow(2, Math.ceil(Math.log(u) / Math.log(2) / 2.0)); }

public int minimum() { return min; }
public int maximum() { return max; }

public boolean member(int x) { /* 实现见上 */ return false; }
public int successor(int x) { /* 实现见上 */ return NIL; }
public int predecessor(int x) { /* 对称实现 */ return NIL; }
public void insert(int x) { /* 实现见上 */ }
public void delete(int x) { /* 实现见上 */ }
}

四、复杂度汇总与推导

4.1 递推关系

设 $T(u)$ 为在宇宙大小为 $u$ 的 vEB 树上执行一次操作的时间。所有基本操作(insert、delete、successor、predecessor、member)最多导致一次递归调用到一个大小为 $\sqrt{u}$ 的子 vEB 树上:

$$T(u) \leq T(\sqrt{u}) + O(1)$$

解这个递推式:

令 $u = 2^k$,则 $\sqrt{u} = 2^{k/2}$。递推式变为 $T(2^k) \leq T(2^{k/2}) + c$。

展开:
$$T(2^k) \leq c + T(2^{k/2}) \leq 2c + T(2^{k/4}) \leq \cdots \leq c \cdot \log_2 k + T(2)$$

因为 $k = \log_2 u$,$\log_2 k = \log_2 \log_2 u$。

因此 $T(u) = O(\log \log u)$。

4.2 空间复杂度

vEB 树的空间复杂度递推式为:

$$S(u) = (\sqrt{u} + 1) \cdot S(\sqrt{u}) + O(1)$$

解的难度更大,结果为 $\Theta(u)$(严格分析表明 $S(u) = O(u)$)。实际上,对于 $u = 2^{32}$,vEB 树的节点数约为 $2u = 2^{33}$——这在天文数字级宇宙中几乎无法接受。

RS-vEB 树(Randomized-vEB) 通过只创建非空簇的懒惰分配,可以将空间降至 $O(n \log \log u)$(其中 $n$ 是实际存储的元素个数),但实现更复杂。

4.3 与其他优先队列的对比

┌──────────────┬──────────────┬──────────────┬──────────────┐
│ 操作 │ 二叉堆 │ Fibonacci堆 │ vEB树 │
│ │ O(log n) │ 均摊 │ O(log log u) │
├──────────────┼──────────────┼──────────────┼──────────────┤
│ insert │ O(log n) │ O(1) │ O(log log u) │
│ findMin │ O(1) │ O(1) │ O(1) │
│ extractMin │ O(log n) │ O(log n) │ O(log log u) │
│ decreaseKey │ O(log n) │ O(1) │ — │
│ successor │ — │ — │ O(log log u) │
│ predecessor │ — │ — │ O(log log u) │
│ member │ O(n) │ O(n) │ O(log log u) │
│ 空间 │ O(n) │ O(n) │ O(u) │
└──────────────┴──────────────┴──────────────┴──────────────┘

vEB 树真正的独特之处在于它支持高效的 successor/predecessor/member 操作——这使它更适合作为有序集合而非纯优先队列。


五、vEB 树的设计哲学与局限

5.1 设计哲学

vEB 树展示了算法设计中的几个重要思想:

  1. 递归平方根分解:将问题大小 $u$ 降为 $\sqrt{u}$(而非二分查找的 $u/2$)
  2. 摘要结构:摘要树 summary 是”自引用”的——vEB 树内部包含一个稍小的 vEB 树来追踪哪些簇非空
  3. min/max 特殊存储:通过把 min 和 max 提升到当前层,避免了递归到底层时处理空集的复杂逻辑

5.2 实际局限

虽然 $O(\log \log u)$ 在理论上比 $O(\log n)$ 快(当 $u = 2^{32}$ 时,$\log \log u \approx 5$ vs $\log n \approx 20$),vEB 树在实际中使用不广泛,因为:

  1. 空间爆炸:$O(u)$ 的空间意味着 $u > 10^6$ 时就不太实用
  2. 常数巨大:每次操作涉及多个函数调用、高位/低位计算、summary 交互
  3. 宇宙限制:键必须是有限宇宙中的整数
  4. 工程替代:实际中 hash table + balanced BST 的组合通常更灵活

适用场景:当 $u$ 相对小($\leq 2^{20}$)且需要频繁的 successor/predecessor 查询时(如 IP 路由表的 LPM 查询、调度器中的 timer wheel),vEB 树或其变体可能很有价值。


六、vEB 树的递归结构可视化

           vEB(16)
┌──────────┼──────────┐
│ │ │
vEB(4) vEB(4) vEB(4) vEB(4) ← clusters[0..3]

├── vEB(2) vEB(2) ← clusters[0..1] of vEB(4)
│ │
│ └── leaf: base case (u=2)

└── summary: vEB(2) ← 追踪 vEB(4)的哪些簇非空

总深度 = log log 16 = log 4 = 2 层

对比二叉搜索树(深度 $\log 16 = 4$),vEB 树只有一半的深度——这就是 $O(\log \log u)$ 的来源。


七、面试常见追问

问题一:vEB 树的 $O(\log \log u)$ 与二叉堆的 $O(\log n)$,在什么规模下 vEB 会更快?

回答:需要在 $O(\log \log u) < O(\log n)$ 的场景。假设 $u$ 和 $n$ 接近:

  • $\log \log u < \log n$ 等价于 $\log u < n$(两边取指数)
  • 对于 $u = 2^{32} \approx 4 \times 10^9$:$\log \log u \approx 5$,而 $\log n \approx \log 10^6 \approx 20$。vEB 操作只需约 5 次递归,二叉堆需要约 20 次。在这个规模下 vEB 理论上更快——但需要考虑 vEB 的每个递归步骤自身的开销。

问题二:为什么 vEB 树空间必须是 $O(u)$?

回答:vEB 树在最坏情况下预分配了所有可能的簇。即使只存储 3 个元素,只要 $u$ 很大,summary 结构和空簇的指针数组依然存在。对于 $u = 2^{32}$,顶层 summary 本身是一个 $u’ = 2^{16}$ 的 vEB 树,其簇数组有 $2^{16}$ 个元素。这种递归分配在最底层有巨大开销。

变体 RS-vEB 树(Randomized van Emde Boas)通过哈希表存储非空簇,将空间降到 $O(n \log \log u)$,但每次操作需要额外的期望时间分析。

问题三:vEB 树支持 decreaseKey 吗?

回答:标准 vEB 树不直接支持 decreaseKey(因为元素是通过整数值而非对象引用定位的)。要实现 decreaseKey,需要先 delete(oldKey)insert(newKey)——$O(\log \log u)$。相比之下斐波那契堆的 $O(1)$ 摊还 decreaseKey 显得更有优势。

vEB 树可扩展为支持 decreaseKey:需要维护从外部对象到键的映射,每次 decreaseKey 时 delete(oldVal) + insert(newVal)

问题四:vEB 树如何处理宇宙大小不是 2 的幂的情况?

回答:可以扩展到任意宇宙大小。将 $u$ 分解为上取整平方根和下取整平方根($\lceil\sqrt{u}\rceil$ 和 $\lfloor\sqrt{u}\rfloor$ 或反过来),使得 upper * lower >= u。这样会有一些无效的索引值,需要在 member 等操作中额外检查边界。

问题五:实现 vEB 树时最容易犯的 bug 是什么?

回答处理 min 和子簇中的副本不一致。当删除当前 min 时,我们需要从子簇中取出下一个最小元素来替代 min,这个元素在子簇中必须也被删除(因为它已经在 min 中被 “代表” 了),否则会出现重复。同样,插入一个新 min 时,旧 min 需要被放入子簇。

另一个常见 bug 是 base case u = 2 的处理——此时没有 summary 和 cluster,所有操作简化为对 minmax 的位操作。


八、总结

van Emde Boas 树是数据结构史上最精妙的发明之一:

  • 突破比较下界:利用键值来自有限宇宙的事实,达到 $O(\log \log u)$ 的操作时间
  • 递归平方根分解:每次递归将宇宙大小从 $u$ 降到 $\sqrt{u}$,深度仅 $O(\log \log u)$
  • min/max 提升 + summary 结构:精巧的工程设计保证了递归的正确性和效率
核心公式:
T(u) = T(√u) + O(1) → O(log log u)
S(u) = (√u+1)·S(√u) + O(1) → O(u)

vEB 树最重要的教诲:当你发现问题的输入有特殊结构时(如键值是有界整数),就可能设计出超越通用下界的数据结构。 这种思想在字符串算法、计算几何和数据流算法中反复出现。

至此,本系列的数据结构篇章涵盖了从最基础的表/栈/队列,到中级(二叉堆、Hash、BST),再到高级(斐波那契堆、并查集、vEB 树)的完整体系。摊还分析作为复杂度分析的利器贯穿始终。希望这一系列能够帮助你建立起坚固的数据结构与算法理论基础。

打赏
  • 微信
  • 支付宝

评论