可持久化数据结构

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了可持久化数据结构相关的知识,希望对你有一定的参考价值。

参考技术A 最近在刷MIT 6.851,记录下笔记。

可持久化数据结构就是能存储、查询数据的历史版本的数据结构。

http://hypirion.com/musings/understanding-persistent-vector-pt-1

https://www.geeksforgeeks.org/persistent-data-structures/?ref=lbp

可持久化数据结构简介

MIT 6.854j-advanced-algorithms
很赞!!可惜没video。

Making Data Structures Persistent
太长没看

总结了下大致包括如下领域应用:

并发事务的原子性(便于Rollback)、隔离性。

https://io-meter.com/2016/09/03/Functional-Go-persist-datastructure-intro/

同上。

便于实现diff,rollback

可持久化数据结构最初设计出来的目的是为了在高维查询中降维,把其中一个维度当做时间,用可持久化数据结构处理效率更高
可以看下MIT 6.851老师的介绍。

这种思想在高维处理中很常见,比如求二维range sum query时候,把其中一个维度当时间、拿来做扫描线也是这种降维思路(见《算法竞赛进阶指南》):

可持久化数据结构简介

自己轮一个.net可持久化库 Persistent Data Structures 下面有讨论use case
中文翻译见 可持久化数据结构

Functional Go: 持久化数据结构简介

这部分可以看6.851视频

6.851把链式数据结构的模型叫pointer machine model。对于基于pointer machine model的数据结构,有没有通用的方法将他们改造成persistent?

note see https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-854j-advanced-algorithms-fall-2005/lecture-notes/persistent.pdf
video see https://www.bilibili.com/video/BV1iE411n7yJ

O(1)写,读时对于每个点都要执行O(logm)的查询。假设一次查询要读v个点,时间复杂度O(vlogm)
所以对写友好,对读不友好

看到这里有个问题:每个点的history放hashmap里不就行了?读时也只需要O(1)的查找
但这样做的话,hashmap不支持ceil操作,因此要支持ceil没办法只能用有序结构、log级别查询
(这里的假设是有个全局时间戳,而不是每个key对应一个自己的自增时间戳)

个人理解,想做可持久化key/value Map的话即可按这种方法,每个key对应的entry存放所有历史值,这也可以看成是邻接表。

说白了就是写时分裂节点,从root开始分裂到要写的点。将所有version的root存到字典里

上面两个要么对写不好,要么对读不好,能否兼得?

难以置信,说白了就是给path copying方法中每个node加一个log cache,最后算出来的写时amortized time complexity就是O(1)了。

What about non-tree data structures?

平衡树怎么处理?
平衡树要旋转,想想就感觉很难改造成持久化。6.854课件讲的很粗没懂。 算法竞赛中常用的可持久化平衡树一般就是 可持久化无旋转 Treap ,省去了旋转可持久化的复杂。

a. fat nodes
每个点存的log从version queue变成version tree,查询每个点时要从树中找到最近祖先

b. path copying
没区别

c. modification box
怎么找根节点?
i. pointer per version,可能多个pointer指向一个root
ii.存root tree,查询时先找最近祖先

怎么修改old version
i. 修改时放box,满了就分裂出来一个新节点

但这样有问题:分裂出来的还是满的,如果整条链都是满的,每次修改复制全部,下次修改还得复制全部。而且这样还不好存root,比如最右边图,代表0的root有俩

ii. 修改时放box,满了就分裂出来一个新节点,但分裂时自动apply有用的log、丢弃没用的log

What about non-tree data structures?
6.851有讲,分裂成两个节点,log分成两部分,新节点拿一个子树的log,新节点apply log直到自己的子树,每部分丢弃自己用不到的。

6.851讲了 太复杂没听懂。

网上关于可持久化数据结构的优质资料都是算法竞赛的,因此收集总结了下竞赛常用的。看了下基本都是partial persistent,有的是fully persistent,都没用到modification box技术。
https://github.com/Misaka233/algorithm/blob/master/%E9%99%88%E7%AB%8B%E6%9D%B0%EF%BC%9A%E5%8F%AF%E6%8C%81%E4%B9%85%E5%8C%96%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E7%A0%94%E7%A9%B6.pdf

https://www.bilibili.com/video/BV1C4411u7rK?p=1

https://www.geeksforgeeks.org/persistent-trie-set-1-introduction/
和可持久化线段树类似的方法,基于path copy实现partial persistence。
问题是每个点在拷贝时都要复制O(R)个指针,插入的时间复杂度为O(len*R)
查询时先从字典(数组/哈希表)里找到指定version的root,然后访问,O(len)

在竞赛中常用的是可持久化01字典树,比如xor问题,见 https://oi-wiki.org/ds/persistent-trie/ ,没看懂

算法竞赛中常用的可持久化平衡树一般就是 可持久化无旋转 Treap ,省去了旋转可持久化的复杂。

【AgOHの数据结构】可持久化数组

https://www.luogu.com.cn/problem/P3919

方案:
a. fat nodes
每个节点放所有历史,查询时在所有历史版本中找最近祖先版本

b. path copying
存到可持久化线段树里。
为什么好好的线性结构要树化?直觉上理解,是为了分裂新版本时减少指针复制。

如果是稀疏数组,朴素方法太浪费空间了,可以基于动态开点来优化空间存储,见 https://io-meter.com/2016/11/06/functional-go-intro-to-hamt/

【AgOHの数据结构】可持久化并查集

并查集基于数组,可持久化并查集就基于可持久化数组。可持久化数组用可持久化线段树实现,因此可持久化并查集用可持久化线段树实现

《 可持久化数据结构研究 》
不过文中写的太简略了,个人推测的维护方法为:

https://io-meter.com/2016/09/03/Functional-Go-persist-datastructure-intro/

讨论支持如下操作的抽象数据结构(ADT)如何可持久化:

a. 基于普通数组
只能copy on write
b. 基于可持久化数组/可持久化线段树
问题是怎么处理新增节点、删除节点?得想办法魔改线段树
c. 可持久化链表
d. 可持久化块状链表
e. 可持久化平衡树
f. 文中提到的vector trie
名字挺骚的没反应过来,看了一会发现:这玩意就是可持久化数组(可持久化线段树),只不过是多叉树,叫“可持久化多叉线段树”比较形象。文章也没提怎么处理新增节点、删除节点。

persistent-hash-table-implementation
a. 可持久化平衡树
b. 还用hashmap,但是每个entry改造成fat node(存放所有log),查询时先找到entry,再在所有log 中找最近祖先版本
c. 可持久化数组。
考虑到HashMap本来就能只用一个数组实现(解决冲突时用open addressing方法,不用Chaining),那么实现了可持久化数组就相当于实现了可持久化HashMap

d. Hash_trie
hash(key)的值存在trie里,value放到trie的叶子节点。优化版本包括 Hash array mapped tries and Ctries
HAMT这名字起的很奇怪。原理就是块状hash trie(所谓块状是我自己起的名,指每个节点区分儿子时不是单纯的比较某1位,而是比较某2位甚至多位)(或者理解成hash trie+动态开点多叉线段树也行)(反正就是bitwise hash trie加上一堆优化,懒得记就记hash trie就行了,真要自己实现的时候这些优化trick也能想到)
文中讲hamt的碰撞处理有点扯,个人理解可以chaining,可以open addressing。细节没深究,可以看 论文 和 讨论 。按作者的意思AMT是之前他提出的一种Trie的优化,比Tenary search trie要好。

https://www.cnblogs.com/tedzhao/archive/2008/11/12/1332112.html

// TOOD

https://www.cnblogs.com/zinthos/p/3899565.html
Planar Point Location问题见
https://courses.csail.mit.edu/6.851/spring12/scribe/lec3.pdf
https://www.bilibili.com/video/BV1iE411n7yJ?p=3

http://acrossthesky.logdown.com/posts/712254-some-basic-data-structures
https://github.com/Misaka233/algorithm/blob/master/%E9%99%88%E7%AB%8B%E6%9D%B0%EF%BC%9A%E5%8F%AF%E6%8C%81%E4%B9%85%E5%8C%96%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E7%A0%94%E7%A9%B6.pdf
https://download.csdn.net/download/u011808175/6477705

朴素的方法是线段树套线段树,以便支持二维RMQ,时间复杂度O(logN*logN)。
个人理解,最优的二维RMQ数据结构应该是用modification box实现的可持久化值域线段树,O(N)的构造时间、空间,O(logN)的查询。

如果统计操作具有“区间可加性”、“区间可减性”,那么该操作二维统计问题可以使用可持久化线段树。
range minimum query中的min()操作其实不具有区间可减性,但是range minimum query问题可以归约成range select query问题,进而可以归约成range count query问题,而count()操作具有区间可加减性,因此也能用可持久化线段树。
所以我们得到了一类二维统计问题的通用数据结构:对于可归约成具有“区间可加性”、“区间可减性”统计操作的二维统计问题,可以使用可持久化线段树存储,以便支持快速查询(统计)。

能否找到一类高维统计问题,具有通用的logN级别解?个人理解可以借鉴代数的思想,只要有区间可加减性的都能归约成K维RMQ问题,用modification box实现的可持久化值域线段树解决。
// TOOD 只是个人畅想,没细想。

bigtable(hbase)可以看成外存模型下的可持久化map:

但需要注意的是删除操作会真的删掉之前的老版本数据:

其实任何支持前缀匹配的db都能作为可持久化map,你只需要把rowkey设计成"key@@timestamp"即可
http://hbase.apache.org/book.html#reverse.timestamp

以上是关于可持久化数据结构的主要内容,如果未能解决你的问题,请参考以下文章

[您有新的未分配科技点]可,可,可持久化!?------可持久化线段树普及版讲解

可持久化线段树

可持久化数据结构

可持久化平衡树

可持久化数据结构

可持久化数据结构