红黑树
1、红黑树的性质
一颗红黑树是满足下面红黑性质的二叉搜索树
- 每个结点或是红色的,或是黑色的。
- 根结点是黑色的。
- 每个叶节点(NIL)是黑色的。
- 如果一个结点是红色的,则它的两个子结点是黑色的。
- 对每一个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。
1、为了便于处理红黑树代码中的边界条件,使用一个哨兵来代表NIL,哨兵T.nil是一个与树中普通结点有相同属性的对象,它的color为黑色,而其他属性可以为任意值,所有指向NIL的指针都用指向哨兵T.nil的指针替换。
2、树中每个结点包含5个属性:color、key、left(左孩子)、right(右孩子)和p(父结点)。
3、如果一个结点没有子结点或父结点,则该结点相应指针属性的值为NIL。
4、一棵有n内部结点的红黑树的高度至多为2lg(n+1)
2、旋转
指针结构的修改是通过旋转(ratation)来完成的,这是一种能保证二叉搜索树性质的搜索树局部操作。分为:左旋和右旋。
左旋伪代码如下:
//LEFT-ROTATE的伪代码中,假设x.right 不等于 T.nil且根结点的父结点为T.nil
LEFT-ROTATE(T,x)
//赋值y,并把y的左子树赋值于x的右子树
y = x.right
x.right = y.left
//将y的左子树设置成x
if(y.left != T.nil){
y.left.p = x;
}
//把x的父结点赋值于y的父结点
y.p = x.p;
if(x.p == T.nil){
T.root = y;
}else if(x == x.p.left){
x.p.left = y;
}else{
xp.right = y;
}
//把x设置为y的左孩子
y.left = x;
x.p = y;
3、插入
我们可以在O(lg n) 时间内完成像一个含n个结点的红黑树中插入一个新结点,并通过一个辅助程序对结点重新着色并旋转,保证红黑树的性质。
插入新结点(RB-INSERT)
思路:将z作为一个叶子结点的左孩子或右孩子,并对其维护相应的指针,同时将结点染成RED,之后在对其进行重新着色并旋转。
伪代码如下:
RB-INSERT(T,z)
//找到待插入结点z的父结点,z的父结点是一个叶子结点(非NIL结点)
y = T.nil;
x = T.root;
while(x != T.nil){
y = x;
if(z.key < x.key){
x = x.left;
}else{
x = x.right;
}
}
z.p = y;
//插入z
if(y == T.nil){
T.root = z;
}else if(z.key < y.key){
y.left = z;
}else{
y.right = z;
}
//设置z的子结点属性并赋值红色
z.left = T.nil;
z.right = T.nil;
z.color = RED;
//z为红色,可能违反一条红黑树性质,重新染色
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){
y = z.p.p.left;
if(y.color == RED){
//case 1 (z的叔结点y是红色的) ,执行结果,保证z和z的父结点和叔结点保证红色树的性质
z.p.color = BLACK;
y.color = BLACK;
//此可能会造成违反性质4,并转化为case 2
z.p.p.color = RED;
//将指针z在树中上移两层,()
z = z.p.p;
}else if( z == z.p.right){
//case 2 (:z的叔结点y是黑色的且z是一个右孩子)
z = z.p;
//使用一个左旋转换成case 3
LEFT-ROTATE(T,z);
}
//case 3( : z的叔结点y是黑色的且z是一个左孩子)
z.p.color = BLACK;
z.p.p.color = RED;
RIGHT-ROTATE(T,z.p.p);
}else{
//另一种情况
}
T.root.color = BLACK;
}
1、插入一个红色结点两个子结点都是哨兵结点,我们可知红黑树的1,3,5性质均成立。性质2和性质四可能遭到破坏,即跟结点必须为黑色,一个红色结点不能有红孩子。
2、case 1 z的叔结点y是红色的(y是z的父结点的兄弟结点)。case 1 修正了被唯一破坏的性质4,但是也可能造成性质4破坏。
3、性质1、3、和5 都在case2和case3中得以保持,同时case2和case3修正了对性质4的违反。
4、最后改变跟结点颜色 保证性质2。
5、在该算法中,仅有case1 导致指针z沿树上升两成,while循环才会重复执行。此外该程序执行所做的旋转不会超过两次,只要执行了case2或case3,while循环就结束了。
伪代码执行示意图
4、删除
删除一个结点需要花费O(lg n)时间。
替代值(RB-TRANSPLANT)
//u是需要删除的结点,v是需要删除结点的子孩子
RB-TRANSPLANT(T,u,v)
if(u.p == T.nil){
T.root = v;
}else if(u == u.p.left){
u.p.left = v;
}else{
u.p.right = v;
}
v.p = u.p;
删除结点(RB-DELETE)
伪代码如下:
//T是红黑树,z是删除的结点
RB-DELETE(T,z)
//记住要删除的结点和颜色
y = z;
y-original-color = y.color;
//只有一个孩子
if(z.left == T.nil){
x = z.right;
RB-TRANSPLANT(T,z,z.right);
}else if(z.right == t.nil){
x = z.left;
RB-TRANSPLANT(T,z,z.left);
}else{
//两个孩子情况下
//找到z的右孩子的最小值并赋值y(因此y是不存在左孩子的结点),并记录其颜色
y = TREE-MINIMUM(z.right);
y-original-color = y.color;
x = y.right;
if(y.p == z){
//如果y 是z的直接孩子,改变y的右孩子的指向
x.p = y;
}else{
//如果y不是z的直接孩子,则将红黑树中y的右孩子结点代替y
RB-TRANSPLANT(T,y,y.right);
y.right = z.right;
y.right.p = y;
}
//移除z,并使用y替代
RB-TRANSOLANT(T,z,y);
y.left = z.left;
y.left.p = y;
y.color = z.color;
}
//将x移到原本y的位置,y是黑色结点的话,会破坏性质5,因此进行性质5校验
if(y-original-color == BLACK){
RB-DELETE-FIXUP(T,x);
}
辅助过程(RB-DELETE-FIXUP) ,改变颜色和执行旋转来恢复红黑树性质
伪代码如下:
//
RB-DELETE-FIXUP(T,x)
while(x != T.root and x.color == BLACK){
if(x == x.p.left){
w = x.p.right;
if(w.color == RED){
//case1:x的兄弟结点w是红色的。
w.color = BLACK;
x.p.color = RED;
LEFT-ROTATE(T,x.p);
w = x.p.right;
}
if(w.left.color == BLACK and w.right.color == BLACK){
//case2:x的兄弟结点w是黑色的,而且w的两个子结点都是黑色的。
w.color = RED;
x = x.p;
}else if(w.right.color == BLACK){
//case3:x的兄弟结点w是黑色的,w的左孩子是红色的,w的右孩子是黑色的,做以下操作将case3转换成case4
w.left.color = BLACK;
w.color = RED;
RIGHT-ROTATE(T,w);
w = x.p.right
}
//case4 :x的兄弟结点w是黑色的,且w的右孩子是红色的。
w.color = x.p.color;
x.p.color = BLACK;
w.right.color = BLACK;
LEFT-ROTATE(T,x.p);
x = T.root;
}else{
//如果x是右孩子结点,相同的情况处理
}
}
x.color = BLACK;
case1、case3、case4执行常数次的颜色改变和至多3次旋转后便终止,case2是while循环可以重复执行的唯一情况