拒绝枯燥,用生动的例子带你学习红黑树

Posted 郭霖

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了拒绝枯燥,用生动的例子带你学习红黑树相关的知识,希望对你有一定的参考价值。



/   今日科技快讯   /


2019胡润百富榜于今日(10月10日)揭晓,今年百富榜前十位的上榜门槛比去年上升100亿元,达到1200亿元。马云家族以2750亿元财富位列第一,第三次成为中国首富;马化腾财富上涨200亿元,以2600亿元重返第二;许家印财富缩水400亿元,以2100亿元退居第三。


/   作者简介   /


本篇文章来自verzqlis的投稿,分享对红黑树的理解,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。


另外提醒下大家,今天虽然是周五了,明天还得要继续上班哦。


https://juejin.im/user/58363decd20309005fe84db3


/   前言   /


红黑树(Red Black Tree) 是一种自平衡二叉查找树,相对于普通的二叉树具有通过自旋和变色来保持树两端保持平衡的特点,从而获得较高的查找性能。


红黑树的最坏情况运行时间也是非常良好的,并且在实践中是高效的:它可以在O(log n)时间内做查找,插入和删除。


/   二叉查找树   /


在正式介绍红黑树前,先简要介绍下二叉查找树(BST),二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:


  • 若左子树不空,则左子树上所有节点的值均小于它的根节
  • 若右子树不空,则右子树上所有节点的值均大于它的根节点的值
  • 左、右子树也分别为二叉排序树
  • 没有键值相等的节点(这个看实际需求,非固定)


如下图就是一个典型的二叉查找树


拒绝枯燥,用生动的例子带你学习红黑树


那么这种树有什么好处呢,如同它的名字一般,它对查找非常便利,在上面数据中如果你想查找是否存在11,你只需要分别和10,15,13,11比较就可以得出,数据越多,它查找的优势就更体现出来。它查找所需的最大次数等同于二叉查找树的高度。但是它也有缺陷所在,当构成它的数据导致树两端不平衡时,查找性能就大打折扣了

拒绝枯燥,用生动的例子带你学习红黑树


当插入的数据是有序的时候,生成的二叉树就类似于一个链表,这种情况下查找时,就需要遍历全部数据


总结


  • 最好的情况是 O(logn):在数据符合完全二叉树类似情况下,其查找性能接近于二分查找,理想的树的高度为logN。
  • 最差时候会是 O(n):极端情况下比如插入的数据是有序的,生成的二叉查找树就是一个链表,这样树的高度就为N。


/   红黑树   /


基于上面提到BST存在的问题,一种新的树--平衡二叉树(Balanced BST)被提了出来。平衡二叉树在插入和删除的时候,会通过旋转操作将树的高度一直保持在logN。具有代表性的平衡二叉树有两种,分别为AVL树和红黑树,AVL树因为性能更差的缘故,在实际运用的情况下远不如红黑树。

红黑树(Red-Black Tree,以下简称RBTree)的实际应用非常广泛,常见的函数库,如C++ STL中,很多部分(包括set, multiset, map, multimap)应用了红黑树的变体,以及Java中的TreeMap,TreeSet, Java8中的HashMap的实现也将链表替换成了红黑树。

RBTree为了保持树严格的平衡性质,在原来BST的基础上添加了以下五点性质:

  • 根节点是黑色。
  • 树的任一节点是红色或黑色。
  • 每个红色节点的两个子节点都是黑色。
  • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
  • 空节点默认是黑色的

  
    
    
  
class TreeMapEntry{
    K key;
    V value;
    TreeMapEntry<K,V> left;
    TreeMapEntry<K,V> right;
    TreeMapEntry<K,V> parent;
    boolean color = BLACK;
}
RBTree节点的数据结构


RBTree在本质上还是一棵BST树,但是它在插入和删除数据的时候会通过变色和自旋来保持树的平衡,即保证树的高度在[logN,logN+1],将树的查找时间复杂度始终保持在logN,同时RBTree的插入和删除时间复杂度也都是logN,所以RBTree的查找接近于理想的BST。


红黑树的自旋


RBTree的自旋主要目的是为了让节点的颜色符合上面的性质,从而使树的高度达到平衡。RBTree的旋转分为左旋和右旋,左边子节点升到父节点位置为右旋,右边子节点升到父节点为左旋。


拒绝枯燥,用生动的例子带你学习红黑树


如上图中,当某个节点(100)左旋时,它自身往右降一级,左节点不变,右节点(150)断开然后连上右节点(150)的左节点(125),然后右节点(150)当家做主称为父节点,至此完成一个左旋

拒绝枯燥,用生动的例子带你学习红黑树


同理,当某个节点(100)右旋时,它自身往左降一级,右节点不变,左节点(50)断开然后连上左节点(50)的右节点(75),然后左节点(50)当家做主称为父节点,至此完成一个右旋


RBTree的自旋主要是因为插入或删除后节点的颜色不符合上述的五条性质,导致树整体不平衡,需要通过自旋对树进行降层保持树的平衡


Java的RBTree就是一个典型的红黑树例子,下面也就TreeMap的源码来对解析RBTree的插入和删除操作

RBTree的插入


每个新插入的节点都是红色的,如果插入的父节点是黑色的,那么操作结束。如果父节点是红色,那么则违反了规则3:每个红色节点的两个子节点都是黑色,则需要改变父类的颜色,如果父类颜色和祖父类冲突,那么就需要继续变色,甚至是自旋来使树节点颜色符合规则。

  
    
    
  
  public V put(K key, V value) {
        TreeMapEntry<K,V> t = root;
        // 如果当前没有数据,就用此点当做根节点
        if (t == null) {
            compare(key, key);
            root = new TreeMapEntry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        TreeMapEntry<K,V> parent;
        // 比较大小的方式,如果已经自定义过就用自己设定的,不然就用系统默认的比较方式
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        //找到插入的父节点,生成当前节点,然后根据大小放在左节点还是右节点
        TreeMapEntry<K,V> e = new TreeMapEntry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
       // 数据插入完成后开始对树进行颜色平衡处理
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }


fixAfterInsertion

  
    
    
  
 //这里对颜色的处理其实只看一半就行了,你会发现else后面的代码和上面是一样的,只不过 
 //左右做一下镜像处理
 private void fixAfterInsertion(TreeMapEntry<K,V> x) {
        //新添加的节点设为红色
        x.color = RED;
        /** **/
        因为规则3:每个红色节点的两个子节点都是黑色。新添加的节点都为红色,
        那么循环条件 要加上父类不为红色
        当新加入的点为根节点时也没必要循环了,直接最后面设置为黑色即可
        **/
        while (x != null && x != root && x.parent.color == RED) {
              //如果当前父节点为祖父节点的左节点
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
              //这个y是祖父节点的另一个节点,从关系上来说,就是添加节点的叔叔节点
                TreeMapEntry<K,V> y = rightOf(parentOf(parentOf(x)));
                /**
                如果叔叔节点为红色,那么同样因为规则3可知祖父节点一定为黑色
                此时把父节点设为黑色和叔叔节点 设为黑色,把祖父节点设为红色
                这么做的主要目的是为了符合规则4:从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点
                把祖父节点变成红色后,因为不知道祖父的父节点颜色,因为可能会违反规则3(祖父的父节点为红色),所以需要对祖父节点进行循环校验
                **/

                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    /*
                     * 如果叔叔节点为黑色,因为当前节点和父节点都为红色,祖父节点也一定为黑色
                     * 这种情况一定是经过上面那种情况变色后得出来的,因为叔叔和祖父是黑色,父亲节点是红色,
                     * 这种情况就违反了规则4:从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点
                     * 因为此时的叔叔节点比父节点多了一个黑色,这种情况只有可能是因为原来父节点是黑色的,
                     * 由于添加了新节点后因为上面的变换setColor(parentOf(parentOf(x)), RED);导致的
                     *
                     * 这种情况下单纯变色已经不管用了,只能通过自旋来平衡
                     * 先把父节点变黑,祖父节点变红
                     * 这时候通过右旋把父节点的右孩子变成祖父节点的左孩子,这时候达到颜色平衡,详情后面看动图例子
                     *
                     *
                     * 上面的操作是正常情况下的操作,但是在这个操作前需要先做一个判断
                     * 如果当前节点是父类的右孩子,那么需要对父节点进行左旋转
                     * 因为如果不做这个操作的话,由于当前节点是红色的,上面操作【右旋把父节点的右孩子变成祖父节点的左孩子】
                     * 也就是把当前节点给了祖父的左孩子,但是因为祖父节点已经被设为了红色,这样两个红色节点就违反了规则3,
                     * 所以如果当前节点是父类的右孩子时需要对父节点进行左旋,
                     * 左旋后当前节点变成了父节点,父节点变成了左孩子,红色的节点也移到了左边,移到祖父节点的也是黑色的,不会起冲突
                     * 这样,最多通过两次自旋就可以解决冲突
                     */

                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateLeft(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
                //这里就不赘述了,原理一模一样,只是方向相反罢了
                TreeMapEntry<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;
    }


实例


枯燥的讲解永远没有生动的例子有效,下面简单举几个例子

  • 单纯添加一个数据,不变色也不自旋


拒绝枯燥,用生动的例子带你学习红黑树


  • 添加一个数据后,进行变色


  
    
    
  
   while (x != null && x != root && x.parent.color == RED) {
   TreeMapEntry<K,V> y = rightOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));


拒绝枯燥,用生动的例子带你学习红黑树


通过结合代码可以看到添加新节点25后,因为父节点(50)是红色的,进入循环。又因为叔叔节点是红色的,所以将父节点和叔叔节点设为黑色,祖父节点设为红色,再将当前节点x设为祖父节点,因为x成了根节点,所以退出循环,在最后将根节点设为黑色


  • 添加一个数据后,进行变色和一次自旋,只经过了一次自旋,注释的代码并没有调用


  
    
    
  
  if (colorOf(y) == RED) {
      setColor(parentOf(x), BLACK);
      setColor(y, BLACK);
      setColor(parentOf(parentOf(x)), RED);
      x = parentOf(parentOf(x));
  } else {
  //  if (x == rightOf(parentOf(x))) {
  //      x = parentOf(x);
  //     rotateLeft(x);
  //  }
      setColor(parentOf(x), BLACK);
      setColor(parentOf(parentOf(x)), RED);
      rotateRight(parentOf(parentOf(x)));
  }


拒绝枯燥,用生动的例子带你学习红黑树


这里可以看到添加一个新数据5后,第一步因为父节点(10)和叔叔节点(26)为红色,所以做了一次变色,把父节点(10)和叔叔节点(26)变黑,祖父节点(25)变红,当前节点x变为祖父节点(25)

这里x(25)变红色后和父节点(50)颜色起了冲突。但是为什么这里不能继续通过变色来平衡呢,因为这里如果将25或者50节点中一个变为黑色后,那么最左侧这一条路径的黑色节点数量就比其他路径黑色节点数量多,所以这时候就需要进行自旋

所以将父节点(50)设为黑色,将祖父节点(100)设为红色,然后右旋,。右旋完成后,因为父节点(50)变成了黑色,退出了循环,平衡完成

  • 添加一个数据后,进行变色和两次自旋


  
    
    
  
    if (colorOf(y) == RED) {
        setColor(parentOf(x), BLACK);
        setColor(y, BLACK);
        setColor(parentOf(parentOf(x)), RED);
        x = parentOf(parentOf(x));
    } else {
        if (x == rightOf(parentOf(x))) {
            x = parentOf(x);
            rotateLeft(x);
        }
        setColor(parentOf(x), BLACK);
        setColor(parentOf(parentOf(x)), RED);
        rotateRight(parentOf(parentOf(x)));
    }


拒绝枯燥,用生动的例子带你学习红黑树


这里第一次变色就不说了,经过第一次变色后,当前节点X为75


经过变色后可以看到叔叔节点(150)为黑色,所以需要进行自旋。但是又因为x(75)是父节点(50)的右孩子,所以需要对父节点(50)进行左旋,同时将当前节点x设为50。

第一次左自旋后,当前节点x为50,然后将左旋后的父节点(75)设为黑色,祖父节点(100)设为红色,然后对祖父节点(100)进行右自旋,右自旋后当前节点的父节点为(75)为黑色,退出循环,平衡完成

  • 总结


学习红黑树的过程中最困惑的不是自旋和变色,而是自旋和变色的时机,钻了不少死胡同。这里总结一下(仅当是左边树情况下,右边树情况只要将左右颠倒即可):

1、当叔叔节点为红色时,将新加节点和父节点变色即可
2、当叔叔节点为黑色时,如果当前节点位于左节点,那么将父节点变黑,祖父节点变红,然后右旋即可。
3、当叔叔节点为黑色时,如果当前节点位于右节点,那么需要先以父节点左旋,然后/2操作即可

RBTree的删除


删除操作相对于插入多了一层复杂度,但还是有迹可循。

删除操作会先删除节点,如果是叶子结点就直接删除,如果非月子节点,会先遍历找到该点删除后的继承点。在删除节点后,需要做修复操作,使树重新达到颜色和高度平衡。

修复操作是只有在删除黑色节点时才有,因为删除黑色节点会违反规则4:从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点

需要做的处理是从兄弟节点上借调黑色的节点过来,如果兄弟节点没有黑节点可以借调的话,就只能往上追溯,将每一级的黑节点数减去一个,使得整棵树符合红黑树的定义。

删除操作的总体思想是从兄弟节点借调黑色节点使树保持局部的平衡,如果局部的平衡达到了,就看整体的树是否是平衡的,如果不平衡就接着向上追溯调整。

  
    
    
  
    private void deleteEntry(TreeMapEntry<K,V> p) {
        modCount++;
        size--;
      /**
       * 如果被删除的节点P非叶子节点,那么需要在删除他之前找到合适的点来继承这个节点位置
       */

        if (p.left != null && p.right != null) {
             //找到继承点后,将被删除点的值都赋给继承点,这里注意了,继承点s的关系赋值给了p            
             //变成了要删除的点
             //这个节点是找位于p点右子树的最小数节点
            TreeMapEntry<K,V> s = successor(p);
            p.key = s.key;
            p.value = s.value;
            p = s;
        } // p has 2 children

      /**
       * 如果进入了上面的循环,最后的p是位于删除节点右子树的最左端点 ,而且有且最多只有一 
       * 个右子节点,这时候这个``replacement``就是就是p.right
       * 
       * 如果没有经过上面的循环,那么被删除节点有且最多只有一个子节点
       * 而且这个唯一的子节点必然是叶子节点(因为如果该节点还有子节点就违反规则4了)
       * 这时候用这个叶子节点替换即可
       */

        TreeMapEntry<K,V> replacement = (p.left != null ? p.left : p.right);
        if (replacement != null) {
            replacement.parent = p.parent;
            if (p.parent == null)
                root = replacement;
            else if (p == p.parent.left)
                p.parent.left  = replacement;
            else
                p.parent.right = replacement;
            p.left = p.right = p.parent = null;
             /**
               * 这里两种可能
               * 1:如果该P有两孩子,那么他变成了找继承者出来的s,如果他是黑色节点,
               * 那么等于是删除节点后面缺了一个黑色s(拿去顶删除节点位置了),所以需要调整树
               * 
               * 2:如果该p没有两个孩子节点,那么他就是被删除的节点,删除了黑色节点就意味着有一边树少了一个黑色
               * 比另一边树整体路径就少了一个黑色节点,所以也需要调整树
               * 
               * 如果两种情况都是红色的话,对整体没有影响,所以不需要变色
               * 
               */

            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        } else if (p.parent == null) { // return if we are the only node.
            root = null;
        } else { //  No children. Use self as phantom replacement and unlink.
             /**
               * 如果被删除节点p没有孩子节点,如果p点是黑色,因为删除了黑色节点违反了规则4
               * 那么需要调整树,如果p点为红色,那么直接删除即可
               */

            if (p.color == BLACK)
                fixAfterDeletion(p);
            //颜色调整完毕后将p和其他节点断开联系,然后删除
            if (p.parent != null) {
                if (p == p.parent.left)
                    p.parent.left = null;
                else if (p == p.parent.right)
                    p.parent.right = null;
                p.parent = null;
            }
        }
    }


找寻删除节点后合适的继承点

  
    
    
  
   static <K,V> TreeMapEntry<K,V> successor(TreeMapEntry<K,V> t) {
        if (t == null)
            return null;
        else if (t.right != null) {
            TreeMapEntry<K,V> p = t.right;
            while (p.left != null)
                p = p.left;
            return p;
        } else {
            TreeMapEntry<K,V> p = t.parent;
            TreeMapEntry<K,V> ch = t;
            while (p != null && ch == p.right) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }


fixAfterDeletion

  
    
    
  
 //这里和前面一样,看一半逻辑就行了,另一半逻辑是镜像对称的
 private void fixAfterDeletion(TreeMapEntry<K,V> x) {
        while (x != root && colorOf(x) == BLACK) {
            if (x == leftOf(parentOf(x))) {
                TreeMapEntry<K,V> sib = rightOf(parentOf(x));
                    /**
                      * 这里和插入操作不一样的地方是着重判断兄弟节点的颜色,而不是叔叔的
                      * 如果兄弟节点是红色,那么因为自己这边删除了一个黑色,整体黑色就比兄弟那边少一个、
                      * 这时候需要将父节点变红,兄弟节点变黑,通过左旋从兄弟节点那边借一个黑色节点过来
                      */

                if (colorOf(sib) == RED) {
                    情况一
                    setColor(sib, BLACK);
                    setColor(parentOf(x), RED);
                    rotateLeft(parentOf(x));
                    sib = rightOf(parentOf(x));
                }
                 /**
                   * 这里注意,就算兄弟节点没有孩子节点,他的左右树连接的是null,null节点也是黑色的
                   * 这里的判断只是判断他是否有红色孩子节点
                   * 如果兄弟节点是红色的,经过上面步骤变化,这里兄弟节点变成了红色
                   */

                if (colorOf(leftOf(sib))  == BLACK &&
                    colorOf(rightOf(sib)) == BLACK) {
                    情况二
                    //在兄弟节点是黑色且两孩子都是黑色的情况下,需要将兄弟节点变红
                    //继续循环调整其父节点
                    setColor(sib, RED);
                    x = parentOf(x);
                } else {
                   /**
                    * 进入这个右孩子是黑色的判断,那么左孩子一定是红色(因为null节点也是黑色)
                    * 将兄弟节点变红,左孩子(红色)变黑,右旋将红色节点移到右子树
                    */

                    if (colorOf(rightOf(sib)) == BLACK) {
                    情况三
                        setColor(leftOf(sib), BLACK);
                        setColor(sib, RED);
                        rotateRight(sib);
                        //经过右旋后  找出新的叔叔节点
                        sib = rightOf(parentOf(x));
                    }
                    情况四
                /**
                 * 到了这一步,兄弟节点是黑色的,兄弟节点的左孩子是黑色的,右孩子是红色的
                 * 这时候删除节点后需要从兄弟节点那边先借节点过来
                 * 先把兄弟节点颜色赋值为父节点颜色,再把兄弟节点的右孩子和父节点变为黑色
                 * 因为左旋前父节点以前都是平衡的,兄弟节点左旋后替代父节点的位置就要和父节点颜色一致
                 * 然后兄弟节点的位置由兄弟节点的右孩子接替了,颜色要变成黑色,
                 * 左孩子,也就是左旋前的父节点也要变得和上面接替的右孩子一样变成黑色,这样就平衡了
                 * 最后设为root 退出循环,
                 */

                    setColor(sib, colorOf(parentOf(x)));
                    setColor(parentOf(x), BLACK);
                    setColor(rightOf(sib), BLACK);
                    rotateLeft(parentOf(x));
                    x = root;
                }
            } else { // symmetric
                TreeMapEntry<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);
    }


删除步骤


在演示实例前,先总结下删除操作的思想:找到删除节点后的的继承者,将继承者的值赋给被删除节点,这时候把删除节点变为继承者,被删除节点没有子节点就直接删除,然后维持树的平衡。主要分为两个步骤


第一步:将红黑树当作一颗二叉查找树,将节点删除。


1. 被删除的节点没有孩子节点,即叶子节点。可直接删除。
2. 被删除的节点只有一个孩子节点,那么直接删除该节点,然后用它的孩子节点顶替它的位置。这种情况下他不用找继承点,replacement为他的孩子节点。
3. 被删除的节点有两个孩子节点。这种情况下要找他的继承点successor。找到继承点后用继承点S替换被删除点X,replacement则为继承点S的左或右孩子节,若S没有孩子后续操作和操作1一样,如果有则后续操作和操作2一样,这样就回到了最初的问题。


第二步:通过自旋和重新填色等一系列操作来修正树,使之重新成为一棵红黑树。


实例


上面动图演示的那个网站的红黑树源码是从左子树找最大点来当继承点,而TreeMap是从右子树找最小点来当继承点,所以动图就不适合用了,所以我根据TreeMap画了个自定义view,但是没有动态效果了,仅供展示。


  • 删除没有孩子节点的红色节点就不演示了,直接删除即可

  • 删除有一个孩子节点的红色节点,也只需要将那个孩子节点替换为删除节点即可。

  • 删除有两个孩子的红色节点(50),且右孩子节点没有左右孩子节点时,如下图:


拒绝枯燥,用生动的例子带你学习红黑树


  
    
    
  
    if (p.left != null && p.right != null) {
        TreeMapEntry<K,V> s = successor(p);
        p.key = s.key;
        p.value = s.value;
        p = s;
    } // p has 2 children


上图这种情况下,因为被删除点P(50)有两个孩子节点,所以就需要为他找继承successor, 这个点就是P右子树的最小点也就是75。然后把被删除点的值变为继承点的值,然后被删除点P就变成了原successor(75)

因为现在的P没有左右子树,所以replacement也就为null,此时P点为黑色点,所以需要fixAfterDeletion平衡树

P点在右子树,所以找到兄弟节点Sib(25),Sib节点为黑色且左右孩子(null)都为黑色,此时向上追溯,将P点的父节点也就是原P(红色的50,现红色的75)继续循环。但此时P点就变成了红色,退出循环,在最后setColor(x, BLACK);将红色的75变为黑色,最后删除P(75)即完成了树的平衡。


  • 删除有两个子节点的红色节点(100),且子节点还有子节点如下图:


拒绝枯燥,用生动的例子带你学习红黑树


这个例子我举完才发现和上面的步骤一模一样,只不过找寻successor的时候多向下找寻了一层而已,详细操作看上个例子解析

总结下:当被删除的节点为红色时,操作很简单,如果只有一个子节点,就将子节点替换上来。如果有两个子节点,就通过找寻successor,将successor的值赋给被删除点,然后将被删除点替换为successor即可。

  • 删除没有孩子节点的黑色节点,且兄弟节点为红色时,如下图:


拒绝枯燥,用生动的例子带你学习红黑树


这种情况下比较简单,因为P点无孩子节点,也就不存在successor和replacement。因为P点这边树删除了一个黑色节点,就比兄弟树少了一个黑色节点,此时就需要从兄弟那边借一个黑色节点过来。

所以将sib(75)设为黑色,父节点(60)设为红色,然后左旋,这样左子树就从右子树借了个黑色节点(70)过来。然后此时的sib点重新赋值给了70,为了助于理解,这里看下图

拒绝枯燥,用生动的例子带你学习红黑树


  
    
    
  
 if (colorOf(leftOf(sib))  == BLACK &&
                        colorOf(rightOf(sib)) == BLACK) {
                    setColor(sib, RED);
                    x = parentOf(x);


左旋后如图,P点是待删除点,70成了新的sib点。此时可以发现如果P点被删了那么最左侧这边树整体就少了一个黑色节点。此时sib的左节点P因为子节点都为黑色(null)所以将sib(70)设为黑色,将P点设为其父节点,因为父节点(60)为红色,所以退出循环,最后 setColor(x, BLACK) ; 将60设为黑色,再将P点删除即完成平衡。

  • 删除没有孩子节点的黑色节点(25),且兄弟节点为黑色时,如下图:


拒绝枯燥,用生动的例子带你学习红黑树


和上个例子一样,也不存在找继承点的情况,直接来平衡树。

因为sib(70)是黑色,sib()的左右节点都为黑色(null),所以直接sib(75)设为红色,然后追溯用father(60)带入继续循环。

和上一步原理一样,此时的sib(150)黑色,左右节点也为黑色,于是将sib(150)设为红色,将父节点(100)带入继续循环,但因为100已经是root节点,所以退出循环,删除p点完成平衡

  • 删除没有孩子节点的黑色节点(65),兄弟节点为黑色且其右节点为黑色时,如下图:


拒绝枯燥,用生动的例子带你学习红黑树


  
    
    
  
 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;
                }


这里看似树很简单,但是这个操作是最复杂的一个,这里要进行两步自旋,下面就主要解释下作用。

当sib节点有两个子节点,但是其颜色不一致时,此时左旋从sib借黑色节点时可能会把红色节点给过去,所以要如果红色节点在左边时先需要做个右旋将红色节点移到右边。

右旋完成后,现在要从sib这边借黑色节点,因为左旋后sib节点就成了原来的父节点,sib右节点就成了原来的sib节点,所以为了保持颜色平衡,让他们分别继承他们要接替位置的颜色,而原来的父节点就接替了被删除点P的位置,所以将其颜色设为黑色,至此,平衡完成。

  • 删除有子节点的黑色节点,如下图:


拒绝枯燥,用生动的例子带你学习红黑树


拒绝枯燥,用生动的例子带你学习红黑树


拒绝枯燥,用生动的例子带你学习红黑树


当删除一个有子节点的黑色节点时,将这个操作分解后问题就变成了上面【删除没有孩子节点的黑色节点】和【删除红色节点】,然后继续上面的操作即可,这里给出了三个例子。

图一:删除点P(60):找到其继承点successor(70),赋值变换后这个问题就变成了:删除一个黑色节点70,且他的兄弟节点为黑色

图二:删除点P(60):找到其继承点successor(65),replacement点为70,赋值变换后这个问题就变成了:删除一个红色节点70

图二:删除点根节点(100):找到其继承点successor(125),赋值变换后这个问题就变成了:删除一个黑色节点125

总结:当删除的黑色节点无子节点时,根据兄弟节点的颜色来做具体操作,当删除有子节点的黑色节点时,可以通过找继承点将问题分别为删除一个无子节点的黑色或红色节点,所以问题就顺利简化了。

/   总结   /


东西还是自己再写一遍才能看出自己是否真正理解了,在看了几篇文章后我以为我理解了红黑树,所以我尝试着按自己理解去写一篇笔记,结果写的过程中发现看似理解,实则根本描述不出其原因,因而重新又去梳理了一遍,有了很多新的感悟。写的比较凌乱,如如果觉得文章有错误,欢迎指出并交流。

红黑树演示图地址:
https://www.cs.usfca.edu/~galles/visualization/Algorithms.html



推荐阅读:


欢迎关注我的公众号
学习技术或投稿



长按上图,识别图中二维码即可关注


以上是关于拒绝枯燥,用生动的例子带你学习红黑树的主要内容,如果未能解决你的问题,请参考以下文章

红黑树背了又忘?带你深入红黑树本质

红黑树旋转

Java集合详解6:这次,从头到尾带你解读Java中的红黑树

一文带你网罗HashMap面试考点!

红黑树最全手撕教程(建议马!)

带你深入理解STL之RBTree