红黑树原理

     
      1.每个节点要么是红色,要么是黑色
      2.根节点永远是黑色的
      3.所有的叶节点都是是黑色的(注意这里说叶子节点其实是上图中的 NIL 节点)
      4.每个红色节点的两个子节点一定都是黑色
      5.从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点

      性质 3 中指定红黑树的每个叶子节点都是空节点,而且并叶子节点都是黑色。但 Java 实现的红黑树将使用 null 来代表空节点,因此遍历红黑树时将看不到黑色的叶子节点,反而看到每个叶子节点都是红色的。
      性质 4 的意思是:从每个根到节点的路径上不会有两个连续的红色节点,但黑色节点是可以连续的。
      因此若给定黑色节点的个数 N,最短路径的情况是连续的 N 个黑色,树的高度为 N - 1;最长路径的情况为节点红黑相间,树的高度为 2(N - 1) 。
      性质 5 是成为红黑树最主要的条件,后序的插入、删除操作都是为了遵守这个规定。
      红黑树并不是标准平衡二叉树,它以性质 5 作为一种平衡方法,使自己的性能得到了提升。




  1. 黑色高度
      从根节点到叶节点的路径上黑色节点的个数




  1. 红黑树的左旋右旋
      左右旋的目的是调整红黑节点结构,转移黑色节点位置,使其在进行插入、删除后仍能保持红黑树的 5 条性质
      比如 x 左旋(右图转成左图)的结果,是让在 y 左子树的黑色节点跑到 X 右子树去
     



  1. TreeMap 指定节点 x 的左旋(右图转成左图)
      x 节点的左旋就是把 x 变成 右孩子 y 的左孩子,同时把 y 的左孩子送给 x 当右子树
      简单点记就是:左旋把右子树里的一个节点(上图 β)移动到了左子树

//这里 p 代表 x
private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> r = p.right; // p 是上图中的 x,r 就是 y
        p.right = r.left;      // 左旋后,x 的右子树变成了 y 的左子树 β
        if (r.left != null)       
            r.left.parent = p;  //β 确认父亲为 x
        r.parent = p.parent;        //y 取代 x 的第一步:认 x 的父亲为爹
        if (p.parent == null)      //要是 x 没有父亲,那 y 就是最老的根节点
            root = r;
        else if (p.parent.left == p) //如果 x 有父亲并且是它父亲的左孩子,x 的父亲现在认 y 为左孩子,不要 x 了
            p.parent.left = r;
        else                            //如果 x 是父亲的右孩子,父亲就认 y 为右孩子,抛弃 x
            p.parent.right = r;
        r.left = p;    //y 逆袭成功,以前的爸爸 x 现在成了它的左孩子
        p.parent = r;
    }
}




  1. TreeMap 指定节点 y 的右旋(左图转右图)
      y 节点的右旋就是把 y 变成 左孩子 x 的右孩子,同时把 x 的右孩子送给 y 当左子树
      简单点记就是:右旋把左子树里的一个节点(上图 β)移动到了右子树

private void rotateRight(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> l = p.left;
        p.left = l.right;
        if (l.right != null) l.right.parent = p;
        l.parent = p.parent;
        if (p.parent == null)
            root = l;
        else if (p.parent.right == p)
            p.parent.right = l;
        else p.parent.left = l;
        l.right = p;
        p.parent = l;
    }
}




  1. 红黑树的平衡插入
     
      首先和二叉查找树的插入一样,查找、插入
      然后调整结构,保证满足红黑树状态
           对结点进行重新着色
           以及对树进行相关的旋转操作

      红黑树的插入在二叉查找树插入的基础上,为了重新恢复平衡,继续做了插入修复操作

      红黑树的第 5 条特征规定,任一节点到它子树的每个叶子节点的路径中都包含同样数量的黑节点。也就是说当我们往红黑树中插入一个黑色节点时,会违背这条特征。
      同时第 4 条特征规定红色节点的左右孩子一定都是黑色节点,当我们给一个红色节点下插入一个红色节点时,会违背这条特征。
      因此我们需要在插入黑色节点后进行结构调整,保证红黑树始终满足这 5 条特征。

      插入一个节点后要担心违反特征 4 和 5,数学里最常用的一个解题技巧就是把多个未知数化解成一个未知数。我们这里采用同样的技巧,把插入的节点直接染成红色,这样就不会影响特征 5,只要专心调整满足特征 4 就好了。这样比同时满足 4、5 要简单一些。
      染成红色后,我们只要关心父节点是否为红,如果是红的,就要把父节点进行变化,让父节点变成黑色,或者换一个黑色节点当父亲,这些操作的同时不能影响 不同路径上的黑色节点数一致的规则。

      插入后我们主要关注插入节点的父亲节点的位置,而父亲节点位于左子树或者右子树的操作是相对称的,这里我们只介绍一种,即插入位置的父亲节点为左子树。

     插入、染红后的调整有 2 种情况:
          情况1:父节点和叔叔节点都是红色
                假设插入的是节点 N,这时父亲节点 P 和叔叔节点 U 都是红色,爷爷节点 G 一定是黑色。
                红色节点的孩子不能是红色,这时不管 N 是 P 的左孩子还是右孩子,只要同时把 P 和 U 染成黑色,G 染成红色即可。这样这个子树左右两边黑色个数一致,也满足特征 4。
                但是这样改变后 G 染成红色,G 的父亲如果是红色又违反特征 4。
                需要以 爷爷节点 G 为新的调整节点,再次进行调整操作,以此循环,直到父亲节点不是红的。
               
          情况2:父节点为红色,叔叔节点为黑色
                假设插入的是节点 N,这时父亲节点 P 是红色,叔叔节点 U 是黑色,爷爷节点 G 一定是黑色。
                红色节点的孩子不能是红色,但是直接把父亲节点 P 涂成黑色也不行,这条路径多了个黑色节点。
                通过把 爷爷节点 G 右旋,P 变成了这个子树的根节点,G 变成了 P 的右子树。
                右旋后 G 跑到了右子树上,这时把 P 变成黑的,多了一个黑节点,再把 G 变成红的,就平衡了。
               
                上面是插入节点 N 在父亲节点 P 的左孩子位置,如果 N 是 P 的右孩子,就需要多进行一次左旋,把情况化解成上述情况。
                N 位于 P 的右孩子位置,将 P 左旋,就化解成上述情况了。
               
      TreeMap 在插入后进行调整的代码

private void fixAfterInsertion(Entry<K,V> x) {
    x.color = RED;  //直接染成红色,少点麻烦

    //这里分析的都是父亲节点为红色的情况,不是红色就不用调整了
    while (x != null && x != root && x.parent.color == RED) {
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { // 插入节点 x 的父亲节点位于左孩子   
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));  // y 是 x 的叔叔节点
            if (colorOf(y) == RED) {    //如果 y 也是红色,只要把父亲节点和 y 都变成黑色,爷爷节点变成红的,就 Ok 了
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {    //如果叔叔节点 y 不是红色,就需要右旋,让父亲节点变成根节点,爷爷节点去右子树去,然后把父亲节点变成黑色、爷爷节点变成红色
                    //特殊情况:x 是父亲节点的右孩子,需要对父亲节点进行左旋,把 x 移动到左子树
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x)));
            }
        } else {    //和上面对称的操作
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    root.color = BLACK;
}





  1. 红黑树的平衡删除
     
     红黑树的删除也是分两步
           二叉查找树的删除
           结构调整

      根据红黑树的第 5 个特性:
           如果当前待删除节点是红色的,它被删除之后对当前树的特性不会造成任何破坏影响。
           而如果被删除的节点是黑色的,这就需要进行进一步的调整来保证后续的树结构满足要求。

      为了保证删除节点父亲节点左右两边黑色节点数一致,需要重点关注父亲节点没删除的那一边节点是不是黑色。如果删除后父亲节点另一边比删除的一边黑色节点多,就要想办法搞到平衡,具体的平衡方法有如下几种方法:
           把父亲节点另一边(即删除节点的兄弟树)其中一个节点弄成红色,也少一个黑色
           或者把另一边多的黑色节点转过来一个

      删除节点在父亲节点的左子树还是右子树,调整方式都是对称的,以当前节点为父节点的左孩子为例进行分析:
      删除后的调整分三步:
          第一步:
                兄弟如果是红的,说明孩子都是黑的 【旋转的情况 1 】
                     把兄弟搞成黑的
                     父亲搞成红的
                     左旋转父亲(兄弟给我分一个黑孩子)
                     接下来对比旋转后的兄弟
               目的是将兄弟节点变成黑的,转变成第二步两种情形中的某一种情况, 在做后续变化前,这棵树还是保持着原来的平衡。

          第二步:有两种情况
               情况1:兄弟节点的孩子都是黑色的
                     把兄弟搞成红的
                     continue 下一波(这个子树搞完了,研究父亲节点,去搞上一级树,进入第三步)
                将兄弟节点变成红色后,从它的父节点到下面的所有路径就都统一少了 1 个,同时也不影响别的特征,但是把兄弟节点变红后,如果有父亲节点也是红的,就可能违反红黑树的特征 4,因此需要到更高一级树进行鉴别、调整。
               
               情况2:兄弟节点的孩子至多是有一个是黑色的
                     把不是黑的那个孩子搞黑 【旋转的情况 2 】
                          兄弟搞红
                          兄弟右旋转
                          以后对比旋转后的兄弟
                     把兄弟涂成跟父亲一样的颜色 【旋转的情况 3 】
                     然后把父亲搞黑
                     把兄弟的右孩子搞黑
                     父亲节点左旋
                     研究根节点,进入第三步
                旋转的情况 2 是将兄弟节点的左右孩子都移动到右边,方便后续操作
               
                旋转的情况 3 将兄弟的孩子移到左边来,同时黑色的父亲变到了左边(让左边多些黑色节点)
               
          第三步:
                如果研究的不是根节点并且是黑的,重新进入第一步,研究上一级树;
                如果研究的是根节点或者这个节点不是黑的,就退出
                     把研究的这个节点涂成黑的。
                第三步中选择根节点为结束标志,是因为在第二步中,有可能出现我们正好给删除黑色节点的子树补上了一个黑色节点,同时不影响其他子树,这时我们的调整已经完成,可以直接设置调整节点 x = root,等于宣告调整结束。
                因为我们当前调整的可能只是一棵树中间的子树,这里头的节点可能还有父节点,这么一直往上到根节点。当前子树少了一个黑色节点,要保证整体合格还是不够的。
                这里需要在代码里有一个保证。假设这里 B 已经是红色的了。那么调整结束,最后对 B 节点,也就是调整目标 x 所指向的这个节点涂成黑色。这样保证前面亏的那一个黑色节点就补回来了。
                前面讨论的这4种情况是在当前节点是父节点的左子节点的条件下进行的。如果当前节点是父节点的右子节点,则可以对应的做对称的操作处理,过程也是一样的。
                其中具体旋转方向根据调整节点在父节点的左/右位置决定。

     TreeMap 代码

private void fixAfterDeletion(Entry<K,V> x) {
    while (x != root && colorOf(x) == BLACK) {
        if (x == leftOf(parentOf(x))) {
            Entry<K,V> sib = rightOf(parentOf(x));

            //左旋,把黑色节点移到左边一个
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateLeft(parentOf(x));
                sib = rightOf(parentOf(x));
            }

            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                if (colorOf(rightOf(sib)) == BLACK) {
                    setColor(leftOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateRight(sib);
                    sib = rightOf(parentOf(x));
                }
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(rightOf(sib), BLACK);
                rotateLeft(parentOf(x));
                x = root;
            }
        } else { //处理的节点在 右边,相同逻辑,只不过旋转的方向相反
            Entry<K,V> sib = leftOf(parentOf(x));

            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateRight(parentOf(x));
                sib = leftOf(parentOf(x));
            }

            if (colorOf(rightOf(sib)) == BLACK &&
                colorOf(leftOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                if (colorOf(leftOf(sib)) == BLACK) {
                    setColor(rightOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateLeft(sib);
                    sib = leftOf(parentOf(x));
                }
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(leftOf(sib), BLACK);
                rotateRight(parentOf(x));
                x = root;
            }
        }
    }

    setColor(x, BLACK);
}

      当调整的节点属于父亲节点的左子树时,调整方法对应的流程图如下:
     
         
     当调整的节点属于父亲节点的右子树时,调整方法也类似,旋转的方向相对称。
     这里列出删除后调整的全部逻辑流程图
     

红黑树并不是真正的平衡二叉树,但在实际应用中,红黑树的统计性能要高于平衡二叉树,但极端性能略差。

红黑树的插入、删除调整逻辑比较复杂,但最终目的是满足红黑树的 5 个特性,尤其是 4 和 5。

在插入调整时为了简化操作我们直接把插入的节点涂成红色,这样只要保证插入节点的父节点不是红色就可以了。

而在删除后的调整中,针对删除黑色节点,所在子树缺少一个节点,需要进行弥补或者对别人造成一个黑色节点的伤害。具体调整方法取决于兄弟节点所在子树的情况。

红黑树有其特殊的平衡规则,而为了维持平衡,根据邻树的状况进行旋转或者涂色。

红黑树的有序、快速特性在很多场景下都有用到,比如 Java 集合框架的 TreeMap, TreeSet 等。











































































































展开阅读全文

没有更多推荐了,返回首页