我一直在实现一个 LLRB 包,它应该能够在Sedgewick 描述的自下而上 2-3 或自上而下 2-3-4 两种模式中的任何一种模式下运行(代码- 改进的代码,虽然只处理 2-这里有3 棵树,感谢 RS 的指针)。
Sedgewick 对 2-3 模式的树操作提供了非常清晰的描述,尽管他花了很多时间谈论 2-3-4 模式。他还展示了在插入过程中对颜色翻转顺序的简单更改如何改变树的行为(在 2-3-4 中向下拆分或在 2-3 向上拆分):
private Node insert(Node h, Key key, Value value)
{
if (h == null)
return new Node(key, value);
// Include this for 2-3-4 trees
if (isRed(h.left) && isRed(h.right)) colorFlip(h);
int cmp = key.compareTo(h.key);
if (cmp == 0) h.val = value;
else if (cmp < 0) h.left = insert(h.left, key, value);
else h.right = insert(h.right, key, value);
if (isRed(h.right) && !isRed(h.left)) h = rotateLeft(h);
if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);
// Include this for 2-3 trees
if (isRed(h.left) && isRed(h.right)) colorFlip(h);
return h;
}
然而,他用以下内容掩盖了 2-3-4 LLRB 中的删除:
下一页的代码是 LLRB 2-3 树的 delete() 的完整实现。它基于在自顶向下的 2-3-4 树中插入方法的逆向:我们在沿着搜索路径向下的途中执行旋转和颜色翻转,以确保搜索不会在 2 节点上结束,这样我们就可以删除底部的节点。我们使用方法 fixUp() 在 insert() 代码中的递归调用之后共享颜色翻转和旋转的代码。使用 fixUp(),我们可以在搜索路径上留下右倾的红色链接和不平衡的 4 节点,确保这些条件在向上树的过程中得到修复。(该方法对 2-3-4 树也是有效的,但是当搜索路径之外的右节点是 4 节点时需要额外的旋转。)
他的 delete() 函数:
private Node delete(Node h, Key key)
{
if (key.compareTo(h.key) < 0)
{
if (!isRed(h.left) && !isRed(h.left.left))
h = moveRedLeft(h);
h.left = delete(h.left, key);
}
else
{
if (isRed(h.left))
h = rotateRight(h);
if (key.compareTo(h.key) == 0 && (h.right == null))
return null;
if (!isRed(h.right) && !isRed(h.right.left))
h = moveRedRight(h);
if (key.compareTo(h.key) == 0)
{
h.val = get(h.right, min(h.right).key);
h.key = min(h.right).key;
h.right = deleteMin(h.right);
}
else h.right = delete(h.right, key);
}
return fixUp(h);
}
我的实现为 2-3 树上的所有树操作正确维护 LLRB 2-3 不变量,但对于 2-3-4 树上的右侧删除子类失败(这些失败的删除导致右倾斜的红色节点,但滚雪球到树不平衡,最后是空指针取消引用)。从对讨论 LLRB 树并包括在任一模式下构建树的选项的示例代码的调查来看,似乎没有一个正确实现从 2-3-4 LLRB 中删除(即没有一个提到额外的旋转,例如 Sedgewick 的 java上面和这里)。
我很难弄清楚他所说的“当搜索路径之外的正确节点是 4 节点时额外旋转”是什么意思;大概这是向左旋转,但是在何时何地?
如果我在调用 fixUp() 之前或在 fixUp 函数结束时向左旋转通过 4 节点等效项(即 RR 节点)或右倾斜 3 节点等效项(BR 节点),我仍然会得到相同的不变矛盾.
这是我发现的最小失败示例的树状态(通过从 0 到相应最大值的顺序插入元素生成)。
第一对树显示了从删除元素 15 之前的不变一致性状态到之后的明显破坏状态的转换。
第二个与上面基本相同,但删除了 0..16 的 16 个(删除 15 个导致相同的拓扑)。请注意,不变矛盾设法跨越根节点。
关键是要了解如何将在树向下遍历到目标节点期间产生的违规恢复。以下两棵树分别显示了上面的第一棵树在分别向左和向右走之后的样子(没有删除,在使用 fixUp() 向上走之前)。
尝试在不使用 fixUp 的情况下删除“-1”后:
尝试在不使用 fixUp 的情况下删除“16”后:
当节点只有一个红色的右孩子时,尝试向左旋转似乎是解决方案的一部分,但它不能正确处理连续的两个红色右孩子,当两个孩子都是红色时,在此之前使用 FlipColor似乎进一步改善了这种情况,但仍然违反了一些不变量。
如果我进一步检查右孩子的右孩子在其兄弟姐妹为黑色时是否为红色,如果这是真的,我只失败一次,但此时我觉得我需要一个新的理论而不是一个新的本轮.
有任何想法吗?
作为参考,我的实现在这里可用(不,它不是 Java)。
跟进:
我对此感兴趣的部分原因是为了证实许多人声称 2-3 LLRB 树比 2-3-4 LLRB 树更有效。我的基准测试已经证实了插入和删除的这一点(2-3 大约快 9%),但我发现 2-3-4 树的检索速度要快得多。
以下时间在运行中具有代表性且一致:
BU23:
BenchmarkInsert 1000000 1546 ns/op
BenchmarkDelete 1000000 1974 ns/op
BenchmarkGet 5000000 770 ns/op
TD234:
BenchmarkInsert 1000000 1689 ns/op
BenchmarkDelete 1000000 2133 ns/op
BenchmarkGet 5000000 753 ns/op
第一列是工作台名称,第二列是操作数,第三列是结果。i5M 2.27 上的基准测试。
我已经查看了 2-3 树和 2-3-4 树的分支长度,其中几乎没有解释检索差异(从根到节点的平均距离和 1000 棵树的 SD,每棵树都有 10000 个随机插入):
Means:
TD234 leafs BU23 leafs
12.88940 12.84681
TD234 all BU23 all
11.79274 11.79163
StdDev:
TD234 leafs BU23 leafs
1.222458 1.257344
TD234 all BU23 all
1.874335 1.885204