红黑树分为红和黑有什么好处_深入浅出红黑树(附源码分析)

602f68b48afc6b6d3a3531cb63bbd98e.png

前言

红黑树是一种重要的数据结构,java数据结构的Treeset、hashmap等数据结构的实现都用到了红黑树。网上的红黑树教程大多过于复杂,尤其是删除操作,各种旋转变色递归操作把人都搞晕了,这篇博客中我将以自己的理解,系统的梳理一下红黑树的各种操作,并会进行相应的源码解读。

这里默认大家已经了解了二叉搜索树、平衡二叉树、左旋右旋、变色的相关知识。

红黑树性质

红黑树的各种恢复平衡的操作都是为了保证红黑树的性质不变,因此要牢记红黑树的性质,这是我们理解各种操作的关键。红黑树的主要性质如下:

  1. 每个节点要么是红色要么是黑色。(空节点默认为黑色节点)
  2. 红黑树的根节点为黑色。
  3. 不能有两个连续的红色节点,也就是红色节点的子节点肯定为黑色。
  4. 红黑树上的任意一个节点到其每个叶子节点的所有路径上的黑色节点数量相同。

性质4非常重要,对于后面理解红黑树的恢复操作非常有用。

红黑树的删除操作

先讲红黑树的删除操作,这个最复杂了。

首先讲一个后继节点的概念,当一个非叶子结点被删除后,需要用后继节点来代替被删除节点的位置,后继节点就是搜索二叉树中大于被删除节点的最小节点。图中,15的后继节点就是17

5c849bfdbc422f04f4d2e01a02b042fc.png

删除操作最开始可以根据子节点个数来分:

  • 情况1: 待删除的节点是一个叶子结点,如果是红色直接删除,如果是黑色,需要进行恢复操作。
  • 情况2: 待删除的节点只有一个子节点,那么该节点肯定是黑色节点,子节点肯定是红色节点(根据红黑树性质4),那么直接删除节点,并用子节点代替删除节点的位置,将颜色改为黑色。
  • 情况3: 待删除的节点有两个子节点,那么找到该节点的后继节点,交换待删除节点和后继节点的值,同时将待删除节点的引用指向后继节点。

根据后继节点的性质,我们很容易就可以分析出来,后继节点最多有1个子节点,那么删除该节点,又转化为情况1和情况2,情况2很好处理,那么我们现在只需要考虑情况1中删除的是黑色叶子结点的情形,因为只有这种情况需要恢复操作。

删除黑色叶子结点:

删除黑色叶子结点必然会破坏红黑树的平衡性,我们首先考虑黑色叶子结点的父节点作为根节点的这颗子树,把被删除节点记为x,x的父节点记为p。这颗子树是不平衡的,因为少了一个黑色叶子结点,最好的方式就是通过变色旋转等方式把这颗子树调整成黑色节点平衡的,并调整前后的子树路径上的黑色节点保持不变,这样能够使得整个红黑树继续保持平衡

  • 假如p节点、x的兄弟节点、x的兄弟节点的子节点中有一个是红色:那么该子树一定可以调整成功,相当于是把红色节点染黑,把他放到被删除的节点的位置(实际上是通过变色旋转操作完成的,因为要保证key的顺序)。比如下面列举的情况,我们一定能将该子树调整为平衡,并且调整前后的子树路径上的黑色节点保持不变(后面会讲具体的变色旋转操作,这里只是分析一下):

25166adc3368e41c257386f650b56a05.png
情况1.1
  • 假如p,x的兄弟节点,x的兄弟节点的子节点全是黑色:这种情况下,我们可以将该颗子树调整为平衡状态,但是其路径上的黑色节点个数却减少了,导致整棵红黑树依然不平衡,需要继续递归恢复:

f859cda5ecbc40e1cea1a5543dfb6992.png
情况1.2

所以我们可以总结出来删除黑色叶子节点的两种情况:

  • 情况1.1 如果待删除节点的父节点、兄弟节点、兄弟节点的子节点中有一个是红色,那么该子树一定可以调整成功,并能保证整个红黑树平衡。
  • 情况1.2 如果待删除节点的父节点、兄弟节点、兄弟节点的子节点全是黑色,该子树一定可以调整成功,但是整个红黑树依然不平衡,需要将P节点作为当前节点,继续往上层递归处理,也就是继续进行情况1.1和情况1.2的判断,并进行相应调整。(这里需要好好理解为什么可以这么做,其实可以将x看作一个子树而不是一个叶子结点,结合性质4去考虑)

接下来对情况1.1和1.2中的每种类型的调整过程进行具体介绍,这里不需要死记硬背,只要知道我们的目标是干嘛就行了,这样自己画图也能够分析出来,比如情况1.1我们的目标就是子树调整后保持原来每条路径上的黑色节点个数不变,情况1.2就是要将子树调整为颜色平衡,但是会导致路径上的黑色节点数量减少,需要继续递归处理。我们就可以通过旋转和变色两大利器的各种组合来完成的目标,当前节点记为x,父节点记为p,兄弟节点记为s:

  • 当前节点是父节点的左节点
    • 兄弟节点为黑色,兄弟节点有红色子节点

1a226c4e631ebb8df1be851fc64650ac.png
    • 兄弟节点为黑色,兄弟节点没有红色子节点

b3e75325065990af8a1dde5f530c5087.png
    • 兄弟节点为红色

ee0909336f48d3e66cbeec8e6bf65e1b.png
  • 当前节点是父节点的右节点,与当前节点是父节点的左节点的情况完全对称,就是把左改成右,把右改成左而已。

以上的分类已经包括了全部的情况,我们现在看看TreeMap的源码实现,我们直接看恢复操作,可以看到TreeMap的恢复源码完全符合我们上面的分析:

private void fixAfterDeletion(TreeMap.Entry<K,V> x) { //传入的x是待删除的节点
        while (x != root && colorOf(x) == BLACK) {
            /**
             * x是父节点的左子节点
             */
            if (x == leftOf(parentOf(x))) {
                TreeMap.Entry<K,V> sib = rightOf(parentOf(x)); // 兄弟节点

                /**
                 * 情况e:
                 * 兄弟节点为红色,转换为兄弟节点为红色的情况
                 */
                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会被改为黑色(情况c)
                     * 如果是黑色,继续循环(情况d)
                     */
                    x = parentOf(x);

                } else {//兄弟节点有红色子节点

                    /**
                     * 情况b:
                     * colorOf(rightOf(sib)) == BLACK
                     * 等价于兄弟节点有红色左儿子,转化为兄弟节点有红色右儿子的情况
                     *
                     */
                    if (colorOf(rightOf(sib)) == BLACK) {
                        setColor(leftOf(sib), BLACK);
                        setColor(sib, RED);
                        rotateRight(sib);
                        sib = rightOf(parentOf(x));
                    }
                    /**
                     * 情况a:
                     * 处理兄弟节点有红色右儿子的情况
                     * 把p的颜色给s,s的右儿子设置为黑色,根据p左旋
                     */
                    setColor(sib, colorOf(parentOf(x)));
                    setColor(parentOf(x), BLACK);
                    setColor(rightOf(sib), BLACK);
                    rotateLeft(parentOf(x));
                    x = root;
                }
            } else { // x是父节点的右子节点,与x是父节点的左子情况完全对称
                TreeMap.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);
    }

红黑树的插入操作

最复杂的删除操作也讲完了,插入操作就更加简单了。

红黑树节点一般保存的是键值对,分别用key和value表示,我们根据key值来进行插入,刚开始插入的节点是红色的。 插入主要包括以下4种情况:

  • 情况1: 插入节点已经存在,那么直接重写value值就行了。
  • 情况2: 插入一颗空树,插入节点作为根节点,需要改为黑色。
  • 情况3: 插入节点的父节点是黑色的,直接插入,不会破坏红黑树的性质。
  • 情况4: 插入的父节点是红色,这样的话就出现了红色节点相连的情况,需要进行恢复操作。

情况123很好处理,情况4破坏了红黑树的性质,我们需要对红黑树的不满足性质的区域进行小范围调整,其中我们调整的目标主要有3个,如果满这三个目标 ,那么依然可以保持整棵红黑树的性质:

​ a. 进行变色操作,消除红红相连的情况

​ b. 进行旋转操作,保证旋转后每条路径上的黑色结点数量与变色之前相同。

​ c. 调整后的子树的根节点颜色为黑色,因为如果变成红色可能又会出现上层节点继续出现红红相连的情况。这里需要说明的是,被调整之前的子树的根节点肯定是黑色的,因为当前节点的父节点是红色,那么爷爷节点肯定是黑色。

有了这三个目标,我们就可以通过旋转和变色这两个操作来进行调整,第四种情况可以分为两种,符号规定g(grandparent,爷爷节点),p(parent,父节点),u(uncle,叔叔节点),x(当前节点):

  • 情况4.1: 叔叔节点是红色,将父节点和叔叔节点改为黑色,同时爷爷节点改为红色,此时目标ab已经满足了,但是目标c无法满足,这时需要将爷爷节点设置为当前节点,继续递归(如果爷爷的父节点是黑色节点,结束,如果爷爷的父节点是红色节点又需要分类情况4.1和情况4.2)

4adb91592d92ea78ed1ecfb62b50aaee.png
  • 情况4.2: 叔叔节点为黑色或则不存在,那么需要进行变色旋转操作。如果叔叔节点黑色,我们一定可以通过旋转变色等操作来满足abc三个条件,其实就相当于通过变色旋转操作对四个节点进行各种组合,两黑两红肯定能排出符合要求的情况。只不过有些情况旋转的次数多一点而已,遇到情况4.2就不需要继续递归了,小范围调整就能解决。具体操作如图所示:

37f728bc6f70e162eb66967959fda769.png
LL红红相连

22e38718668e243b9a266ae8c2d04e94.png
LR红红相连,可以转化为LL红红相连情况

0f59102f66db3f6c73572eabb6a66624.png
RR红红相连

0e140580853679689bf4095e64e6afdc.png
RL红红相连,可以转化为RR红红相连情况

提醒:这上面四幅图,有些图片看起来不满足红黑树的性质4,是因为这里的图片展示的是只是红黑树的某个局部范围,是在递归过程中遇到的情况。如果实在看不惯的话,可以把叔叔节点看作空节点(空节点也是黑色节点),这样也是对的,这样的话就是刚开始插入的情况了,因为刚开始插入叔叔节点肯定是空的(根据红黑树的性质4可以推出来)。

插入总结:红黑树的插入操作其实比较简单,主要是为了消除红红相连的情况,红红相连的情况根据叔叔节点的颜色来分类,叔叔节点是红色,需要改色并继续递归,叔叔节点是黑色或者不存在(空节点也是黑色),那么通过变色或者旋转来进行小范围调整就可以恢复红黑树,不需要继续递归。调整规则其实也不需要死记硬背,只需要按照保持整棵红黑树性质来调整,基本自己也就可以推出来。

TreeMap插入恢复操作的源码分析

private void fixAfterInsertion(TreeMap.Entry<K,V> x) {
        x.color = RED;

        while (x != null && x != root && x.parent.color == RED) { //插入节点的父节点是红色
            /**
             * 父节点是爷爷节点的左子节点
             */
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                TreeMap.Entry<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)); //爷爷节点作为当前节点,继续循环处理

                } else { // 叔叔节点为黑色的
                    /**
                     * 如果是LR情况,转化为LL情况
                     */
                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateLeft(x);
                    }
                    /**
                     * 转化为LL情况
                     */
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            } else { //父节点是爷爷节点的右子节点,与父节点是爷爷节点的左子节点情况完全对称
                TreeMap.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;
    }

插入和删除总结

插入和删除其实都需要分为递归和非递归两种情况,非递归只需要局部调整就能保证整个红黑树平衡,而递归是因为局部调整依然无法保证整个红黑树的平衡,比如插入时叔叔节点为红色的情况,删除时父节点、兄弟节点、兄弟节点的子节点全为黑的情况,都需要递归处理,递归后如果遇到非递归的情况,就可以结束调整。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值