红黑树
红黑树是一棵二叉搜索树,它在每个结点上增加了一个存储位来表示结点的颜色。通过对任何一条从根到叶子的简单路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因而是近于平衡的。
定义
- 每个结点要么是黑色要么是红色。
- 根节点一定是黑色。
- 红色结点的孩子一定是黑色。
- 从任一节点到它所能到达得叶子节点的所有简单路径都包含相同数目的黑色节点。
- 用黑色的哨兵nil结点代替原来树中的每个叶子结点的左子树和右子树。即叶子(NIL)结点是黑色的。(根据《算法导论》中对红黑树的定义)
树中的每个结点包含5个属性:color,key,left,right,p。(p:父结点)
一颗红黑树的示意图如下:(NIL结点可以用一个结点代替)
引理13.1:一棵有n个内部结点的红黑树的高度至多为2lg(n+1)
详细的证明细节见《算法道路》,直观分析如下:
从根节点到叶子结点中的黑结点数目相等(定义4),又因为红结点下面一定是黑结点(定义3)。因此一条根节点到叶子结点的简单路径上的红色结点一定不会超过黑色结点。因此能直观的得到结论:每棵子树的根节点到叶子结点的最长路径不会超过最短路径的2倍。(即一个叶子结点要想足够深,他需要填满他深度一半的满二叉树)因此,对于结点n的内部红黑树的最高高度一定是log(N)级别的。
因此,如果用红黑树来代替AVL树,在查找的复杂度上是合适的,如果增加和删除结点不比AVL树复杂,即达到log(n)级别,那就好了。下面进行分析。
旋转
左右旋的示意图如下,在红黑树中也会用到,具体分析和代码在之前一篇博客中有学习笔记_二叉搜索树与平衡二叉树。
需要注意一点,旋转后不改变二叉搜索树的性质(即左子树最大值<=根节点<=右子树最小值)。
插入
为了使插入操作的复杂度为log(n),在二叉搜索树的基础上进行修改。
步骤如下:将Z节点结点插入树T中(二叉搜索树的插入方式),然后将Z染色为红色,再调用一个程序RB-INSERT-FIXUP来对结点重新染色并且旋转。
整个插入函数命名为RB-INSERT(T,x)。
分析《算法导论》中给出的伪代码(迭代版本)
RB-INSERT(T,z) //在树T中插入z
y = T.nil //y为插入点的父结点(对于根节点则为哨兵nil结点)
x = T.root //通过x来找到插入位置
while x != T.nil //最后的插入位置一定是叶子结点
y = x; //继续判断x的孩子结点,因此更新y为新的父结点
if z.key < x.key
x = x.left
else x = x.right
z.p = y; //此时找到了插入位置,因此插入结点Z的父结点为y
if y == T.nil //此时父结点竟然是T.nil!,说明这是一颗空树
T.root = z //因为是空树,那么z就是根节点
elseif z.key<y.key //看看是插到y的左边还是右边
y.left = z;
else y.right = z;
z.left = T.nil //插入的Z为叶子结点,所以把Z的左右子树设置为哨兵结点nil
z.right = T.nil
z.color = RED //不妨设它为红色(可以避免违反到叶子结点的黑色结点数相等的定义,但是可能违反连续两个红色,需要调整)
RB-INSERT-FIXUP(T,z) //调用该函数来保持红黑树的性质
对于RB-INSERT-FIXUP函数,也先给出伪代码分析
RB-INSERT-FIXUP(T,z)
while z.p.color == RED //如果该结点的父结点是红色,则连红了,要调整!
if z.p == z.p.p.left //z的父结点是左孩子 (参考备注1)
y = z.p.p.right //y是z的父亲的右兄弟
if y.color ==RED //参考示意图情况1
z.p.color = BLACK //把z的父亲变黑色
y.color = BLACK //把z的父亲的右兄弟变黑
z.p.p.color = RED //把z的父亲的父亲变红色
z = z.p.p //此时对于z来说他的父亲已经变成了黑色,因为他的爷爷变成了红色,所以判断他的爷爷是否满足性质。
/* 情况1
B z(R)
/ \ / \
R R =====> B B
/ /
z R
*/
else if z == z.p.right //z是右孩子,参考示意图情况2
z = z.p
LEFT-ROTATE(T,z)
z.p.color = BLACK //参考示意图的情况3
z.p.p.color = RED
RIGHT-ROTATE(T,z.p.p)
/* 情况2和情况3:z是右孩子
B B B
/ \ 情况2 / \ 情况3 / \
R B ========> R B =======> z R
\ / \
z z B
*/
else(same as then clause with “right” and left exchanged) //z的父亲是右孩子,则处理过程类似,略去
T.root.color = BLACK //修正因为情况1而导致的根节点变红!
备注1:z的父亲的父亲一定是存在的,因为z的父亲不是根节点(根节点是黑色)
分析:
- 对于情况1,很好理解,当z的叔叔结点是红色时,则把z.p.p的两个结点都染成黑色,z.p.p染成红色,这样做只有连续红色定义可能不满足(和刚插入时候的情况一样),因此z=z.p.p,继续修改z。
- 对于情况2和情况3,z的叔叔结点都是黑色,根z是左孩子还是右边孩子分情况处理。考虑先把情况2转换成情况3再统一处理。
- 对于情况2,z是右边孩子,为了把情况2中的z变成左孩子,直接令z=z.p,(因为z和他的父亲都是红色),再左旋z即可!此时z(原本的父亲)就是需要处理冲突的红色左孩子。
- 最后处理情况3,把z.p变成黑色,再把z.p.p变成红色,此时,对于z.p.p的右边子树,总黑的个数减少了1个,因此把z.p.p右旋,z.p.p被z.p代替(右子树的黑结点个数终于也正常了!)这时发现,z.p也变成了黑色,那么我们可以直接跳出while循环。
复杂度分析:因为层数是log(n)级别的,因此修正也是log(n)级别的,综上,插入操作的修正是log(n)级别的。
删除
删除结点比插入结点要复杂。先定义一个删除结点的子过程,RB-TRANSPLANT(T,u,v)(用v子树来替换u子树,结点u的双亲就变成v的双亲,注意v没有继承u的孩子!是一整颗树的替换),在删除结点时使用。伪代码如下:
RB-TRANSPLANT(T,u,v)
if u.p == T.nil //如果u是树根节点
T.root = v
elseif u == u.p.left //如果u是左孩子
u.p.left = v
else u.p.right = v //如果u是右孩子
v.p = u.p //修改v的父亲指针。
注意上面的代码,即使v是T.nil哨兵时,也可以进行。
下面给出删除结点的伪代码。RB-DELETE(T,z)
RB-DELETE(T,z)
y=z;
y-original-color = y.color //被删除的结点的颜色,如果是红色,且不是同时有左右孩子时,那删了就删了!
if z.left == T.nil //z的左子树为空时,直接用右子树代替z即可
x = z.right //用z.right 代替x即可
RB-TRANSPLANT(T,z,z.right)
elif z.right ==T.nil
x = z.left
RB-TRANSPLANT(T,z,z.left) //同理,当右边子树为空时
else //左右孩子都不空时
y = TREE-MINIMUM(z.right) //找到z的后继(即z的右边孩子中最小的结点,该函数很容易实现)
y-original-color = y.color //y需要替换到z上,因此当该颜色是黑色时,xxxxxxxxxxxxxxx
x = y.right
if y.p == z //如果z的后继就是z.right
x.p = y
else
RB-TRANSPLANT(T,y,y.right)
y.right = z.right
y.right.p = y
RB-TRANSLANT(T,z,y) //用z的后继代替z(继承他的父亲)
y.left = z.left //y继承z的左孩子
y.left.p = y
y.color = z.color //y继承z的颜色
if y-original-color == BLACK //该出的解释见备注1
RB-DELETE-FIXUP(T,x) //注意,这里处理的是x而不是y见后面分析(即x的路径需要补一个黑色)
备注1:
- 当z的左右子树有空(哨兵)的时,删除的如果是红色结点,那没问题,但是如果是黑色结点,显然是需要调整的,因为该子树的路径上黑色结点少了一个,并且可能连红。
- 当z的左右子树都不空时,新的结点y继承了z.color的颜色,因此如果z是红色,y是红色,对所有路径的黑色和没有影响,z是黑色,y是红色,但是y继承了z的黑色,因此,也没问题。总结前面两种颜色组合:即y是红色就肯定没问题。但是如果y是黑色,z是红色,就使得原本y子树里的路径黑色结点少1,如果y是黑色,z是黑色,依然导致y子树里的路径黑色结点少1。同时可能因为连红导致需要调整。总结前面两种颜色组合,即y使黑色就肯定需要调整因此就需要调整。
对于上述产生的删除问题,需要调用RB-DELETE-FIXUP(T,x)函数进行补救,总结一下上述造成的问题,有3种:
- 当z为黑色且用左右孩子代替时(有一个孩子为空,即哨兵结点),则当前子树的叶子结点黑色路径少1。并且可能出现x,x.p连红。
- 当z有左右孩子,y是黑色时,如果x和x.p是红色,则会出现连红。
- 当z有左右孩子,y是黑色时,不仅可能出现连红,也会使得(原本)y的任何简单路径上的黑结点数少1。
现在有一种直观的解释是给x再染上一层黑色,则可以解决x子树上简单路径少1的问题。(即x为红黑色或者黑黑色),此时计算黑结点数的简单路径时,x可以使得黑色结点+1或者+2,这时,当黑结点y删除时,可以把他下推给结点x。现在的问题编程了如果x原本是红色,现在变成了红黑,怎么表示他。实际上X的color属性仍然是红色。现在看看怎么用RB-DELETE-FIXUP(T,x)函数来处理他。
RB-DELETE-FIXUP(T,x)
while x!=T.root and x.color == BLACK //如果x是红色,则直接补为黑色就解决了!
if x == x.p.left //如果x是左孩子
w = x.p.right //w是x的兄弟
if w.color = RED //见下图的情况1
w.color = BLACK
x.p.color = RED //因为w是黑色,所以x.p原本只可能是红色
LEFT-ROTATE(T,x.p)
w = x.p.right //情况1结束 实际上情况1的目的就是把w.color转变成red
/* 情况1 w(B)
/
B R 左旋 R
/ \ =====> / \ =====> / ====> w = x.p.right
x(B) w(R) X(B) w(B) x(B)
*/
if w.left.color == BLACK and w.right.color ==BLACK //如果此时w的两个孩子都是黑 //情况2
w.color = RED //w.color原本一定是黑色 //情况2
x = x.p //此时把问题传递给了x.p,变成了x.p的子树路径少1///情况2结束
/* 情况2 未知 x(未知)
/ \ / \
x(B) w(B) B W(R)
/ \ ====> / \
B B B B
*/
elseif w.right.color ==BLACK //情况3开始
w.left.color == BLACK
w.color = RED
RIGHT-ROTATE(T,w)
w = x.p.right // 情况3结束,情况3操作目的是令w.right变成红色
/* 情况3 未知 未知 未知
/ \ / \ 右旋w / \
x(B) w(B) ====> x(B) W(R) ====> x(B) w(B)
/ \ / \ \
R B B B R
\
B
*/
w.color = x.p.color //情况4
x.p.color = BLACK
w.right.color =BLACK
LEFT-ROTATE(T,x,p)
x = T.root //情况4结束
/* 情况4 未知0 B w(未知0)
/ \ / \ 左旋根 / \
x(R) w(B) ====> x(R) w(未知0) ======> B B ==>X = T.root
\ \ /
R B X(R)
*/
else(same as then clause with "right" and "left" exchanged)
//当x是右孩子时的处理方式和x是左孩子类似
x.color = BLACK
《算法导论》上的图如下需要说明的一点是:第四种情况的根节点也可能是黑色,带进去可以发现这种变换也可以解决这个问题。
简单分析一下RB-DELETE-FIXUP的过程
1 当x是红时,直接转为黑色即可!结束!
2 当x是黑时,如果x的兄弟是红色,通过情况1的变换把兄弟转成黑色进行后续分析。
3 此时x的兄弟已经是黑色了,这时判断x的兄弟的孩子
4 如果两个孩子都是黑色,可以通过情况2的变换把x上推一个(x=x.p)。进行下一轮循环判断。
5 如果右边孩子黑,左孩子红,则可以通过情况3的变换把x的兄弟的右孩子变成红色。
6 如果右边孩子本来就是红色(或者通过步骤5变成了红),直接通过情况4的变换即可调整成功。
总结
红黑树的插入操作的逻辑还是很好理解的,删除操作比较复杂。
B树
B树是为磁盘或其他直接存取的辅助存储设备而设计的一种平衡搜索树。
B树与红黑树的不同之处在于B树的结点可以有很多个孩子。B树的高度也是O(lgN)但是由于表示高度的对数的底数可能非常大,因此高度比红黑树低许多。
定义
- B树中如果一个结点有x.n个关键字,那么该结点就有x.n+1个孩子。x结点中的关键字就是分隔点,它把结点x中所处理的关键字的属性分割为x.n+1个子域,每个子域都由x的一个孩子处理。