介绍
红黑树是一种特殊的平衡二叉树(AVL),可以保证在最坏的情况下,基本动态集合操作的时间复杂度为O(logn)。因此,被广泛应用于企业级的开发中。
红黑树的性质
在一棵红黑树中,其每个结点上增加了一个存储位(属性color)来表示结点的颜色,且颜色只能是red or black。通过对任何一条从根到叶子的简单路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因而是近似于平衡的。
树中每个结点包含5个属性:color、val、lchild、rchild和p(可选)。如果一个结点没有子结点或父结点,则该结点相应指针属性的值为NIL。我们可以把这些NIL视为指向二叉搜索树的叶结点(外部结点)的指针,而把带关键字的结点视为树的内部结点。
一棵红黑树是满足下面红黑性质的二叉搜索树:
- 每个结点或是红色的,或是黑色的。
- 根结点是黑色的。
- 每个叶结点(NIL)是黑色的。
- 如果一个结点是红色的,则它的两个子结点都是黑色的。
- 对每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。
为了便于处理红黑树代码中的边界条件,使用一个哨兵来代表NIL。对于一棵红黑树tree,哨兵NIL是与一个与树中普通结点有相同属性的对象。它的color属性为black,其他属性可以为任意值。
旋转
在一棵含有n个关键字的红黑树上,进行插入和删除操作,需要的时间复杂度为O(logn),由于这两个操作,会导致插入和删除后的树不满足红黑树的性质。为了维护这些性质,需要改变树中某些结点的颜色以及指针结构。
指针结构的修改是通过旋转来完成的,这是一种能保持二叉搜索树性质的搜索树局部操作,旋转分为左旋和右旋。如下图所示:
下面给出左旋和右旋操作的代码为:
1 template<typename T> 2 void RedBlackTree<T>::LeftRotation(RedBlackNode<T>* &t){ 3 RedBlackNode<T> *temp = t->rchild; 4 t->rchild = temp->lchild; 5 if(Parent(t)==NIL){ 6 root = temp; 7 } 8 temp->lchild = t; 9 Parent(t)->rchild = temp; 10 } 11 12 template<typename T> 13 void RedBlackTree<T>::RightRotation(RedBlackNode<T>* &t){ 14 RedBlackNode<T> *temp = t->lchild; 15 t->lchild = temp->rchild; 16 if(Parent(t)==NIL){ 17 root = temp; 18 } 19 temp->rchild = t; 20 Parent(t)->lchild = temp; 21 }
左旋和右旋的伪码可以参考《算法导论》(砖头书)。
红黑树的插入操作
前面说过,在一棵含有n个关键字的红黑树上,执行插入操作,需要时间复杂度为O(logn)。为了做到这一点,需要往原红黑树中插入一个红色的结点。那么问题来了,为什么插入的是红色结点,而不是黑色结点呢?我们知道,红黑树有五个性质,如果插入红色结点,则可能会违反性质4,而如果是插入黑色结点,则一定会违反性质5。也就是说,插入红色结点比插入黑色结点更不容易违反红黑树的性质,而违反性质条数越多,相应的要做的调整操作也就越多,导致算法的时间复杂度也就越高,从而影响算法的执行速度。在《数据结构算法与解析》(高一凡著,清华大学出版社)一书中,给出了插入结点为红色以及插入结点为黑色两种操作的算法,本文以插入结点为红色进行讲解。
对于一棵红黑树来说,插入一个红色结点,主要有六种情况,其中三种与另外三种是对称的。这一点取决于插入结点 z 的父亲结点是插入结点的祖父结点的左孩子还是右孩子。
下面给出两种对称下,所对应的三种情况:
- case1:插入结点 z 的叔结点 y 是红色的。
上图显示了该情况的情形,这种情况实在插入结点z的父结点z.p和其叔结点y都是红色时发生的。因为插入结点z的祖父结点z.p.p是黑色的,所以将z.p和y都着为黑色,来解决z和z.p都是红色的问题,而由于性质5的要求,如果只是把z.p和y的颜色改为黑色,则会破坏该性质,因此需要对z.p.p结点的颜色进行调整,以保证性质5的满足。
但是,性质1调整以后,就一定能维持红黑树的性质吗?我们以z表示新插入的结点,z‘表示经过此次操作后的结点,由上述操作可以知道,z‘=z.p.p。则经过此次操作后,有以下结果:
-
- 因为这次操作把z.p.p着为红色,结点z‘在下次操作的开始是红色的。
- 在这次操作中结点z‘.p是z.p.p.p,且这个结点的颜色不会改变。如果它是根结点,则在此次迭代之前它是黑色的,且它在下次操作的开头任然是黑色的。
- 我们也知道,case1保持性质5,而且它也不会引起性质1或性质3的破坏。
如果结点z‘在下一次操作开始时是根结点,则在这次操作中case1修正了唯一被破坏的性质4。由于z‘是红色的而且是根结点,所以性质2成为唯一被违反的性质,这是由z‘导致的。
如果结点z‘在下一次操作开始时不是根结点,则case1不会导致性质2被破坏,case1修正了在这次操作的开始唯一违反的性质4。然后把z’着色为红色而z‘.p不变,因此,如果z‘.p是黑色的,则没有违反性质4,若是z‘.p是红色的,则违反了性质4。
- case2:插入结点 z 的叔结点 y 是黑色的且 z 是一个右孩子。
- case3:插入结点 z 的叔结点 y 是黑色的且 z 是一个左孩子。
在case2和case3中,z的叔结点是黑色的。通过z是z.p的右孩子还是左孩子来区别这两种情况(叔结点都是黑色,无法在逻辑上进行区别)。对于这两种情况,如下图所示:
左图为case2,右图为case3
我们发现case2与case3存在某种指针结构上的关系,很明显二者之间可以通过左旋和右旋操作进行相互转换。由于z和z.p都是红色的,因此,旋转操作对树的黑高和性质5都无影响。无论怎么进入哪两种情况,y总是黑色的,否则就要执行case1对应的操作。此外,这种旋转操作,有个好处是,并不改变旋转后,z的身份,尽管会导致z的左右孩子身份改变了,但依旧是z.p的孩子。在case3中,我们可以通过改变某些结点的颜色,并作一次右旋,就能保证性质5。这样,由于在一行中不会再存在有两个红色结点,因此,保证了红黑树的性质,所有的处理也到此完毕了。如下所示:
可以看到,case2和case3的操作,会最终使得插入结点后的树,维持红黑树的性质。由此,不禁怀疑,这样的操作能完全保证吗?答案是肯定的。下面来证明:
- case2让z指向红色的z.p。在case2和case3中,z或z的颜色都不会改变,所以,在由case2转为case3后,这并不会产生其他性质的改变。
- case3把z.p着成黑色,使得如果z.p在下一次操作开始时是根结点,则它是黑色的。
- 和case1一样,红黑树的性质1、3、5都在case2和case3中得以保持。
由于结点z在case2和case3中都不是根结点,因此,性质2未被破坏,这两种情况因此也不会引起性质2的违反。由此,证明了z.p为z.p.p的左孩子时候,对插入z后的红黑树,按照上述调整,可以做到恢复红黑树的性质。而当z.p为z.p.p的右孩子时,由于与前面一大情况是对称的,因此,通过修改left和right的对应,就可实现。而完全实现树的回复,可以通过while循环来保持。以下是实现树的插入的代码:
1 template<typename T> 2 bool RedBlackTree<T>::Insert(T e){ 3 RedBlackNode<T> *p, *f; 4 p = f = NULL; 5 6 if(!searchBST(root, p, e, f)){//not found, need to create, p points to the last node. 7 8 RedBlackNode<T> *s = createNewNode(e); 9 if(root==NULL){ 10 root = s; 11 root->color = "black"; 12 } 13 else{ 14 if(e<p->val){ 15 p->lchild = s; 16 } 17 else{ 18 p->rchild = s; 19 } 20 if(p->color == "red"){//double red node, need to adjust 21 adjustDoubleRed(s, p); 22 } 23 } 24 return true; 25 } 26 else{//node exists. return false 27 return false; 28 } 29 } 30 31 template<typename T> 32 RedBlackNode<T>* RedBlackTree<T>::Parent(RedBlackNode<T>* &t)const{ 33 /* 34 *@Parameter: 35 *q: a queue to save rb-node. 36 *t: a point which points to a node in the RBTree. 37 *father: a point which points to the father node of t. 38 */ 39 queue<RedBlackNode<T>*> q; 40 RedBlackNode<T>* father; 41 if(root!=NULL){ 42 q.push(root); 43 while(!q.empty()){//BFSTraverse to find the father node of t. 44 father = q.front(); 45 q.pop(); 46 if((father->lchild!=NIL&&father->lchild==t)||(father->rchild!=NIL&&father->rchild==t)){ 47 return father; 48 } 49 else{ 50 if(father->lchild!=NIL){ 51 q.push(father->lchild); 52 } 53 if(father->rchild!=NIL){ 54 q.push(father->rchild); 55 } 56 } 57 } 58 } 59 return NIL; //not found, return NIL 60 } 61 62 template<typename T> 63 bool RedBlackTree<T>::searchBST(RedBlackNode<T>* &t, RedBlackNode<T>* &p, T &e, RedBlackNode<T>* f)const{ 64 //在树中t中递归地查找其值等于e的数据,若查找成功,则指针p指向该数据 65 //结点,并返回true,否则指针p指向查找路径上访问的最后一个结点以便插入 66 //并返回false,指针f指向p的双亲,其初始调用值为NULL。Insert()调用 67 if(t==NULL||t==NIL){ 68 p = f; 69 return false; 70 } 71 if(e==t->val){ 72 p = t; 73 return true; 74 } 75 else if(e<t->val){ 76 return searchBST(t->lchild, p, e, t); 77 } 78 else{ 79 return searchBST(t->rchild, p, e, t); 80 } 81 } 82 83 template<typename T> 84 void RedBlackTree<T>::LeftRotation(RedBlackNode<T>* &t){ 85 RedBlackNode<T> *temp = t->rchild; 86 t->rchild = temp->lchild; 87 if(Parent(t)==NIL){ 88 root = temp; 89 } 90 temp->lchild = t; 91 Parent(t)->rchild = temp; 92 } 93 94 template<typename T> 95 void RedBlackTree<T>::RightRotation(RedBlackNode<T>* &t){ 96 RedBlackNode<T> *temp = t->lchild; 97 t->lchild = temp->rchild; 98 if(Parent(t)==NIL){ 99 root = temp; 100 } 101 temp->rchild = t; 102 Parent(t)->lchild = temp; 103 } 104 105 template<typename T> 106 void RedBlackTree<T>::adjustDoubleRed(RedBlackNode<T>* &s, RedBlackNode<T>* &p){ 107 /* 108 *@Parameter: 109 *s: rb-node. 110 *p: the father node of s. 111 */ 112 RedBlackNode<T> *y, *gp; 113 while(p->color=="red"){ 114 gp = Parent(p); 115 if(p==gp->lchild){ 116 y = gp->rchild; 117 if(y->color=="red"){//case 1 118 p->color = "black"; 119 y->color = "black"; 120 gp->color = "red"; 121 s = gp; 122 p = Parent(s); 123 } 124 else if(s==p->rchild){//case 2 125 s = p; 126 LeftRotation(p); 127 } 128 else{ 129 p->color = "black"; 130 gp->color = "red"; 131 RightRotation(gp); 132 } 133 } 134 else{ 135 y = gp->lchild; 136 if(y->color=="red"){//case 1 137 p->color = "black"; 138 y->color = "black"; 139 gp->color = "red"; 140 s = gp; 141 p = Parent(s); 142 } 143 else if(s==p->lchild){//case 2 144 s = p; 145 RightRotation(s); 146 } 147 else{ 148 p->color = "black"; 149 gp->color = "red"; 150 LeftRotation(gp); 151 } 152 } 153 } 154 root->color = "black"; 155 }
代码的操作,与前文图片所描述的操作相一致。
红黑树的删除操作
由于红黑树与BST树相似,因此,其删除操作与BST树在逻辑上是基本一致的,唯一的区别在于,红黑树需要对删除结点后的树进行调整,使其符合红黑树的性质。对于一棵红黑树来说,如果先不考虑结点的颜色,删除一个结点无非是三种情况,这一点与BST树是一致的,即:
- 被删除结点没有左右子结点;
- 被删除结点仅有一个子节点(左或右都有可能);
- 被删除结点左右子结点都存在;
根据上述三种情况,可以编写出BST树的删除结点操作的代码,下面给出BST树的删除操作示意图:
很明显,红黑树在结点的结构上,也是符合上述形式的,即左<根<右,因此,红黑树的删除操作是从BST输的删除操作的基础上,修改得到的,为什么需要修改呢?就是因为红黑树的每个结点具有红黑属性。
由于红黑属性的影响,导致,删除结点后红黑树将不符合红黑树原有的特性,我们知道,删除某个结点,按照上述调整,将会使得被删除结点所在的子树不符合原红黑树的特性1、2、4或5(非删除结点不受影响)。因此,只需要对子树进行颜色调整,就能使红黑树性质保持不变。
伪码中Transplant函数的实现
如何删除的原理已经讲明白了,那么我们看,两个结点是如何替换(也就是发生删除操作的)。
在伪码中,结点u为被替换结点,你可以理解为,被删除结点,而v是用来替换被删除结点的结点(通常为u的子节点或者u的右子树结点的最小节点)。
下面是我实现的transplant函数:
1 template<typename T>
2 void RedBlackTree<T>::Transplant(RedBlackNode<T>* &u, RedBlackNode<T>* &v){ 3 /* 4 *a function to achieve node u is replaced by v. 5 *@Parameter: 6 *u: a node which is replaced by v. 7 *v: a node wants to replace u. 8 */ 9 if(Parent(u) == NIL){//待删除结点为根结点. 10 root = v; 11 } 12 else if(u==Parent(u)->lchild){ 13 Parent(u)->lchild = v; 14 } 15 else{ 16 Parent(u)->rchild = v; 17 } 18 }
红黑树删除操作的具体实现
下面给出删除操作的伪代码(源自《算法导论》)。
在上述代码中,结点z为删除结点,y为指向结点z的指针。我们知道,BST的删除操作是很容易实现的,对于红黑树来说,关键在于,删除操作以后,什么情况下,会破坏红黑树的红黑性质。
由于y的颜色有可能发生改变(因为根据代码,y始终指向树结构中被删除结点的位置),用变量y_original_color存储了发生改变前的y位置的颜色。第2行和第10行在给y赋值之后,立即设置该变量。当z有两个子结点时,则y!=z且结点y移至红黑树中结点z的原始位置;第20行给y赋予和z一样的颜色。然后保存y的原始颜色,以在删除操作结束时,测试它;如果它是黑色的,那么删除或移动y会引起红黑性质的破坏,为什么会是黑色引起红黑性质的破坏呢?
- 对于情况1,即不存在子结点的被删除结点来说,什么情况下删除该结点以后会改变原有红黑树的性质呢?很显然,被删除结点是黑色的时候,删除它会违背红黑树的性质5,而被删除结点为红色的时候,删除它并不会影响红黑树的性质,直接修改即可(如下所示),在这里,就产生了删除结点后,后续修改颜色的第一种情况。
如上图所示,如果删除结点5,对于左侧的树,如果删除结点y,则结点7不会违反任何红黑树的性质,因为结点y的子结点为必定为黑色(由于红黑树的性质),因此,y为红色不会引起红黑树性质的改变;对于右侧的树,如果删除结点y,则如果结点y的子结点为NIL(黑色),不会引起结点7与结点y子结点之间都为红色,从而不违反了红黑树的性质。
- 对于情况2,即存在左结点或者右结点,由于红黑树结点存在color属性,因此,常见的做法是将用来替换删除结点的结点的颜色改成与删除结点颜色一致,这样,只需要对修改过指针结构后的子树进行修改其颜色,即可完成红黑树性质的保持。那么,由于颜色的存在,又会有那些情况的出现呢?我们知道,一个结点无非是红色或者黑色,且根据性质,红结点的子结点颜色必为黑色,那么,以被删除结点为左结点为例,有下面两种情况:
从左边的红黑树来看,如果待删除结点颜色为黑色,当对该结点进行操作时,则由于,其子结点为红色,与y结点的父结点同为红色,因此,会违背红黑树的性质,而如果是右边的情况,则不会,因为删除结点y以后,由于结点4依旧为黑色,不会破坏红黑树的性质。对于这种情况下,左子树结点不存在而右子树结点存在的情况,也是同样的道理,读者可以自己画图思考一下。
- 对于情况3,即被删除结点同时存在左右子结点,如下图所示:
从上图来看,如果删除结点5,会与情况2一样而违反性质,而对于右边的树,则不会,因为,我们删除的方式,是将z结点也就是结点5的左子树,连接到结点5右子树的值最小的结点上。然后用这个最小结点来替换原来结点z(也就是图上结点5的位置)。这样做的好处是,仍可以保证删除后的树仍满足BST树的性质,我们只需要对被删除结点的子树进行修改颜色性质就可以了,而且,不论最小结点的颜色如何,都不会导致出现两个红结点的情况。这一点可能很多人会存在疑惑,我们来分析一下:
对于左右子树都存在的的删除结点来说,此时,y从指向z转向了指向删除结点z的右子树种最小的一个结点(右子树的最左结点),这样的指向,无非两种情况,一种是右子树的最左结点就是被删除结点z的右孩子,即其右孩子的左孩子为NIL,这样,以上右图为例,y指向了结点6,然6后,用y_original_color来保存结点6的颜色,用x指向6的右孩子,由于在这里,y的父亲结点依旧为删除结点z,因此,设置好结点属性x.p = y,然后,执行替换操作(Transplant)来实现删除的目的,可以看到,transplant操作是对删除结点z和替换结点y进行操作的,对于这种情况来说,是将z与其右孩子进行替换,根据伪码,结点7的左孩子指向了结点6。然后结点6也就是现在的结点y的左孩子指向了结点z的左孩子,这样就完成了然后将结点y的颜色,改成结点z的颜色,为什么这么做呢,是为了保持与原来红黑树相同的特性,因为我们知道,在删除结点5之前,结点5左右两棵子树的一条路径上的黑色结点数目是相同的,但是,由于结点6的上位(替换了其父结点),而父结点的左孩子直接成为其左子树,这就导致了左右两子树的不平衡,调整为与z结点相同的颜色以后,可以使得对红黑树修改操作仅局限于结点y这棵子树中进行。
另一种情况则是右子树的最左结点不是被删除结点z的右孩子,即其右孩子的左孩子非空,那么,其余操作不变,比较巧妙的是,这里运用了最左结点的左孩子为NIL的特性,将最左结点的右子树接到了其父节点的左边(这就保证了BST树的有序性),且这样的做法,使得该结点与树在结构上断开,无法通过树的根结点访问到,因此,后续再执行一次删除结点z与结点y的替换操作,就可以完成这样的删除操作。这种情况下,会产生y为红色或者黑色结点的问题,同样,也只会有黑色会违背红黑树的性质。
红黑树删除操作后的修改结点颜色操作
通过上面的分析,我们很容易得到这样的结论,那就是,只有当y为黑色结点的时候,才会发生违背红黑树性质的情况,因此,需要对这样的情况进行修改,来保持红黑树的性质。下面给出《算法导论》中,关于该操作的伪代码:
在分析源码以前,我们首先来分析,执行删除操作以后,会出现哪些违背红黑树性质的情况。在这个操作中,结合删除操作的代码,我们可以发现,x始终指向具有双重黑色的非根结点。那么,就会有4种情况,这四种情况与插入操作中的是类似的,请读者结合上述删除操作,自行分析。
下面附上红黑树删除操作的代码。
1 template<typename T> 2 void RedBlackTree<T>::Delete(RedBlackNode<T>* &t){ 3 /* 4 *function to delete node t in redblacktree. 5 *@Parameter: 6 *t: a node need to be deleted. 7 */ 8 RedBlackNode<T> *y; 9 RedBlackNode<T> *p; 10 y = t; 11 string y_original_color = y->color; 12 if(t->lchild==NIL){ 13 p = t->rchild; 14 Transplant(t, t->rchild); 15 } 16 else if(t->rchild==NIL){ 17 p = t->lchild; 18 Transplant(t, t->lchild); 19 } 20 else{ 21 y = TreeMinimum(t->rchild); 22 y_original_color = y->color; 23 p = y->rchild; 24 if(Parent(y)!=t){ 25 Transplant(y, y->rchild); 26 y->rchild = t->rchild; 27 } 28 Transplant(t, y); 29 y->lchild = t->lchild; 30 y->color = t->color; 31 } 32 33 if(y_original_color=="black"){ 34 RBDeleteFixup(p); 35 } 36 delete t; 37 t = NULL; 38 }
红黑树删除后,修改颜色的操作代码。
template<typename T> RedBlackNode<T>* RedBlackTree<T>::TreeMinimum(RedBlackNode<T>* t){ RedBlackNode<T> *p; while(t!=NIL){ p = t; t = t->lchild; } return p; } template<typename T> void RedBlackTree<T>::RBDeleteFixup(RedBlackNode<T>* &t){ RedBlackNode<T> *p; RedBlackNode<T> *f; while(t!=NIL&&t->color=="black"){ if(t==Parent(t)->lchild){ p = Parent(t)->rchild; if(p->color=="red"){ p->color = "black"; Parent(t)->color = "red"; f = Parent(t); LeftRotation(f); p = Parent(t)->rchild; } if(p->lchild->color=="black"&&p->rchild->color=="black"){ p->color = "red"; t = Parent(t); } else if(p->rchild->color=="black"){ p->lchild->color = "black"; p->color = "red"; RightRotation(p); p = Parent(t)->rchild; } else{ p->color = Parent(t)->color; Parent(t)->color = "black"; p->rchild->color = "black"; f = Parent(t); LeftRotation(f); t = root; } } else{ p = Parent(t)->lchild; if(p->color=="red"){ p->color = "black"; Parent(t)->color = "red"; f = Parent(t); RightRotation(f); p = Parent(t)->lchild; } if(p->rchild->color=="black"&&p->lchild->color=="black"){ p->color = "red"; t = Parent(t); } else if(p->lchild->color=="black"){ p->rchild->color = "black"; p->color = "red"; LeftRotation(p); p = Parent(t)->lchild; } else{ p->color = Parent(t)->color; Parent(t)->color = "black"; p->lchild->color = "black"; f = Parent(t); RightRotation(f); t = root; } } } t->color = "black"; }