【问题标题】:What additional rotation is required for deletion from a Top-Down 2-3-4 Left-leaning Red Black tree?从自上而下的 2-3-4 左倾红黑树中删除需要什么额外的旋转?
【发布时间】:2012-07-05 09:01:00
【问题描述】:

我一直在实现一个 LLRB 包,它应该能够在两种模式下运行,Bottom-Up 2-3 或 Top-Down 2-3-4 described by Sedgewickcode - 改进了代码,不过仅处理 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


【解决方案1】:

更新和验证

对此进行测试的关键是该实现不支持删除不存在或以前删除的节点!我花了太长时间试图弄清楚为什么我的工作解决方案被“破坏”了。这可以通过对键进行初步搜索来解决,如果它根本不在树中,则返回 false,并且在底部的链接代码中采用了该解决方案。

Sedgewick 似乎没有为 2-3-4 删除编写公开可用的删除。他的结果专门针对 2-3 树(他只是粗略地提到了 2-3-4 树,因为它们的平均路径长度(以及搜索成本)以及其他红黑树的平均路径长度与2-3 例)。其他人似乎也没有一个容易找到的,所以这是我在调试问题后发现的:

首先,获取 Sedgewick 的代码并修复过时的位。在幻灯片here(第 31 页)中,您可以看到他的代码仍然使用 4 个节点的旧表示,它是通过连续两个左红色而不是平衡来完成的。那么,编写 2-3-4 删除例程的第一步是修复这个问题,以便我们可以进行健全性检查,这将有助于我们稍后验证我们的修复:

private boolean is234(Node x)
{         
   if (x == null)
      return true;
   // Note the TD234 check is here because we also want this method to verify 2-3 trees
   if (isRed(x.right))
      return species == TD234 && isRed(x.left);

   if (!isRed(x.right))
      return true;

   return is234(x.left) && is234(x.right);
} 

一旦我们有了这个,我们就会知道一些事情。一,从论文中我们看到,当使用 2-3-4 树时,不应该在向上的过程中破坏 4 个节点。第二,搜索路径上有一个右 4 节点的特殊情况。还有第三种特殊情况没有提到,那就是当你倒退一棵树时,你可能会在h.right.left 变成红色的地方结束,这会让你只需要向左旋转就无效。这是本文第 4 页所描述的案例的镜子。

您需要的 4 节点的旋转修复如下:

    private Node moveRedLeft(Node h)
    {  // Assuming that h is red and both h.left and h.left.left
       // are black, make h.left or one of its children red.
       colorFlip(h);
       if (isRed(h.right.left))
       { 
          h.right = rotateRight(h.right);

          h = rotateLeft(h);
          colorFlip(h);

          if (isRed(h.right.right) )
             h.right = rotateLeft(h.right);
       }
      return h;
    }

这消除了 2-3-4 上的拆分,并添加了第三种特殊情况的修复

private Node fixUp(Node h)
{
   if (isRed(h.right))
   {      
      if (species == TD234 && isRed(h.right.left))
         h.right = rotateRight(h.right);
      h = rotateLeft(h);
   }

   if (isRed(h.left) && isRed(h.left.left))
      h = rotateRight(h);

   if (species == BU23 && isRed(h.left) && isRed(h.right))
      colorFlip(h);

   return setN(h);
}

最后,我们需要对此进行测试并确保它有效。它们不一定是最有效的,但正如我在调试过程中发现的那样,它们必须实际使用预期的树行为(即不插入/删除重复数据)!我用一个测试辅助方法做到了这一点。当我调试时,注释行就在那里,我会打破并检查树是否有明显的不平衡。我已经用 100000 个节点尝试过这种方法,它的表现完美无缺:

   public static boolean Test()
   {
      return Test(System.nanoTime());
   }
   public static boolean Test(long seed)
   {
      StdOut.println("Seeding test with: " + seed);
      Random r = new Random(seed);
      RedBlackBST<Integer, Integer> llrb = new RedBlackBST<Integer,Integer>(TD234);
      ArrayList<Integer> treeValues = new ArrayList<Integer>();
      for (int i = 0; i < 1000; i++)
      {
         int val = r.nextInt();
         if (!treeValues.contains(val))
         {
            treeValues.add(val);
            llrb.put(val, val);
         }
         else
            i--;
      }
      for (int i = 0; i < treeValues.size(); i++)
      {
         llrb.delete(treeValues.get(i));
         if (!llrb.check())
         {
            return false;
         }
//         StdDraw.clear(Color.GRAY);
//         llrb.draw(.95, .0025, .008);
      }
      return true;
   }

完整的源码可以在here找到。

【讨论】:

  • 恐怕这不能解决问题,实际上会破坏 2-3 模式(我怀疑你的模式测试倒置了,但改变它并不能解决问题)。跨度>
  • 已编辑,是的,我改变了模式。你看到什么情况不起作用,因为图像是我通过递归和备份(尽管通过deleteMax(),因为这就是delete(15) 等价的。
  • 现在设置一个 JDK,所以我应该能够运行一个快速测试套件并返回报告。
  • 有趣的是,作者在 is234 检查中自相矛盾。他的代码声称 234 树允许两个红色链接沿左侧树连续排列,而不是平衡。我通过将检查更改为 return x == null || (((isRed(x.left) &amp;&amp; isRed(x.right)) || !isRed(x.right)) &amp;&amp; is234(x.left) &amp;&amp; is234(x.right)); 来解决这个问题,现在编写测试生成套件。
  • 是的,原始jave中的校验码不正确。新链接的代码更好,但只处理 2-3 棵树。我相当确定我的测试是正确的,因此您可以通过查看论文和他的树形图来了解它们。当我醒来时我会重新运行测试并告诉你失败的地方。
猜你喜欢
  • 2012-11-01
  • 2021-05-22
  • 2020-09-19
  • 1970-01-01
  • 2016-06-27
  • 2013-11-11
  • 2023-03-03
  • 2013-05-02
  • 1970-01-01
相关资源
最近更新 更多