Text Editor 的 Piece Table 结构
2011-04-25 16:12 netwy 阅读(1577) 评论(0) 编辑 收藏 举报Charles Crowley 在 Data Structures for Text Sequences 中描述了一种用于存储和编辑文本的数据结构 Piece Table。这种结构被大多数 Professional 的文本编辑器/字处理器所使用。另一种广为应用的(更简单)的数据结构是 Gap,最初被用在 Emacs 中,现在的 Scintilla,Java Swing Text Field 等文本编辑组建都使用这种结构。Charles Crowley 的文章中有详细的关于这两种结构的性能比较。
在处理大型文档的时候,“Piece Table + 缓存优化的 ItemAt 操作”的性能要高于“Gap”结构。开源字处理软件 AbiWord 就使用了这种结构。在 AbiWord 开发者的博客上有一个将 Piece Table 所有操作都提高到 O(log n) 的方法,参见 Improving the AbiWord’s Piece Table。AbiWord 的开发计划里也有相应 Speed Up Piece Table 的计划。这里的优化方法是将本来用 Linked List 存储的 Piece 以 Red-Black Tree 代替,由此将 ItemAt 操作提高到 O(log n)。
Piece Table 的结构由几部分组成:
1. Sequence:
Sequence 是整个数据结构的接口,用户程序唯一的访问方式。逻辑上 Sequence 由一系列 Item 组成。Item 是存储和操作的最基本单元,在文本编辑器中可以与 Character 对应。Sequence 所能提供的操作与 List 相同,包括:Insert, Delete, Replace, ItemAt, Undo/Redo 等。物理上 Sequence 并不直接存储 Item(Item 存储在最后一层 Buffer 中),Sequence 存储的是一系列的 Piece Descriptor。每个 Piece Descriptor 指向了一个 Piece。
Sequence 和 Piece Table 是等价词。Sequence 用来讨论逻辑结构(接口)而 Piece Table 用来说明物理结构(实现)。
2. Piece:
Piece 是一组连续的 Item。这里的连续有两层意义,逻辑上连续和物理上连续。简单说 Item Index in Sequence 的连续是逻辑连续,而 Item Offset in Buffer 的连续是物理连续。Piece 中的 Items 既是逻辑连续又是物理连续。有些文章里将 Piece Descriptor 称为 Piece,而将 Piece 称为 Span。Piece 的成员包括:指向 Buffer 的指针,在 Buffer 中的 Offset,以及长度 Length. 每次添加操作都会增加一个新的 Piece,根据情况有可能将旧的 Piece 拆分成两个。每次删除操作也会相应的 Piece 删除或拆分。
3. Buffer:
Buffer 是真正存储 Item 的地方。在 Charles Crowley 的文章中 Piece Table 仅仅包括两个 Buffer,一个存储整个被编辑的文件,另一个存储添加的字符(被删除的字符也保留在 Buffer 中,可用于 Undo/Redo)。第一个 Buffer 是 Fixed Length 的,而第二个 Buffer 是 Append Only 的。试作中我们可以使用多种方法优化这两种 Buffer。
可以看出对于 Sequence (Piece Table) 所有的添加删除操作本身都是 O(1),与 Linked List 相同。但其访问操作(ItemAt)是 O(k),这里 k 为 Piece 的数量。这是由于我们使用 List 或 Array 存储 Piece Descriptor。我们也注意到所有的添加删除操作都需要先找到相应的 Piece,而这个操作也是 O(k) 的,这时我们可以简单的将最近一次访问的 Piece 缓存起来来大幅度提高性能。这也是 AbiWord 的做法。对于 O(k) 的操作我们可以尽可能的减少 k (Piece 的数量),最简单的方法是避免每个字符插入操作都产生一个 Piece,将一系列连续的插入删除操作产生一个 Piece。
更高级的优化技术是将 List 结构的 Piece Table 转换为 Red-Black Tree 结构。每个 Tree Node 包含:一个 Piece,其左分支的所有 Piece 的 Length (注意与 Index in Sequence 不同)。