红黑树
- 红黑树也是一种自平衡二叉搜索树
- 以前也叫做
平衡二叉B树
(Symmetric Binary B-tree)
红黑树必须满足一下
5
条性质
- 节点是
Red
或者Black- 根节点必须是Black
叶子节点(外部节点、空节点)
都是Black,强制每个节点度为2
,子节点挂一个null
节点(空想,不需要实际添加空节点)Red
节点的子节点,必须都是Black
Red
节点的父节点,必须都是Black- 从根节点到叶子节点的所有路径上不能有
2
个连续的Red
节点- 从任一节点到叶子节点的所有路径都包含相同数目的Black节点
问题
为什么在这5条性质的约束下,就能保证二叉树的平衡
判断红黑树
请问下面这颗是红黑树吗
不是红黑树
- 满足大部分红黑树的性质
- 不满足
从任一节点到叶子节点的所有路径都包含相同数目的Black节点
- 将红黑树的null节点补齐
- 可观察到38节点的子节点个数不同
红黑树与4阶B树等价变换
如图所示的红黑树,令所有
红色节点
向上提升一层
红黑树
与4阶B树
具有等价性- Black节点与它的
Red
子节点融合在一起,形成1个B树节点- 红黑树中Black节点的个数
=
4阶B树的节点总个数 (4阶B树的每个节点中,有且只有一个Black节点)
分类
具体的,又可以分为4
种情况
红黑红
黑红
红黑
黑
红黑树术语
名称 | 说明 |
---|---|
parent | 父节点 |
sibling | 兄弟节点 :同层的其他节点 |
uncle | 叔父节点 :父节点的兄弟节点 |
grand | 祖父节点 :父节点的父节点 |
基本代码
基本代码
添加节点
关键
心中有B树
已知:
- B树中,新元素必定添加到叶子节点中
- 4阶B树所有节点的元素个数
x ∈ [1, 3]
- 推荐新添加的节点默认为
Red
,这样能够让红黑树的性质尽快满足(性质——Red
节点的父子节点必定是Black不一定满足)- 如果添加的节点是根节点,将节点染色为Black
添加的所有情况
如图所示,一共有
12
种情况
不需要做处理的4种情况(parent为Black)
- 默认新添加的节点颜色为
Red
- 如果新节点的父节点为Black,则满足红黑树
Red
节点parent节点为Black的性质,一共有4
种情况(5、10、11、12
)可以满足- 同样满足4阶B树的性质(新添加的
Red
节点融入原本的B树节点)- 因此不需要做任何额外处理
8种不满足红黑树Red父子节点为Black、出现连续红节点情况(parent为Red)
出现连续
Red
节点,Double Red
- 一共
8
种情况(1、2、3、4、6、7、8、9
)- 其中,
4
种情况满足新节点的uncle
为Red
,4
种情况满足uncle
不是Red
1、2、3、4
,新节点的uncle
为Red
5、6、7、8
,新节点的uncle
不是Red
添加——修正连续红色节点——uncle不是Red
LL|RR 单旋 (2种情况)
对应情况
7
、8
解决:
判定条件:
uncle
不是Red
RR:
- 将
parent
染为Black (图中50染为黑色)grand
染为Red
(图中46染为红色)grand
左旋(grand grand parent指向parent) (图中46左旋,46成为50的左子树,38指向50)LL:
- 将
parent
染为Blackgrand
染为Red
grand
右旋
LR|RL 双旋 (2种情况)
对应情况
6
、9
判定条件:
uncle
不是Red
- 新节点自己染为Black
grand
染为Red
- 旋转
- RL:
parent
右旋;grand
左旋- LR:
parent
左旋;grand
右旋
添加——修正连续红色节点——uncle是Red(红黑红)——上溢
LL 染色
对应情况
1
,此处对根节点进行简化,方便理解
- 根据4阶B树性质,发生上溢,需要将一个节点向上合并,原先的节点发生分裂
先处理分裂后的左右子树
- 新节点的
parent
从Red
染为Black (图中17)uncle
从Red
染为Black (图中33)
再处理向上合并节点
grand
向上合并 (图中25)
重要思路
grand
的向上合并,看一看做是对上层节点添加一个新节点的操作- 根据这个思路,可以将合并节点染为
Red
,之后无非就是红黑树添加的12
种情况,可以重复处理,即递归
合并的实现
- 将
grand
染为Red
,递归添加即可
注意
grand
向上合并时,可能发生上溢- 若上溢持续到根节点,只需将根节点染为Black
RR 染色
与LL染色一样
parent
染黑、uncle
染黑grand
向上合并,染红,递归添加
LR 染色
同理
RL 染色
同理
添加代码实现
- 重写
afterAdd(Node<E> newNode)
方法- 处理不需要额外操作的4种情况,即
parent
为Black的情况
- 不作处理,直接返回
- 处理
uncle
不是Red
的4种情况,需要旋转
- 旋转实现方式与
AVL树
一模一样,代码可以直接复用- LL——
grand
右旋,parent
染为Black,grand
染为Red
- RR——
grand
左旋,parent
染为Black,grand
染为Red
- LR——
parent
左旋、grand
右旋、自己染为Black,grand
染为Red
- RL——
parent
右旋,grand
左旋、自己染为Black,grand
染为Red
- 处理
uncle
是Red
的4种情况,仅需染色
parent
染为Blackuncle
染为Blackgrand
向上合并:将grand
当做新添加的节点,添加到上层节点中,即,grand
染为Red
再递归添加
注意
重写createNode方法,实例化红黑树节点,默认实例化的是Node节点,在染色方法中会触发强制类型转换异常
测试
重写string方法,这个方法是打印二叉树工具类的那个方法
- 对于红黑树的打印,我们希望观察节点的颜色
- 打印红节点的颜色,黑色节点默认不打印颜色
将这颗红黑树整理成4阶B树的形式
删除节点(维持红黑树)
注意:再次强调
在B树中,最终实际在内存层面上删除的,都是叶子节点
与添加类似,叶子节点这一层,会出现
红黑红
、红黑
、黑红
、黑
,四种情况
删除红色节点
不需要做任何处理,因为不会破坏红黑树的5条性质
删除黑色节点
一共有
3
种情况:
- 拥有
2
个Red
节点的Black节点 (红黑红)
- 不可能被直接删除,因为会找它的子节点替代删除(本质上讲,是其
前驱
或后继
节点)- 其两个子节点都是
Red
,而Red
的删除不需要做任何处理- 故,直接删除,不需要做任何处理
- 拥有
1
个Red
节点的Black节点 (黑红 | 红黑)- 删除的是Black叶子节点
删除拥有一个红色子节点的黑色节点(红黑|黑红)
对应图中46和76
判定条件:用以替代的子节点是
Red
(前驱 | 后继为Red
)
- 删除节点
- 将替代节点染为Black即可
删除黑色叶子节点
特点
分为两大情况
- 被删除节点的兄弟节点sibling为黑色
- 被删除节点的兄弟节点sibling为红色
第一类——兄弟节点为黑色
- 首先考虑:整个树中只有一个黑色节点,即,它是根节点,也是叶子节点——直接删除不做额外处理
- 删除真正的黑色叶子节点:如图中46、88节点,会发生下溢,需要从同辈兄弟节点哪里借节点
- 存在同辈兄弟节点的情况:且同辈兄弟节点必须为黑色(红色同辈节点实际上是4阶B树中的父节点,不是同辈兄弟节点)且同辈兄弟节点至少具有一个红色子节点
- 同辈为
黑红
- 同辈为
红黑
- 同辈为
红黑红
- 存在同辈兄弟节点的情况:且同辈兄弟节点必须为黑色,但孤苦伶仃,没有任何红色子节点
- 同辈为
黑
- 判定条件:如果兄弟sibling至少有
1
个Red
子节点
- 进行旋转操作
- 旋转之后的中心节点继承被删除节点的
parent
的颜色- 旋转之后,中心节点的左右子节点都保证是Black,不是Black就进行染色染成黑色
如下图所示
同辈为 黑红,要删除88,被借用元素为78,被借用元素属于被删除节点88的父节点80的L R,因此对78的p 76进行左旋,78的grand 80进行右旋,之后78称为中心节点,继承被删除节点88的父节点80的颜色(此处为红色),最后确保78的左右节点76、80都为黑色,因为80是红色,故将80染黑
同辈为 红黑,要删除88,被借用元素为72,被借用元素属于被删除元素88的父节点80的L L,因此对72的grand 80进行右旋,之后76称为中心节点,需要继承被删除元素88的父节点80的颜色,此处80为红色,故将76从黑染红,最后,确保76的左右节点72、80都为黑色,此处72、80都是红色,故将72、80都染色为黑色
同辈为 红黑红,既可以按照LL
处理,也可以按照LR
处理
- 由于
LL
进行一次右旋,LR
进行一次左旋一次右旋,方便起见,按照LL
处理 - 按照
LL
与LR
处理的结果并不相同,但结果都满足红黑树的性质
要删除88,被借用元素为72,是被删除节点88的父节点80的LL,故对72的grand 80进行右旋,之后76成为中心节点,要继承被删除节点88的父节点80的颜色(此处为红色),故将76从黑染红,最后,确保76的左右子节点72、80都为黑色,此处72、80都为红色,故将72、80染黑
判定条件:如果兄弟sibling没有一个
Red
子节点
- 对应B树中,兄弟没得借的情况,按照B树的处理方式,应当将父节点中的元素向下合并
- 将sibling染成
Red
,被删除节点的parent
染成Black,维持红黑树的性质
- 如果被删除节点的
parent
也是Black- 下溢,触发parent向下合并,导致parent的位置也发生下溢
- 此时,将parent视作一个被删除的节点处理即可 (递归)
第二类——兄弟节点为红色
- 如果sibling为
Red
- 被借用节点不是sibling的红色子节点,而是看sibling的黑色子节点有没有红色的子节点(很难获取)
- 解决:强行让sibling的子节点变成被删除节点的兄弟节点sibling
- parent进行旋转(根据红色sibling是被删除节点的具体位置确定旋转方式)
sibling
染为Black;parent染为Red
- 旋转之后,
sibling
发生了变化,现在变成了parent.leftNode
- 再次回到
sibling
为Black的情况
删除代码实现(维持红黑树)
删除拥有一个红色子节点的黑色节点 (红黑 | 黑红)
注意
为了方便实现,对afterRemove(Node<E> node)
方法进行修改,添加一个参数replacement
用来传递代替节点
- 对二叉搜索树中调用
afterRemove
方法的代码进行相应调整 - AVL树重写
afterRemove
方法的代码进行相应调整
在红黑树中,重写
afterRemove(Node<E> node, Node<E> replacement)
方法
- 先被删除节点的颜色,删除
Red
节点不需要做任何处理- 具有一个
Red
子节点的Black节点,直接删除Black节点,将替代节点染为黑色Black
- 此处不需要判断红黑红的情况,即要删除的Black具有2个Red子节点的情况,因为在删除度为
2
的黑色节点时,node
最终指向了要删除黑色节点的替代节点(前驱节点或后继节点),而这个替代节点必然是红色,根据上一条,代码直接return不做任何处理
删除黑色叶子节点
- 获取兄弟节点
sibling
- 叶子节点的删除,一定经历了被删除节点的parent的左节点或右节点被设置为null(利用引用计数、垃圾回收机制删除),因此被删除节点的parent的左右节点已经被修改(因为被删掉的节点就是parent的一个子节点,已经被删了),不能准去获取当前被删除node的兄弟节点,
sibling()
方法结果不准确- 反过来思考,可以根据当前node的parent的子节点是否为空来确定,如果parent的左子节点为null,说明node之前是parent的左,即parent.leftNode,如果是parent的右子节点为null,说明node之前是parent.rightNode,我们取parent相反方向的子节点就是node的兄弟节点了
- 还要兼容被删除node的parent是黑色节点,触发连续下溢递归调用
afterRemove
方法的情况,这种情况下node指向的是连续下溢传进去的原先的parent,这个节点并没有被真正删除,只是进行B树调整,因此应当使用node.isLeftChild()
或者node.isRightChild()
方法来帮助确定sibling
- 判断兄弟节点
sibling
的位置,是node
的左兄弟,还是node
的右兄弟;之后的旋转操作具有对称性
,相反方向就采用相反旋转即可- 判断兄弟节点
sibling
的颜色(按照node为右,sibling为左来书写)
sibling
为Red
—— 通过旋转
、染色
,转化为sibling
为Black的情况
sibling
染黑;parent
染红parent
右旋- 右旋之后,node的sibling发生变化,应当更新为
parent.leftNode
sibling
为Black——观察兄弟节点有没有红色子节点可以借过来用
sibling
有Red
子节点可供借用——对应黑红
、红黑
、红黑红
三种情况——判定条件:sibling至少有1
个Red
子节点
- 进行旋转:
黑红——LR
:sibling左旋,parent右旋——判定条件:sibling的左节点不是红红黑——LL
:parent右旋——sibling左节点是红色红黑红——LL
:parent右旋——sibling左节点是红色Tips:
经过分析,可以先处理黑红——LR
情况,sibling经过一次左旋之后,接下来三种情况都是parent
右旋,可以重用代码注意更新sibling
Tips:
红黑——LR情况下sibling
发生了旋转,旋转过后,当前sibling
指向的已经不是node
的兄弟节点,所以要对sibling
进行更新,更新为parent.leftNode
- 进行染色:
- 旋转过后,兄弟节点
sibling
变为中心节点,令其继承原先parent
的颜色- 确保中心节点的
左右子节点
为黑色sibling
孤苦伶仃,没有Red
子节点可以借用——对应黑
一种情况——判定条件:sibling没有Red
子节点
- 先判断父节点
parent
的颜色parent
为红色:父节点parent
向下与sibling
进行合并
parent
染黑;sibling
染红parent
本来就是黑色:parent
节点向下合并会在parent
这里再次触发下溢,进行递归调用
sibling
染红- 递归调用
afterRemove()
,传入parent
节点
实现
测试
测试代码
优化删除节点后的维持红黑树方法
注意
这部分优化是为了代码形式上的统一,并不强制要求
原先的代码更利于思路上的理解
优化
afterRemove(Node<E> node, Node<E> replacement)
方法,去除replacement
参数,使得方法形式统一
- 修改二叉搜索树代码,
afterRemove
方法移除replacement
参数- AVL树代码做响应调整
- 修改二叉搜索树
remove
方法,其中对afterRemove()
方法的调用,都只传node
,但在删除度为1
节点的代码部分对afterRemove
进行调用时,传递replacement
参数,不传node
- 分析传
replacement
和传node
的区别
- 对于AVL树,没有区别,因为AVL的调整逻辑是,顺着传入的节点,一直向上找祖先节点,直到找到第一个失衡节点,维护高度,恢复平衡
- 对于红黑树,有区别,因为红黑树删除节点时,传递
replacement
只发生在删除度为1
的节点时,这种情况下,replacement
需要从红色被染为黑色,afterRemove第一行就是判断节点颜色为红色,直接返回,不做任何处理- 解决:将afterRemove一开始的判断红色移除,同之后判断replacement颜色并染黑的代码合并
删除前@Overrideprotected void afterRemove(Node<E> node, Node<E> replacement) {// 先判断被删除节点的颜色,如果删除的是红色节点,则不需要做任何处理if (isRed(node)) return;// 来到这里:删除的是 黑色 节点// 用来取代node的子节点是红色节点 (红黑 或 黑红情况)if (isRed(replacement)) {black(replacement);return;}...}删除、合并后@Overrideprotected void afterRemove(Node<E> node) {// 先判断被删除节点的颜色,如果删除的是红色节点,则不需要做任何处理// if (isRed(node)) return;// 来到这里:删除的是 黑色 节点// 用来取代node的子节点是红色节点 (红黑 或 黑红情况)if (isRed(node)) {black(node);return;}...}合并后,针对先前直接删除红色节点的情况,多做了一次染色,但是这个被染色的节点实际上没有引用,会被垃圾回收机制清除,因此也对程序没有结果上的影响
红黑树的平衡
回到最初的问题
为什么红黑树必须满足的5条性质,就可以保证红黑树是平衡二叉搜索树?
- 红黑树的5条性质,可以绝对保证
红黑树
等价于4阶B树
- 从B树的角度看,红黑树是平衡的
- 从二叉搜索树的角度看,红黑树并不像AVL那么平衡,是一种比较宽泛的平衡,但也是平衡的,能够确保树不会退化成线性表
- 红黑树宽泛平衡的准确描述:相比AVL树,红黑树
没有一条路径会大于其他路径的二倍
,即最极端情况下,红黑树的最长路径不会超过最短路径的二倍- 满足上述平衡特性的原因:
- 红黑树所有路径上黑色节点的数目相同,推出路径长度的区别一定是因为红色节点的数目不同
- 红黑树不允许出现连续红色节点,因此一个路径上
n
个黑色节点之间最多额外添加n
个红色节点,即整个路径最多不超过2n
个节点;最短路径一定是只有黑色节点的路径,根据上一条,纯黑色节点路径的长度一定是n
,综上:可推出红黑树的宽泛平衡特性
- 红黑树可视为
弱平衡
,或称黑高度平衡
——只算路径上的黑色节点的话,满足严格平衡- 红黑树的最大数高是
2 * log(n + 1)
,依然是O(logn)
级别
平均时间复杂度
决定因素
- 作为平衡二叉搜索树,其平均时间复杂度依然取决于树的具体高度
- 出现递归调用,会传播多大的节点规模
操作 | 红黑树 时间复杂度 | AVL树 时间复杂度 |
---|---|---|
搜索 | O(logn) | O(logn) |
添加 | O(logn), O(1)次旋转 | O(logn), O(1)次旋转 |
删除 | O(logn), O(1)次旋转 | O(logn), 最多需要O(logn)次旋转操作 |
红黑树删除操作时的afterRemove
方法递归调用,根据红黑树的5条性质,基于统计学,发生概率极低,最多递归3
次,故认为复杂度为O(1)
红黑树 对比 AVL树
特征 | AVL | 红黑树 |
---|---|---|
平衡 标准 | 强平衡:任一节点左右子树高度差不超过1 | 弱平衡:最长路径不会长于最短路径的2 倍 |
最大 高度 | 1.44 * log(n + 2) - 1.328 (100万个节点高度不超过28) | 2 * log(n + 1) (100万个节点高度不超过40) |
时间 复杂度 | 搜索、添加、删除O(logn) ,添加O(1) 旋转,删除最多O(logn) 旋转 | 搜索、添加、删除O(logn) ;添加、删除设计的旋转操作复杂度都为O(1) |
AVL树与红黑树的选择
- 搜索次数远远大于插入和删除——选择AVL树
- 树高比红黑树低,搜索更快
- 避免了AVL删除时旋转复杂度高的弱势
- 搜索、插入、删除次数几乎差不多——选择红黑树
- 相对AVL数来讲,树高没有明显增长,依然保证了平衡性
- 优秀的插入和删除性能
- 相对于AVL树,红黑树牺牲了部分平衡性以换取插入、删除操作时更低的旋转复杂度
- 总体上讲,红黑树性能优于AVL树,实际中更多采用红黑树