亲自动手画红黑树

原文发布于: http://blog.ztgreat.cn/article/12

在前面我们学习了平衡二叉树,伸展树,今天我们来看看另外一种平衡二叉树—红黑树,本来这篇博客早在一年前就该写的,后来发生了太多故事,博客停止了更新了一年,如今又有了最初的斗志,决定好好把博客写下去,对知识的梳理的同时,也不断巩固。
对于各种平衡树,我只能进行简单的理解和实现,无法涉及太多的应用,主要的还是本身自己知识广度和深度有限,不过我相信:不积跬步,无以至千里
本文主要参考了算法导论一书

红黑树本质上就是一棵二叉查找树,一般的二叉查找树比较简单,在以前的博客(点这里)中有提到,不过那个时候写得也比较粗糙,后面会花时间改造一番。

红黑树 它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,红黑树有5条性质:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(null)是黑色。 (注意:这里叶子节点,是指为空(null)的叶子节点
(4)如果一个节点是红色的,则它的子节点必须是黑色的(也就是不能连续的两个红色节点)。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

红黑树中 最短的路径就是全是黑色节点,最长的路径就是红黑相间,由于性质5,因此可以确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树,下面是一颗红黑树的“样子”

红黑树的左旋和右旋

在前面的平衡二叉树中涉及到了旋转操作,这里并不会直接引用原来的图解,重新的说一下旋转操作,因为在平衡二叉树中,主要的操作就是旋转,通过旋转来调整树的结构,因为后面涉及到具体的java代码,这里我们先将节点的定义放出来

    public class RBTNode<T> {
        boolean color;        // 颜色
        T value;               // 节点值
        RBTNode<T> left;      // 左孩子
        RBTNode<T> right;    // 右孩子
        RBTNode<T> parent;    // 父结点
    }

1. 左旋

《亲自动手画红黑树》
对节点X 进行左旋,也就是:将x变为原位置节点的左节点,也实际是对右子树进行调整,将右子树提升一层,对于Y左子节点b 比X大,比Y小,自然就该放到X的左子树上,整个操作不麻烦

理解了操作方法后,写出伪代码或者具体的代码应该就不难了,下面展示的java代码(代码并不重要,注重思想)

    private void leftRotate(RBTNode<T> x) {
        // 得到x的右孩子y
        RBTNode<T> y = x.right;

        // x的右孩子 为 y的左孩子
        x.right = y.left;

        // 如果 y的左孩子 非空,y的左孩子的父亲 为 x
        if (y.left != null)
            y.left.parent = x;

        // y的父亲 为 x的父亲
        y.parent = x.parent;

        if (x.parent == null) {
            this.Root = y;            // 如果 x的父亲 是空节点,则说明原来的 x 是 根节点,则根节点为y
        } else {
            if (x.parent.left == x)
                x.parent.left = y;    // 如果 x是它父节点的左孩子,则x的父节点的左孩子 为y
            else
                x.parent.right = y;    // 如果 x是它父节点的右孩子,则x的父节点的右孩子 为y
        }

        // y的左孩子 为 x
        y.left = x;
        //x的父节点 为 y
        x.parent = y;
    }

2. 右旋

《亲自动手画红黑树》
右旋其实和左旋差不多的,只是旋转方向发生了变化。
对节点X 进行右旋,也就是:将节点X变为原节点位置的右节点,也实际是对左子树进行调整,将左子树提示一层,对于Y右子节点b 比X小,比Y大,自然就该放到X的左子树上。

    private void rightRotate(RBTNode<T> x) {

        // 得到x左孩子y。
        RBTNode<T> y = x.left;

        // x的左孩子 为 y的右孩子
        x.left = y.right;

        // 如果 y的右孩子不为空,y的右孩子的父亲 为 x
        if (y.right != null)
            y.right.parent = x;

        // y的父亲 为 x的父亲
        y.parent = x.parent;

        if (x.parent == null) {
            this.Root = y;            // 如果 x的父亲 是空节点,那么说明x是根节点,则根节点 为y
        } else {
            if (x == x.parent.right)
                x.parent.right = y;    // 如果 x是它父节点的右孩子,则x的父节点的右孩子为y
            else
                x.parent.left = y;    // 如果 x是它父节点的左孩子 则x的父节点的左孩子为y
        }

        // y的右孩子 为 x
        y.right = x;

        //  x的父节点为 y
        x.parent = y;
    }

说完最基本的左旋右旋,接下来才真正进入红黑树的操作部分

红黑树的添加

红黑树作为一颗二叉查找树,将节点插入,那么节点必然为叶子节点;然后将插入节点着色为红色,如果违背了红黑树的定义,那么再通过旋转和重新着色等方法来修正该树,使之重新成为一颗红黑树。

为什么将插入的节点着色为红色
在回答之前,我们需要重新温习一下红黑树的特性:
(1) 每个节点要么是黑色,要么是红色。
(2) 根节点是黑色。
(3) 每个叶子节点是黑色。 (这里叶子节点,是指为空的叶子节点)
(4) 如果一个节点是红色的,则它的子节点必须是黑色的。
(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

将插入的节点着色为红色,不会违背 性质5,少违背一条特性,就意味着我们需要处理的情况越少。

将插入节点着色为红色之后,不会违背性质5。那它到底会违背哪些特性呢?

  • 对于”性质1″,显然不会违背了。因为我们已经将它涂成红色了。

  • 对于”性质2″,显然也不会违背。插入操作不会改变根节点。所以,根节点仍然是黑色。

  • 对于”性质3″,显然不会违背了。这里的叶子节点是指的空叶子节点,插入非空节点并不会对它们造成影响。

  • 对于”性质4″,是有可能违背的,因为我们插入的节点的父节点可能为红色。

那接下来,想办法使之满足”性质4″,就可以将树重新构造成红黑树了,如果插入节点的父节点是黑色节点,那岂不是也满足了性质4呢,是的,也就是说,只有在父节点为红色节点的时候才需要插入修复操作的,插入修复操作如果遇到父节点的颜色为黑则修复操作结束,这种情况下会出现三种情况,接下来我们一步一步的来理解红黑树的插入过程

插入节点的父节点是黑色

解决方案:不用解决

《亲自动手画红黑树》

插入节点的父节点是红色

1. (Case 1)叔叔是红色

当前节点(即,被插入节点)是红色,其父节点也为红色,违背了”性质4 不能连续的两个红色节点”。
解决方案

  1. 将“父节点”设为黑色。
  2. 将“叔叔节点”设为黑色。
  3. 将“祖父节点”设为“红色”。
  4. 将“祖父节点”设为“当前节点”(红色节点);继续对“当前节点”进行修复操作。

下面来看看图,为了更好的理解,这里完整的演示整个过程。

第一步:
《亲自动手画红黑树》

第二步:修复操作(当前节点为 20,用cur指代)
《亲自动手画红黑树》

第三步:继续对新的当前节点进行修复操作
这里因为到达了根节点,所以停止了修复,如果当前节点不是根节点,或者当前节点的父节点不是黑色节点,那么就需要继续向上修复。

第四步:继续插入新节点
《亲自动手画红黑树》

对比图,思考一下,为什么要这样做?
出现这种情况,最简单的想法就是把父节点变成黑色,但是这样父节点路径上就多了一个黑色节点,那这样再把祖父节点变成红色(祖父节点原来肯定是黑色,因为父节点是红色),这个时候父节点的兄弟节点(也就是插入节点的叔叔节点)就很重要了,因为导致了叔叔节点分支少了一个黑色,如果叔叔节点为红色,那么很好办,把叔叔改为黑色就可以了,当然如果叔叔节点为空,那么就更没有影响了。
这就是第一种情况。

第五步:插入新节点–再接再厉
《亲自动手画红黑树》

这个时候叔叔节点是黑色,不是case 1这种情况了,那就接着往下看吧

2. (Case 2)叔叔是黑色(或缺失),且祖父节点、父节点和新节点处于一条斜线上

其大致结构如下(未列举完):
《亲自动手画红黑树》
解决方案

  1. 将“父节点”设为“黑色”。
  2. 将“祖父节点”设为“红色”。
  3. 以“祖父节点”为支点进行旋转(左或者右)。

还是通过图来理解吧(case 1最后的图)

《亲自动手画红黑树》

思考一下:
目前 所有分支的黑色节点个数是一致的,现在出现两个连续红色节点,那么简单点,将其中一个改成黑色节点,将父节点变成黑色,那么以父节点的局部子树是平衡的,但是 父节点的兄弟节点可能是不平衡的,因为父节点路径多了一个黑色,那么这个黑色节点放在哪里? 只能往根节点移,将祖父变红,这样祖父的另一个分支将少一个黑色节点,再把祖父右旋,这样父节点代替了原来的祖父,也就相当于把多的黑色节点,放在了公共的祖父位置上,因为水平有限,可能解释得不是那么通俗,自己思考思考,理解到思想就好了。

3. (Case 3)叔叔是黑色(或缺失),且祖父节点、父节点和新节点不处于一条斜线上。

其大致结构如下(未列举完):
《亲自动手画红黑树》

解决方案

  1. 将“父节点”作为“新的当前节点”。
  2. 以“新的当前节点”为支点进行旋转(左或者右)。

《亲自动手画红黑树》
当前节点48 和父节点45以及祖父节点50不在一条直线上,且叔叔节点为黑色,这是case3,按case3调整后,发现这颗树变成case2中的情况了,接下来再按case2调整就可以了。

这个其实和case 2中大同小异,通过这三种情况,可以看出来,其核心思想就是将红色的节点移到根节点;然后,将根节点设为黑色
看完了图解,是否想看看实际的代码呢,这个在jdk中完全有实现,可以参考下面的博客的内容,里面有代码分析,同时该文章也是基于本文内容写的,因此结合看效果比较好。

Java集合之TreeMap源码分析

红黑树的删除

删除操作首先需要做的也是搜索二叉树的删除操作,删除操作会删除对应的节点,如果是叶子节点就直接删除,如果是非叶子节点,会用对应的中序遍历的后继节点来顶替要删除节点的位置。删除后就需要做删除修复操作,使的树符合红黑树的定义。

第一步:将红黑树当作一颗二叉查找树,将节点删除。
① 被删除节点为叶节点。那么,直接将该节点删除就OK了。
② 被删除节点只有一个孩子。那么,用孩子节点的代替该节点即可。
③ 被删除节点有两个孩子。那么,先找出它的后继节点;然后把“它的后继节点的内容”复制给“该节点的内容”;之后,删除“它的中序后继节点”,其中序后继节点不可能存在有两个孩子节点的情况,因此就转换成了①,②的情况了。

第二步:通过”旋转和重新着色”等一系列来修正该树,使之重新成为一棵红黑树。

在插入操作中,当插入节点的父节点是黑色节点时,那么就可以不用进行修复操作了,同样的,在删除过程中,如果删除的是红色节点,那么就不需要进行删除的修复操作,因此只有再删除黑色节点后,才会涉及到修复操作,删除修复操作在遇到被删除的节点是红色节点或者到达root节点时,修复操作完毕。

实际删除的是红色节点

因为在删除节点的时候,我们会找其中序遍历的后继节点来来代替,将代替节点的值 赋值给要删除的节点,然后再转换成删除替代节点的方法,因此这里的实际删除节点就是指 真正删除的节点,有替代节点的时候就指的是替代节点,否则就是指的它本身。

删除红色节点42:
《亲自动手画红黑树》

删除红色节点40:
《亲自动手画红黑树》

实际删除的是黑色节点

1. (Case 1)当前节点的兄弟节点是红色的节点

当前节点(cur)指代的是实际被删除的节点

由于兄弟节点是红色节点的时候,无法借调黑节点,所以需要将兄弟节点提升到父节点,由于兄弟节点是红色的,那么兄弟节点的子节点是黑色的,就可以从它的子节点借调了。

解决方案

  1. 将 “兄弟节点”设为 “黑色”

  2. 将 “父节点”设为“红色”

  3. 对 “父节点”进行旋转

《亲自动手画红黑树》

上面的当前节点是父节点的左孩子,调整兄弟节点所在的分支,将其提升一层,进行左旋操作,如果当前节点是父节点的右孩子,那么进行右旋操作即可。

2. (Case 2)当前节点的兄弟节点是黑色的节点,且兄弟节点的子节点都是黑色的

case 2的删除操作是由于兄弟节点可以消除一个黑色节点,因为兄弟节点和兄弟节点的子节点都是黑色的,所以可以将兄弟节点变红,这样就可以保证树的局部的颜色符合定义了。这个时候需要从父节点开始,继续向上调整,直到整颗树的颜色符合RBTree的定义为止。

解决方案::

  1. 将 “兄弟节点”设为“红色”

  2. 将 当前节点待调整节点 指向其“父节点”,继续对当前节点进行修复

《亲自动手画红黑树》

兄弟节点45的左右孩子为空,可以看做是黑色的(性质3)。

这种情况下之所以要将兄弟节点变红,是因为如果把兄弟节点借调过来,会导致两边的黑色节点不一致,这样的情况下只能是将兄弟节点也变成红色来达到颜色的平衡。当将兄弟节点也变红之后,达到了局部的平衡了,但是对于祖父节点来说是不符合性质4的。这样就需要回溯到父节点,接着进行修复操作。

3. (Case 3)当前节点 的兄弟节点是黑色,且兄弟节点的左孩子是红色(兄弟节点在右边),右孩子是黑色的

如果兄弟节点在左边的话,就是兄弟节点的左孩子是黑色的,右孩子是红色的

解决方案::

  1. 将 “兄弟节点”的 红孩子设为“黑色”

  2. 将 “兄弟节点”设为“红色”

  3. 对“兄弟节点”进行旋转(左或者右)。

《亲自动手画红黑树》
《亲自动手画红黑树》

在上面图示中,最后虽然达到平衡了,但是有没有发现,直接删掉节点45时,不用调整也是平衡的,这种情况肯定是可能的,至于为什么,其实思考一下,很容易就明白了,删除节点45后,节点45的父节点是红色,同时又是该分支最后一个节点,因此删掉改节点后,就完全删掉了该分支,当然留下的其它分支自然也是平衡的,我们再来看看下面这个例子:

《亲自动手画红黑树》
《亲自动手画红黑树》
《亲自动手画红黑树》

这个开始那个差别就很大了,再删除节点45时,找到替代节点48,转而演变成了删除节点48,删除节点48后,该分支并没有完全去掉,所以该分支是不平衡的,需要进行修复操作。
当调整结束后,我们发现并没有平衡,这个时候就需要继续往下看了。

4. (Case 4)当前节点 的兄弟节点是黑色的节点,且右孩子是红色的(兄弟节点在右边)

如果兄弟节点在左边,则就是对应的就是左节点是红色的

  1. 将 “父节点”颜色 赋值给 “兄弟节点”

  2. 将 “父节点”设为黑色

  3. 将 “兄弟节点”的红孩子设为黑色

  4. 对 “父节点”进行旋转(左或者右)

  5. 设置当前节点为根节点

这种情况就是描述的是case 1中我们最后图示的情况,接下来我们看看如何修复的

《亲自动手画红黑树》
《亲自动手画红黑树》

最后我们再来看看删除操作,如果实际删除的是红色节点,并不会违背红黑树的性质,如果实际删除的是黑色节点,那么就会导致该分支丢掉一个黑色节点,就可能会造成该分支的黑色节点个数和其它分支黑色节点数目不一致,导致红黑树失衡,那么就需要想办法把局部变平衡,而局部变平衡可以通过把兄弟分支减少黑色一个节点或者通过旋转,重新染色,重新调整左右分支达到,局部平衡后,并不意味着就全局平衡了,需要不断的往上调整,当遇到当前节点是红色节点(或根节点)时调整结束(向上调整过程中,其局部已经平衡,遇到红色节点,那么以红色节点为根节点的树是平衡的)

OK!至此,红黑树的理论知识差不多讲完了,断断续续还是花了几天的时间,最开始,因为原来处理过其它平衡树,觉得应该不会太难,当大致明白什么是红黑树后,感觉只要明白怎么操作,应该不会太难了,后来网上找了一些分析红黑树的,发现怎么越看越晕,大部分博客都是大同小异,而且各有各的说法,有些细节又不说明白,配的图都是为了演示case ,导致理解不透彻(当然可能我的领悟能力有点渣),后来又去看了看实现代码,发现有些代码又和图描述有差异,最后通过代码和描述结合才明白,然后通过自己的理解,画了一下例子,方便理解,自己能力有限,有些地方还是阐述的不太通俗,如果有发现误导或者很迷惑的地方,欢迎大家指出,互相学习。

本文图解可以结合下面的代码一起来分析,看看jdk中是如何实现红黑树的。

Java集合之TreeMap源码分析

    原文作者:算法小白
    原文地址: https://blog.csdn.net/u014634338/article/details/78007490
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞