红黑树插入学习

红黑树

红黑树主要规则

  • 根节点永远是黑色.
  • 新插入的红点默认永远是红色.
  • 空节点为黑色节点计算.
  • 任何一个节点只能是红色或者黑色.
  • 任何一条从根节点到叶子节点的路径内,不能包含两个相邻的红色节点.
  • 任何一条从根节点到叶子节点的路径内,包含相同的黑色节点(空节点为黑色节点计数).

网上大部分公认为5条规律,但其实更为官方的说明是4条. 但我认为6条更为清楚,其中大家忽略的是一条,所有新插入节点颜色为红色,会让你在只了解5条的情况下。不知道如何下手写代码。

In addition to the requirements imposed on a binary search tree the following must be satisfied by a red–black tree:[17]

Each node is either red or black.
All leaves (NIL in the figure 1) are considered black.
If a node is red, then both its children are black.
Every path from a given node to any of its descendant NIL nodes goes through the same number of black nodes.
Some authors, e.g. Cormen & al.,[17] claim "the root is black" as fifth property; but not Mehlhorn & Sanders.[18] or Sedgewick & Wayne[16]:432–447 Since the root can always be changed from red to black, this rule has little effect on analysis. This article also omits it, because it slightly disturbs the recursive algorithms and proofs.

以上信息来自维基百科.维基百科认为4条规则
https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal_of_a_black_non-root_leaf#Properties

在我们开始编码之前,我想先说明几点。

以上的6条规则,不需要强记,但对真实编码有帮助的是最后两条。直接看规则,会让人感觉无从下手。所以每个学习者,都需要根据几个切入点去理解规律的使用。

  • 任何一条从根节点到叶子节点的路径内,不能包含两个相邻的红色节点. 此规律用于解决插入数据的冲突检测. 插入操作仅需要检测此条规则既可. 检测到冲突到,根据父节点的兄弟节点来决定是重新染色,还是旋转节点。

    • 如果父节点兄弟节点为红色,进行染色操作。
    • 如果父节点兄弟节点为黑色,进行旋转操作。
      以上仅需要简单了解,会在后续流程中,详细说明。
  • 任何一条从根节点到叶子节点的路径内,包含相同的黑色节点(空节点为黑色节点计数).
    新删除节点,如果破坏了这条规则,则需要修复树的黑色节点平衡性,也基于此规则去重新平衡红黑色。

其他任何规则都是辅助规则,如根节点永远是黑色,在某些操作后,可能会破坏掉根节点是黑色这条特性,将新的根节点重新置为黑色即可。

插入新节点

我们以一个示例来更好的理解插入,以及红黑树自平衡过程

准备数据

[1,3,5,7,6,8,9,10]

开始插入数据

  • 插入元素:|1|

    1. 播放操作: 新元素始终为红色,因为根节点为空,根节点被赋值 ,根据规则1:根节点永远是黑色. 将根节点置为黑色。
    [xx] 包含的为黑色节点,|xx|包含的为红色节点
        [01]
        /  \
      [xx] [xx] 
    
    1. 冲突检测: 无
  • 插入元素:|3|

    1. 插入操作: 新元素始终为红色,因为根节点不为空,且元素大于根节点:[01],放置右侧。
    [xx] 包含的为黑色节点,|xx|包含的为红色节点
        [01]
        /  \
      [xx] |03|
    
    1. 检测检测: 发现没有相邻的两个红色节点,结束操作。
  • 插入元素:|5|

    1. 插入操作:新元素始终为红色,因为根节点不为空,且元素大于[01,03],放于元素|03|的右侧。
    [xx] 包含的为黑色节点,|xx|包含的为红色节点
          [01]      
          /  \      
      [xx]    |03|  
      /  \    /  \  
    [xx][xx][xx]|05|
    
    1. 平衡检测:检测是否有冲突,检测到相信两个红色节点:[05,03],根据插入的平衡修复规则.

      • 如果父节点兄弟节点为红色,进行染色操作。
      • 如果父节点兄弟节点为黑色,进行旋转操作。

    父节点兄弟节点为:[xx] 为黑色节点,执行旋转操作,这里提及一点,如果文章不说明,对哪个节点进行旋转,编码将中止,并且需要学习者再搜索其他文章。这是很多博客的问题。所以很难找到一篇博客根据描述,直接写出代码。本文将尝试将细节描述清楚,读者可以根据这些描述,直接进行编码。

    所有冲突检测的关键节点为,检测到冲突的节点,这里为红色节点|05|。但操作实际为父节点的父节点(Grand-parent):[01]. 以下简称为G节点,对节点G:[01],根据不同的节点分布,进行旋转。这里一定有四种不同的旋转分类。

      * left-left case 执行 rightRotate(Grand-parent)
      * left-right case 执行 leftRotate(parent) 然后执行 right(Grand-parent)
      * right-right rotation 执行 leftRotate(Grand-parent)
      * right-left rotattion 执行 rightRotate(parent) 然后执行 leftRotate(Grand-parent)
    

    具体的旋转这里不进行过多描述,避免本章内容过多。这里明显是right-right类型,所以我们执行leftRotate(Grand-parent).

       |03|  
       /  \  
     [01]|05|     
    

    旋转后,有一个固定的对新的根节点重新染色的过程。通过任何旋转操作完后平衡性保障后,都需要确保,新的父级节点为黑色,左侧俩个孩子为红色。

      [03]  
      /  \  
    |01||05|  
    

    以上解决了所有冲突,此处需要明确,并不是检测一次,而是解决完当前节点后,仍然需要递归向上,检测所有节点是否存在相邻红色节点。并解决所有相邻红色节点冲突。

  • 插入元素:|7|

    1. 插入操作:新元素始终为红色,因为根节点不为空,且元素大于{03,05},放于元素|05|的右侧。
            [03]      
            /  \      
        |01|    |05|  
        /  \    /  \  
      [xx][xx][xx]|07|
    
    1. 冲突检测: 检测到新插入节点与父级节点:[07,05] 为两个相邻的红色节点,其父级节兄弟节点:|01| 为红色,根据规则2,父级兄弟节点为红色,执行染色操作。根据上面提过的隐藏的操作条件,所以规则操作节点都是G节点,对G进行重新染色操作。

    确保染色后,父节点为红色,两个子节点为黑色,但需要确认操作的节点是不是树的根节点,如果是的话,会与另一条规则:根节点始终为黑色,起冲突,所以我们将根节点:[03]置为黑色。

            [03]      
            /  \      
        [01]    [05]  
        /  \    /  \  
      [xx][xx][xx]|07|
    

    以上,解决了所有冲突。

  • 插入元素:|6|

    1. 插入操作:新元素始终为红色,因为根节点不为空,且元素大于[03,05],放于元素[05]的左侧。
            [03]      
            /  \      
        [01]    [05]  
        /  \    /  \  
      [xx][xx]|06||07|
    
    1. 冲突检测,没有相邻的两个红色节点。
  • 插入元素:|8|

    1. 插入操作:新元素始终为红色,因为根节点不为空,且元素大于[03,05,07],放于元素|07|的右侧。
                  [03]              
                  /  \              
          [01]            [06]      
          /  \            /  \      
      [xx]    [xx]    |05|    |07|  
      /  \    /  \    /  \    /  \  
    [xx][xx][xx][xx][xx][xx][xx]|08|
    
    1. 冲突检测,检测到两个相邻的红色节点:[08,07], 其父亲兄弟节点为红色节点:|05|,执行重新染色操作。
                      [03]              
                      /  \              
              [01]            |06|      
              /  \            /  \      
          [xx]    [xx]    [05]    [07]  
          /  \    /  \    /  \    /  \  
        [xx][xx][xx][xx][xx][xx][xx]|08|
    

    染色后,父节点始终为红色,两个子节点为黑色。再往上检测,没有发现任何冲突,结束操作。

  • 插入元素:|9|

    1. 插入操作:新元素始终为红色,因为根节点不为空,且元素大于{03,05,07,08},放于元素|08|的右侧。
                                      [03]                              
                                      /  \                              
                      [01]                            |06|              
                      /  \                            /  \              
              [xx]            [xx]            [05]            [07]      
              /  \            /  \            /  \            /  \      
          [xx]    [xx]    [xx]    [xx]    [xx]    [xx]    [xx]    |08|  
          /  \    /  \    /  \    /  \    /  \    /  \    /  \    /  \  
        [xx][xx][xx][xx][xx][xx][xx][xx][xx][xx][xx][xx][xx][xx][xx]|09|
    
    1. 冲突检测,检测到两个相邻的红色节点:[09,08], 其父亲兄弟节点为黑色节点:[xx],执行节点旋转操作。

    根据以上提过的操作节点为G节点:[07], 且应对旋转模型:right-right. 对G节点执行左旋操作。

                    [03]              
                    /  \              
            [01]            |06|      
            /  \            /  \      
        [xx]    [xx]    [05]    |08|  
        /  \    /  \    /  \    /  \  
      [xx][xx][xx][xx][xx][xx][07]|09|
    

    根据上面提及的规则,旋转后确保父节点为红色,两个子节点始终为黑色。

                    [03]              
                    /  \              
            [01]            |06|      
            /  \            /  \      
        [xx]    [xx]    [05]    [08]  
        /  \    /  \    /  \    /  \  
      [xx][xx][xx][xx][xx][xx]|07||09|
    

    冲突解决完毕,向上继续检测没有其他问题,结束操作。

  • 插入元素:|10|

    1. 插入操作:新元素始终为红色,因为根节点不为空,且元素大于[03,06,08,09],放于元素|09|的右侧。
                                   [03]                              
                                   /  \                              
                   [01]                            |06|              
                   /  \                            /  \              
           [xx]            [xx]            [05]            [08]      
           /  \            /  \            /  \            /  \      
       [xx]    [xx]    [xx]    [xx]    [xx]    [xx]    |07|    |09|  
       /  \    /  \    /  \    /  \    /  \    /  \    /  \    /  \  
     [xx][xx][xx][xx][xx][xx][xx][xx][xx][xx][xx][xx][xx][xx][xx]|10|
    
    1. 冲突检测,检测到两个相邻的红色节点:[10,09], 其父亲兄弟节点为红色节点:|07|,对节点G:[08]进行染色操作。

    染色后父节点为红色,两个子节点为黑色。

                                   [03]                              
                                   /  \                              
                   [01]                            |06|              
                   /  \                            /  \              
           [xx]            [xx]            [05]            |08|      
           /  \            /  \            /  \            /  \      
       [xx]    [xx]    [xx]    [xx]    [xx]    [xx]    [07]    [09]  
       /  \    /  \    /  \    /  \    /  \    /  \    /  \    /  \  
     [xx][xx][xx][xx][xx][xx][xx][xx][xx][xx][xx][xx][xx][xx][xx]|10|
    

    向上继续检测,检测到两个相邻的红色节点:[08,06], 其父亲兄弟节点为黑色节点[01],根据规则,对节点G:[03]进行左旋操作.

                   |06|              
                   /  \              
           [03]            |08|      
           /  \            /  \      
       [01]    [05]    [07]    [09]  
       /  \    /  \    /  \    /  \  
     [xx][xx][xx][xx][xx][xx][xx]|10|
    

    旋转后确保新的根节点为黑色,两个子节点为红色。这里,根节点更新为新的节点:06,且根节点需要始终为黑色,将根节点|06|置为黑色。

                   [06]              
                   /  \              
           |03|            |08|      
           /  \            /  \      
       [01]    [05]    [07]    [09]  
       /  \    /  \    /  \    /  \  
     [xx][xx][xx][xx][xx][xx][xx]|10|
    

    Okay, 解决了所有冲突。

以上是使用测试数据:[1,3,5,7,6,8,9,10] 插入的全部过程。根据这些过程,了解如何解决红黑色的冲突。以及应用红黑树的几个常用规则。

操作检测

我们需要使用严格的自检测机制来确保所有操作的正确性,节点关系的正确性。

重新审视这几条规则,如果没有自检测机制,那么可能在真实编码中,任何一个节点,忘记染色,节点路径出现异常,都会导致大量的时间浪费。

  • 根节点永远是黑色.
  • 新插入的红点默认永远是红色.
  • 空节点为黑色节点计算.
  • 任何一个节点只能是红色或者黑色.
  • 任何一条从根节点到叶子节点的路径内,不能包含两个相邻的红色节点.
  • 任何一条从根节点到叶子节点的路径内,包含相同的黑色节点(空节点为黑色节点计数).

以此,我们需要有强大的异常检测机制,去辅助大家编写逻辑正确的代码。

  • 根节点永远是黑色
  • 任何一条从根节点到叶子节点的路径内,不能包含两个相邻的红色节点.
  • 任何一条从根节点到叶子节点的路径内,包含相同的黑色节点(空节点为黑色节点计数).

为此,我为以上三条规则编写了自检测逻辑,并附加了一条子父节点关系的检测逻辑。

@ForTest
private boolean validate(Node<E> node){
    if(null == root){
        return true;
    }
    //My own rule, The relationship between children and parent after rotation was always correct.
    //Which is really important to help me check my code after any operations.
    if(!validateNodeRelationShip(node)){
        throw new AssertionError("The relationship between each node are ruined!");
    }
    //Check the rule: The path from root-leaf have the same number of the black nodes.
    if(!validateBlackNode(node,0,0)){
        throw new AssertionError("The path from root-leaf have the different number of the black nodes.");
    }
    //Check the rule: the root node should always be black.
    if(!validateRootNode(node)){
        throw new AssertionError("The root node was not black.");
    }
    //Check the rule: There are no consecutive red nodes.
    if(!validateConsecutiveRedNode(node)){
        throw new AssertionError("There have two consecutive red node.");
    }
    return true;
}

 /**
 * Check if the relationship between the children and the parent is corrected
 * After insertion, we should always know wherever the rotation ruins the relationship or not.
 * @param node
 * @return
 */
@ForTest
private boolean validateNodeRelationShip(Node<E> node){
    if(null == node){
        return true;
    }
    if(null != node.left && node.left.parent != node ||
            null != node.right && node.right.parent != node){
        return false;
    }
    return validateNodeRelationShip(node.left) && validateNodeRelationShip(node.right);
}

/**
 * Validate the rule:
 * The root node was always black.
 * @param node
 * @return
 */
private boolean validateRootNode(Node<E> node){
    return node.color == BLACK;
}

/**
 * Validate black node
 * The rule:The path from root-leaf have the same number of the black nodes.
 * @param node
 */
private boolean validateBlackNode(Node<E> node,int preBlackSize,int blackSize) {
    if(null == node){
        //Check if another path has the same black nodes as this path.
        return preBlackSize == blackSize;
    }
    if(node.color == BLACK){
        blackSize++;
    }
    if(0 == preBlackSize && (null == node.left || null == node.right)){
        preBlackSize = blackSize;
    }
    return validateBlackNode(node.left,preBlackSize,blackSize) && validateBlackNode(node.right,preBlackSize,blackSize);
}

/**
 * Validate the rule:
 * No path can have two consecutive red nodes, But the path can have two consecutive black nodes.
 * @param node
 * @return
 */
private boolean validateConsecutiveRedNode(Node<E> node){
    if(null == node){
        return true;
    }
    if(node.color == RED && (null != node.left && node.left.color == RED||null != node.right && node.right.color == RED)){
        return false;
    }
    return validateConsecutiveRedNode(node.left) && validateConsecutiveRedNode(node.right);
}

以上,将测试代码放于添加逻辑后

@Override
public boolean add(@NonNull E e) {
    if(null == root){
        root = new Node<>(e);
        root.color = BLACK;
    } else {
        Node<E> newNode = addInternal(root, e);
        fixViolationAfterInsertion(newNode,e);
        //After inserting the new element. We check if all the rules are corrected.
        validate();
    }
    size++;
    return true;
}

测试插入逻辑是否正确

@Test
public void testInsert(){
    final Random random = new Random();
    //test 10 times, make sure all the operation is correct.
    for(int i=0;i<10;i++) {
        RedBlackTree<Integer> tree = new RedBlackTree<>();
        Deque<Integer> deque = new ArrayDeque<>();
        for (int i1 = 0; i1 < 1000; i1++) {
            int value = random.nextInt(100000);
            while (deque.contains(value)) {
                value = random.nextInt(100000);
            }
            deque.push(value);
            tree.add(value);
        }
        assert 1000 == tree.size();
    }
}

以上,红色树插入相关。

另请查阅:红黑树删除相关

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容

  • 红黑树删除 删除数据 红黑树,删除操作较为复杂,要确保逻辑的正确性,更为麻烦。所以我们仍然需要用自检测代码进行删除...
    章鱼脑袋阅读 379评论 0 0
  • 定义 一种含有红黑节点并能自平衡的二叉查找树(BST) 性质 1.每个节点有红/黑标记位 2.根节点是黑色(硬性规...
    KeyLiu7阅读 459评论 0 0
  • 一、背景引入 二叉排序树的性能取决于二叉树的层数,最好的情况O(log n)存在于完全二叉排序树的情况下,其访问性...
    Ferrari1001阅读 1,027评论 0 0
  • 介绍 红黑树是一个平衡的二叉树,但不是一个完美的平衡二叉树。虽然我们希望一个所有查找都能在~lgN次比较内结束,但...
    小陈阿飞阅读 1,214评论 0 3
  • 什么是红黑树 红黑树是带有着色性质的二叉查找树。 性质如下:① 每一个节点或者着成红色或者着成黑色。② 根节点为黑...
    sunpy阅读 1,903评论 1 0