Lec 1时序数据结构:持久化
这是当前课程已收录的第一篇。
Lec 1 时序数据结构:持久化(Temporal Data Structures: Persistence)
对应 MIT 6.851 Lecture 1 · 讲师:Erik Demaine · 核心文献:Driscoll, Sarnak, Sleator, Tarjan (STOC 1986 / JCSS 1989), Brodal (1996)
TL;DR
- 持久化数据结构旨在保留所有历史版本,与就地修改(mutate)丢失旧状态的瞬态结构形成对比。
- 定义了持久化的四个严格层级:部分持久化(线性版本链)、完全持久化(分支版本树)、汇合持久化(DAG 版本图)与函数式持久化(节点不可变)。
- 在有界入度 的指针机模型下,利用修改日志(Mods Log)、回指针与势函数摊还分析,部分持久化与完全持久化均可实现每步操作 额外摊还时空开销。
一、计算模型:有界指针机
通常我们对数据结构做更新时,会就地修改(mutate)现有数据或指针,旧状态信息随之丢失。持久化数据结构(Persistent Data Structure)的核心思想是:在更新的同时保留所有历史版本,支持跨版本的时间旅行查询。
我们把数据结构形式化为一组有界大小节点的集合,每个节点包含若干数据字段与指针字段。允许的原语操作包括:
x = new Node():创建新节点x = y.field:读取字段x.field = y:写入字段x = y + z:标量数据运算destroy(x):销毁节点
该模型涵盖链表、二叉搜索树、平衡树等经典指针结构。后续所有构造均假设每个节点的入度(In-degree,即指向它的指针总数)满足 。
二、持久化的四个层级
持久化能力依操作自由度分为四个逐层增强的级别:
- 部分持久化(Partial Persistence):
- 允许查询任意历史版本,但只能在最新版本上进行更新。
- 接口:
read(var, version)与newversion = write(var, val)。 - 版本之间形成一条线性的单向链。
- 完全持久化(Full Persistence):
- 允许在任意历史版本上同时进行查询和更新。
- 接口:
read(var, version)与newversion = write(var, version, val)。 - 版本从旧状态分叉,版本拓扑演变为一棵树。
- 汇合持久化(Confluent Persistence):
- 在完全持久化基础上,支持版本合并操作(以两个旧版本为输入产出新版本)。
- 接口:
newversion = combine(var, val, version1, version2)。 - 版本拓扑扩展为有向无环图(DAG)。
- 函数式持久化(Functional Persistence):
- 源于函数式编程中的不可变(Immutable)原则,节点一旦创建绝不就地修改,所有变动必须通过生成新节点完成。
- 属于实现约束层面的超集,自然蕴含汇合持久化。
三、部分持久化的构造(Node-Copying 与修改日志)
Driscoll 等人于 1986 年提出了通用变换框架,将任意入度有界的瞬态数据结构转换为部分持久化结构。
3.1 节点结构设计
每个持久化节点包含三个组成部分:
- 只读数据区:存放原结构的数据与指针字段(容量为 )。
- 回指针区(Back Pointers):存储指向当前节点的所有前驱节点引用(容量为 )。
- 修改日志区(Mods Log):定长数组,存储形如
(field, version, value)的三元组记录,容量上限设为 。
3.2 读写操作机制
- 读操作
read(var, v): 在目标节点的修改日志中,线性扫描找到版本号 的最新修改记录并返回对应值;若无记录则返回只读区初始值。由于日志上限为 ,读操作耗时严格为 。 - 写操作
write(var, val):- 若当前节点的修改日志未满(条目 ),直接追加一条新日志,耗时 。
- 若修改日志已满,执行节点复制(Node Splitting):
- 分配新节点 ,将旧节点的最新状态写入 的只读区;
- 复制回指针,并将所有出边目标节点的回指针重定向到 ;
- 对所有前驱节点 (至多 个),递归调用写操作,将指向旧节点的指针更新为指向 。
3.3 势函数摊还分析
若采用朴素分析,写操作的递归可能会沿前驱链级联传播。利用势能分析法,定义势函数:
- 未触发分裂:消耗实际时间 ,。摊还代价为 。
- 触发节点分裂:旧节点的 条日志被吸收到新节点的只读区,新节点初始日志为空,。而分裂引起向前驱递归写入的代价至多为 。 摊还代价递归式:
解得摊还代价恒为 。Brodal(1996)进一步通过去摊还化技巧,给出了最坏情况单次 的实现。
四、完全持久化的构造(版本树与顺序维护)
完全持久化允许在任意旧版本分叉,带来两个本质困难:一是版本之间构成树状分支,无法仅通过标量数字比较版本先后;二是每个节点都可能面临任意历史分支的写入。
4.1 版本树的线性化与顺序维护
利用深度优先遍历(DFS)对版本树进行欧拉括号化序列编码:进入节点 时标记 ,离开时标记 。
- 祖先判定定理:在版本树中, 是 的祖先当且仅当 且 。
- 借助 Dietz 和 Sleator(1987)提出的顺序维护结构(Order Maintenance),两项比较可在 时间内判定任意两个版本的先后因果关系,插入新版本亦仅需 。
4.2 节点分裂与版本树划分
修改日志容量扩展为 。当日志写满时,不能按时间简单截断,而是根据版本树的拓扑关系将日志划分为两半:
- 节点 分裂为 与 , 继承版本子树较新的一半, 保留较旧的一半;
- 通过势函数 ,同样证明完全持久化写操作的摊还时间复杂度为 。
五、汇合与函数式持久化
5.1 汇合持久化的指数路径挑战
当版本图演变为 DAG 时,不同分支多次合并会导致从根到某节点的有效路径数呈 指数级爆炸。Fiat 与 Kaplan(2003)证明,在此类模型下无法维持 的通用上界,其变换开销受限于有效深度 。
5.2 函数式持久化与路径复制(Path Copying)
在纯函数式模型中,任何修改均不得改变旧节点。以平衡二叉搜索树为例:
- 修改叶子节点时,必须沿查找路径一路复制从根到叶的所有祖先节点。
- 若树高为 ,单次更新需要创建 个新节点,耗时 。
- 路径复制结构天然具备完全持久化与汇合能力,且保证了无锁与线程安全。
六、持久化技术全景对比
| 持久化层级 | 版本拓扑 | 每次操作额外开销 | 核心机制 |
|---|---|---|---|
| 部分持久化 | 线性链 | 摊还 / 最坏 | 修改日志( 槽位) + 回指针 + 节点复制 |
| 完全持久化 | 分支树 | 摊还 | 顺序维护(Order Maintenance) + 版本树切分 |
| 汇合持久化 | DAG | 有效深度评估 + 局部重构 | |
| 函数式持久化 | 不可变 DAG | 路径复制(Path Copying) |
七、我的理解
持久化技术最迷人的地方在于它在极其苛刻的约束下达成了看似不可能的常数开销。其底层秘密在于两点:
- 拓扑约束是常数开销的先决条件:入度 保证了一个节点被替换时,需要级联通知的前驱数量是有限的;
- 日志容量 与势能抵消的精确设计:为什么日志容量必须选为 ?因为每次分裂要消耗 次级联写入,必须在满溢前积累足够的势能储备( 次安全写入),从而使势能变化 恰好将级联扩散的成本清零。这种“通过局部缓存把暴发性递归摊平为常数流”的思路,是现代高性能存储引擎与不可变数据架构的通用哲学。 :::