我正在阅读Joaquin Cuenca Abela的this great article。他谈到使用红黑树来实现一个分段表,而不是一个双向链表。
我在理解这可能与正在进行更改的缓冲区有什么关系时遇到了一些麻烦。例如,取这两个缓冲区(原始,追加):
Hello!\0 Original
y Append
让我们说这个表格看起来像这样:
buffer start length
original 0 2
original 5 2
append 0 1
我们最终应该:
Hey!\0
使用双向链表,这可以实现如下:
------------------ ------------------- ----------------
Buffer = ORIGINAL| |Buffer = ORIGINAL| |Buffer = APPEND
Start = 0 |<--|Start = 5 |<--|Start = 0
Length = 2 | |Length = 2 | |Length = 1
Next = 0x01 |-->|Next = 0x02 |-->|Next = NULL
Previous = NULL | |Previous = 0x01 | |Previous = 0x01
------------------ ------------------- ----------------
如果文件最初是作为char数组加载的,那么在编辑之后,这似乎非常简单和快速。另一方面,据我所知,红黑树看起来像这样:
------------------
size = 2
size_left = 1
size_right = 2
colour = black
------------------
/ \
/ \
/ \
---------------- ----------------
size = 1 size = 2
size_left = 0 size_left = 0
size_right = 0 size_right = 0
colour = red colour = red
---------------- ----------------
/ \ / \
/ \ / \
NULL NULL NULL NULL
我没有看到在编辑后重新绘制文档其余部分的明确方法。当然,插入/删除/查找会更快地向树中添加片段。但我错过了如何构建编辑后的缓冲区以供查看。
我错过了什么?如果我有一个编辑器,并且我删除/插入了一大块文本,我将如何遍历树以重新绘制缓冲区并正确反映此编辑?而且,这会如何比链表提供的O(n)时间复杂度更快?
答案 0 :(得分:3)
我不太了解您提供的树图,因为(与链表图不同)它们似乎与实际存储的数据无关。在实践中,它们将具有基本相同的数据字段(缓冲区,开始和长度)加上一个,大小,即由节点为首的子树中的块的总大小。
代替上一个和下一个指针,它们将具有左和右(子)指针。当然,为了保持树的平衡,他们需要一些额外的数据(在红/黑树的情况下是红/黑位,但我不认为保持平衡的机制很重要例如,你可以使用AVL树而不是红/黑树。所以我将在这里忽略节点的那一部分。
大小字段是必要的,以便在给定的偏移量处找到数据(因此,如果从来没有需要进行这样的查找,则可以省略。)我认为链接的文章测量大小为块,而我倾向于以字符(甚至字节)来衡量大小,这就是我将在这里说明的内容。正如链接文章所指出的那样,Size字段可以很容易地保持在对数时间内,因为它指的是子树的大小,而不是它在数据流中的位置。
使用“大小”字段按缓冲区偏移量查找节点。如果偏移量小于左孩子的大小,则递归到左孩子;如果它至少是当前长度加上左子项的大小,则从偏移量中减去该总和并递归到正确的子项中。否则,当前节点包含所需的偏移量。这不能超过最大树深度,如果树是合理平衡的,则为O(log N)。
我对你的链表图表感到有点困惑,在我看来它代表缓冲区He|!\0|y
,而我希望它是He|y|!\0
:
------------------ ------------------- -------------------
Buffer = ORIGINAL| |Buffer = APPEND | |Buffer = ORIGINAL|
Start = 0 |<--|Start = 0 |<--|Start = 5 |
Length = 2 | |Length = 1 | |Length = 2 |
Next = 0x01 |-->|Next = 0x02 |-->|Next = NULL |
Previous = NULL | |Previous = 0x01 | |Previous = 0x01 |
------------------ ------------------- -------------------
等效的平衡树是:
-------------------
| Size = 5 |
| Buffer = APPEND |
| Start = 0 |
| Length = 1 |
-------------------
/ \
/ \
/ \
------------------- -------------------
|Size = 2 | |Size = 2 |
|Buffer = ORIGINAL| |Buffer = ORIGINAL|
|Start = 0 | |Start = 5 |
|Length = 2 | |Length = 2 |
------------------- -------------------
/ \ / \
/ \ / \
NULL NULL NULL NULL
从给定节点按顺序查找下一个节点的算法如下:
当右子指针为NULL时,返回父级。继续移动到父节点,直到找到一个节点,其右子节点既不是NULL也不是从子节点返回的。
转移到合适的孩子。
当左子指针不为NULL时,移动到左子项
该算法的给定应用程序显然可以采用步骤1和/或3的O(log N)次迭代。但是,重复的应用程序(如通过多个节点按顺序遍历缓冲区的情况)将是线性总计因为任何给定的链接(父⇆子)将被遍历两次,每个方向一次。因此,如果遍历整个树,则遍历的链接总数是树中链接数的两倍。 (并且树的节点数少于节点,因为它是一棵树。)
如果以字符为单位测量大小,则可以避免使用“长度”字段,因为节点直接引用的数据长度只是节点的子树大小与其子节点之和的差异。子树大小。这可以(几乎)将节点的大小减小到链表节点的大小,假设您可以找到一些方法来编码红/黑位(或其他平衡信息),否则将填充位。
另一方面,看到使用父指针的二叉树实现以及两个子指针有点常见。 (通过查看上面的遍历算法可以明白这是如何帮助的。)但是,没有必要存储父指针,因为它们可以在例如索引的父指针数组中的树的任何给定遍历期间被维护。树的深度。这个数组显然不大于最大树深度,因此可以使用一个小的(~50)固定长度数组。
这些优化也远远超出了这个答案。
答案 1 :(得分:0)
如果我有一个编辑器,并且删除/插入了一大块文本,我将如何遍历树以重新绘制缓冲区并正确反映此编辑?而且,这会如何比链表提供的O(n)时间复杂度更快?
假设片段表很大并且重新绘制屏幕上可见的缓冲区部分通常只需要访问几个连续的节点。 假设在特定编辑之后您需要访问的节点位于文档的中间或接近结尾。
使用双向链表,您可能需要从开始表中遍历许多节点才能进入编辑的开头。那是O(n)。从那里,你走过接下来的几个节点进行绘画。
使用平衡树,您可以在O(log_2 n)中找到第一个节点。从那里,您可以进行有序遍历,以访问绘制所需的下几个节点。
在添加,删除或修改片段后更新树中的位置只是从新/已修改节点的父节点开始添加/减去祖先位置的值。那也是O(log_2 n)。