文章目录
1 引言
红黑树
是树
的数据结构
中最为重要的一种。Java
的容器TreeSet
、TreeMap
均使用红黑树
实现。JDK1.8
中HashMap
中也加入了红黑树
。C++ STL
中的map
和set
同样使用红黑树
实现。之前的文章已经详细介绍了2-3-4树
的性质与操作。本篇文章将从2-3-4树
与红黑树
关系出发,详细阐明红黑树
。
2-3-4树
和红黑树
是完全等价
的,但是2-3-4树
的编程实现相对复杂,所以一般是通过实现红黑树
来实现替代2-3-4树
,而红黑树
也同样保证在O(logN)
的时间内完成查找
、插入
和删除
操作。
2 定义
红黑树
是每个节点
都带有颜色属性
的平衡二叉查找树
,颜色
为红色
或黑色
。除了二叉查找树
一般要求以外,对于任何有效的红黑树
我们增加了如下的额外要求
:
(1)节点
是要么红色
或要么是黑色
。
(2)根
一定是黑色节点
。
(3)每个叶子节点
都带有两个空
的黑色节点
(称之为NIL节点
,它又被称为黑哨兵
)。
(4)每个红色节点
的两个子节点
都是黑色
(或者说从每个叶子
到根
的所有路径上
不能有两个连续
的红色节点
)。
(5)对于任一节点
而言,其到叶节点树尾端NIL指针
的每一条路径
都包含相同数目
的黑节点
从性质5又可以推出:
性质5.1:如果一个结点存在黑子结点
,那么该结点肯定有两个子结点
图1就是一颗简单
的红黑树
。其中Nil
为叶子结点
,并且它是黑色
的。(值得提醒注意的是,在Java
中,叶子结点
是为null
的结点。)
根据红黑树以上五条定义,我们可以推导出以下几点性质和可能情景:
- 性质1. 父子节点不可能同时是红节点,即红节点不连续。否则违反定义4
- 性质2. 如果某节点的一个子节点是黑色,那么该节点必然存在另一个子节点。否则违反定义5
- 情景1. 一个结点的两个子节点出现一个是红色,一个是黑色的情况是可能出现的
- 情景2. 父子节点同时是黑色的情况是可能出现的
- 规律1. 新插入的节点初始化为红色,能最小化变色操作
现在开始说明红黑树的自平衡。
首先是插入节点的着色。所有关于红黑树自平衡的介绍都说,插入的节点为红色。至于为什么,笔者认为,由于性质3与性质5,任意一结点到每个叶子结点的路径都包含数量相同的黑,且每个叶子节点(NIL)是黑色。新插入的节点初始位置必为替换某个叶子节点,同时自己又会新产生左右两个叶子节点。若新插入的节点为黑,则将会引起从根节点到新节点的路径上黑色节点数量比其它路径多了1。而插入红色节点,则不会有此情况,但是有可能会违反性质4,每个红色结点的两个子结点一定都是黑色,出现连续的两个红色节点,但是不是必然发生。所以新插入的节点为红色,可以使得需要自平衡操作的几率降低。
例如:下图2.1所示的一棵红黑树
:
定义节点名称:
父节点——P(Parent)
祖父节点——G(GrandParent)
叔叔节点——U(Uncle)
当前节点——C(Current)
兄弟节点——B(Brother)
左孩子——L(Left)
右孩子——R(Right)
前面讲到红黑树能自平衡,它靠的是什么?三种操作:左旋、右旋和变色。
- 左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。如图3。
- 右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。如图4。
- 变色:结点的颜色由红变黑或由黑变红。
图3 左旋
图4 右旋
上面所说的旋转结点也即旋转的支点,图4中的P结点。
我们先忽略颜色,可以看到旋转操作不会影响旋转结点的父结点,父结点以上的结构还是保持不变的。
左旋只影响旋转结点和其右子树的结构,把右子树的结点往左子树挪了。
右旋只影响旋转结点和其左子树的结构,把左子树的结点往右子树挪了。
所以旋转操作是局部的。另外可以看出旋转能保持红黑树平衡的一些端详了:当一边子树的结点少了,那么向另外一边子树“借”一些结点;当一边子树的结点多了,那么向另外一边子树“租”一些结点。
但要保持红黑树的性质,结点不能乱挪,还得靠变色了。怎么变?具体情景又不同变法,后面会具体讲到,现在只需要记住红黑树总是通过旋转和变色达到自平衡。
相信你对红黑树有一定印象了,那么现在来考考你:
思考题1:黑结点可以同时包含一个红子结点和一个黑子结点吗? (答案见文末)
3 性质
根节点
到任意叶子节点
的路径长度
,最多相差一半
。若树
存在最短路径
,则最短路径
上均为黑色节点
,那么第五条性质
保证根节点到达最长路径
与最短路径
所包含
的黑色节点数目相同
,若最短路径长
为N
,则最长路径M=N+红色节点数目
,性质4
要求红色节点
必定不连续
,因此红色节点数目最多
为N
,则最长路径
与最短路径
最多相差N
。
4 2-3-4树和红黑树的等价关系
如果一棵树
满足红黑树
,把红色节点收缩
到其父节点
,就变成了2-3-4树
,所有红色节点
都与其父节点
构成3
或4节点
,其它节点
为2节点
。一颗红黑树
对应唯一形态
的2-3-4树
,但是一颗2-3-4树
可以对应多种形态
的红黑树
(主要是3节点
可以对应两种不同
的红黑树形态
)。
例如:
5 查找
红黑树的查找操作与二叉搜索树查找方式一致,这里不再赘述。
因为红黑树是一颗二叉平衡树,并且查找不会破坏树的平衡,所以查找跟二叉平衡树的查找无异:
- 从根结点开始查找,把根结点设置为当前结点; 若当前结点为空,返回null;
- 若当前结点不为空,用当前结点的key跟查找key作比较;
- 若当前结点key等于查找key,那么该key就是查找目标,返回当前结点;
- 若当前结点key大于查找key,把当前结点的左子结点设置为当前结点,重复步骤2;
- 若当前结点key小于查找key,把当前结点的右子结点设置为当前结点,重复步骤2;
6 插入
插入操作包括两部分工作:一查找插入的位置;二插入后自平衡。查找插入的父结点很简单,跟查找操作区别不大:
- 从根结点开始查找;
- 若根结点为空,那么插入结点作为根结点,结束。
- 若根结点不为空,那么把根结点作为当前结点;
- 若当前结点为null,返回当前结点的父结点,结束。
- 若当前结点key等于查找key,那么该key所在结点就是插入结点,更新结点的值,结束。
- 若当前结点key大于查找key,把当前结点的左子结点设置为当前结点,重复步骤4;
- 若当前结点key小于查找key,把当前结点的右子结点设置为当前结点,重复步骤4;
ok,插入位置已经找到,把插入结点放到正确的位置就可以啦,但插入结点是应该是什么颜色呢?答案是红色。理由很简单,红色在父结点(如果存在)为黑色结点时,红黑树的黑色平衡没被破坏,不需要做自平衡操作。但如果插入结点是黑色,那么插入位置所在的子树黑色结点总是多1,必须做自平衡。
所有插入情景如图7所示。
图7 红黑树插入情景
插入情景1:红黑树为空树
最简单的一种情景,直接把插入结点作为根结点就行,但注意,根据红黑树性质2:根节点是黑色。还需要把插入结点设为黑色。
处理:把插入结点作为根结点,并把结点设置为黑色。
插入情景2:插入结点的Key已存在
插入结点的Key已存在,既然红黑树总保持平衡,在插入前红黑树已经是平衡的,那么把插入结点设置为将要替代结点的颜色,再把结点的值更新就完成插入。
处理:
- 把I设为当前结点的颜色
- 更新当前结点的值为插入结点的值
6.1(情景3) 插入节点的父节点为黑色
若待插入节点
的父节点
为黑色
,则直接插入节点
,并将插入
的节点涂红
,插入结束。父节点
为黑色
,插入红色节点
并不会
使红黑树违背5条性质
。
此种情形对应的在2-3-4树
中的插入操作
为:待插入位置
的节点
为2-节点
或者3-节点
,直接插入节点
。
图解:
6.2(情景4) 插入节点的父节点为红色,叔叔节点为黑色
单纯从插入前来看,也即不算情景4.1自底向上处理时的情况,叔叔结点非红即为叶子结点(Nil)。因为如果叔叔结点为黑结点,而父结点为红结点,那么叔叔结点所在的子树的黑色结点就比父结点所在子树的多了,这不满足红黑树的性质5。后续情景同样如此,不再多做说明了。
前文说了,需要旋转操作时,肯定一边子树的结点多了或少了,需要租或借给另一边。插入显然是多的情况,那么把多的结点租给另一边子树就可以了。
若待插入节点
的父节点
为红色
,叔叔节点
为黑色
,此种情形又需要区分节点
的插入位置
,根据插入位置
的不同
进行相应的调整。此种情形共有四种
:
6.2.1(情景4.2.1) 父节点P为G左孩子,插入位置为左孩子
**若待插入节点C
的父节点P
是祖父节点G
的左孩子
,并且插入节点
的位置位于父节点P
的左孩子
。调整过程如下:
处理:
- 将P设为黑色
- 将PP设为红色
- 对PP进行右旋
图解:
1:插入后违背性质4
,首先以祖父节点G
为中心
,执行右旋
操作。**
2:右旋
操作结束,将父节点P涂黑色
,祖父节点G涂红色
,调整完毕。
图11 插入情景4.2.1
由6.2.1中第一步可得,左边两个红结点,右边不存在,那么一边一个刚刚好,并且因为为红色,肯定不会破坏树的平衡。
咦,可以把P设为红色,C和G设为黑色吗? 答案是可以!看过《算法:第4版》的同学可能知道,书中讲解的就是把P设为红色,I和PP设为黑色。但把P设为红色,显然又会出现情景4.1的情况,需要自底向上处理,做多了无谓的操作,既然能自己消化就不要麻烦祖辈们啦~
6.2.2(情景4.2.2) 父节点P为G左孩子,插入位置为右孩子
若待插入节点C
的父节点P
是>祖父节点G
的左孩子
,并且插入节点
的位置位于父节点P
的右孩子
。调整过程如下:
处理:
- 对P进行左旋
- 把P设置为插入结点,得到情景4.2.1
- 进行情景4.2.1的处理
图解:
1:插入后违背性质4
,首先以父节点P
为中心
,执行左旋
操作。
2:左旋
操作结束后,仍然违背性质4
,此时的情形符合6.2.1
。在以节点G
为中心
,执行右旋
操作。右旋
结束后,将节点C涂黑色
,节点G涂红色
,调整完毕。
图12 插入情景4.2.2
6.2.3(情景4.3.1) 父节点P为G右孩子,插入位置为右孩子
若待插入节点C
的父节点P
是祖父节点G
的右孩子
,并且插入节点
的位置位于父节点P
的右孩子
。此种情形是6.2.1
情形的镜像
。调整过程如下:
处理:
- 将P设为黑色
- 将G设为红色
- 对G进行左旋
图解:
1:插入后违背性质4
,首先以祖父节点G
为中心
,执行左旋
操作。
2:左旋
操作结束,将父节点P涂黑色
,祖父节点G涂红色
,调整完毕。
图13 插入情景4.3.1
6.2.4(情景4.3.2) 父节点P为G右孩子,插入位置为左孩子
处理:
- 对P进行右旋
- 把P设置为插入结点,得到情景4.3.1
- 进行情景4.3.1的处理
若待插入节点C
的父节点P
是祖父节点G
的右孩子
,并且插入节点
的位置位于父节点P
的左孩子
。此种情形是6.2.2
的镜像
。调整过程如下:
图解:
1:插入
后违背性质4
,首先以父节点P
为中心
,执行右旋
操作。
2:右旋
操作结束后
,仍然违背性质4
,此时的情形符合6.2.3
。在以节点G
为中心
,执行左旋
操作。左旋
结束后,将节点C涂黑色
,节点G涂红色
,调整完毕。
好了,讲完插入的所有情景了。可能又同学会想:上面的情景举例的都是第一次插入而不包含自底向上处理的情况,那么上面所说的情景都适合自底向上的情况吗?答案是肯定的。理由很简单,但每棵子树都能自平衡,那么整棵树最终总是平衡的。好吧,在出个习题,请大家拿出笔和纸画下试试(请务必动手画下,加深印象):
习题1:请画出图15的插入自平衡处理过程。(答案见文末)
图15 习题1
6.3(情景4.1) 插入节点的父节点为红色,叔叔节点为红色
若待插入节点C
的父节点P
为红色
,且叔叔节点U
为红色
,则需要根据插入位置
的不同
,做出相应的调整。此种情形共有两种
:
6.3.1(情景4.1) 插入位置为左子树
6.3.2(情景4.1) 插入位置为右子树
从红黑树性质4可以,祖父结点肯定为黑结点,因为不可以同时存在两个相连的红结点。那么此时该插入子树的红黑层数的情况是:黑红红。显然最简单的处理方式是把其改为:红黑红
处理:
- 将P和U设置为黑色
- 将G设置为红色
- 把G设置为当前插入结点
可以看到,我们把G结点设为红色了,如果G的父结点是黑色,那么无需再做任何处理;但如果G的父结点是红色,根据性质4,此时红黑树已不平衡了,所以还需要把G当作新的插入结点,继续做插入操作自平衡处理,直到平衡为止。
试想下G刚好为根结点时,那么根据性质2,我们必须把G重新设为黑色,那么树的红黑结构变为:黑黑红。换句话说,从根结点到叶子结点的路径中,黑色结点增加了。这也是唯一一种会增加红黑树黑色结点层数的插入情景。
我们还可以总结出另外一个经验:红黑树的生长是自底向上的。这点不同于普通的二叉查找树,普通的二叉查找树的生长是自顶向下的。
7 删除
红黑树插入已经够复杂了,但删除更复杂,也是红黑树最复杂的操作了。但稳住,胜利的曙光就在前面了!
红黑树的删除操作也包括两部分工作:一查找目标结点;而删除后自平衡。查找目标结点显然可以复用查找操作,当不存在目标结点时,忽略本次操作;当存在目标结点时,删除后就得做自平衡处理了。删除了结点后我们还需要找结点来替代删除结点的位置,不然子树跟父辈结点断开了,除非删除结点刚好没子结点,那么就不需要替代。
二叉树删除结点找替代结点有3种情情景:
- 情景1:若删除结点无子结点,直接删除
- 情景2:若删除结点只有一个子结点,用子结点替换删除结点
- 情景3:若删除结点有两个子结点,用后继结点(大于删除结点的最小结点)替换删除结点
补充说明下,情景3的后继结点是大于删除结点的最小结点,也是删除结点的右子树种最左结点。那么可以拿前继结点(删除结点的左子树最左结点)替代吗?可以的。但习惯上大多都是拿后继结点来替代,后文的讲解也是用后继结点来替代。另外告诉大家一种找前继和后继结点的直观的方法(不知为何没人提过,大家都知道?):把二叉树所有结点投射在X轴上,所有结点都是从左到右排好序的,所有目标结点的前后结点就是对应前继和后继结点。如图16所示。
图16 二叉树投射x轴后有序
接下来,讲一个重要的思路:删除结点被替代后,在不考虑结点的键值的情况下,对于树来说,可以认为删除的是替代结点!话很苍白,我们看图17。在不看键值对的情况下,图17的红黑树最终结果是删除了Q所在位置的结点!这种思路非常重要,大大简化了后文讲解红黑树删除的情景!
图17 删除结点换位思路
基于此,上面所说的3种二叉树的删除情景可以相互转换并且最终都是转换为情景1!
- 情景2:删除结点用其唯一的子结点替换,子结点替换为删除结点后,可以认为删除的是子结点,若子结点又有两个子结点,那么相当于转换为情景3,一直自顶向下转换,总是能转换为情景1。(对于红黑树来说,根据性质5.1,只存在一个子结点的结点肯定在树末了)
- 情景3:删除结点用后继结点(肯定不存在左结点),如果后继结点有右子结点,那么相当于转换为情景2,否则转为为情景1。
二叉树删除结点情景关系图如图18所示。
图18 二叉树删除情景转换
综上所述,删除操作删除的结点可以看作删除替代结点,而替代结点最后总是在树末。有了这结论,我们讨论的删除红黑树的情景就少了很多,因为我们只考虑删除树末结点的情景了。
同样的,我们也是先来总体看下删除操作的所有情景,如图19所示。
图19 红黑树删除情景
跟插入操作一样,存在左右对称的情景,只是方向变了,没有本质区别。同样的,我们还是来约定下,如图20所示。
图20 删除操作结点的叫法约定
图20的字母并不代表结点Key的大小。R表示替代结点,P表示替代结点的父结点,S表示替代结点的兄弟结点,SL表示兄弟结点的左子结点,SR表示兄弟结点的右子结点。灰色结点表示它可以是红色也可以是黑色。
值得特别提醒的是,R是即将被替换到删除结点的位置的替代结点,在删除前,它还在原来所在位置参与树的子平衡,平衡后再替换到删除结点的位置,才算删除完成。
万事具备,我们进入最后的也是最难的讲解。
删除情景1:替换结点是红色结点
7.1 删除红色叶子节点
我们把替换结点换到了删除结点的位置时,由于替换结点时红色,删除也了不会影响红黑树的平衡,只要把替换结点的颜色设为删除的结点的颜色即可重新平衡。
图解:
7.2 删除红色节点,只有左子树或只有右子树
图解:
7.3 删除红色节点,既有左子树又有右子树
如果要删除的节点不是叶子节点,用要删除节点的后继节点替换(只进行数据替换即可,颜色不变,此时也不需要调整结构),然后删除后继节点,后继节点的删除同样要遵循红黑树的删除过程。
7.4 删除的黑色节点仅有左子树或者仅有右子树
图解:
图中填充为绿色的节点代表既可以是红色也可以是黑色,下同。
7.5 删除黑色的叶子节点
当替换结点是黑色时,我们就不得不进行自平衡处理了。我们必须还得考虑替换结点是其父结点的左子结点还是右子结点,来做不同的旋转操作,使树重新平衡。
删除情景2.1:替换结点是其父结点的左子结点
删除情景2.1.1(情形1):替换结点的兄弟结点是红结点
若兄弟结点是红结点,那么根据性质4,兄弟结点的父结点和子结点肯定为黑色,不会有其他子情景,我们按图21处理,得到删除情景2.1.2.3(后续讲解,这里先记住,此时R仍然是替代结点,它的新的兄弟结点SL和兄弟结点的子结点都是黑色)。
处理:
- 将B设为黑色
- 将P设为红色
- 对P进行左旋,得到情景2.1.2.3
- 进行情景2.1.2.3的处理
图解:
图21 删除情景2.1.1
删除情景2.1.2:替换结点的兄弟结点是黑结点
当兄弟结点为黑时,其父结点和子结点的具体颜色也无法确定(如果也不考虑自底向上的情况,子结点非红即为叶子结点Nil,Nil结点为黑结点),此时又得考虑多种子情景。
删除情景2.1.2.1(情形3):替换结点的兄弟结点的右子结点是红结点,左子结点任意颜色
即将删除的左子树的一个黑色结点,显然左子树的黑色结点少1了,然而右子树又又红色结点,那么我们直接向右子树“借”个红结点来补充黑结点就好啦,此时肯定需要用旋转处理了。如图22所示。
处理:
- 将B的颜色设为P的颜色
- 将P设为黑色
- 将BR设为黑色
- 对P进行左旋
图解:
图22 删除情景2.1.2.1
平衡后的图怎么不满足红黑树的性质?前文提醒过,D是即将替换的,它还参与树的自平衡,平衡后再替换到删除结点的位置,所以D最终可以看作是删除的。另外图2.1.2.1是考虑到第一次替换和自底向上处理的情况,如果只考虑第一次替换的情况,根据红黑树性质,BL肯定是红色或为Nil,如果是黑色节点的话,违背了性质5,所以最终结果树是平衡的。如果是自底向上处理的情况,同样,每棵子树都保持平衡状态,最终整棵树肯定是平衡的。后续的情景同理,不做过多说明了。
删除情景2.1.2.2(情形5):替换结点的兄弟结点的右子结点为黑结点,左子结点为红结点
兄弟结点所在的子树有红结点,我们总是可以向兄弟子树借个红结点过来,显然该情景可以转换为情景2.1.2.1。图如23所示。
处理:
- 将B设为红色
- 将BL设为黑色
- 对B进行右旋,得到情景2.1.2.1
- 进行情景2.1.2.1的处理
图解:
图23 删除情景2.1.2.2
删除情景2.1.2.3(情形7):替换结点的兄弟结点的子结点都为黑结点
好了,此次兄弟子树都没红结点“借”了,兄弟帮忙不了,找父母呗,这种情景我们把兄弟结点设为红色,再把父结点当作替代结点,自底向上处理,去找父结点的兄弟结点去“借”。但为什么需要把兄弟结点设为红色呢?显然是为了在P所在的子树中保证平衡(R即将删除,少了一个黑色结点,子树也需要少一个),后续的平衡工作交给父辈们考虑了,还是那句,当每棵子树都保持平衡时,最终整棵总是平衡的。
处理:
- 将S设为红色
- 把P作为新的替换结点
- 重新进行删除结点情景处理
图24 情景2.1.2.3
删除情景2.2:替换结点是其父结点的右子结点
好啦,右边的操作也是方向相反,不做过多说明了,相信理解了删除情景2.1后,肯定可以理解2.2。
删除情景2.2.1(情形2):替换结点的兄弟结点是红结点
处理:
- 将B设为黑色
- 将P设为红色
- 对P进行右旋,得到情景2.2.2.3
- 进行情景2.2.2.3的处理
因为以P作为D节点的父亲节点,要删除D的话,就符合情景2.2.2.3,D的兄弟节点的子节点都是黑节点(java中NIL节点为null,图中没有画出)
图解:
图25 删除情景2.2.1
删除情景2.2.2:替换结点的兄弟结点是黑结点
删除情景2.2.2.1(情形4):替换结点的兄弟结点的左子结点是红结点,右子结点任意颜色
处理:
- 将B的颜色设为P的颜色
- 将P设为黑色
- 将BL设为黑色
- 对P进行右旋
图解:
图26 删除情景2.2.2.1
删除情景2.2.2.2(情形6):替换结点的兄弟结点的左子结点为黑结点,右子结点为红结点
处理:
- 将B设为红色
- 将BR设为黑色
- 对B进行左旋,得到情景2.2.2.1
- 进行情景2.2.2.1的处理
图解:
图27 删除情景2.2.2.2
删除情景2.2.2.3(情形8):替换结点的兄弟结点的子结点都为黑结点
处理:
- 将B设为红色
- 把P作为新的替换结点
- 重新进行删除结点情景处理
图解:
图28 删除情景2.2.2.3
情景2.2.2.3对应2-3-4树删除操作中兄弟节点为2节点,父亲节点也为2节点,父节点key下移与兄弟节点合并,已父节点看成新的节点D,继续回溯。
情形7:父节点P为红色,兄弟节点B和兄弟节点的两个孩子(只能是NIL节点)都为黑色的情况。
图解:
情形7对应2-3-4树删除操作中兄弟节点为2节点,父节点至少是个3节点,父节点key下移与兄弟节点合并。
综上,红黑树删除后自平衡的处理可以总结为:
- 自己能搞定的自消化(情景1)
- 自己不能搞定的叫兄弟帮忙(除了情景1、情景2.1.2.3和情景2.2.2.3)
- 兄弟都帮忙不了的,通过父母,找远方亲戚(情景2.1.2.3和情景2.2.2.3)
哈哈,是不是跟现实中很像,当我们有困难时,首先先自己解决,自己无力了总兄弟姐妹帮忙,如果连兄弟姐妹都帮不上,再去找远方的亲戚了。这里记忆应该会好记点~
最后再做个习题加深理解(请不熟悉的同学务必动手画下):
习题2:请画出图29的删除自平衡处理过程。
习题2
8 总结
红黑树是数据结构最为重要的一种树形结构,其应用场景也是十分广泛。本文通过详细的图解过程介绍了红黑树的插入与删除过程,希望读者可以深刻理解红黑树的性质与操作。
9 思考题和习题答案
思考题1:黑结点可以同时包含一个红子结点和一个黑子结点吗?
答:可以。如下图的F结点:
习题1:请画出图15的插入自平衡处理过程。
答:
习题2:请画出图29的删除自平衡处理过程。
答:
推荐阅读
来源:CSDN
作者:Kevin-Zeng
链接:https://blog.csdn.net/weixin_38927257/article/details/104156238