红黑树
红黑树是一种特殊的二叉查找树(binary search tree,以下简称BST),它用来解决BST的致命缺点。BST在动态查找时,不能保证树的平衡性,尤其关键字有序地插入时,二叉树的查找效率等价于链表了。红黑树的目的在于时刻保持二叉树的平衡性,即保证二叉树的高度最小来保证二叉树的查找效率。
五个基本特性
节点颜色非黑即白
根节点必须是黑色的
叶子节点必须是黑的
红色节点的孩子为黑色
每个节点到所有叶子节点的路径上经过的黑色节点数(黑高度)相同
二叉树基本操作
-
旋转
rotate.jpg左旋
Left-Rotate(T, y) //1.处理y的左孩子β x.r = y.l if(y.l != T.nil) y.l.p = x //2.处理父节点 y.p = x.p if(x.p == T.nil) T.root = y else if(x.p.l == x) x.p.l = y else x.p.r = y //3.处理x,y的关系 y.l = x x.p = y
右旋
Right-Rotate(T, x) //1.处理x的右孩子β y.l=x.r if(x.r != T.nil) x.r.p = y //2.处理父节点 x.p = y.p if(y.p == T.nil) T.root = x else if(y.p.l == y) y.p.l = x else y.p.r = x //3.处理x,y的关系 x.r = y y.p = x
-
取代
即用子树v取代子树u。需要注意最后一行,u.p为T.nil时 v.p = u.p也执行,这是v为root,v.p为T.nil
RB-Transplant(T, u, v) if(u.p == T.nil) T.root = v else if(u.p.l == u) u.p.l = v else u.p.r = v v.p = u.p
-
求 min/max
Tree-Min(x) while(x != NIL) x = x.l return x
Tree-Max(x) while(x != NIL) x = x.r return x
-
查找
循环方式
Tree-Search(x, k) while(x != NIL and x.key != k) if(x.key < k) x = x.r else x = x.l return x
递归方式
Tree-Search(x, k) if(x == NIL or x.key == k) if(x.key < k) Tree-Search(x.r, k) else Tree-Search(x.l, k)
-
插入
RB-Insert(T, z) y = T.nil x = T.root while(x != T.nil) if(x.key< z.key) x = x.r else x= x.l z.p = x if(x == T.nil) T.root = z else if(x.key < z.key) x.r = z else x.l = z z.l = T.nil z.r = T.nil z.color = RED RB-Insert-Fix(T, z)
当这个红色节点插入后,会违反哪些红黑树的规则?
规则2:根节点为黑色。原本树为空,当插入红节点后,违反此规则
规则4:红色节点的孩子为黑色。新节点z的父节点y原本为红色,插入后红色节点有了红孩子,出现了两个相邻的红色节点。
case1: 新节点z的叔叔节点y为红色。 不论新节点z为其父节点的左右孩子,操作是一样的。
rb-insert-case1.jpgy = z.p.p.r if(uncle.color == RED) z.p.color = BLACK y.color = BLACK z.p.p.color = RED z = z.p.p
case2,3: 新节点z的叔叔节点y为黑色。
case2和case3按z是其父节点的左孩子还是右孩子分开,其实二者只差一个旋转
rb-insert-case2-3.jpgy = z.p.p.r if (z == z.p.r) z = z.p Left_Rotate(T, z) z.p.p.color = RED z.p.color = BLACK Right_Rotate(T, z.p.p)
最终程序
RB-Insert-Fix(T, z) while(z.p.color == RED) if(z.p == z.p.p.l) y = z.p.p.r if (uncle.color == RED) z.p.color = BLACK y.color = BLACK z.p.p.color = RED z = z.p.p else if (z == z.p.r) z = z.p Left_Rotate(T, z) z.p.p.color = RED z.p.color = BLACK Right_Rotate(T, z.p.p) else //symmetic ... T.root.color = BLACK
总结:
- 程序按z.p为左孩子还是右孩子分为镜像的两部分。
- while循环用来处理违反红黑树规则4的情况,因为处理过程中新节点z会不断更新上移,所以使用了while循。
- 最后一行用来处理违反红黑树规则2的情况。
-
删除
case1:
z的左右孩子至少有一个为空,直接用非空的孩子替代z。
bst-delete-case1.jpgif (z.l == T.nil) x = z.r RB-Transplant(T, z, x) else if (z.r == T.nil) x = z.l RB-Transplant(T, z, x)
case2:
z的左右孩子都不为空,且z的后继是其右孩子。直接用其右孩子替代z。
bst-delete-case2.jpgif (z.l !== T.nil and z.l=r !== T.nil) y = Tree-Min(T, z.r) y-original-color = y.color if(y.p == z) x.p = y //y取代z RB-Transplant(T, z, y) //安排z.l y.l = z.l z.l.p = y //继承颜色 y.clolr = z.color
case3:
bst-delete-case3.jpg按照中序遍历依次为 z, y, x, r, 如果删除z,后继们要依次补上。这里操作为
x覆盖y, r覆盖x,然后y覆盖z。if (y.p != z) //1. x覆盖y BR-Transplant(T, y, y.r) //2. z的右孩子成为y的右孩子,这时x仍然为y的后继 y.r = z.r z.r.p = y //3. y覆盖z RB-Transplant(T, z, y) y.l = z.l y.l.p = y y.color = z.color
最终程序
RB-Delete(T, z) //case 1 y = z y-original-color = y.color if (z.l == T.nil) x = z.r RB-Transplant(T, z, x) else if (z.r == T.nil) x = z.l RB-Transplant(T, z, x) else y = Tree-Min(T, z.l) y-original-color = y.color x = y.r //case 2 if (y.p == z) x.p = y //case 3 else RB-Transplant(T, y, x) y.r = z.r z.r.p = y BR-Transplant(T, z, y) //处理l y.l = z.l z.l.p = y y.color = z.color if (y-original-color == BLACK) BR-Delete-Fix(T, x)
几个解释点:
x变量,指向y的右孩子,是y的后继。程序中需要它覆盖y。
y-original-color
记录y的原始颜色,因为在调用tansplant函数后y的颜色可能会改变。y的原色时决定是否要执行修正的关键:红不违反,黑违反。
为什么黑违反:
- 规则2:y为根(一开始赋值y=z),孩子x为红,x取代y后出现根为红色的情况。
- 规则4:x取代y后,x和x.p都为红色
- 规则5:y被取代后,之前包含y的路径中黑节点的数量少1。
为什么红不违反:
- y为红,不可能为根节点
- y为红,x必为黑,x取代y后,x与x.p不可能同时为红
- y为红,被取代后,黑高度不变
修正
其实违反规则5的情况可以转化为违反了规则1(节点颜色非黑即白):为弥补包含y的路径上黑高度少一的情况,我们给x加一个黑色属性,(即x为balck-red或black-black),同时在修正操作中保证不违反规则5这样就是实现完美转化了。所以,我们要修正的对象是规则1,2,4
修复规则2,当红节点作为根节点时,只需要直接把x的颜色(red-black)设置为黑色即可
修复规则4,违反这一规则的情况只有一种,那就是x取代y后,x与x.p同时为红,因为这时x的兄弟必为黑。这时也只需要把x的颜色(red-black)直接设置为黑即可。
if (x.color == RED or T.root == x)
x.color = BLACK
剩下的就是修复规则1了。这是相对复杂的流程。流程的核心思想就是把x多出来的黑色不断向上转移。
分情况讨论
图例说明,黑色表示黑,深灰色表示红,浅灰色表示该节点任意色可黑可红,用c表示。w表示x的兄弟节点。
case1 x的兄弟节点w为红色
B D互换颜色,然后以B为轴点左旋, w更新为老w的一个孩子之一,这时w必为黑色,由此,case1装换后进入了case2,3,4。目的就是让x与w同为黑色,这样去除x额外的黑色数时方便一点。
if (w.color == RED)
x.p.color = RED
w.color = BLACK
Left-Rotate(T, x.p)
//w 继续成为x的兄弟,他来自老w的一个孩子,黑色
w = x.p.r
case2 w为黑,x的两个孩子为黑
同时从x与w上拿掉一点黑色数:w变红,x的黑色数向上转移,把新的x设置为x的父节点。
if(w.color == BLACK and w.left.color == BLACK and w.right.color == BLACK)
w.color = RED
x = x.p
case3 w为黑,x的孩子左红右黑
C D 交换颜色,以w为轴点右旋,w设置为x的兄弟。
完全没啥道理,如果程序执行过程中遇到这种情况就把他向case4转化好了,是一个过渡case。但要保证转化的过程中黑高度不变。
w.l.color = BLACK
w.color = RED
Right_Rotate(T, w)
w = x.p.r
case4 w为黑,x的右孩子为红,左孩子任意
D变为B的颜色,B,E变为黑色,以B为轴点左旋。
这很玄学, 需要注意程序最后,让变量x指向根节点也没啥意思,目的就是为了终止循环,因为这时x额外的黑色树已经移除了,规则1修复完毕。
w.color = x.p.cololr
x.p.color = BLACK
w.right.color = BLACK
Left-Rotate(T, x.p)
x = T.root
至此规则1修复完毕。核心就是使x额外的黑色数向上移,但保证变换前后黑高度不变。接下来就是代码整合了。应为这看似是4个case,其实他们之间是有前后关系的,可不是简单的if/else就能搞定。即使搞定了程序也不简洁。
代码汇总
RB-Delete-Fix(T, x)
while(x != T.root and x.color == BLACK)
if(x == x.p.l)
w = x.p.r
//-------------------------case1-----------------------
if(w.color == RED)
w.color = BLACK
x.p.color = RED
Left-Rotate(T, x.p)
//w 继续成为x的兄弟,他来自老w的一个孩子,黑色,这时w变黑了
w = x.p.r
//-------------------------case2------------------------
//这里没有用else,因为case1如果执行,w变黑,会自动转到case2,3,4
if (w.l.color == BLACK and w.r.color == BLACK)
w.color = RED
x = x.p
//-------------------------case3------------------------
else if (w.r.color == BLACK)
w.l.color = BLACK
w.color = RED
Right-Rotate(T, w)
w = x.p.r
//-------------------------case4-----------------------
w.color = x.p.color
x.p.color = BLACK
w.r.color = BLACK
Lefte-Rotate(T, x.p)
x = T.root
else //symmetric
x.color = BLACK
应用
java的TreeMap中使用了红黑树,可以去看看相关的源码
参考文献
[1]Cormen, T., &Leiserson, C., &Rivest, R., &Stein, C.(2009). Introduction to Algorithms(pp.287-329).London:The MIT Press.