算法导论红黑树的探讨
第一部分:红黑树表示与旋转
红黑树是一种平衡二叉树,其性质不再赘述。
红黑树的数据结构如下:
#define RED 0 #define BLACK 1 #define INFINITY 345879790 struct RB_node { RB_node *left; //左孩子 RB_node *right; //右孩子 RB_node *parent; //父节点 int color; //结点的颜色 int key; RB_node(RB_node* init,int c_init,int num):left(init),right(init),parent(init),color(c_init),key(num){} }; struct RB_tree { RB_tree *root; RB_tree *nil; RB_tree() { nil=new RB_node(NULL,BLACK,INFINITY); //树的哨兵必然为黑色 root=nil; } };
值得特别说明的是,这里的T->nil表示的是哨兵结点,是为了方便之后的运算。
红黑树的旋转过程,如下图所示:
注意旋转的前后,从左到右,子树依次是y->left y->right x->right,但是y->right的parent变成了x了!
红黑树旋转的过程如下给出:
#include "RB_tree.h" void left_rotate(RB_tree *T,RB_node *x) { RB_node *y=x->right; //左孩子结点 x->right=y->left; //这一部处理的是加在x,y之间的“内结点”,y->left原先 //夹在x->left和y->right之间,旋转之后子树的相对顺序不变,最外边的结点是 //x->left和y->right,注意是围绕x和y进行的旋转,所以子树的相对位置保持不变 if(y!=NULL && y->left!=NULL) y->left->parent=x; //旋转之后需要重新更新parent结点信息 y->parent=x->parent; //这个时候y作为子树的根了,y要连到祖先中! if(x->parent==NULL) T->root=y; else if(x->parent->left==x) x->parent->left=y; else x->parent->right=y; //保证x与祖先结点相连接 //最后处理x和y的关系 y->left=x; x->parent=y; } //右旋,对称的代码 void right_rotate(RB_tree *T,RB_node *x) { //只需要把上述代码的相对位置,right和left互换就可以了 RB_node *y=x->left; x->left=y->right; if(y!=NULL && y->right!=NULL) y->right->parent=x; //旋转之后需要重新更新parent结点信息 y->parent=x->parent; if(x->parent==NULL) T->root=y; else if(x->parent->left==x) x->parent->left=y; else x->parent->right=y; //保证x与祖先结点相连接 //最后处理x和y的关系 y->right=x; x->parent=y; }
红黑树的旋转左旋和右旋是对称的。
第二部分:红黑树的插入和性质的维护
#include "RB_tree.h" void RB_insert_fixup(RB_tree *T, RB_node *z); void RB_insert(RB_tree *T, RB_node *z) { RB_node *y=NULL; //y作为遍历指针的双亲 RB_node *x=T->root; while(x!=NULL) { y=x; if(z->key<x->key) x->x->left; else x=x->right; } z->parent=y; //y作为遍历指针x的母结点,x遍历寻找合适的插入位置 if(y==NULL) T->root=z; else if(z->key<y->key) y->left=z; else y->right=z; z->left=NULL; z->right=NULL; z->parent=RED; RB_insert_fixup(T,z); //最后插入的结点是红色的,与此同时进行红黑树性质的调整 } void RB_insert_fixup(RB_tree *T,RB_node *z) { while(z->parent->color==RED) { if(z->parent==z->parent->parent->left) { RB_node *y=z->parent->parent->right; //设置叔结点 if(y->color==RED) { z->parent->color=BLACK; //父节点的黑色可以下放给两个结点 y->color=BLACK; z->parent->parent->color=RED; z=z->parent->parent; //z沿树上升 } else { if(z==z->parent->right) //内结点先旋转成外节点 { z=z->parent; left_rotate(T,z); } z->parent->color=BLACK; //改变颜色之后,z的兄弟结点和z颜色相同 z->parent->parent->color=RED; //红黑树局部恢复平衡 right_rotate(T,z->parent->parent); //这个技巧是:改变颜色之后,然后旋转,这样不破坏红黑树性质 } //与此同时,结点沿树上升 } else { RB_node *y=z->parent->parent->left; if(y->color==RED) { z->parent->color=BLACK; y->color=BLACK; z->parent->parent->color=RED; z=z->parent->parent; } else { if(z==z->parent->left) { z=z->parent; right_rotate(T,z); } z->parent->color=BLACK; z->parent->parent->color=RED; left_rotate(T,z->parent->parent); } } } T->root->color=BLACK; }
实现过程说明:
红黑树的插入是比较容易理解的,下面着重来讨论红黑树插入的调整:
情形一:
这个时候只要把根部A这个点的黑色,下放给两个孩子结点,这样红黑树的性质得到保持,这个时候刚插入的节点沿树上升就可以了。
情形二:
如果z是内节点,要首先旋转成外结点,因为外节点的相对位置是不变的,内节点在旋转的过程中还会更换parent!所以用外节点处理方便,值得注意的是,在旋转过程中,z的相对高度要保持不变,所以要执行z=z->parent,然后再旋转!这样沿树上升的时候,保证所有的结点都得到处理!
最后一步改变颜色,做到了红黑树的局部平衡,这个时候改变颜色之后再执行旋转,不破坏红黑树的平衡性,与此同时,z结点沿树上升,这样局部平衡得到保持。最后只要树根置为black就可以了。
第三部分:红黑树的删除
红黑树删除的代码和普通二叉树的删除没有多少区别,主要是要关注:
删除一个结点之后,记得寻找这个结点的后继,后继不一定是right,而是min(x->right)
#include "RB_tree.h" //删除调整,这个时候x有两重黑色 void RB_delete_fixup(RB_tree *T, RB_node *x) { RB_node *w=NULL; while (x!=T->root && x->color==BLACK) { if(x==x->parent->left) { w=x->parent->right; //w为x的兄弟结点 if(w->color==RED) { w->color=BLACK; //将这种情况划归为下面一种情况 w->parent->color=RED; left_rotate(T,x->parent); w=x->parent->right; } if(w->left->color==BLACK && w->right->color==BLACK) { W->color=RED; x=x->parent; //x和w的黑色沿树上升,注意到x有双重黑色,所以x的颜色不变 } //变得是w,x->parent的颜色 else { if(w->right->color==BLACK) { w->left->color=BLACK; w->color=RED; //同样,内节点变为外节点 right_rotate(T,w); w=x->parent-?right; } w->color=x->parent->color; //这里x->parent的颜色不确定,但是w的颜色是黑色 //x有双重黑色,通过改变颜色加上旋转,可以将双重黑色表现在图中,这样完成了红黑树的局部平衡 x->parent->color=BLACK; w->right->color=BLACK; left_rotate(T,x->parent); //红黑树局部平衡了 x=T->root; } } else { w=x->parent->left; if(w->color==RED) { w->color=BLACK; x->parent->color=RED; right_rotate(T,x->parent); w=x->parent->left; } } if(w->left->color==BLACK && w->right->color==BLACK) { w->color=RED; x=x->parent; } else { if(w->left->color==BLACK) { w->right->color=BLACK; W->color=RED; left_rotate(T,w); w=x->parent->left; } w->color=x->parent->color; x->parent->color=BLACK; w->left->color=BLACK; right_rotate(T,x->parent); x=T->root; } } x->color=BLACK; } void transplant(RB_tree *T, RB_node *u, RB_node *v) { if(u->parent==T->nil) T->root=v; else if(u==u->parent->left) u->parent->left=v; else u->parent->right=v; if(v!=NULL) v->parent=u->parent; } RB_node *tree_minimum(RB_tree *T,RB_node *x) { while (x->left!=NULL) { x=x->left; } return x; } void RB_delete(RB_tree *T, RB_node *z) { RB_node *y=z, *x; int y_original_color=y->color; if(z->left==NULL) { x=z->right; transplant(T,z,z->right); } else if(z->right==NULL) { x=z->left; transplant(T,z,z->left); } else { y=tree_minimum(T,z->right); //这里是查找z的后继,为y,用y来代替z的位置 y_original_color=y->color; x=y->right; if(y->parent==z) { x->parent=y; } else { transplant(T,y,y->right); y->right=z->right; y->right->parent=y; } transplant(T,z,y); y->left=z->left; y->left->parent=y; y->color=z->color; } if(y_original_color==BLACK) RB_delete_fixup(T,x); //x为y的孩子结点,y的删除会影响x的相关性质 }
具体来看红黑树删除的调整:
情形一:
这个时候x的兄弟节点w是red,一个红节点必然跟着两个黑节点,这个时候改变颜色,并且执行旋转,可以将这种情况划归为第二种情况,就是x和w都是黑色。为什么要这么做?主要的原因是x有双重黑色,我们需要让x的黑色沿树上升,w的黑色也沿树上升,w为白色,要想办法让它变成黑色。改变颜色+旋转是常用的方法。
情形二:
这个时候需要让x和w的黑色都沿树上升。注意到这个时候x有双重黑色,所以x的黑色不改变。
情形三:
这个时候和插入调整一样,内节点要转化成外节点。
值得注意的是,这个时候w->left的颜色一定是red,因为不是的话就变成了第二种情况啦!
这个时候红色的结点是内结点,要将它旋转成外节点处理,理由在插入调整的过程中已经说了很清楚了。
情形四:
这个时候要注意的是:x->parent的颜色没有确定下来,但是w的颜色一定是black。
交换颜色的代码是:
w->color=x->parent->color; x->parent->color=BLACK; w->right->color=BLACK;
如图所示,改变颜色+完成旋转之后,x的双重黑色已经体现出来了。这个时候红黑树达到了局部平衡。
所以将x置为根节点,确定颜色为黑色就可以了。
第四部分:算法导论课后习题解答
13.1红黑树的性质
13.1-1
13.1-2
1、不行,违反性质4
2、不行,违反性质5
13.1-3
是,我们在调整过程中就是利用松弛红黑树的性质,最后将T->root-color置为black。
13.1-4
红黑树的黑高为从该点开始,所含有的黑节点的个数,所以红黑树的黑高保持不变。
可能的度为:
2 这个时候它的两个子节点原来都是黑色
3 这个时候一个子节点为红色,另一个为黑色,红色子节点又跟随两个黑色子节点,度为3
4 两个子节点都是红色
13.1-5
$rh(x)≤bh(x)$
$h(x)=rh(x)+bh(x)≤2bh(x)$
其中,rh(x)表示红高,bh(x)表示黑高
所以最长的一条最多是最短一条的两倍
13.1-6
黑高为k,内部节点最少为:$2^k-1$
最多为:
由13.1-5的结论,最长路径$≤2k+1$
最多节点为: $2^{2k+1}−1$
13.1-7
比值最小为0,结点全部为黑色。
比值最大为2:1
参见13.1-1图,一个black两个结点都是红的,每个红节点又有两个黑节点,红节点和黑节点的比值为:
4:2=2(内部节点,不算根节点)
13.2旋转
13.2-1
已写出
13.2-2
n个节点有n-1条边,所以可能有n-1种旋转
13.2-3
可以发现b的深度不变,a的深度增加,c的深度减少
13.2-4
最坏的情况,仅有左子树,假设左子树有n个节点,可以看出左子树旋转成右子树需要n-1次。
这样左子树变成一条右侧伸展的链。
从左侧伸展的链旋转成为右侧伸展的链,最坏需要n-1的时间,其余任何一种情况,所需时间均少于
n-1
13.2-5
T1可以右转成T2,可知若有i个结点,需要旋转i-1条边
$$
sum_{i = 1}^n a_i=O(n^2)
$$
13.3插入
13.3-1
如果将z染色成黑色,很显然违反红黑树性质5,违反红黑树性质5,做调整的时候是针对高度来调整。
这个时候红黑树还必须满足一种高度平衡:就是任意路径,从root到nil,红黑树的黑高差恒等于0
在调整的时候比较麻烦。实际上这是属于类AVL树了。
高度平衡的二叉树,是AVL树,这是另外一种平衡二叉树。
13.3-2
13.3-3
13.3-4
z为叶子节点,则z->parent为根节点。根节点的颜色肯定恒为黑色,这个时候退出循环while(z->parent->color==RED)
那么也就遍历不到z->parent->parent了。
13.3-5
插入的n-1个节点都为black,当插入第n个节点的时候,新插入的节点为红色,满足红黑树性质。
13.3-6
RB_node *tree_search(RB_tree *T, RB_node *x, int k, RB_node *par, RB_node *p_par) { //人为定义一个tree_search函数,从根节点遍历,寻找x的parent和parent->parent while(x!=T->nil && k!=x->key) { par=x; if(k<x->key) x=x->left; else x=x->right; if(k!=x->key) //如果没有找到待查找的值,此时x=x->child { //记录下遍历前的x=p_par,遍历后的x=par,返回x,此时用par和p_par表示 p_par=par; par=x; } } return x; } void RB_insert_fixup(RB_tree *T, RB_node *z) { //相关代码 RB_node *par=T->nil, *p_par=T->nil; tree_search(T,T->root,z->key,par,p_par); //将原来的z->parent用par代替,z->parent->parent用p_par代替 }
13.4删除
13.4-1
在执行RB_delete_fixup之后,可以发现,x=root, x->color=BLACK,所以树根一定是黑色的
13.4-2
此时不进入循环,x->color=BLACK,恢复性质4
13.4-3
13.4-4
1) 1、4、5行检查哨兵,因为被删除的节点没有孩子,所以检查T->nil的情况
2) 左旋,右旋的时候,w的兄弟没有孩子,此时检查哨兵
13.4-5
检查在稿纸上进行
13.4-6
情况一,w的颜色为红色,所以x->parent的颜色一定是黑色
13.4-7
不一样
如下图