Lec 11整数数据结构:模型、前驱、vEB 树、x-fast 树与 y-fast 树
Lec 11 整数数据结构:模型、前驱、vEB 树、x-fast 树与 y-fast 树(Integer Data Structures: Models, Predecessor, vEB, x-fast & y-fast Trees)
对应 MIT 6.851 Lecture 11 · 讲师:Erik Demaine · 核心文献:van Emde Boas (FOCS 1975), Willard (IPL 1983)
TL;DR
- 本讲探讨非基于比较的整数数据结构,在 Transdichotomous Word RAM 模型(字长 )下求解前驱问题(Predecessor Problem)。
- 经典 van Emde Boas 树(vEB 树)通过递归将宇宙规模开方(),利用内外分层与空集常数检查实现 的前驱与更新,但朴素结构空间高达 。
- Willard 提出的 x-fast 树结合 Trie 树与层级哈希表,利用树高的二分搜索实现 查询;进一步通过两级间接寻址(Indirection)构建 y-fast 树,达成 最优空间与 均摊操作时间。
一、计算模型与前驱问题定义
在传统的基于比较的模型中,集合元素的有序维护与前驱查询受限于 的下界。然而在实际计算机硬件中,数据以固定长度的二进制字(Word)存储,允许按位进行逻辑与算术运算。利用字的内部并行性与直接寻址能力,我们可以突破比较模型的瓶颈。
1.1 Transdichotomous Word RAM 模型
::: definition Transdichotomous Word RAM 模型
- 字长约定:计算机内存被划分为字长为 位的单元,要求字长 (即机器字足以作为指针寻址整个输入规模 的内存空间)。
- 支持原语:每个原语操作在 时间内完成,包括标准的 C 语言整数运算:加减乘除()、按位逻辑运算(
AND,OR,XOR,NOT)以及左右位移(<<,>>)。 - 空间衡量:通常以使用的机器字(Word)数量来衡量空间复杂度。 :::
1.2 前驱问题(Predecessor Problem)
维护一个容量为 的动态整数集合 ,其中全集大小 。 数据结构需支持以下核心操作:
- :向 中添加元素 。
- :从 中移除元素 。
- :返回 中小于 的最大元素 。
- :返回 中大于 的最小元素 。
目标是将查询与更新时间从 降低至关于字长 的对数级别:。
二、van Emde Boas 树(vEB 树)
Peter van Emde Boas 于 1975 年提出的 vEB 树是整数前驱领域最具里程碑意义的结构,其设计核心是宇宙规模开方递归。
2.1 递归分解与位拆分
对于全集规模为 的整数 ,其二进制表示长度为 位。我们可以将 拆分为高半部分和低半部分:
- (高 位,指示所属的簇索引)。
- (低 位,指示簇内部的偏移量)。
- (由簇号 与簇内偏移 重组原始整数)。
2.2 树结构与 Min/Max 设计
一个规模为 的 vEB 结构包含:
- :一棵规模为 的 vEB 树,记录哪些子簇(cluster)当前非空。
- :一个包含 个子指针的数组,第 个指针指向一棵规模为 的 vEB 树,存储所有 的元素。
- 与 属性:
- 记录当前结构中的最小值与最大值。
- 核心设计绝妙之处: 元素仅存储在该层节点的属性中,不递归插入到下层的任何子簇内;而 既保存在该属性中,同时也实际存在于对应的子簇中。
vEB(u)
┌─────────────────────────┐
│ min: 2 max: 61 │
└────────────┬────────────┘
│
┌──────────────┴──────────────┐
▼ ▼
summary: vEB(√u) clusters: array[√u] of vEB(√u)
┌─────────────────────┐ ┌──────────┬──────────┬─────┬──────────┐
│ 追踪非空 cluster 号 │ │ cluster[0]│cluster[1]│ ... │cluster[k]│
└─────────────────────┘ └──────────┴──────────┴─────┴──────────┘
2.3 操作实现与复杂度分析
查询操作
- 若 ,直接返回 。
- 考察 所在的子簇 :
- 若 小于该子簇的最大值(即 ),说明其后继仍在当前簇内,递归调用 进入 。
- 否则,后继必定位于之后的某个非空簇中。通过 在 递归步内找到下一个非空簇号 ,并直接返回 。
- 任何分支在每层均只做至多一次子结构递归:
插入操作
- 若当前结构为空(),直接令 ,立即返回。
- 若 ,交换 与 (确保旧的 继续向下插入,新的 停留在当前层)。
- 检查 所属子簇 是否为空:
- 若为空:首先递归调用 ;然后由于该子簇此前为空,将 放入子簇仅需更新该子簇的 ,耗时 ,无需进一步递归!
- 若非空:直接递归调用 。
- 同样保证了每层至多产生一次规模为 的递归,单次插入严格为 。
2.4 空间优化:从指数空间到线性空间
朴素 vEB 树为每一层分配大小为 的静态指针数组。由于空间递归方程为:
其总空间复杂度为 ,对大字长(如 )完全无法接受。
哈希动态化:改用动态散列表(如 Cuckoo Hashing)存储非空簇指针。只有在子簇插入首个元素时才分配对应节点。经过哈希优化后,空子簇不占存储,总节点数与集合元素量成正比,空间降至 ,且查询期望时间保持 。
三、x-fast 树(x-fast Trees)
Willard 在 1983 年提出了基于 Trie 树与层级哈希的确定性解法,将树高二分与前缀匹配结合。
3.1 结构定义
- 底层满二叉 Trie:构建一棵高度为 的完全二叉字典树。根节点深度为 0,叶子节点深度为 。
- 叶子双向链表:所有实际存在的集合元素对应深度为 的叶子,这些叶子按从小到大的顺序由双向链表串联。
- 层级哈希表:对于树的每一层 ,维护一个哈希表 。哈希表中仅存储该层所有拥有后代叶子节点的内部节点(以从根到该节点的路径前缀作为键)。
Level 0 (根) [ L0: {root} ]
/ \
Level 1 [ L1: {0, 1} ]
/ \ / \
... ... ... ...
/ \
Leaf(4) <-> Leaf(9) <-> Leaf(15) (双向链表)
3.2 前驱查询:对层级进行二分搜索
在 层的 Trie 中,要找到 的前驱,关键在于定位 与集合中已有元素在 Trie 中的最长公共前缀(Lowest Common Ancestor, LCA)。
- 在层数序列 上进行二分查找(Binary Search on Levels):
- 取中间层 。
- 提取 的前 位前缀,查询哈希表 是否存在该前缀。
- 若存在:说明在深度 处有后代节点与 匹配,最长前缀至少在深度 ,令 。
- 若不存在:说明最长前缀深度小于 ,令 。
- 二分查找耗时 步,最终确定最深存在祖先节点 。
- 节点 必然恰有一个分支缺失(即 离开已有路径的分叉点)。利用 维护的后代极值指针跳转到底层叶子,并在叶子链表上左移或右移一步,即在 时间内确定 的精确前驱与后继!
- 查询总时间:。
3.3 空间与更新代价
- 空间:每个插入的元素在每一层最多贡献一个祖先节点,深度为 。因此 个元素在整棵树中最多产生 个节点。总空间为 。
- 更新:插入或删除一个元素需沿路径更新 个哈希表和叶子指针,耗时 。
四、y-fast 树与两级间接寻址(Indirection)
x-fast 树在查询时间上达到了优秀的 ,但其空间开销 偏大,且更新时间 较慢。Willard 运用**间接寻址(Indirection)**技术,构造出在空间和时间上均达到最优的 y-fast 树。
4.1 间接寻址架构
- 分块划分:
- 将 个元素划分为 个连续的有序小组(Group)。
- 每个小组的大小维持在 之间。
- 底层局部平衡树:
- 每个小组内部使用一棵传统的红黑树(或 AVL 树)存储其实际元素。
- 节点总数为 ,因此组内红黑树的高度为 。
- 顶层代表元索引:
- 从每个小组中选出一个代表元(如该组的最大元素)。
- 将所有代表元(共 个)存入一棵顶层 x-fast 树 中。
[ 顶层 x-fast 树 ]
(仅存 Θ(n/w) 个代表元)
/ | \
Rep_1 Rep_2 Rep_k
│ │ │
▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐
│ Group │ │ Group │ │ Group │ <- 底层红黑树 (每组大小 Θ(w))
└───────┘ └───────┘ └───────┘
4.2 性能全面优化
空间复杂度:达到严格
- 顶层 x-fast 树仅包含 个元素,根据 x-fast 树空间分析,其占用空间为 个字。
- 底层各组红黑树节点总和为 ,占用空间 。
- 总空间: 机器字!
查询复杂度:严格
- 在顶层 x-fast 树中查询 的代表元后继,耗时 。
- 根据代表元定位到对应的底层候选红黑树(以及相邻的小组,至多常数个)。
- 在大小为 的平衡二叉树内搜索 的前驱,耗时 。
- 总查询时间:。
更新复杂度:均摊
- 插入新元素时,首先找到对应小组,向该组红黑树插入,耗时 。
- 若某小组元素个数超过 ,将其分裂为两个大小均为 的小组,并向顶层 x-fast 树插入一个新代表元:
- x-fast 树插入耗时 。
- 但小组从规模 增长到 至少需要经过 次插入!
- 将 的分裂开销均摊到这 次插入上,每次更新的分摊成本仅为 。
- 删除操作对称地通过合并与借位实现,同样具有均摊 的顶层开销。
- 总均摊更新时间:。
五、方案综合对比
| 数据结构 | 查询时间 | 更新时间 | 空间复杂度 | 核心机制 |
|---|---|---|---|---|
| 平衡二叉搜索树 | 基于比较的三向分支 | |||
| 朴素 vEB 树 | 宇宙规模开方递归、Min/Max 隔离 | |||
| 动态哈希 vEB 树 | 期望 | 动态散列压缩空子簇 | ||
| x-fast 树 | 层级哈希表 + 树高二分查找 | |||
| y-fast 树 | 均摊 | 最优 | 两级间接寻址:x-fast 索引 + 组红黑树 |
六、我的理解
整数前驱结构的精妙之处在于对“规模”这一维度的变换:
- vEB 树的分治发生在“数值位宽”上:普通二分把元素数量减半,而 vEB 把位宽 减半(),将原本属于搜索树的几何级数压缩为超几何级数,从而把 降为 。
- y-fast 树的间接寻址是权衡艺术的典范:x-fast 树虽然空间膨胀了 倍、更新慢了 倍,但查询极快。间接寻址通过在底层挂接大小为 的粗粒度块,巧妙地让 倍的开销与 的代表元数量精准对消!原本高昂的 重构代价被 的局部操作均摊吸收,最终以极简的形式达成了时空双优。 :::