好怕怕的红黑树(一文带你从2-3-4树理解红黑树)

好怕怕的红黑树(一文带你从2-3-4树理解红黑树)

百度百科:红黑树是一种特定类型的二叉树,它是在计算机科学中用来组织数据比如数字的块的一种结构。若一棵二叉查找树是红黑树,则它的任一子树必为红黑树。红黑树是一种平衡二叉查找树的变体,它的左右子树高差有可能大于 1,所以红黑树不是严格意义上的平衡二叉树(AVL),但对之进行平衡的代价较低, 其平均统计性能要强于 AVL 。由于每一棵红黑树都是一颗二叉排序树,因此,在对红黑树进行查找时,可以采用运用于普通二叉排序树上的查找算法,在查找过程中不需要颜色信息。

本文默认读者已经对二叉搜索树、AVL树中的相关概念已经熟悉,对于一些二叉搜索树、AVL树中的操作,例如左旋,右旋等不再解释

一、红黑树的性质

红黑树有五大性质:

性质1:结点是红色或黑色

性质2:根结点是黑色

性质3:所有叶子都是黑色(叶子是NIL结点)

性质4:每个红色结点的两个子结点都是黑色(从每个叶子到根的所有路径上不能有两个连续的红色结点)

性质5:从任一节结点其每个叶子的所有路径都包含相同数目的黑色结点

二、红黑树的本质

在开头百度百科对红黑树的介绍中我们知道,红黑树是一颗二叉查找树,二叉查找树的特点就是对于任意非叶子节点,它的左子树的所有值都比根节点值小,它的右子树的所有值都比根节点值大。事实上,红黑树并非仅仅是一颗平衡二叉查找树的变体那么简单,红黑树其实是一种4阶B树(2-3-4树)的等价形式,所以我们在学习红黑树之前,先了解一下2-3-4树的基本概念,对我们学习、理解并且手动推导红黑树的性质有着很大的帮助,死记结论是没有意义的。借助2-3-4树还能更好的理解红黑树的新增和删除

三、2-3-4树

2-3-4树是一种4阶的B树,它有三种结点,分别是2节点、3节点和4节点(所以叫2-3-4树)

2节点:包含一个元素和两个子节点

3节点:包含两个元素和三个子节点

4节点:包含三个元素和四个子节点

下图是2节点、3节点、4节点和一颗合法的2-3-4树的图示:

节点演示
2-3-4树演示

简单介绍了一下2-3-4树后,接下来是2-3-4树和红黑树的等价关系。在2-3-4树中的每一种节点,在红黑树中都有对应的等价关系,具体如下:

2-3-4树~红黑树 等价关系

由上面这个图我们可以很清楚的看到

2节点在红黑树中就是一个黑色的节点

3节点在红黑树中有两种形态,一种是父节点连着一个右孩子,一种是父节点连着一个左孩子,都是上黑下红。这两种形态都是等价的,只不过是右倾和左倾的区别而已,不会破坏红黑树的任何性质

4节点在红黑树中对应一个父节点,下面有左孩子和右孩子,同样也是上黑下红

最后一个是在上面没有提到的裂变形态,就是当有一个元素,添加到了一个四节点时,此时该四节点就要裂变。在2-3-4树中,裂变时就是将四节点中,中间那个元素升级为父节点的元素(注意,此时讲的是2-3-4树),然后连接下边的两个孩子节点即可。等价到红黑树,只需要将原父节点变红,然后两个孩子节点变黑,最后插入元素,元素为红色即可。(注意,此时的红黑树只是裂变形态的一个中间状态而已,父节点变为红色后有可能破坏整棵树的性质,所以后续还需要判断调整)

根据等价关系,我们可以将一棵2-3-4树转换为多棵不同的红黑树。(为什么是多棵?实际上是因为2节点有左倾和右倾的区别,所以可以画出不同的红黑树,但是一棵红黑树只能画出一棵2-3-4树)

图示2-3-4树转换成一棵等价的红黑树:

2-3-4树转换红黑树

有了等价关系,我们很容易就能推导并理解红黑树的五大性质

推导:

1、结点是红色或黑色:这个基本是废话来着,结点本来就要么是红色要么是黑色

2、根节点是黑色:我们知道2-3-4树只有三种节点,根节点要么是2节点,要么是3节点,要么是4节点。无论是那种节点,根据等价关系,根节点都是黑色的,所以有了红黑树的根节点是黑色的这个性质

3、所有叶子都是黑色(叶子是NIL结点):2-3-4树中,我们知道2节点、3节点、4节点的含义,但是我们会发现最下面一层下面是没有节点的,这不就破坏了性质吗?其实不是,下面挂了空节点(NIL,JAVA中是null),只是图中没有画出来而已,单独的null节点是一个特殊的2节点,根据等价关系,自然所有的叶子都是黑色的(这一点其实很重要,不然TreeMap中的添加和删除源码是看不懂的)

空节点演示

4、每个红色结点的两个子结点都是黑色(从每个叶子到根的所有路径上不能有两个连续的红色结点):根据性质3,如果是2-3-4树上最后一层上的节点,则根据等价关系,红色节点下方一定挂着两个黑色的空节点。如果不是最后一层上的节点,由2-3-4树节点性质可知,下方一定挂着不为空的节点(起码是2节点,根据等价关系一定是上黑下红),所以红色节点下方还是有两个黑色子节点

5、从任一节结点其每个叶子的所有路径都包含相同数目的黑色节点:这个可以由一张图来解释,2-3-4树种每一个节点中都会有一个元素被黑色标记,根据等价关系转换后,自然每一条路径的黑色节点数都是相同的

性质5演示

四、红黑树的插入

我们已经知道了红黑树其实就是2-3-4树的一种等价形态,那么对红黑树的插入操作,我们都可以用2-3-4树的情况进行推演,进而得到红黑树的情况,便于理解

在2-3-4树中,节点只可能有四种情况,分别是2节点,3节点,4节点,以及空树,那么我们插入的时候,也只可能出现这四种情况,我们就可以对这四种情况进行分析,得出红黑树的等价情况

注意:在红黑树插入时,插入节点默认初始颜色都为红色(因为插入黑色一定会破坏性质5的平衡,一定需要调整,但是插入红色不一定会破坏性质)

在下文讨论中,以下名词的释意:

1、插入的是空树:

如果是空树的话,直接插入,然后让节点变黑即可

图示:

空树插入演示

2、插入的是2节点:

插入的是2节点的话,根据值寻找插入位置后,也是直接插入即可,不需要调整

图示:

2节点插入演示

3、插入的是3节点:

插入的如果是3节点话,对应到红黑树中有些复杂,下图一共列出了5种情况,但实际上,情况1、3和情况4、5是等价的,只是调整方向相反而已,以下只解释情况1、2和5,情况3和4只需要将方向镜像调整即可

情况1:

这种情况的特点是父节点(2)为红色、插入节点(3)的爷爷节点(1)的左孩子为空,且我们插入的是父节点(2)的右孩子,插入后出现2、3两个连续红节点破坏了性质4,由等价关系可知与4节点的等价形态不符,因此需要调整成为4节点对应的形态,只需要先将爷爷节点(1)改为红色,父节点(2)改为黑色,再将爷爷节点(1)左旋即可

情况2:

这种情况的特点是插入节点的父节点是黑色且插入节点的兄弟节点存在,处理等同插入2节点的处理,直接插入即可。其实由此我们也可以看出,如果插入节点的父节点是黑色,直接插入即可无需调整

情况5:

这种情况的特点是父节点(3)为红色、插入节点(2)的爷爷节点(1)的左孩子为空,且我们插入的是父节点(3)的左孩子,其实也是很好调整的,只需要把它先调整成情况1,然后按照情况1接着调整即可。调整成情况1,只需要沿着父节点(3)右旋即可。

图示:

3节点插入演示

4、插入的是4节点:

插入的是4节点其实比较难理解的只有递归调整而已。插入之后,需要将爷爷节点(2)改为红色,将父节点(3)和叔叔节点(1)改为黑色,然后递归调整爷爷节点

为什么需要递归调整爷爷节点呢?这是因为一个元素插入4节点后,这个4节点会变成5个元素,不符合2-3-4树的定义。怎么办呢?就把中间的节点往上挤,去找原来4节点的父节点(此时讨论的是2-3-4树),与他合并。这个操作就等价于向4节点的父节点插入一个元素,既然是插入元素,就有可能破坏性质,所以需要向上接着递归调整

图示:

4节点插入演示

伪代码:

介绍完所有的情况,我们来写一写伪代码,加深对整个红黑树插入的理解

public void put(RBNode rbNode){
    //获取插入位置的父节点
    RBNode insert = get(rbNode);
    //如果是空树
    if(insert == null){
        root = rbNode;
        //父节点是黑色
        setColor(root,BLACK);
        return;
    }
    //插入位置节点的父节点颜色是黑色,说明是插入的是2节点,或者插入的是3节点中的情况2
    if(insert.color == BLACK){
        //插入
    }else{
        //插入的是4节点
        if(insert.parent.left != null && insert.parent.right != null){
            //把爷爷节点变红
            //把父节点和叔叔节点变黑
            //插入
            //递归调整爷爷节点
        }else{
            //插入3节点,这里是考虑插入的3节点是左倾的情况,右倾只需要镜像调整
            if(insert.parent.right == null){
                //情况4
                if(rbNode.compareTo(insert) > 0){
                    //左旋
                }
                //插入
                //右旋
            }else{
                //右倾
                //情况5
                if(rbNode.compareTo(insert) < 0){
                    //右旋
                }
                //插入
                //左旋
            }
        }
    }
    
}

五、红黑树的删除:

红黑树的删除可以说是整个红黑树情况最多最复杂的部分了,红黑树的插入在2-3-4树的等价替代下理解起来比较简单,但是删除操作即便是在2-3-4树的等价替代下,其实情况还是比较复杂的,下面介绍红黑树的删除,尽量理清红黑树删除的各种情况,如有错误之处请读者指正

红黑树的删除,单独的来看,其实也是二叉树的删除,只不过带了颜色,删除之后可能导致黑不平衡,需要调整。所以红黑树的删除可以归结为以下步骤:

删除结点→调整平衡

其中,删除结点又分为以下三种情况:

1、删除的节点是叶子节点

2、删除的节点只有一个孩子

3、删除的节点有两个孩子

首先,我们来说第一种情况的处理,如果删除的节点是叶子节点,只需要直接删除,调整平衡就好,不需要其他操作

如果是第二种情况,我们只需要让他的孩子节点替代这个节点即可

第三种情况,我们不能直接删除这个节点,这样他的两个孩子就断了。对于第三种情况,我们需要寻找这个节点的前驱结点(比它小的最大值,也就是节点左子树的最右边的节点)或者后继节点(比它大的最小值,也就是节点右子树的最左边的节点)来替代这个节点的值,然后删除前驱节点或者后继节点即可,此时前驱结点或者后继节点只可能是情况1或2,便转换成了前两种情况

情况3转换演示

接下来是调整平衡,对于删除一个节点,他的颜色只可能是红色或者黑色

如果是红色,那直接删除即可,不会破坏红黑树的性质

如果是黑色,则需要调整,因为删除黑色节点一定会在某一路径上导致黑色节点少了一个,从而破坏了红黑树的性质5。删除黑色节点,根据2-3-4树与红黑树的等价性质,我们知道删除的黑色节点一定是对应2-3-4树上的2节点或者3节点,不可能是4节点。因为如果是4节点,它的黑色节点有两个孩子,根据上面分析的,我们会去找他的前驱节点或者后继节点来替代,所以不会有删除4节点的黑色节点的情况。所以,我们分两种情况讨论

1、删除的是3节点的黑色节点

这种情况是最简单的,通过等价关系,黑色节点下方有一个红色节点,删除黑色节点时,只需要用红色节点去替代黑色节点,然后把红色节点变黑就可以了

3节点删除黑色节点转换演示,省略了红黑树的其他部分

2、删除的是2节点的黑色节点

这种情况就比较复杂了,根据2-3-4树的性质,除了最后一层外,每种节点都有它对应数目的子节点,因为2节点只有一个元素,删除之后这个节点就会消失,会破坏2-3-4树的性质

2节点删除

这时候怎么办呢?这里又要分为两种小情况讨论

2.1、删除的是2节点,兄弟节点是3节点或者4节点

如图示2-3-4树,我们不能直接删除节点,因为这样会导致父节点非法,这时我们的兄弟节点是有多余节点的,所以在2-3-4树上,我们可以让父节点的一个元素来顶替我们要删除的节点,然后让兄弟节点的一个元素上去顶替父节点,这样就不会破坏2-3-4树的性质。

这里有人可能就要问了,为什么不直接让兄弟节点的一个元素去替代被删除节点呢?而是要绕一大圈,让父节点的一个元素去顶替,然后兄弟节点去顶替父节点。其实这里是因为2-3-4树是有序的,左边的节点小于根节点,右边的节点大于根节点,如果直接让右边的节点替代左边的节点,就会导致左边的节点大于根节点了。正确做法是让父节点替代待删除节点,然后兄弟节点去替代父节点

对应到红黑树,如果兄弟是3节点,待删除节点是在左边,我们只需要把兄弟节点调整为右倾状态,然后旋转父节点,再将兄弟节点的孩子改为黑色,断开待删除节点即可

如果兄弟是4节点,这时候的调整有两种方案,一种是兄弟节点只借一个给父亲,一种是借两个。在算法导论中,实现的是只借一种的情况,看图示我们也发现了只借一个的话,需要旋转两次。在JAVA TreeMap中的实现是一次借两个,只需要旋转一次即完成调整,具体做法是:直接绕待删除节点的父节点左旋,然后将待删除节点的兄弟节点的右孩子变为黑色,断开删除节点即可

讲到这里,细心的小伙伴就会发觉,以上都是父节点是2节点的情况,那父节点要是3节点,4节点咋办?这确实是一个问题,如图

父节点不是2节点

这时候,在2-3-4树里,假如我要删除1,1的兄弟节点是45,但是在红黑树里,1的兄弟节点就变成6了,变成父节点的元素了,这应该怎么办呢?

其实也很好解决,只需要在我们上文进行调整时,先判断待删除节点的兄弟节点是不是真正的兄弟节点即可,如果是真正的兄弟节点,那么应该是黑色的,如果是红色的,则说明不是真正的兄弟节点,是2-3-4树里父节点的的一个元素,这时我们只需要沿着待删除节点的父节点(红黑树中的)左旋,就可以把1和4真正的调整到同一层,再按照上面说的方法调整即可

2.2、删除的是2节点,兄弟也是2节点

这种情况,是比较难理解,但是却处理比较简单的(代码上)

现在要删除的节点是2节点,兄弟也是2节点,自然就借不了了,如果删除掉这个节点,就会导致删除节点这条路径上黑色节点个数少了一个,跟右边不平衡,怎么办呢?最快的方法,就是直接把兄弟节点变成红色,让右边的路径也少一个黑的,然后递归处理父节点(即我们看做要删除父节点)即可。

其实这种情况个人有点难以解释,可能是功力不够,这里贴一段别人的解释,来源

这种情况也就是说当前节点是一个2node(2节点),当被删除时自己搞不定(没有多余的红色的节点来顶替),这个时候问兄弟节点借,但是兄弟节点恰好也是一个2node,也没有“多余”的节点可以借,这个时候只能问父节点来借节点数据来填补将要删除的黑色节点;

那么简单的总结一下这种情况就是:

  1. 被删除节点是一个2node
  2. 被删除节点的兄弟节点是一个2node(兄弟节点的双子都是黑色的或者就没有双子也就是都是叶子节点)

那么继续分析,触发了这种场景的情况下需要怎么操作。很简单只要将兄弟节点染红。怎么理解呢?

从红黑树的角度理解:既然一边一个黑色节点被删除了,当然另外一边为了平衡也需要减少一个黑色节点,最快的方法自然就是将兄弟节点染红。而两边黑色节点个数少1了这个怎么补上就找父节点了,具体父节点是自己就能搞定(自己就是3node 或者 4node)还是说找兄弟节点借(自己2node,兄弟3或4node)又或者继续向祖父节点借(自己和兄弟都是2node)这就得让父节点自己来处理了;

从2-3-4树的角度理解:染红是为了后续向父亲节点借到黑色节点的情况下合并形成3node来弥补黑色节点少1的现状;也就是2-3-4树在删除时的一种合并操作;

如上图:如果删除的是节点“55”这个时候会将兄弟节点染红来保持平衡,但是以问号为根的子树中黑色节点是少1的,所以这个时候需要问父节点“?”借。这里假设我们必然能借到一个黑色节点来使这个子树平衡。那么子树其实就已经调整完成。

第二步,在子树调整完成的基础上,我们只要继续分析父节点“?”的情况就可以了,不再需要关注子树,因为目前的情况是子树会借到一个黑色节点重新保持平衡,而节点“?”将要失去一个黑色节点,因此只要分析节点“?”在将要失去一个黑色节点的情况下去要做出的操作。那么继续分析首先就要清楚节点“?”有哪几种情况了,如下:

ok,这里我们分情况来做讨论

  1. 节点“?”为2node的情况,这个时候只能把自己“借给”子节点用于合并平衡,那当前节点又处于少一个节点的情况,这个时候就重新回到一开始的准则“自己搞不定问兄弟借,兄弟借不到问父亲借”的情况来。
  2. 节点“?”为3node的情况,3node有两种情况,但是不论是哪种情况连接着刚刚节点“35”和“55”的必然都是节点“B”,因此只要把节点“B”染黑借给子节点就完事了;
  3. 节点“?”为4node的情况,4node的情况不论连接节点“35”和“55”的是节点“B”还是节点“C”情况都可以和上面3node一样来处理;

删除操作就不写伪代码了,有兴趣的可以阅读java TreeMap的源码

ok,至此,红黑树的内容全部结束,有兴趣大家可以留言一起交流,文中若有错误还请指正。

posted @ 2021-03-14 15:15  ForYou丶  阅读(881)  评论(0编辑  收藏  举报