红黑树【转载】

红黑树

红黑树可以算是树状结构中的“明星”了,应该计算机专业都听过红黑树这个专业名词,而且红黑树的应用也很广泛,比方说, java 集合中的 TreeSet 和 TreeMap ,以及 jdk8 的 HashMap 链表长度超过7之后会转成红黑树等等。但实际上红黑树却很复杂,他并不是像前面讲过的树一样是棵平衡树,即红黑树并没有定义从根节点到叶子节点的长度一致或高度差为1,然而他却能保证树大致上是平衡的—-从根到叶子的最长的可能路径不多于最短的可能路径的两倍长,这需要从他的性质中去推导出来。先来看一下红黑树的性质:

(1)节点是红色或黑色;

(2)根节点是黑色;

(3)每个叶节点(NIL节点,空节点)是黑色的;

(4)从每个叶子到根的所有路径上不能有两个连续的红色节点;

(5)从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
在这里插入图片描述
关于从根到叶子的最长的可能路径不多于最短的可能路径的两倍长这个结论,下面简单证明一下。

从根节点出发,假设根节点从左子树到叶节点的长度为 Ln ,根节点从右子树到叶节点的长度 Rn ,设在 Ln 路径中包含的黑色节点的数目为 k ,则根据性质(5),即在 Rn 路径中包含的黑色节点也为 k ,证明过程如下图所示,
在这里插入图片描述
即最终得到结果2 ≥ Ln/Rn,所以 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长 结论成立。

红黑树从本质上来说是一棵2-3-4树,在历史上,也是先提出了2-3-4树,后来红黑树由它发展而来。这是由于2-3-4树的节点可以是1个key或2个key或3key,其子节点也可以是2个或3个或3个节点,并且2-3-4树还是一棵高度平衡的树,所以实现一棵2-3-4树非常繁琐。而红黑树在结构上只是一棵二叉树,节点只有一个key,虽然理解红黑树比较难,但是其实现却并不特别复杂。

红黑树与2-3-4树之间是可以互相转化的,如果一棵树满足红黑树,把红色的节点收缩到其父节点 ,就变成了2-3-4树,所有红色节点都与其父节点构成2个key或3个key的节点,其它节点为1个key的节点。如下图,
在这里插入图片描述
 关于红黑树的节点插入,首先,新节点直接插入并初始化颜色为红色

因为要满足红黑树的五条性质,如果我们插入的是黑色节点,直接违反了性质(5),红黑树肯定需要进行调整,但是如果我们插入的是红色节点,不一定需要对树进行调整(父节点是黑色时,不需要调整,只有当其父节点是红色时,才需要调整)所以,我们把要插入的节点的颜色变成红色。

下面进行红黑树节点插入的情况分析

(1)若新插入节点是根节点,违反了性质(2),则将节点颜色改为黑色即可;

(2)若新插入节点的父节点为黑色,此时无需调整,满足红黑树;

(3)若新插入节点的父节点为红色,违反了性质(4),此时需要对树进行调整,出现调整的场景大体上为两种,即父节点的兄弟节点–叔父节点,是黑色的NIL节点或者为红色节点。如下图所示。另外,具体细分下去还有红父节点是祖父节点的左孩子还是右孩子,插入节点是红父节点的左孩子还是右孩子,下面会逐一分析。
在这里插入图片描述

有些文章中将情况就简单的分成叔父节点的颜色为红色或黑色两种,但是我觉得这点很误导人,因为从上图就可以看出来,如果左图节点“30”存在左孩子为非NIL的黑色节点“22”,那么此时在新节点“70”插入之前,该树就已经不符合红黑树了,不符合性质(5),所以我觉得这点很重要,很有必要讲清楚,即,要么叔父节点为黑色则一定为NIL节点,要么叔父节点为红色。

a.若新插入节点的叔父节点为红色,如上的右图,此时只需将祖父节点(“30”)改为红色,然后将父节点(“50”)与叔父节点(“22”)都改为红色即可,最后将祖父节点(红色“30”)看作向上层新插入的当前节点,重复(1)(2)(3)的判断,直到满足红黑树。实际上这种场景无论新插入节点为父节点的左孩子还是右孩子,父节点是祖父节点的左孩子还是有孩子,都是这样调整,即只需修改颜色,无需旋转变换,如下图。
在这里插入图片描述

b.若新插入节点的叔父节点为黑色NIL节点,则存在插入节点是父节点的左孩子还是右孩子,父节点是祖父节点的左孩子还是有孩子,4 种情况,如下:

i.父节点为祖父节点的左孩子,插入节点为父节点的左孩子,如下图,调整只需对祖父节点右旋,然后将右旋后的父节点涂黑,左右子节点涂红即可,此时满足红黑树。
在这里插入图片描述
ii.父节点为祖父节点的左孩子,插入节点为父节点的右孩子,如下图,调整比上面的 i 多一步,即将先对父节点左旋,变成 i 的情况,然后后面跟 i 一样,对祖父节点进行右旋,然后将右旋后的父节点涂黑,左右子节点涂红,此时满足红黑树。
在这里插入图片描述
 iii.父节点为祖父节点的右孩子,插入节点为父节点的右孩子,如下图,实际上情况 iii 与情况 i 互为镜像,调整只需对祖父节点左旋,然后将左旋后的父节点涂黑,左右子节点涂红即可,此时满足红黑树。
在这里插入图片描述
 iv.父节点为祖父节点的右孩子,插入节点为父节点的左孩子,如下图,调整比上面的 iii 多一步,即将先对父节点右旋,变成 iii 的情况,然后后面跟 iii 一样,对祖父节点进行左旋,然后将左旋后的父节点涂黑,左右子节点涂红,此时满足红黑树。实际上情况 iv 与情况 ii 互为镜像。
在这里插入图片描述
如果用2-3-4树的角度去理解红黑树的节点插入的话,其实非常简单。

红黑树节点插入2-3-4树节点插入
父节点为黑色,则直接插入待插入的叶子节点为1个key,则直接插入
红父红叔,则将父节点与叔父节点涂黑,再将祖父节点涂红,最后将祖父节点看作向上插入的当前节点,不断向上回溯直到满足红黑树待插入的叶子节点为3个key,则将待插入的节点进行分裂,然后将分裂后的父节点看作向上插入的当前节点,不断向上回溯直到满足2-3-4树
红父黑NIL叔,则根据父节点与插入节点位置的情况进行调整,最终形成1个黑色的父节点与2个红色的子节点待插入的叶子节点为2个key,则找到对应位置,直接插入,最终形成3个key的节点

红黑树节点的插入就讲到这里,下面讲一下红黑树的节点删除。

红黑树的节点删除,可以学习 java 的 TreeMap 的源码,因为 TreeMap 内部就是红黑树的结构,我本地用的 jdk 是 8 的,所以就以 jdk8 的 TreeMap 源码来讲。

在看源码前,可以先猜想红黑树的节点删除,这也可以使我们更好的理解源码的实现,实际上看什么源码也都是这样,如果干看源码,在不知道源码想表达什么意思的情况下,会看的一脸懵逼,然后看没几行就从入门到放弃了,所以,我认为看源码比较正确的方式,就是先大概知道源码想表达什么意思。但是像现在我们什么都不知道,那怎么办?那就猜嘛!!!猜个大概,让脑子里一条思路,再看源码,你就知道源码在说什么。

下面是我的思路:
首先,红黑树结构上也是一棵“半成品”的AVL,那么红黑树节点的删除,应该跟AVL差不多,那我们类比AVL节点删除的两个判断:①删除的是什么类型的节点?②删除了节点之后是否满足红黑树?

AVL节点的类型有三种:1.叶子节点;2.只有左子树或只有右子树;3.既有左子树又有右子树。但是在AVL章节中删除节点的情况,2与3的处理逻辑是一样的,即采用中序遍历寻找前驱或后继节点,与待删除的节点互换位置,然后把问题转为删除叶子节点来处理。

所以,总结一下,我觉得的,对于红黑树删除的节点,其类型可以分为三种去考虑:(1)非叶子节点;(2)红色叶子节点;(3)黑色叶子节点

也就是说,

(1)若删除的节点是非叶子节点,就采用中序遍历,找到前驱或后继节点,然后做互换值处理,并不置换颜色,然后将删除非叶子节点的问题转化为删除叶子节点。但是这里有个疑问,就是红黑树中序遍历找到的后继节点,就一定是叶子节点吗?

答案是,不一定,因为像前面说的AVL树、2-3树、2-3-4树也好,都是平衡树,即叶子层次都是满节点的,但是像红黑树,并不是严格的平衡树,所以对于红黑树非叶子节点的前驱或后继节点有两种情况a.叶子节点;b.是只有左孩子或只有右孩子的父节点(黑色)

即,若前驱或后继节点为叶子节点,那问题就转为删除叶子节点;若前驱或后继节点为只有左孩子或右孩子的父节点,则此时该父节点颜色肯定为黑色,且仅有的左孩子或右孩子肯定为红色,此时只需将孩子节点(左或右)替换该父节点,并将孩子的颜色改成黑色即可。如下图,
在这里插入图片描述
节点”50“与后继节点”70“互换位置之后,如下图,
在这里插入图片描述
最后,红黑树调整完成,如下图。
在这里插入图片描述

“若前驱或后继节点为只有左孩子或右孩子的父节点,则此时该父节点颜色肯定为黑色,且仅有的左孩子或右孩子肯定为红色“,该结论可以简单推论一下,以后继节点为例,目标节点肯定只有右孩子,因为如果目标节点有左孩子,则此时的后继节点应该为目标节点的左孩子,并且为叶子节点;而目标节点在只有一支右孩子的情况下,若目标节点为红,右孩子为黑,那肯定不符合前面定义红黑树的性质(5);若目标节点为黑,右孩子为黑,也不符合性质(5);若目标节点为红,右孩子为红,那更不对,直接破坏性质(4);所以,若前驱或后继节点为只有左孩子或右孩子的父节点,则此时仅有一种可能,即该父节点颜色肯定为黑色,且仅有的左孩子或右孩子肯定为红色。

前面搞定了非叶子节点删除的情况,最终也是把问题转化为删除叶子节点,那么再来分析删除叶子节点的情况,

(2)若删除的节点是红色叶子节点,那么这种就很简单了,无论该节点是左孩子还是右孩子,将该节点直接删除即可,此时不破坏红黑树的性质,满足红黑树。

(3)若叶子节点是黑色,那么就得逐个分析情况了,但是调整肯定也就两种,变色跟旋转,下面就来走读下 TreeMap 的源码,验证下上面猜的,跟记录下删除的叶子节点是黑色的可能情况及其调整。

那么既然是看删除节点的源码,那就直接从 remove 方法开始看起,如下,直接搜索 remove 方法,发现实际调用的删除节点的方法是 deleteEntry ,则走读 deleteEntry 方法

在这里插入图片描述
下面拉取下 deleteEntry 的源码,并追加我的注释,

    /**
     * Delete node p, and then rebalance the tree.
     */
    //方法的注释就说了,删除节点再重新平衡树
    private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--;

        // If strictly internal, copy successor's element to p and then make p
        // point to successor.
        //p就是要删除的节点,而此处判断就是判断p是否为非叶子节点
        if (p.left != null && p.right != null) {
            Entry<K,V> s = successor(p); //若为非叶子节点,则找到p的后继节点
            p.key = s.key;
            p.value = s.value;
            p = s; 
            //将后继节点s的值替换p,然后删除p的操作替换为删除后继节点s
        } // p has 2 children

        // Start fixup at replacement node, if it exists.
        //这里就是前文说到的内容,红黑树的后继节点不一定是叶子节点,所以,如果有p有左孩子或右孩子,则直接将左孩子或右孩子直接替换p即可
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);

        if (replacement != null) {
            // Link replacement to parent
            replacement.parent = p.parent; //将replacement的parent节点替换为p的parent
            if (p.parent == null)
                root = replacement; //p.parent为null即说明p为根节点,则直接将replacement设置为根节点
            else if (p == p.parent.left) //若原p为parent的左孩子,则设置replacement为parent的左孩子
                p.parent.left  = replacement;
            else
                p.parent.right = replacement; //若原p为parent的右孩子,则设置replacement为parent的右孩子,实际上这几步操作都是将不为null的replacement替换待删除的节点p

            // Null out links so they are OK to use by fixAfterDeletion.
            //将p关联的节点统统设为null,即这一步操作实际上就是将p节点删除
            p.left = p.right = p.parent = null;

            // Fix replacement
            if (p.color == BLACK)
                fixAfterDeletion(replacement); //fixAfterDeletion方法即为调整树的方法,但是这一步我觉得有点多做了,实际上,如果非叶子节点p是只有左孩子或只有右孩子,那么肯定只可能是一种情况,就是p是黑色的,replacement是红色的,直接替换就好了,并不需要调整replacement。
        } else if (p.parent == null) { // return if we are the only node.
            root = null; //若replacement为null,p的parent也为null,即此时红黑树只有一个节点p且为根节点,此时直接将root设为null即可
        } else { //  No children. Use self as phantom replacement and unlink.
            //此处else体中的内容即为p为叶子节点的逻辑
            if (p.color == BLACK)
                fixAfterDeletion(p); //当叶子节点p为黑色,则调用fixAfterDeletion方法调整树,并且当前待删除的节点为p

            //上面步骤调整完成之后,下面的逻辑就是将节点p的关联关系设为null,即将节点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;
            }
        }
    }

上面的代码段,应该把 deleteEntry 方法讲的很清楚了,也验证了删除节点为非叶子节点的逻辑,下面来走读 删除节点为黑色的叶子节点时,调整红黑树的方法—-fixAfterDeletion

    /** From CLR */
    private void fixAfterDeletion(Entry<K,V> x) {
        //因为删除黑色叶子节点,破坏了红黑树的性质,可能需要不断的回溯到root去调整
        //但是实际上并不需要从叶子节点开始一直做旋转调整到根,在删除节点调整红黑树中,旋转不超过三次,从下面的代码中也可以体现
        //先看眼下的代码吧,首先,当x不为根且x为黑色时,才进入循环
        while (x != root && colorOf(x) == BLACK) {
            if (x == leftOf(parentOf(x))) { //当x为其父节点的左孩子时,进入此处逻辑
                Entry<K,V> sib = rightOf(parentOf(x));

                if (colorOf(sib) == RED) { //sib是x父节点的右孩子,即sib为当前节点x的兄弟节点,所以此处,情况1:当兄弟节点为红色时,互换父节点与兄弟节点的颜色,然后对x的父节点进行左旋,然后重新修正sib节点
                    setColor(sib, BLACK);
                    setColor(parentOf(x), RED);
                    rotateLeft(parentOf(x));
                    sib = rightOf(parentOf(x));
                }

                //注意,此处并不是上面if的else逻辑,即此处的逻辑可以是上面if判断后再执行的逻辑
                if (colorOf(leftOf(sib))  == BLACK &&
                    colorOf(rightOf(sib)) == BLACK) {
                    //情况2:当兄弟节点的左孩子与右孩子都为黑色时,设置兄弟节点的颜色为红色,然后将当前节点设置为父节点,重新进入while判断,多说一点,在colorOf方法中,当节点为null时在colorOf方法也返回BLACK,也就是说,此处,当兄弟节点没有子节点时,也走这个逻辑
                    setColor(sib, RED);
                    x = parentOf(x);
                } else { //当兄弟节点的孩子节点中,至少存在一个红色节点,则走此处else逻辑
                    if (colorOf(rightOf(sib)) == BLACK) {
                        //进到该if逻辑,即情况3:为当兄弟节点的左孩子为红色,右孩子为黑色或空时,则互换兄弟节点与兄弟节点左孩子节点的颜色,再对兄弟节点进行右旋,然后重新修正sib节点
                        setColor(leftOf(sib), BLACK);
                        setColor(sib, RED);
                        rotateRight(sib);
                        sib = rightOf(parentOf(x));
                    }
                    //注意,此处并不是上面if的else逻辑,即此处的逻辑可以是上面if判断后再执行的逻辑
                    //情况4:当兄弟节点的右节点为红色时(或者上面情况3的后续调整),兄弟节点与父节点互换颜色(因为该处逻辑兄弟节点的颜色肯定为黑色),并修改兄弟节点的右孩子的颜色为BLACK,然后对当前节点x的父节点进行左旋,然后设置当前节点为根,退出while循环
                    //所以从上往下的代码流程看,在情况1、情况3、情况4,这3处代码出现旋转,而且若当前节点为父节点的右孩子时,与这里流程互为镜像,对应的场景也仅会发生一次旋转,所以关于红黑树节点删除,最多只会进行3次旋转
                    setColor(sib, colorOf(parentOf(x)));
                    setColor(parentOf(x), BLACK);
                    setColor(rightOf(sib), BLACK);
                    rotateLeft(parentOf(x));
                    x = root;
                }
            } else { // symmetric
                //下面的逻辑是判断当前节点是其父节点的右孩子的情况,与上面的逻辑是互为镜像的,所以下面就的代码就不分析了
                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;
                }
            }
        }

        //此处是while循环体外的内容,即当x为根节点,或者节点颜色为红色时,则设置当前节点x为黑色,之后方法就结束了
        setColor(x, BLACK);
    }

源码的部分就大概是这样,也不难,整理总结一下几个调整的点吧

以删除的黑色叶子节点为其父节点的左孩子为例,如下:

  1. 当兄弟节点为红色时,互换父节点与兄弟节点的颜色,然后对当前节点的父节点进行左旋
  2. 当兄弟节点的左孩子与右孩子都为黑色(或没有子节点)时,设置兄弟节点的颜色为红色,然后将父节点看作当前节点,然后重新根据1,2,3,4的情况,调整当前节点
  3. 为当兄弟节点的左孩子为红色,右孩子为黑色或空时,则互换兄弟节点与兄弟节点左孩子节点的颜色,再对兄弟节点进行右旋,修正当前节点的兄弟节点,再将兄弟节点与父节点互换颜色,并修改兄弟节点的右孩子的颜色为黑色,最后对当前节点x的父节点进行左旋
  4. 当兄弟节点的右节点为红色时,兄弟节点与父节点互换颜色,并修改兄弟节点的右孩子的颜色为黑色,然后对当前节点x的父节点进行左旋
    若删除的黑色叶子节点为其父节点的右孩子,与以上互为镜像。
  5. 以上的情况,最后当前节点若为根或者为红色,则将当前节点调整为黑色,然后完成调整。

另外,红黑树的本质是一棵2-3-4树,所以红黑树节点删除可以用2-3-4树节点删除去思考,但是变换结果不一定一致。因为对于一棵红黑树,有与之对应的一棵2-3-4树,但是对于一棵2-3-4树却不一定只对应一棵红黑树。

如下,以删除红黑树的黑色叶子节点“80”为例,类比2-3-4树的形态。
在这里插入图片描述
类比2-3-4树,实际上可以更好的理解红黑树。

红黑树节点的删除就大概讲这些,实际上在做这篇笔记的时候我画了很多图,试图以2-3-4树的角度来完全讲清楚红黑树删除,但是发现不好讲,因为2-3-4树会对应不止一棵的红黑树,最后变换的结果不一定一致。也试图跟别的文章一样,穷举红黑树删除节点的所有情况,然后画图记录,但是后续会再画更多的图,而且讲的不一定清楚,所以就放弃了。而且本来红黑树开始并没有打算写这么多内容,后来为了内容的完整性,才把红黑树的内容多补一点,没想到补着补着多出这么多的内容。

最后说一下红黑树的性能,红黑树能够以 O(log2(N)) 的时间复杂度进行搜索、插入、删除操作。

一般红黑树也被拿出来跟 AVL树 做比较,都说红黑树的综合性能比 AVL树 要好,但是好在哪里呢?

​  实际上大体来讲 AVL树 的插入,删除还有查询的时间复杂度跟红黑树是一样的,都是 O(log2(N)) 。有人说,红黑树比AVL树性能好的原因是,红黑树节点的删除最多只需要旋转3次,而 AVL树 删除节点的删除可能会不断的回溯直到根,这点确实是,但不是最主要的原因,因为红黑树删除节点过程中也可能需要不断的回溯最终到根(情况2),只是不需要做那么多旋转而已(而且旋转也不会耗费很大的性能,只是将节点的引用关系做下改变而已,可以认为是 O(1) ), 红黑树比 AVL树 性能好最主要的原因是,AVL树是高度平衡的树(左右子树高度差为1),所以不管是插入节点也好,删除节点也好,都会很容易导致树的不平衡,从而引发调整,而红黑树不是严格的平衡树,节点插入与删除,并不容易会导致树进行调整,所以这点才是红黑树综合性能比 AVL树 好的原因。


以上博文转载自:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值