【数据结构】深入理解Piece Table数据结构
引言
当在vscode中键入文字的时候,这些文字会缓存在内存中的数据结构(piecetable)中。此外另有Abiword,atom,bravo,某些版本的microsoft word都使用了piecetable这种数据结构来表示文本文档的内容。
什么是 “Piece Table”
维基百科中有如下定义:
A Piece Table is a data structure typically used to represent a series of edits on a text document Piece Table是一种数据结构,通常用来表示文本文档中一系列的编辑操作。
概念
在Piece Table中,真实的文本内容都被分类两类:
- 原始文本内容(original) :初始化加载的文本内容,可以为空
- 新增文本内容(append | add) :指的是编辑者通过编辑新增的文本内容
这两类文本内容存储在两个分离的缓存中(buffer),通常最简单的方式是用两个String Buffer来存储。当以某种顺序的获取 原始文本内容 和 新增文本内容 中的部分文本内容(Piece)的时候,就是组成了最终的文档的内容。代指部分文本内容的结构,称之为Piece。
通常,Piece 的基本结构如下:
- buffer type: buffer类型,是original还是append
- start: 在该buffer中所在的开始的index
- length: 内容的长度。
简单例子
原始文本(Original): 这是一段文字
新增文本(Append): 美好,这是另一段文字
buffer type | start | length | 表示内容 |
---|---|---|---|
original | 0 | 4 | 这是一段 |
append | 0 | 2 | 美好 |
original | 4 | 2 | 文字 |
所有的piece连起来就是:**这是一段美好的文字,**这样一篇在编辑中的文档内容就这样被一连串的piece所指代。
操作
Piece Table有以下基本的操作:
- insert(p, str) : 在某个位置p插入(输入)一段文本
- itemAt(p1, p2) : 获取 p1 到 p2 位置的文本内容
- delete(p1, len) : 删除 p1 位置开始 长度为 len 的文本内容,删除其实并不删除真实的文本内容,而是更改piece的索引指向
特点
其特点简单总结为:
- 只增不减(Append-only),初次加载的原始文本和新增文本会被缓存,并不被删除
- 每个piece存储的是对于文本的指针,所以对于任何插入、删除操作其实就是一个index的变更而已,任何真实的文本内容是不变的。这样的特性,可以为无限撤销、重放提供了可能性
- 由于原始文本是不会被改变的,因此也为懒加载、分块加载、大文件加载等提供了可能性
用例子来理解
下面用一个稍微复杂的例子来看PieceTable的各种操作是如何达到编辑的效果的,也能让我们更加直观的理解这个数据结构。
buffer Type | Content |
---|---|
original | 这是一段文字 |
append | (空的) |
buffer type | start | length | 表示内容 |
---|---|---|---|
original | 0 | 6 | 这是一段文字 |
编辑后文档内容:这是一段文字
// 执行操作
insert(4, '美好的')
buffer Type | Content |
---|---|
original | 这是一段文字 |
append | 优美的 |
buffer type | start | length | 表示内容 |
---|---|---|---|
original | 0 | 4 | 这是一段 |
append | 0 | 3 | 优美的 |
original | 4 | 2 | 文字 |
编辑后文档内容:这是一段优美的文字
// 执行操作:位置2,长度1
delete(2, 1)
buffer Type | Content |
---|---|
original | 这是一段文字 |
append | 优美的 |
buffer type | start | length | 表示内容 |
---|---|---|---|
original | 0 | 2 | 这是 |
original | 3 | 1 | 段 |
append | 0 | 3 | 优美的 |
original | 4 | 2 | 文字 |
编辑后文档内容:这是段优美的文字
Piece的管理
Piece Table 本身的实现,核心是 Piece的管理(增删改查) ,这里就需要另外的数据结构来实现之,下面分析列举一些数据结构:
-
数组:最常用的数据结构之一,优势在于实现简单直接,劣势在于随着piece的增多,插入删除数组中的某个值,性能是比较差的。
-
链表:也是最常用的数据结构之一,插入、删除的性能较好[O(1)],但是获取的性能就不稳定了[搜索时间 + O(1)],搜索时间最好是 O(1),最差是O(n),非常不稳定。
-
平衡二叉树:各个操作都在 O(logn),非常的稳定
- 红黑树(red-black tree)
- Splay Tree
从这些常用的数据结构来说,使用平衡二叉树是比较好的选择,即使在piece非常膨胀的时候,也能有稳定的性能表现。
从现实产品的例子可以参考,vscode是使用了红黑树,atom使用的是splay tree。
下面是自己写的一个实验性的piece table实现,用的是红黑树来管理piece。还有添加了一些额外的功能,比如undo/redo,富文本支持等
总结
Piece Table **本身的概念是非常易于理解的,实现层面的难点在于用什么方式来管理piece,兼顾性能,内存开销,undo\redo,树快照等等功能。
Piece Table **给我的另一个启发就是,“真实内容存储和逻辑的分离” ,这个理念隐藏在其背后,真实的文本存储是连续的,而Piece通过索引来指向具体的内容,具体的逻辑(piece管理的数据结构实现)则与具体内容完全隔离开了。