红黑树特征
红黑树的出现,并不是无缘无故的。前面我讲了AVL树,但是在实际的应用中,AVL树并不多见,是因为什么呢?原因还是因为性能,AVL树查找性能肯定优秀,因为特别平衡,但是插入和删除操作因为频繁再平衡,所以性能稍微有点差。在这个背景下,1972年计算机科学家Bayer发明了红黑树。红黑树的思想来源于2-3-4树,有以下五个特征:
1 节点是红色或者黑色;
2 根总是黑色的;
3 所有叶子都是黑色(叶子是NULL节点);
4 如果节点是红色的,那么子节点必须是黑色;
5 从根到叶子或者空叶子的每条路径,必须含有相同数目的黑节点。
例如以下是经典的红黑树
从上述五个特征看红黑树最差的左右子树不平衡情况,深度相差不会超过两倍。这种极端情况就是左子树一黑一红交替,右子树全黑。
1 插入算法
插入时,直接插入一个新的红色节点。因为新的节点为红色只会违背特性4。也只有违背特性4的时候才需要对红黑树做出调整。而违背特性4只有一种情况,就是父节点的颜色为红色。
场景分类
伯父或叔叔红色 | 伯父或叔叔黑色 | |
---|---|---|
LL型(/) | 着色 | 着色,右旋 |
LR型(<) | 着色 | 左旋,转换为LL(/)型 |
RR型(\) | 着色 | 着色,左旋 |
RL型(>) | 着色 | 右旋,转换为RR(\)型 |
这就分为很多种子场景(以下以字母B代表黑色,字母R代表红色),可以分为五种场景:
Case 1. 叔伯红色
Case 2. 叔伯黑色,LL型
Case 3. 叔伯黑色,LR型
Case 4. 叔伯黑色,RR型
Case 5. 叔伯黑色,RL型
场景一 叔伯红色
选用测试数据{10, 8, 4, 5, 2, 1, 9, 6, 3, 7},会出现以下场景:
调整为:
这种场景,在AVL树里是平衡的,但是在红黑树中是不平衡,因为图中4和10都是红色。为了保证黑色高度不变,只需要祖父变成红色,然后将父亲和伯父变成黑色,这样黑色高度就不变,而且没有两个连续的红色。
这个时候,祖父改成红色后,因为祖父的上级可能是红色,所以需要对祖父重新进行判断。
如果祖父是root就无所谓了,因为把root改成红色后,再改回黑色就行了。
Root由红改成黑,整体黑色高度增加,但是还是符合红黑树的规则。
场景二 LL型伯父黑色
LL型,伯父是黑色或不存在,如下图:
这个树是符合黑色高度规范的,因为整个树只有root一个黑节点,所以每个路径黑色高度都是一致的。
但是连续两个红色,不符合红黑树不能连续两个红色的规定。
这个时候呢,需要做一点复杂的操作。
改颜色
再右旋
测试数据:6, 9, 5, 1, 8, 7, 4, 2, 10, 3
场景三 LR型伯父黑色
LR型,或者<号型。如图(null节点视为黑色):
这个时候只需要左旋转换为LL型,如图:
测试数据:{3, 1, 5, 6, 9, 7, 8, 4, 2, 10}
注意,这个转换后的不是第一种场景,是第三种场景。
场景四 RR型叔叔黑色
着色后,4和5的颜色都已经改变:
左旋后
测试数据:{7, 10, 1, 4, 9, 3, 5, 8, 2, 6}
场景五 RL型叔叔黑色
测试数据:{1, 3, 9, 4, 2, 10, 8, 7, 5, 6}。注意,图中是3-9-7三个元素形成一个RL型。
旋转后,3-7-9这三个元素变成了RR型
2 删除算法
删除算法,远比插入算法复杂。
首先考虑删除红节点的情况,因为删除红节点,不会影响黑色高度。也会产生两级连续的红节点,所以直接用排序二叉树的删除算法就行了。
而删除黑节点是最复杂的。因为降低了一个黑色高度,所以必须整体降低一个黑色高度。
红黑树的删除与AVL树的删除算法有点不同。
AVL树是先删除后调整。
当然红黑树也可以先删除后调整,但是如果这样写代码会比较复杂。对于初学者,我建议是先调整再删除,虽然性能上会有点差,但是能更好地理解红黑树的删除算法。
Java的TreeMap是先删除后调整的。
红黑树的删除算法设计到两个节点:删除节点和替换节点。
怎么解释这个呢?
删除后是
比如上图这棵红黑树的删除过程,要删除1,那么1就是待删除节点,而0就是替换节点,因为0要代替1的位置。最后虽然经过了调整,但确实是0代替了1的位置,成为了2的父亲。
红黑树无论是删除后调整,还是删除前调整,都是以替换节点来进行判断的。
自己是弟弟 | 自己是哥哥 | ||
兄弟是红节点 | case 1 父亲变红 哥哥变黑 左旋 进入侄子全黑场景 | case 2 父亲变红 弟弟变黑 右旋 进入侄子全黑场景 | |
兄弟是黑节点 | 小侄子红色 | case 3 哥哥变红 小侄子变黑 右旋 变成大侄子红色 | case 4 小侄子变黑 弟弟变成父亲的颜色 父亲变黑 右旋 完成调整 |
大侄子红色 | case 5 大侄子变黑 哥哥变成父亲的颜色 父亲变黑 左旋 完成调整 | case 6 弟弟变红 大侄子变黑 左旋 变成小侄子红色 | |
侄子全黑色 | case 7 兄弟变红,父亲视为新的替换节点 |
这就看得让人头疼。总共7种场景。
case 1,哥哥红色
case 2,弟弟红色
case 3,哥哥黑色,小侄子红色
case 4,弟弟黑色,小侄子红色
case 5,哥哥黑色,大侄子红色
case 6,弟弟黑色,大侄子红色
case 7,侄子全黑色
场景切换图如下,比较简单:
场景一 哥哥是红色
比如这棵树,要删除5.那么替换节点就是4。用4代替5的位置之后,黑色高度降低了1,那么就必须将4的哥哥黑色高度也降低1,这样才会平衡。
4的哥哥是7。首先将父亲5变红。这样总体黑色高度都降低1,再将哥哥7变黑,这样哥哥那边黑色高度不变。
可以看到变色前,左树高度是3(包含空节点),哥哥黑色高度也是3。
变色后,弟弟黑色高度是2,哥哥黑色高度是3.
再左旋
左旋之后,弟弟黑色高度为3,哥哥黑色高度为3。
而要删除的是5,5是红节点,但是这个删除红节点之后还是不会平衡。因为要删除5,必须用4去替换。因为红黑树的性质,如果有两个子节点,其中一个为黑,另一个不能为空。
为空的是黑色高度1,为黑的,黑色高度肯定大于1。
现在已经调整到场景7终态-兄弟和侄子全黑。
场景二 弟弟是红色
测试数据{8, 9, 6, 5, 7, 0, 4, 3, 1, 2},删除6
这棵红黑树是黑色高度为4的红黑树,虽然看起来难看,但确实是合法的红黑树。
将6删除,替换节点是5,弟弟是1,红色。
变色后
右旋后,主要是按4-1-0这条线右旋,如下图所示:
经过旋转后,黑色高度还是不变。
进入场景七-兄弟和侄子全黑。
场景三 哥哥黑色,小侄子红色
测试数据8, 7, 5, 3, 9, 4, 0, 6, 1, 2,删除0
着色后
右旋后
还是不平衡,还需要继续调整,但是已经进入最后场景五:哥哥黑色,大侄子红色。
场景四 弟弟黑色,小侄子红色(终态)
着色后
右旋后
可以看到左树黑色高度为2,右树黑色高度为2和3。就是有替换节点的地方黑色高度为3,如果将替换节点4删除,那么就完成调整。
场景五 哥哥黑色,大侄子红色(终态)
测试数据:9, 4, 2, 5, 6, 3, 8, 7, 1, 0,删除5
删除前
这个场景是由别的场景(一般是场景三)调整后得到的。
调整前:
着色后
右旋后
可以看到要删除的5,就是黑色高度不平衡的那个点,所以可以直接删除。
场景六 弟弟黑色,大侄子红色
红黑树为
需要删除的是9
调整前
着色后,注意6和7的颜色发生了变化。
左旋后
变成了场景4的终态。
场景七 侄子全黑(终态)
测试数据:7, 8, 1, 2, 3, 6, 0, 4, 9, 5,删除2,如下图:
因为删除2,所以选择的替换节点是1.着色前:
着色后
把3变红,那么就是左子树黑色高度2,右子树黑色高度为1.这样就调整为了可删除状态
这时候可能会担心有两个连续的红(2和3),但是因为2要被1替换,所以最后会平衡的。