1.红黑树的概念
红⿊树是⼀棵⼆叉搜索树,他的每个结点增加⼀个存储位来表⽰结点的颜⾊,可以是红⾊或者⿊⾊。 通过对任何⼀条从根到叶⼦的路径上各个结点的颜⾊进⾏约束,红⿊树确保没有⼀条路径会⽐其他路 径⻓出2倍,因⽽是接近平衡的。
这就是一个简单的红黑树。
红黑树也是一颗平衡搜索二叉树,我们在上一篇讲了AVL树,AVL树的搜索效率已经很高了,为什么还要研究出一个红黑树呢?
在研究出AVL树后,大佬们感觉AVL树的规则过于严格,想让其控制平衡的规则没有那么严格,所以才有了红黑树,而红黑树是怎么控制平衡的呢?
2.红黑树的规则
- 每个结点不是红⾊就是⿊⾊
- 根结点是⿊⾊的
- 如果⼀个结点是红⾊的,则它的两个孩⼦结点必须是⿊⾊的,也就是说任意⼀条路径不会有连续的 红⾊结点。
- 对于任意⼀个结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的⿊⾊结点
说明:《算法导论》等书籍上补充了⼀条每个叶⼦结点(NIL)都是⿊⾊的规则。他这⾥所指的叶⼦结点 不是传统的意义上的叶⼦结点,⽽是我们说的空结点,有些书籍上也把NIL叫做外部结点。NIL是为了 ⽅便准确的标识出所有路径,《算法导论》在后续讲解实现的细节中也忽略了NIL结点,所以我们知道 ⼀下这个概念即可。
第一点我们从上面的图也可以看出来,而第二三四条就是红黑树控制平衡的关键。
我们来思考一个问题:红黑树是如何保证最长路径不超过最短路径的2倍的?
1.由规则4可知,从根到NULL结点的每条路径都有相同数量的⿊⾊结点,所以极端场景下,最短路径就是全是⿊⾊结点的路径,假设最短路径⻓度为bh(black height)。
2.由规则2和规则3可知,任意⼀条路径不会有连续的红⾊结点,所以极端场景下,最⻓的路径就是⼀ ⿊⼀红间隔组成,那么最⻓路径的⻓度为2*bh。
3.综合红⿊树的4点规则⽽⾔,理论上的全⿊最短路径和⼀⿊⼀红的最⻓路径并不是在每棵红⿊树都存在的。假设任意⼀条从根到NULL结点路径的⻓度为x,那么bh <= h <= 2*bh。
从这里我们就可以看出为何红黑树可以来控制平衡,那么红黑树的搜索效率怎么样呢?
3.红黑树的搜索效率
假设N是红⿊树树中结点数量,h最短路径的⻓度,那么2 的h 次方 − 1 <= N < 2 的 2∗ h次方 − 1 , 由此推出h ≈ logN ,也就是意味着红⿊树增删查改最坏也就是⾛最⻓路径 2 ∗ logN ,那么时间复杂度还是O ( logN )。
红⿊树的表达相对AVL树要抽象⼀些,AVL树通过⾼度差直观的控制了平衡。红⿊树通过4条规则的颜⾊约束,间接的实现了近似平衡,他们效率都是同⼀档次,但是相对⽽⾔,插⼊相同数量的结点,红⿊树的旋转次数是更少的,因为他对平衡的控制没那么严格。
对于红黑树的搜索效率,我们只要知道它和AVL树是同一梯队的即可,都为 O ( logN )。
4.红黑树的实现
4.1红黑树的初始化
大家是否还记得在上一篇开头中我们说对于平衡二叉搜索树而言,平衡因子并不是唯一的解决方法。
而在红黑树中我们不再需要平衡因子,而是利用颜色来控制,这里我用枚举来创建两个颜色:红色和黑色,在RBTreeNode中也将平衡因子换成了_col。
4.2红黑树的插入
和AVL树一样,前面的插入都要遵循二叉搜索树的规则,所以前面的代码是一样的,而在下面的操作中AVL树是更新平衡因子,而红黑树没有平衡因子,所以下面就是更新颜色来控制平衡。
而在讲解更新颜色之前我们先来看一下红黑树插入一个值得大概过程:
- 插⼊⼀个值按⼆叉搜索树规则进⾏插⼊,插⼊后我们只需要观察是否符合红⿊树的4条规则。
- 如果是空树插⼊,新增结点是⿊⾊结点。如果是⾮空树插⼊,新增结点必须红⾊结点,因为⾮空树插⼊,新增⿊⾊结点就破坏了规则4,规则4是很难维护的。
- ⾮空树插⼊后,新增结点必须红⾊结点,如果⽗亲结点是⿊⾊的,则没有违反任何规则,插⼊结束。
- ⾮空树插⼊后,新增结点必须红⾊结点,如果⽗亲结点是红⾊的,则违反规则3。进⼀步分析,c是红⾊,p为红,g必为⿊,这三个颜⾊都固定了,关键的变化看u的情况,需要根据u分为以下⼏种情况分别处理。
说明:下图中假设我们把新增结点标识为c (cur),c的⽗亲标识为p(parent),p的⽗亲标识为
g(grandfather),p的兄弟标识为u(uncle)。
所以这里就是为什么我在上面的初始化步骤将颜色默认是红色 ,下面就是根据u的状态来更新颜色:
而u的状态我们分为三类:
1.u存在且为红色
2.u不存在
3.u存在且为黑色
4.2.1情况1:变色
c为红,p为红,g为⿊,u存在且为红,则将p和u变⿊,g变红。在把g当做新的c,继续往上更新。
分析:因为p和u都是红⾊,g是⿊⾊,把p和u变⿊,左边⼦树路径各增加⼀个⿊⾊结点,g再变红,相当于保持g所在⼦树的⿊⾊结点的数量不变,同时解决了c和p连续红⾊结点的问题,需要继续往上更新是因为,g是红⾊,如果g的⽗亲还是红⾊,那么就还需要继续处理;如果g的⽗亲是⿊⾊,则处理结束 了;如果g就是整棵树的根,再把g变回⿊⾊。
情况1只变⾊,不旋转。所以⽆论c是p的左还是右,p是g的左还是右,都是上⾯的变⾊处理⽅式。
这就是第一种情况:u存在且为红的处理方式,下面我们来看这种情况的示意图:
在这种情况中我们是不需要旋转的,只需要变色即可。当然,这只是其中最简单的一种情况,所以我们一般看抽象图。
和上一篇AVL树一样,我们来看一看某些情况来理解为什么我们要看抽象图。
这是hb=0的情况。
这是hb=1的情况。
这是hb=2的情况。
这里可以看出,相比hb=1而言仅仅只多了一个hb,就多了非常多种情况,所以说我们要要看抽象图。
下面我们就实现下变色这种情况的代码:

在控制循环条件中我们不仅要判断此时parent是否为空,还要判断此时的parent是红色才能进入循环,如果parent是黑色就不需要更新颜色。
这里我们先写parent在grandfather的左边的情况,将三种情况写完,parent在右边直接复制过去,再改些值就可以。
里面的代码就是根据我们上面写的变色过程来完成,并且在完成一次变色后,要继续向上判断。
4.2.2情况2:单旋+变色
c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点,u存在且为⿊,则 c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变⾊将c从⿊⾊变成红⾊,更新上来的。
分析:p必须变⿊,才能解决,连续红⾊结点的问题,u不存在或者是⿊⾊的,这⾥单纯的变⾊⽆法解 决问题,需要旋转+变⾊。
如果p是g的左,c是p的左,那么以g为旋转点进⾏右单旋,再把p变⿊,g变红即可。p变成这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且不需要往上更新,因为p的⽗亲是⿊⾊还是红⾊或者空都不违反规则。
如果p是g的右,c是p的右,那么以g为旋转点进⾏左单旋,再把p变⿊,g变红即可。p变成这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且不需要往上更新,因为p的⽗亲是⿊⾊还是红⾊或者空都不违反规则。
根据上面的规则我们来写情况2的代码:
右单旋我们在AVL树那一章节已经讲过,这里就不过多赘述,红黑树中的右单旋与其不同的是不需要判断平衡因子的情况了。
剩下的就和AVL树中的一样,最后将parent和grandfather变色即可。
最后我们来看一下单旋+变色的过程。
4.2.3情况3:双旋+变色
c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点,u存在且为⿊,则 c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变⾊将c从⿊⾊变成红⾊,更新上来的。
分析:p必须变⿊,才能解决,连续红⾊结点的问题,u不存在或者是⿊⾊的,这⾥单纯的变⾊⽆法解决问题,需要旋转+变⾊。
如果p是g的左,c是p的右,那么先以p为旋转点进⾏左单旋,再以g为旋转点进⾏右单旋,再把c变
⿊,g变红即可。c变成这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且不需要往上更新,因为c的⽗亲是⿊⾊还是红⾊或者空都不违反规则。
如果p是g的右,c是p的左,那么先以p为旋转点进⾏右单旋,再以g为旋转点进⾏左单旋,再把c变
⿊,g变红即可。c变成这颗树新的根,这样⼦树⿊⾊结点的数量不变,没有连续的红⾊结点了,且不需要往上更新,因为c的⽗亲是⿊⾊还是红⾊或者空都不违反规则。
我们根据上面的规则来写情况3的代码:
经过上面的单旋其实我们也可以看出,红黑树的单旋和双旋情况和AVL树是很类似的,无非就是需要旋转的条件不同,但是一旦要旋转,结点的分布是一样的。
旋转过后改变cur和grandfather的颜色即可。
最后我们看一下双旋+变色的过程。
下面就是parent在grandfather右边的代码:
这里要注意的是我们在旋转过后就要break来停止循环,否则会出问题的。
这里可能有人会有一个疑惑:那经过上述的操作后,如果根结点被改为红色呢?
我相信在讲上面的操作中,有人疑惑我为什么不判断根结点是否被改变,因为我们这里可以暴力解决:
我不管你中间经过了什么步骤,在循环结束时,我直接对根结点的_col进行修改,将其改为黑色,这样就完美解决上面的问题,并且简单又简洁。
5.红黑树的判断
我们在写完了红黑树,如何判断它是否是红黑树呢?
在AVL树的章节,我们是通过判断高度差来判断是否是AVL树,而在红黑树中显然我们不能这样判断。
那么能不能根据颜色来判断呢?
答案也不能,在红黑树中,我们要判断的话要根据红黑树的四条规则来判断,这样才严谨。
我们判断的思路是:先找出任意一条路径的黑色节点的个数,在遍历其他路径判断黑色节点的个数是否相同,并在遍历的途中判断有没有连续的红色结点,这样就能兼顾红黑树的四条规则。
在上面的代码中,我以最左边的路径为标准,来检查左右子树是否都为红黑树。
其中要注意的就是检查有无两个连续的红色结点时,要找其parent,原因上面我也写了,最后如果左右子树都是红黑树返回true,不满足就返回false。
最后我们来看一下递归过程的示意图。
还有一些像:Find,Inorder,Size和Height这些函数和AVL树那篇中的一样,这里就不写了。
而红黑树的删除和AVL树的删除一样都不在讲解,理由也是一样的,对于两者而言,我们通过插入了解两者是如何控制平衡的即可,熟悉控制平衡的过程,都有哪些情况。
以上就是红黑树的内容。