【发布时间】:2012-07-05 09:01:00
【问题描述】:
我一直在实现一个 LLRB 包,它应该能够在两种模式下运行,Bottom-Up 2-3 或 Top-Down 2-3-4 described by Sedgewick(code - 改进了代码,不过仅处理 2-3 棵树 here,感谢 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以上和here)。
我很难弄清楚他所说的“当搜索路径之外的正确节点是 4 节点时额外旋转”是什么意思;大概这是向左旋转,但在何时何地?
如果我在调用 fixUp() 之前或在 fixUp 函数结束时向上通过 4 节点等效项(即 RR 节点)或右倾斜 3 节点等效项(BR 节点),我仍然会得到相同的不变矛盾。
这是我发现的最小失败示例的树状态(由从 0 到相应最大值的元素顺序插入生成)。
第一对树显示了从删除元素 15 之前的不变一致性状态到之后的明显破坏状态的转换。
第二个与上面基本相同,但删除了 0..16 中的 16(删除 15 会导致相同的拓扑)。请注意,不变矛盾设法跨越根节点。
关键是要了解如何将在遍历树期间生成的违规恢复到目标节点。以下两棵树分别显示了上面的第一棵树在分别向左和向右走之后的样子(没有删除,在使用 fixUp() 向上走之前)。
尝试在不进行修复的情况下删除“-1”后:
在尝试删除“16”而不进行修复后:
当节点只有一个红色右子节点时尝试向左旋转似乎是解决方案的一部分,但它不能正确处理连续两个红色右子节点,在这两个红色右子节点之前使用 FlipColor children are red 似乎进一步改善了这种情况,但仍然违反了一些不变量。
如果我进一步检查右孩子的右孩子在其兄弟姐妹为黑色时是否为红色,如果这是真的,我只失败一次,但此时我觉得我需要一个新理论而不是一个新的本轮。
有什么想法吗?
作为参考,我的实现是可用的 here(不,它不是 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
【问题讨论】:
-
“这个问题显示了研究工作。” - 嗯...检查...
-
我在各种 RBT 实现中发现了多个可以在线找到的错误(例如,在 Thomas Niemann 的“排序和搜索算法”中,这是一个介绍文本 + C 代码)。遗憾的是,我不记得所有细节,除了 Cormen/Leiserson/Rivest/Stein 的著名著作“算法简介”中提供的错误参考伪代码(也用于 Niemann 的代码)。详情请见this answer。
-
我同意,有很多糟糕/草率的代码描述了这一点。
-
看来我的旧 RBT 实现在删除路径中确实有一个额外的块。我什至有这样的评论:“修复已删除节点的子节点为 NIL 的情况(这在其他几个 RB 树文档和源代码中缺失,这个错误的最终结果是红色- 树中所有终端节点的黑色高度不一样,IOW 对数高度功能被破坏”。不能多说,需要刷新我的 RBT 知识并研究代码。
-
Sedgewick 的 deleteMin 实现是错误的。尝试插入 3 个节点,得到一棵高度为 1 的树。之后,尝试调用 deleteMin 两次,之后会得到一棵空树,但是第三个元素呢?
标签: java algorithm data-structures red-black-tree 2-3-4-tree