“不同v的树链共享后缀,H(v)也应该共享结构。若为每点显式复制整条树链的全部偏离,链形图上会重复保存大量相同内容。持久化路径复制让新增局部偏离后,旧H仍可用于其他起点。”
形式陈述
持久化数据结构为每次更新产生一个版本标识,旧版本不会被覆盖。partial persistence 允许查询任意版本但只能更新最新版本;full persistence 允许从任意版本分叉更新;confluent persistence 还允许合并多个版本;retroactivity 则修改历史操作并要求传播其后果,是另一种接口语义。这些能力必须分别声明。
Path copying 在更新根到目标的路径时复制被改动节点,新根指向复制链,未变化子树由新旧版本共享。Fat nodes 则在节点内保存带版本标签的字段修改历史,查询按版本选择有效值,其查找成本取决于历史索引。另一种 node-copying 技术只允许有限个修改槽,槽满时复制节点并修复相关入边;其时间与新增空间界还需要入度、可修改版本等条件。正确性要求每个版本经抽象函数映射到相应ADT状态,版本间共享不得让一次更新改变旧版本的可观察结果。
持久化成本需同时报告操作时间、新增空间和查询某版本的开销。结构不可变只保证语义上不覆盖,不保证实现高效;整份复制仍可能每次花
直觉
普通可变结构只有一条被不断覆盖的现在;持久化结构把每次更新变成历史中的新节点,并让多个版本共享没有变化的部分。每次更新只从一个已有版本分叉时,逻辑版本图是一棵树,partial persistence 的更新历史则是一条链;confluent persistence 才允许一个新版本具有多个父版本。各版本共享的物理节点可以形成另一张 DAG,不能将它与版本间的父子关系混为一谈。
这里的 persistent 指旧逻辑状态仍可查询,不表示进程退出后写入磁盘仍存在。磁盘耐久性、事务日志和崩溃恢复属于存储系统的另一组保证。
例子与边界
持久栈可用不可变链表实现。版本 push(a) 得到 push(d) 得到分支
对平衡搜索树更新一个叶,path copying 复制根叶路径,其他子树共享,新增空间与树高同阶。若节点含可变父指针或外部缓存,并在新版本中原地改写,共享节点会把变化泄露回旧版本,破坏持久性;所有跨版本可观察字段都必须遵守版本规则。
Eppstein游走枚举给出另一项路径复制工作负载:每个顶点沿最短路树继承后继的完全二叉根堆,再插入本地最小偏离边。插入只复制根到新末叶的位置链,保留其他子树共享;旧根仍供其他起点查询,因此每次O(log(n+1))的新节点有具体形状与不变量支撑。
Confluent persistence 不能由“同时引用两个旧根”自动得到:合并可能制造重复边、循环或指数版本依赖,需要特定结构与算法。Retroactivity 更不是读取旧版本,它允许改写过去的操作并重新解释之后状态,两者的问题定义不同。
推论与应用
持久化结构用于撤销、时间旅行查询、函数式程序、版本控制与离线算法。它能保留审计历史,也能让算法同时比较多个分支状态而不复制完整数据。
选择 path copying 还是 fat nodes 取决于每次更新触及的指针数、版本查询模式和节点布局。任何“每次只增常数空间”的结论都需前提;对高度为
参考资料
- James R. Driscoll, Neil Sarnak, Daniel D. Sleator, and Robert E. Tarjan, “Making Data Structures Persistent,” Journal of Computer and System Sciences 38(1), 1989, pp. 86–124.
- Chris Okasaki, Purely Functional Data Structures, Cambridge University Press, 1998, Chs. 2–3.