每日算法 — 使用java实现抢车位:van Emde Boas树与O(log log U)前驱后继查询

想象一个车库小游戏:一共 2^20 个车位(约一百万个),车辆不断驶入驶出。每当一辆车进场,调度器要在一瞬间回答两个问题——”离我最近的空位在哪?”(后继查询 successor)和”我左边最近的一辆已停车是哪辆?”(前驱查询 predecessor)。用有序数组,插入是 O(n);用红黑树,每次 O(log n) = 20 次比较;用哈希表,前驱后继无从谈起。而 van Emde Boas(下称 vEB)树把这三个操作全压到 O(log log U)——对 U = 2^20,只有 5 步。

这个数字不是常数优化,而是复杂度的量级跳变:O(log log U) 意味着即使 U 涨到 2^65536(天文数字的地址空间),查询也只需要 16 步。代价是它要求键必须是 [0, U) 范围内的整数,并且空间是 O(U),不能像平衡树那样按元素个数 O(n) 伸缩。对”密集整数键 + 需要前驱后继”的场景(路由表、进程调度、内存分配、游戏中的空间索引),它是最锋利的工具。

本文从”为什么二分查找不够快”讲起,层层还原 vEB 的三层递归结构,重点讲透那个最容易写错的核心技巧——min 元素不存进簇(cluster),最后给出可直接运行的 Java 实现与车库调度的完整演示。

一、前驱后继问题的下界:为什么 O(log U) 还不够

在 U 个可能键上维护一个动态集合,”成员查询”在比较模型下已有 Ω(log U) 的下界。但注意这个下界的前提是只能比较。如果我们允许使用”键是整数”这个额外信息——具体说,允许按二进制位做位操作和数组下标寻址——下界被打破。

思路极其朴素:既然键是 k 位二进制数,那就把”值域”当成一棵高 n/2 位决定走哪个儿子、低 n/2 位决定在儿子内部怎么走的树。这样树的层数是 log U / log(√U) = 2,不是 log U!

具体地,设 U = 2^k,令 hi = ⌈k/2⌉、lo = ⌊k/2⌋:

  • 一个键 x 可拆为高 hi 位 high(x) = x >> lo 与低 lo 位 low(x) = x & (2^lo - 1)
  • 顶层维护 √U 个簇,第 high(x) 个簇负责所有低位落在 [0, √U) 的键
  • 再维护一个 summary,它是规模 √U 的”簇索引集合”,记录”哪些簇非空”

于是成员查询变成:先问 summary 里有没有这个簇,再问对应簇里有没有这个低位——两层递归,每层值域开平方。递归下去,层数是 log log U:U=2^16 时 k=16 → 8 → 4 → 2,只有 3~4 层。

代价是”每个节点维护 √U 个指针”:T(U) = √U · T(√U) + O(√U)·(summary 开销),解出来是 O(U) 空间。这就是 vEB 的空间换时间契约。

二、结构定义与三条不变式

VEBNode(U):
    min, max        ⟵ 集合中的最小/最大键(延迟存储,见下)
    summary         ⟵ 规模为 ceil(√U) 的 vEB,记录"哪些簇非空"
    cluster[0..√U)  ⟵ 每个都是规模为 floor(√U) 的 vEB

三条不变式是整个算法的骨架,必须背下来:

不变式 1(min 的懒惰性):节点内的 min 不出现在任何子结构中。也就是说,如果 x == node.min,那么 cluster[high(x)] 里不含 low(x)。max 则不同,max 老老实实存在簇里。

不变式 2(summary 的正确性):cluster[i] 非空 ⟺ summary 包含 i。

不变式 3(递归基):U = 2 时不再递归,只有 min/max 两个位(min 可能是 0 或 1,max 同理;空集合时二者为 null)。

为什么会设计不变式 1?因为 min 查询要 O(1)。若 min 也老老实实递归下去,min() 就变成 O(log log U) 了。而把 min 提在节点外面存,min() 直接读字段即可。代价是 insert/delete 里要额外处理这个”特殊元素”,容易出 bug——本文的实现会专门标注三处易错点。

三、五个操作的递推

设 U > 2,hi = high(x),lo = low(x),c = cluster[hi]。

member(x) —— O(log log U)

if min == null: return false                    // 空集合
if x == min or x == max: return true             // 两个延迟元素直接命中
if U == 2: return false                          // 基例
return c != null && c.member(lo)                 // 递归进簇

successor(x) —— 求大于 x 的最小元素,O(log log U)

if U == 2: return (x == 0 && max == 1) ? 1 : null
if min != null && x < min: return min            // x 比全集合都小,答案就是 min
c = cluster[high(x)]
if c != null && c.max != null && low(x) < c.max:
    return index(high(x), c.successor(low(x)))   // 答案还在同一个簇里
s = summary != null ? summary.successor(high(x)) : null
if s == null: return null                        // 没有更靠后的非空簇
return index(s, cluster[s].min)                  // 跳到下一个非空簇,取它的 min

这里第二行 x < min 的提前返回,正是不变式 1 的直接红利:min 不在簇里,若不加这个特判就会漏掉它。

predecessor(x) 完全对称,但有一个不对称的陷阱:

if empty: return null
if U == 2: return (x == 1 && min == 0) ? 0 : null
if x > max: return max                           // 比全集合都大
c = cluster[high(x)]
if c != null && c.min != null && low(x) > c.min:
    return index(high(x), c.predecessor(low(x)))
p = summary != null ? summary.predecessor(high(x)) : null
if p == null: return (x > min) ? min : null      // ★ 兜底必须是 min,不是父层的 max
return index(p, cluster[p].max)                  // 别的簇里要取它的 max

注意 ★ 那行,以及最后一行取 cluster[p].max 而不是 .min ——这是 successor 与 predecessor 唯一的两处非对称。写反了,测试会在大规模随机用例上偶发失败(本人第一次实现就在这里翻车,用一个”阶梯式”随机序列才复现出来)。

insert(x) —— O(log log U)

if min == null: min = max = x; return            // 空集合直接接管
if x < min: swap(x, min)                          // 新元素比 min 小 → min 顶替,旧 min 继续往下沉
if U > 2:
    hi = high(x); lo = low(x)
    if cluster[hi] == null: cluster[hi] = new VEB(√U)
    if cluster[hi].min == null:                   // 该簇原本为空
        summary.insert(hi)                        // 先登记到 summary
    cluster[hi].insert(lo)                        // 再递归插入
if x > max: max = x                               // ★ 必须放在递归之后,不能提前 return

★ 那一行是最常见的错误:很多人写完 cluster[hi].insert(lo) 就 return 了,忘记更新 max。因为 insert 会一路递归下去,中途不会触碰本层的 max。

delete(x) —— O(log log U),但要处理”删 min”这个特殊情形

if min == max: min = max = null; return          // 只剩一个元素,直接清空
if U == 2: min = max = (x == 0 ? 1 : 0); return  // 基例:剩下另一个位
if x == min:
    first = summary.min                          // 找到最小的非空簇
    x = index(first, cluster[first].min)         // 真正的 min 是它的 min
    min = x                                      // min 指针改为指向它
hi = high(x); c = cluster[hi]
c.delete(low(x))                                  // 递归删除(此时 x 一定在簇里)
if c.min == null:                                 // 簇空了
    summary.delete(hi); cluster[hi] = null
if x == max:                                      // 删的正好是 max
    if summary == null || summary.min == null: max = min
    else:
        m = summary.max
        max = index(m, cluster[m].max)

删 min 的技巧值得单独说明:我们不想真的去递归删除 min(因为它压根不在簇里),而是”提升”次小元素为新 min。次小元素藏在”最小的非空簇”的 min 里——这正是 summary.min 的作用。一次”删除最小”被转化成了”把 summary.min 对应的簇的 min 提上来”,min 指针滑动而结构不变,全程只递归一层。

四、完整 Java 项目结构

parking-veb/
├── pom.xml
└── src
    ├── main/java/com/example/veb
    │   ├── VEBTree.java          // 核心结构
    │   ├── SlotAllocator.java    // 车库调度封装(位图 + vEB 混合)
    │   └── ParkingDemo.java      // 演示入口
    └── test/java/com/example/veb
        └── VEBTreeTest.java

pom.xml 与上一篇相同(JDK 17 + JUnit 5),此处略。

4.1 核心实现

package com.example.veb;

/**
 * van Emde Boas 树:在 [0, U) 的整数全域上支持
 * member / insert / delete / successor / predecessor,均为 O(log log U)。
 *
 * 约束:U 必须是 2 的幂(U >= 2)。
 * 空间:O(U)。若 U 过大(如 2^24 以上),请改用 y-fast trie 或位图分块。
 */
public final class VEBTree {

    private final int universe;      // U
    private final int lowBits;       // floor(log2 U) 的一半,即簇内位宽
    private final int highBits;      // 剩余的位宽,即"簇个数"的指数
    private int min = -1;            // -1 表示"无"
    private int max = -1;
    private VEBTree summary;         // 规模 2^highBits,记录非空簇下标
    private VEBTree[] clusters;      // 长度 2^highBits,每个规模 2^lowBits
    private int size;                // 元素个数(可选,便于断言)

    public VEBTree(int universe) {
        if (universe < 2 || (universe & (universe - 1)) != 0) {
            throw new IllegalArgumentException("universe 必须是 >= 2 的 2 的幂: " + universe);
        }
        this.universe = universe;
        int bits = Integer.numberOfTrailingZeros(universe); // log2(U)
        this.lowBits = bits / 2;
        this.highBits = bits - lowBits;
        if (universe > 2) {
            this.clusters = new VEBTree[1 << highBits];
        }
    }

    // ---------- 位拆分 ----------
    private int high(int x) { return x >>> lowBits; }
    private int low(int x) { return x & ((1 << lowBits) - 1); }
    private int index(int h, int l) { return (h << lowBits) | l; }

    public boolean isEmpty() { return min == -1; }
    public int size() { return size; }
    public int min() { return min; }   // -1 表示空
    public int max() { return max; }

    /** 成员查询。O(log log U)。 */
    public boolean contains(int x) {
        if (x < 0 || x >= universe) return false;
        return containsInternal(x);
    }

    private boolean containsInternal(int x) {
        if (min == -1) return false;                 // 空集合
        if (x == min || x == max) return true;       // 两个延迟元素(不变式 1 的红利)
        if (universe == 2) return false;             // 基例:U=2 只有 min/max 两位
        VEBTree c = clusters[high(x)];
        return c != null && c.containsInternal(low(x));
    }

    /** 插入。O(log log U)。重复插入是幂等操作(也可改成计数语义)。 */
    public void insert(int x) {
        if (x < 0 || x >= universe) throw new IllegalArgumentException("越界: " + x);
        insertInternal(x);
    }

    private void insertInternal(int x) {
        if (min == -1) {                 // 空集合:min/max 直接接管
            min = max = x; size = 1; return;
        }
        if (x == min) return;            // 已存在,幂等
        if (x < min) { int t = min; min = x; x = t; }   // 新元素下沉:旧 min 继续往下插
        if (universe > 2) {
            int h = high(x), l = low(x);
            VEBTree c = clusters[h];
            if (c == null) {
                c = new VEBTree(1 << lowBits);
                clusters[h] = c;
            }
            if (c.min == -1) {           // 该簇原本为空 → 先登记 summary(不变式 2)
                if (summary == null) summary = new VEBTree(1 << highBits);
                summary.insertInternal(h);
                size += c.size();        // c 为空,加 0
            }
            int before = c.size();
            c.insertInternal(l);
            size += c.size() - before;
        } else {
            size++;                      // U=2 基例
        }
        if (x > max) max = x;            // ★ 必须在递归之后
    }

    /** 删除。不存在则无操作。O(log log U)。 */
    public void delete(int x) {
        if (x < 0 || x >= universe) return;
        deleteInternal(x);
    }

    private void deleteInternal(int x) {
        if (min == -1 || !containsInternal(x)) return;   // 不存在,静默返回
        if (min == max) {                                 // 只剩一个元素
            min = max = -1; size = 0; return;
        }
        if (universe == 2) {                              // 基例:剩下"另一位"
            min = max = (x == 0 ? 1 : 0); size = 1; return;
        }
        if (x == min) {                                   // ★ 删 min 的特殊路径
            int first = summary.min();                    // 最小的非空簇
            x = index(first, clusters[first].min());      // 真正的次小元素
            min = x;                                      // 指针滑动,不走递归
        }
        int h = high(x), l = low(x);
        VEBTree c = clusters[h];
        int before = c.size();
        c.deleteInternal(l);
        size -= before - c.size();
        if (c.min == -1) {                                // 簇空了 → 摘除登记
            summary.deleteInternal(h);
            clusters[h] = null;
        }
        if (x == max) {                                   // 删的正好是 max
            if (summary == null || summary.min() == -1) {
                max = min;                                // 只剩 min 一个
            } else {
                int m = summary.max();
                max = index(m, clusters[m].max());
            }
        }
    }

    /** 大于 x 的最小元素;无则 -1。O(log log U)。 */
    public int successor(int x) {
        if (x < 0) return min;                            // 比全域下界还小 → 最小元素
        return successorInternal(x);
    }

    private int successorInternal(int x) {
        if (universe == 2) {                              // 基例
            return (x == 0 && max == 1) ? 1 : -1;
        }
        if (min != -1 && x < min) return min;             // ★ 覆盖"min 不在簇里"
        int h = high(x), l = low(x);
        VEBTree c = clusters[h];
        if (c != null && c.max != -1 && l < c.max) {
            return index(h, c.successorInternal(l));      // 答案在同一簇内
        }
        int s = (summary == null) ? -1 : summary.successorInternal(h);
        if (s == -1) return -1;
        return index(s, clusters[s].min());               // 跳下一个非空簇,取其 min
    }

    /** 小于 x 的最大元素;无则 -1。O(log log U)。与 successor 严格对称。 */
    public int predecessor(int x) {
        if (x <= 0) return -1;
        return predecessorInternal(x);
    }

    private int predecessorInternal(int x) {
        if (min == -1) return -1;
        if (universe == 2) {
            return (x == 1 && min == 0) ? 0 : -1;
        }
        if (x > max) return max;                          // 比全域上界还大 → 最大元素
        int h = high(x), l = low(x);
        VEBTree c = clusters[h];
        if (c != null && c.min != -1 && l > c.min) {
            return index(h, c.predecessorInternal(l));
        }
        int p = (summary == null) ? -1 : summary.predecessorInternal(h);
        if (p == -1) {
            return (x > min) ? min : -1;                  // ★ 兜底是 min,不是 -1 就是空
        }
        return index(p, clusters[p].max());               // ★ 前一个簇里取 max
    }

    /** 前驱后继的对称性自检:返回 true 表示结构自洽。 */
    public boolean checkInvariants() {
        if (min == -1) return max == -1 && size == 0;
        if (max < min) return false;
        if (min == max && size != 1) return false;
        int count = 1;                                    // min 本身
        if (universe > 2) {
            for (int i = 0; i < clusters.length; i++) {
                VEBTree c = clusters[i];
                boolean has = c != null && !c.isEmpty();
                boolean registered = summary != null && summary.containsInternal(i);
                if (has != registered) return false;      // 不变式 2
                if (c != null) {
                    if (!c.checkInvariants()) return false;
                    count += c.size();
                    // 不变式 1:min 的低位不能出现在簇中
                    if (i == high(min) && c.containsInternal(low(min))) return false;
                }
            }
            if (max != min && clusters[high(max)] == null) return false;
        } else {
            if (min == max) return true;
            count = 2;
        }
        return count == size;
    }
}

4.2 车库调度封装

真实车库的 U 是几万到几百万,直接开一个 O(U) 的 vEB 是可行的(U=2^20 时内存约几十 MB),但更常见的是位图 + vEB 混合:用 vEB 只管理”每个 64 位字(word)里是否有空位”,字内的具体空位用 long 位图 + numberOfTrailingZeros 一次定址。这样空间降到 O(U/64),查询依旧是 O(log log(U/64)) + O(1)。

package com.example.veb;

/**
 * 车库车位分配器。
 * - vebLevel:登记"哪些 word 内还有空位",全域 = wordCount
 * - words[i]:第 i 个 64 位字,1 表示"已占用"
 * 查询"离 pos 最近的空位":先用位图在同 word 内 O(1) 求,跨 word 时用 vEB 求前驱后继。
 */
public final class SlotAllocator {

    private final int capacity;
    private final long[] words;
    private final VEBTree freeWords;   // 存放"仍有空位的 word 下标"
    private final int wordCount;

    public SlotAllocator(int capacity) {
        this.capacity = capacity;
        this.wordCount = (capacity + 63) >>> 6;
        this.words = new long[wordCount];
        int u = Integer.highestOneBit(Math.max(2, wordCount));
        if (u < wordCount) u <<= 1;              // vEB 全域必须是 2 的幂
        this.freeWords = new VEBTree(u);
        for (int i = 0; i < wordCount; i++) freeWords.insert(i);
    }

    /** 标记占用;返回 false 表示已被占用。O(log log U)。 */
    public boolean park(int slot) {
        int w = slot >>> 6, b = slot & 63;
        long mask = 1L << b;
        if ((words[w] & mask) != 0) return false;
        words[w] |= mask;
        if (words[w] == -1L) freeWords.delete(w);   // 该 word 满了,从 vEB 摘除
        return true;
    }

    /** 释放车位。O(log log U)。 */
    public boolean leave(int slot) {
        int w = slot >>> 6, b = slot & 63;
        long mask = 1L << b;
        if ((words[w] & mask) == 0) return false;
        words[w] &= ~mask;
        if (words[w] != -1L) freeWords.insert(w);   // 重新可用(幂等)
        return true;
    }

    /** 离 pos 最近(向右找)的空位;无则 -1。 */
    public int nextFree(int pos) {
        if (pos < 0) pos = 0;
        if (pos >= capacity) return -1;
        int w = pos >>> 6, b = pos & 63;
        // 同一 word 内:把 [0, b] 位屏蔽掉,剩下的最低置零位就是答案
        long avail = ~words[w] & (-1L << b);
        if (avail != 0) {
            int cand = (w << 6) + Long.numberOfTrailingZeros(avail);
            return cand < capacity ? cand : -1;
        }
        int nw = freeWords.successor(w);            // 跨 word:vEB 求后继
        if (nw == -1) return -1;
        long word = ~words[nw];
        if (word == 0L) return -1;                  // 理论不可达(vEB 保证非空)
        int cand = (nw << 6) + Long.numberOfTrailingZeros(word);
        return cand < capacity ? cand : -1;
    }

    /** 离 pos 最近(向左找)的空位;无则 -1。 */
    public int prevFree(int pos) {
        if (pos >= capacity) pos = capacity - 1;
        if (pos < 0) return -1;
        int w = pos >>> 6, b = pos & 63;
        long avail = ~words[w] & ((1L << (b + 1)) - 1);   // 只保留 [0, b] 位
        if (avail != 0) return (w << 6) + 63 - Long.numberOfLeadingZeros(avail);
        int pw = freeWords.predecessor(w);
        if (pw == -1) return -1;
        long word = ~words[pw];
        return (pw << 6) + 63 - Long.numberOfLeadingZeros(word);
    }

    public int occupiedCount() {
        int n = 0;
        for (long w : words) n += Long.bitCount(w);
        return n;
    }
}

4.3 演示入口

package com.example.veb;

import java.util.Random;

public final class ParkingDemo {
    public static void main(String[] args) {
        final int CAP = 1 << 20;                  // 约 105 万个车位
        SlotAllocator lot = new SlotAllocator(CAP);
        Random rnd = new Random(20261103L);

        // 随机占用 30% 车位,制造"有空有满"的真实分布
        for (int i = 0; i < CAP / 3; i++) lot.park(rnd.nextInt(CAP));
        System.out.println("已占用: " + lot.occupiedCount() + " / " + CAP);

        // 1) 单次查询延迟
        long t0 = System.nanoTime();
        int probes = 200_000, hits = 0;
        for (int i = 0; i < probes; i++) {
            int pos = rnd.nextInt(CAP);
            int slot = lot.nextFree(pos);
            if (slot >= 0) hits++;
        }
        long t1 = System.nanoTime();
        System.out.printf("nextFree  %d 次, %.1f ns/次, 命中 %d%n",
                probes, (t1 - t0) / (double) probes, hits);

        // 2) 对照:朴素线性扫描(同样的查询量,仅取样 2000 次)
        int samples = 2000; long sum = 0;
        long t2 = System.nanoTime();
        for (int i = 0; i < samples; i++) {
            int pos = rnd.nextInt(CAP);
            for (int s = pos; s < CAP; s++) { if (lot.prevFree(s) == s || true) { sum += s; break; } }
        }
        long t3 = System.nanoTime();
        System.out.printf("线性扫描 %d 次, %.1f us/次 (仅供对比)%n",
                samples, (t3 - t2) / 1000.0 / samples);

        // 3) 结构与不变式自检
        VEBTree t = new VEBTree(1 << 10);
        for (int i = 1; i < 900; i += 7) t.insert(i);
        System.out.println("vEB 不变式自检: " + t.checkInvariants());
        System.out.println("successor(500) = " + t.successor(500)
                + ", predecessor(500) = " + t.predecessor(500));
    }
}

4.4 单元测试(关键是对拍)

package com.example.veb;

import org.junit.jupiter.api.Test;
import java.util.TreeSet;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.*;

class VEBTreeTest {

    /** 与 TreeSet 对拍:member / successor / predecessor / min / max 全部一致。 */
    @Test
    void fuzzAgainstTreeSet() {
        Random rnd = new Random(7L);
        int[] universes = {2, 4, 8, 16, 64, 256, 4096};
        for (int u : universes) {
            VEBTree veb = new VEBTree(u);
            TreeSet<Integer> ref = new TreeSet<>();
            for (int step = 0; step < 3000; step++) {
                int x = rnd.nextInt(u);
                double r = rnd.nextDouble();
                if (r < 0.45) { veb.insert(x); ref.add(x); }
                else if (r < 0.75) { veb.delete(x); ref.remove(x); }
                else {
                    Integer expSucc = ref.higher(x);
                    int gotSucc = veb.successor(x);
                    assertEquals(expSucc == null ? -1 : expSucc.intValue(), gotSucc, "successor@" + u + " x=" + x);

                    Integer expPred = ref.lower(x);
                    int gotPred = veb.predecessor(x);
                    assertEquals(expPred == null ? -1 : expPred.intValue(), gotPred, "predecessor@" + u + " x=" + x);
                }
                assertEquals(ref.isEmpty() ? -1 : ref.first().intValue(), veb.min());
                assertEquals(ref.isEmpty() ? -1 : ref.last().intValue(), veb.max());
                assertEquals(ref.size(), veb.size());
                assertTrue(veb.checkInvariants(), "不变式被破坏 step=" + step);
            }
        }
    }

    @Test
    void deleteMinKeepsStructureConsistent() {
        VEBTree t = new VEBTree(1 << 12);
        for (int i = 100; i < 200; i++) t.insert(i);
        for (int i = 100; i < 190; i++) {
            assertEquals(i, t.min());
            t.delete(i);                    // 反复删 min,检验 summary.min 提升逻辑
            assertTrue(t.checkInvariants());
        }
        assertEquals(190, t.min());
        assertEquals(199, t.max());
    }

    /** 边界:全域两端、单元素、连续增删。 */
    @Test
    void edges() {
        VEBTree t = new VEBTree(16);
        assertEquals(-1, t.successor(0));
        t.insert(0); t.insert(15);
        assertEquals(15, t.successor(0));
        assertEquals(0, t.predecessor(15));
        assertEquals(-1, t.predecessor(0));
        assertEquals(-1, t.successor(15));
        t.delete(0); t.delete(15);
        assertTrue(t.isEmpty());
    }
}

五、复杂度与实测

操作 有序数组 红黑树/TreeSet 位图 vEB 位图+vEB 混合
insert O(n) O(log n) O(1) O(log log U) O(log log(U/64))
delete O(n) O(log n) O(1) O(log log U) O(log log(U/64))
successor O(log n) 二分 O(log n) 最坏 O(U/64) O(log log U) O(log log(U/64))
min/max O(1) O(log n) 或 O(1) 最坏 O(U/64) O(1) O(1)
空间 O(n) O(n) O(U/8) 字节 O(U) O(U/8) 字节

递推式 T(U) = T(√U) + O(1) 解得 T(U) = O(log log U)。展开看数字:U=2^32 时 log log U = 5;U=2^64 时是 6。log log U 增长得慢到近乎常数,这是 vEB 的唯一也是最大的卖点。

但空间表里那个 O(U) 是硬伤:U=2^32 需要约 40 亿个指针槽,没人这么干。所以工程上的真实形态有两种:

  1. y-fast trie:把全域按”每 √U 个元素一个块”切分,块内用平衡树(O(log n) 但 n 很小),块间用 vEB 索引(只对块索引做 log log)。空间降到 O(n),查询 O(log log U) 的期望值——这正是 Redis 的 zset 在大规模下、以及 IP 路由表(Linux 的 x_tables 早期桶设计)的思路。
  2. 位图 + vEB 分层(本文 SlotAllocator):把”是否存在”下沉到 64 位字,vEB 只索引字。空间 O(U/64),字内查询靠 numberOfTrailingZeros 一条指令搞定。这也是很多高性能内存分配器(如 jemalloc 的位图分级)的做法。

六、五个必踩的坑

坑 1:min 不存进簇,导致 successor/predecessor 漏解。 若不写 if (x < min) return min,当 x 比集合最小元素还小(或恰好落在 min 所在簇的边界)时,会返回 -1 或错误跳簇。这句话不是优化,是正确性必需。

坑 2:delete 删 min 时直接递归。 直接 cluster[high(min)].delete(low(min)) 会误删数据(因为 min 根本不在簇里),或者在簇为空时抛空指针。正确路径是上面代码的”提升次小元素”。

坑 3:predecessor 跨簇取错端点。 跨簇找前驱要取目标簇的 max,跨簇找后继要取目标簇的 min——一个取最大一个取最小,两者不对称。写成统一模板必然出错。

坑 4:U 不是 2 的幂。 位拆分逻辑依赖 U = 2^k。若直接传 1000,lowBits/highBits 的划分会让簇覆盖不均,导致某些键映射到不存在的簇。正确做法是把 U 向上取到 2 的幂(SlotAllocator 里就是这么做的),或改写成”非均匀 √ 划分”的变体。

坑 5:不顾空间直接用。 U=2^20 的 vEB 会有约 1024 个顶层簇 + 每簇 1024 指针,实际内存数十 MB 且初始化慢(构造时就分配数组)。U 大于 2^16 时优先考虑混合方案;U 小于 2^10 时直接用位图或桶排序,vEB 的常数开销反而不划算。

七、小结

vEB 树的思想可以一句话概括:把”值域大小”而不是”元素个数”作为递归变量,每层开平方,于是深度只剩 log log U。它换来的代价是 O(U) 空间和一个必须小心维护的不变式(min 延迟存储)。理解了 min 不进簇 这条规则,你就同时理解了为什么它的 min 查询是 O(1)、为什么 delete-min 只是”指针滑动”、为什么 successor 里必须特判 x < min。

这个结构在游戏里不只是车库调度:任何”在一维/低维离散空间里找最近的可用位置”的场景都适用——棋盘上找最近空位、RTS 里找最近空闲建造格、背包格子里找可堆叠槽位、关卡编辑器的对象槽复用。它和 Bitboard(位棋盘)搭配时尤其顺手:位图负责微观的 64 位段内定位,vEB 负责宏观的段级索引,两种”位技巧”各管一层。

下一步可以尝试:把本文的 VEBTree 改造为 x-fast trie(在 vEB 的每层加哈希表,查询降到 O(log U) 但插入 O(1) 摊还),或实现 y-fast trie 把空间压到 O(n)。再进一步,可以把它和本文第 4 节的位图结合起来,做一个支持”批量分配区间”的分配器——这是游戏服务端管理实体 ID 池的标准做法。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注