红黑二叉查找树
是2-3查找树
的简单表示方式,它的代码量并不大,并且保证了平衡性.
阅读本文前需先了解 《Algorithms,4th Edition》读书笔记-2-3查找树
概述
红黑树
是一种自平衡的二叉查找树
,它的基本思想是用标准的二叉查找树
(完全由2-节点
构成)和一些额外的信息(替换3-节点
)来表示2-3树
. 可以说红黑树
是2-3树
的一种等同.
红黑树
中的链接可以分为两种类型:
- 红链接 : 它将两个
2-节点
连接起来构成一个3-节点
(也可以说是将3-节点
表示为由一条红色左链接(两个2-节点
其中之一是另一个的左子节点)相连的两个2-节点
).
- 黑链接 : 表示
2-3树
中的普通链接.
这种表示方式带来的优点如下:
- 无需修改就可以直接使用标准的
二叉查找树
中的查找方法(其他与链接颜色不关联的方法也可以直接使用).
- 对于任意的
2-3树
,只要对节点进行转换,我们都可以立即派生出一棵对应的二叉查找树
.
红黑树的性质
红黑树
是含有红黑链接并满足下列条件的二叉查找树
(满足这些条件的红黑树
才是与相应的2-3树
一一对应的).
- 红链接均为左链接(这条仅限于偏向左红链接实现的
红黑树
).
- 每个节点不是红色就是黑色的.
- 没有任何一个节点同时和两条红链接相连(不可以有两条连续的红链接).
- 该树是完美黑色平衡的,即任意空链接到根节点的路径上的黑链接数量相同.
根节点
是黑色的.
- 所有
叶子节点
(即null节点)的颜色是黑色的.
与2-3树的对应关系
假如我们将一棵红黑树
中的红链接画平,我们会发现所有的空链接到根节点的距离都将是相同的.如果再把由红链接相连的节点合并,得到的就是一棵2-3树
.
相对的,如果将一棵2-3树
中的3-节点
画作由红色左链接相连的两个2-节点
,那么不会存在能够和两条红链接相连的节点,且树必然是完美黑色平衡的,因为黑链接就是2-3树
中的普通链接,根据定义这些链接必然是完美平衡的.
通过这些结论,我们可以发现红黑树
即是二叉查找树
,也是2-3树
.
节点的实现
我们使用boolean
类型的变量color
来表示链接的颜色.如果指向它的链接为红色,则color
变量为true
,黑色则为false
(空链接也为黑色).
并且定义了一个isRed()
函数用于判断链接的颜色.
这里节点的颜色指的是指向该节点的链接的颜色.
|
|
旋转
当我们在实现某些操作时,可能会产生一些红色右链接或者两条连续的红色左链接.这时就需要在操作完成前进行旋转操作来修复红黑树
的平衡性(旋转操作会改变红链接的指向).
旋转操作保证了红黑树
的两个重要性质 : 有序性和完美平衡性.
左旋转
假设当前有一条红色右链接需要被修正旋转为左链接.这个操作叫做左旋转
.
左旋转
函数接受一条指向红黑树
中的某个节点的链接作为参数.然后会对树进行必要的调整并返回一个指向包含同一组键的子树且其左链接为红色的根节点的链接.
也可以认为是将用两个键中的较小者作为根节点变为将较大者作为根节点(右旋转中逻辑相反).
旋转操作返回的链接可能是左链接也可能是右链接,这个链接可能是红色也可能是黑色的(在实现中我们使用x.color = h.color
保留了它原本的颜色).这可能会产生两条连续的红链接,但算法会在后续操作中继续使用旋转操作修正这种情况.
旋转操作只影响了根节点(返回的节点的子树中的所有键和旋转前都相同,只有根节点发生了变化).
具体的实现如下图:
右旋转
实现右旋转
的逻辑基本与左旋转
相同,只需要将left
和right
互换即可.
颜色转换
颜色转换操作也是用于保证红黑树
的性质的.它将父节点
的颜色由黑变红,将子节点
的颜色由红变黑.
这项操作与旋转操作一样是局部变换,不会影响整棵树的黑色平衡性.
根节点总是为黑
颜色转换可能会使根节点
变为红色,但红色的根节点
说明根节点
是一个3-节点
的一部分,实际情况并不是这样的.所以我们需要将根节点
设为黑色.
每当根节点
由红变黑时,树的黑链接高度就会加1.
插入
在红黑树
中实现插入操作是比较复杂的,因为需要保持红黑树
的平衡性.但只要利用好左旋转
、右旋转
、颜色转换
这三个辅助操作,就能够保证插入操作后树的平衡性.
向单个2-节点中插入新键
当一棵只含有一个键的红黑树
只含有一个2-节点
时,插入另一个键后需要马上进行旋转
操作修正树的平衡性.
- 如果新键小于老键,只需要新增一个红色的节点即可(这时,新的
红黑树
等价于一个3-节点
).
- 如果新键大于老键,那么新增的红色节点将会产生一条红色的右链接,这时就需要使用
左旋转
修正根节点的链接.
- 以上两种情况最终的结果均为一棵等价于单个
3-节点
的红黑树
,它含有两个键,一条红链接,树的黑链接高度为1.
向树底部的2-节点插入新键
和二叉查找树
一样,向红黑树
中插入一个新键会在树的底部新增一个节点,但在红黑树
中总是用红链接将新节点和它的父节点相连.
如果它的父节点是一个2-节点
,那么上一节讨论的方法依然适用.
- 如果指向新节点的是父节点的左链接,那么父节点就直接成为一个
3-节点
.
- 如果指向新节点的是父节点的右链接,那么就需要一次
左旋转
进行修正.
向一棵双键树(一个3-节点)中插入新键
当向一个3-节点
中插入新键时,会发生以下三种情况且每种情况都会产生一个同时连接到两条红链接的节点,我们需要修正这一点.
- 如果
新键大于原树中的两个键
: 这是最容易处理的一种情况,这个键会被连接到3-节点
的右链接.此时树是平衡的,根节点为中间大小的键,它有两条红链接分别和较小和较大的节点相连.只需要把这两条链接的颜色都由红变黑,那么就可以得到一棵由三个节点组成、高度为2的平衡树(其他两种情况最终也会转化为这样的树).
- 如果
新键小于原树中的两个键
: 这个键会被连接到最左边的空链接,这样就产生了两条连续的红链接.此时只需要将上层的红链接右旋转
即可得到第一种情况(中值键为根节点并和其他两个节点用红链接相连).
- 如果
新键介于原树中的两个键之间
: 这种情况依然会产生两条连续的红链接:一条红色左链接接一条红色右链接.此时只需要将下层的红链接左旋转
即可得到第二种情况(两条连续的红色左链接).
通过以上这三种情况可以总结出 : 我们只需要通过0次、1次、2次旋转以及颜色转换就可以完成对红黑树
的修正.
将红链接向上传递
当每次旋转操作之后都会进行颜色转换
,它会使得中间节点变为红色.从父节点的角度来看,处理这样一个红色节点的方式和处理一个新插入的红色节点完全相同(继续将红链接转移到中间节点).
这个操作对应于2-3树
中向3-节点
进行插入的操作 : 即在一个3-节点
下插入新键,需要创建一个临时的4-节点
,将其分解并将中间键插入父节点(在红黑树
中,是将红链接由中间键传递给它的父节点).重复这个过程,直至遇到一个2-节点
或者根节点.
当根节点变为红色时需要将根节点的颜色转换为黑色(对应2-3树
中的根节点分解).
实现
插入操作的实现除了每次递归调用之后的对平衡性修正的操作,其他与二叉查找树
中的插入操作没什么不同.
|
|
总结
只要在沿着插入点到根节点的路径向上移动时在所经过的每个节点中顺序完成以下操作,就能够实现红黑树
的插入操作.
- 如果
右子节点
是红色的而左子节点
是黑色的,那么进行左旋转
.
- 如果
左子节点
是红色的而且它的左子节点
也是红色的,那么进行右旋转
.
- 如果
左右子节点
都是红色的,那么进行颜色转换
.
删除
删除操作也需要定义一系列局部变换
来在删除一个节点的同时保持树的完美平衡性.然而,这个过程要比插入操作还要复杂,它不仅要在(为了删除一个节点而)构造临时4-节点
时沿着查找路径向下进行变换,还要在分解遗留的4-节点
时沿着查找路径向上进行变换(同插入操作).
自顶向下的2-3-4树
2-3-4树
是一种允许存在4-节点
的树.它的插入算法就是一种沿着查找路径既能向上也能向下进行变换的算法.
- 沿查找路径向下进行变换(向下变换与
2-3树
中分解4-节点
所进行的变换完全相同)是为了保证当前节点不是4-节点
(这样树的底部才有足够的空间插入新的键).
- 沿查找路径向上进行变换是为了将之前创建的
4-节点
配平.
- 如果
根节点
是一个4-节点
,就将它分解成三个2-节点
,树的高度加1.
- 如果在向下查找的过程中,遇到了一个
父节点
为2-节点
的4-节点
,就将4-节点
分解为两个2-节点
并将中间键
传递给它的父节点
(这时父节点
变为了一个3-节点
).
- 如果遇到了一个
父节点
为3-节点
的4-节点
,将4-节点
分解为两个2-节点
并将中间键
传递给它的父节点
(这时父节点
变为了一个4-节点
).
- 不必担心遇见
父节点
为4-节点
的4-节点
,算法本身保证了不会出现这种情况,到达树的底部之后,只会遇到2-节点
或者3-节点
.
如果要使用红黑树
来实现这个算法,需要以下步骤 :
- 将
4-节点
表示为由三个2-节点
组成的一棵平衡的子树,根节点
和两个子节点都用红链接相连.
- 在向下的过程中分解所有
4-节点
并进行颜色转换
.
- 在向上的过程中使用
旋转
将4-节点
配平.
只需要将插入一节中的put()
实现方法里的flipColors
语句(及其if语句)移动到递归调用之前(null判断和比较操作之间)就能实现2-3-4树
的插入操作.
删除最小键
从2-节点
中删除一个键会留下一个空节点,一般会将它替换为一个空链接,但这样会破坏树的完美平衡性.所以在删除操作中,为了避免删除一个2-节点
,我们沿着左链接
向下进行变换时,需要确保当前节点不是2-节点
.
根节点
可能有以下两种情况:
- 如果
根节点
是一个2-节点
且它的两个子节点都是2-节点
,可以直接将这三个节点变成一个4-节点
.
- 否则,需要保证
根节点
的左子节点不是2-节点
,必要时可以从它右侧的兄弟节点借走一个键.
在沿着左链接
向下的过程中,保证以下情况之一成立:
- 如果当前节点的左子节点不是
2-节点
.
- 如果当前节点的左子节点是
2-节点
而它的兄弟节点不是2-节点
,将左子节点的兄弟节点中的一个键移动到左子节点中
- 如果当前节点的左子节点和它的兄弟节点都是
2-节点
,将左子节点、父节点中的最小键和左子节点最近的兄弟节点合并为一个4-节点
,使父节点由3-节点
变为2-节点
(或是从4-节点
变为3-节点
).
只要保证了以上的条件,我们最终能够得到一个含有最小键的3-节点
或4-节点
(然后进行删除即可),之后再不断向上分解所有临时的4-节点
.
代码实现
在删除操作中,颜色转换
的操作与插入操作中的实现略微有些不同(需要将父节点设为黑,而将两个子节点设为红).
|
|
删除最大键
|
|
删除操作
同样也需要像删除最小键那样在查找路径上进行变换来保证查找过程中任意当前节点均不是2-节点
.如果目标键在树的底部,可以直接删除它;如果不在,则需要将它和它的后继节点交换.
在删除操作之后需要向上变换分解余下的4-节点
.
|
|
总结
无论键的插入顺序如何,红黑树
都几乎是完美平衡的,基于它实现的有序符号表操作的运行时间均为对数级别(除了范围查询).
在红黑树
的实现中复杂的代码仅限于put()
和delete()
方法,像get()
这些不会涉及检查颜色的方法与二叉查找树
中的实现一致(因为这些操作与平衡性无关).
end
- Author : SylvanasSun
- Email : sylvanassun_xtz@163.com
- 本文参考资料引用自《Algorithms,4th Editio》