如何在C++中实现Rope数据结构
我正在尝试从头实现Rope数据结构,但在节点权重和大小应该如何处理方面遇到了一个概念性的问题。
据我所知,Rope的每个内部节点会存储一个 weight,它等于左子树的总长度。然而,我对在构建过程中如何确定较大子树的总大小感到困惑。
例如,考虑我有四个叶子节点:
- F1 = 长度10
- F2 = 长度10
- F3 = 长度10
- F4 = 长度10
现在我构建一些中间节点:
- NA1 = concat(F1, F2) = F1 → 权重 = 10
- NA2 = concat(F3, F4) = F3 → 权重 = 10
到目前为止,这很直观。
但当我尝试构建一个这样的节点时:
- NB1 = concat(NA1, NA2) → 权重 = 20? 难道NA1的权重不是10吗?
这个节点在逻辑上表示一个长度为20的字符串。然而,NA1(其实只是F1)只有权重10。
我的困惑是:
- NB1如何“知道”它的左子树的总大小是10,完整大小是20?
- 我应该在每次创建新节点时递归地计算子树大小吗?
- 还是每个节点都应显式地存储其子树的总大小(例如使用额外的
size字段)?
换句话说, Rope节点的总大小应该是:
-
隐式的(需要时再递归计算),还是
-
显式存储并维护的(例如,与
weight一起)?
如果是隐式的,在诸如拼接和分割这样的操作中,如何保持高效?
如果是显式的,这仍然算是一种“正确”的Rope实现吗?
我感觉在树中大小信息的传播上有一些基本点我还没理解清楚。
任何澄清(或最小实现示例)都将非常有帮助。
我是在用C++来做这件事,如果你知道能帮助初学者创建Rope的文档,那就太好了。
解决方案
我应该在每次创建新节点时递归地计算子树大小吗?
请注意,你并不需要花很多功夫来计算子树大小;如果你已经掌握了左子节点(n) 和右子节点(n) 的所有权重,那么weight(n) 只是weight(left-child(n)) +左子节点(n) 的右侧后代的权重之和,因此
weight(left-child(n)) + weight(right-child(left-child(n))) + weight(right-child(right-child(left-child(n)))) … 依此类推,因为这是左侧树中唯一一条不再对左子节点的权重作出贡献的路径。这只是对数级的操作(前提是树是平衡的,你在修改它时需要确保这一点,否则效率就会大打折扣),而且没有任何Rope修改操作的最坏情况低于这点。因此,从正确性角度来看,即实现时间复杂度正确的意义上,按需计算树的一部分的总长度或将其存储起来,其实并没有区别。如果你确实决定存储它,那么在执行操作时你仍然只需要更新一条“路径”,也就是相同数量的操作次数。所以你可以二者择一。
这可能会根据你所处的具体情况产生差异,例如在某些情况下访问子节点比读取父节点的字段慢得多;在其他情况下,保存在每个节点中的这类信息所需的额外存储空间也可能成为影响因素。
顺便说一句,这些都与C++无关,无论你选择哪种编程语言,情况都是一样的(这也是数据结构的美妙之处)。