首页 > 代码库 > 红黑树

红黑树

红黑树,顾名思义,就是把平衡二叉搜索树的节点赋予两种颜色,通过定义几条规则,达到约束的目的。红黑树可以保证,每次插入删除操作后的重平衡,全树拓扑结构的改变仅需要常数个节点,最坏情况下需要对logn个节点重染色,但是就分摊意义仍然为O(1)。

需要满足的条件:

(1)树根始终为黑色

(2)外部节点均为黑色

(3)红节点的孩子必然为黑色

(4)任一外部节点到根节点的路径,黑节点数量相同

从红黑树的规则,也可以得到一些结论,比如:红节点的父亲必然是黑节点;任一路径黑节点必然不少于红节点。

根据第(4)条规则,可以定义,从根节点通往任一节点的,除去根节点本身,沿途所经过黑节点的总数称为节点的黑深度(black depth)。所以也可以理解为,所有外部节点的黑深度一致。与树高类似,从任一节点通往其任一后代的沿途,经过的黑节点总数称为该节点的黑高度(black height)。

从黑高度的定义可以自然想到,红节点依赖于黑节点而存在,它们的存在甚至都不影响深度,是一种伴随的状态。事实上,红黑树可以认为是一种特殊的B-树,即(2,4)-树。因为红节点的父亲必然为黑节点,故可以把黑节点和他的红节点孩子看作一个B-树节点的关键码,这样就与(2,4)-树等效,其中黑节点必然位于中间,红关键码不超过两个。

根据红黑树的定义,红黑树的高度范围为[log2(n+1),2*log2(n+1)],任一节点的左右高度(不是黑高度)相差不超过两倍。

红黑树的实现

红黑树可以继承二叉搜索树,只需要重写插入和删除操作即可。同时,还需要重新定义高度,此时高度仅指黑高度。一般地外部节点(即使不存在)都认为是黑节点。

插入

根据红黑树的4个要求,可以自然想到,插入的节点不可能为黑,否则将彻底打破全树的平衡,如果父亲以及兄弟节点均为黑,那么将难以调整。反之,插入的节点统一为红色,这样仅会导致局部不满足规则(3)。因此,插入后产生这种问题,称为“双红”。

考虑插入后的几种情况:

(1)父亲节点为黑,这样不需要调整。

(2)父亲节点为红,父亲的兄弟节点为黑。这时即构成了一个3-4问题,对这几个节点进行旋转操作,并将父亲节点染色为黑,祖父染为红即可。这种情况只需要1-2次旋转,2次染色,调整即可完成。

技术分享

(3)父亲节点为红,父亲的兄弟节点也为红。此时无法通过旋转操作,考虑类似B-树的上溢操作,即节点因为超过4度而上溢。从B-树的观点看,将祖父上溢一层,两侧的节点分裂为两个新节点,并将父亲节点染黑,父亲的兄弟节点也染黑即可。祖父上溢后,可能会导致上一层的双红,于是需要继续向上递归。如果递归至根节点,那么需要把g染黑,同时全树的高度+1。这种情况每次操作只需要三次染色,但可能最多需要O(logn)次迭代。

技术分享

删除

红黑树的删除操作,与一般二叉搜索树相同,先找到要删除的元素,然后再删除即可。先进行查找以及替换工作,如果被删除的元素两个孩子不都存在,替换为存在的孩子;如果两个孩子都存在,与直接后继交换。x为实际被删除的节点,而r是它的接替者,r的兄弟为外部节点w=NULL(因为x是后继,x不可能有左孩子),这样可以将x统一视为双分支的节点,方便统一处理。(要注意,这里已经进行了替换,即x是实际删除的节点,后继需要按照二叉树的后继来寻找,并交换他们的数值。如果要删除的是叶节点,那么不存在后继,w本来即为NULL,不需要假设一个,直接释放掉这个节点。总之,实际处理的节点是原来节点的后继,不过已经交换了数值,r是该节点的后继,即原来节点位置后继的后继)

替换以及删除的代码如下:

 1 template<typename T> static BinNodePosi(T) removeAt(BinNodePosi(T)& x, BinNodePosi(T)& hot)
 2 {
 3     BinNodePosi(T) w = x;//实际被摘除的节点
 4     BinNodePosi(T) succ = NULL;
 5     if (!(x->lc))//如果左孩子为空
 6         succ = x = x->rc;//直接替换为右子树
 7     else if (!(x->rc))//如果右孩子为空
 8         succ = x = x->lc;
 9     else//左右孩子都存在,选择x的直接后继作为实际摘除的节点(可以画个图来理解)
10     {
11         w = w->succ();
12         swap(x->data, w->data);//交换x与其直接后继的数值
13         BinNodePosi(T) u = w->parent;
14         ((u == x) ? u->rc : u->lc) = succ = w->rc;//隔离节点w,把w的孩子与原树连接起来
15     }
16     hot = w->parent;//记录被删除节点的父亲
17     if (succ) succ->parent = hot;//将被删除节点的接替者与hot连接
18     release(w->data); release(w); //释放被删除的节点
19     return succ;//返回后继(接替)
20 }
21 template<typename T> bool RedBlack<T>::remove(const T& e)
22 {
23     BinNodePosi(T)& x = search(e); if (!x) return false;
24     BinNodePosi(T) r = removeAt(x, _hot); if (!(--_size)) return true;//删除后不存在元素直接返回
25     if (!_hot)//删除的节点为根节点
26     {
27         _root->color = RB_BLACK; updateHeight(_root); return true;
28     }
29     if (BalckHegihtUpdated(*_hot)) return true;//如果祖先黑深度依然平衡,即可返回
30     if (IsRed(r))//如果接替的节点为红,直接转为黑即可
31     {
32         r->color = RB_BLACK; r->height++; return true;
33     }
34     //否则,需要进行双黑调整
35     solveDoubleBlack(r); return true;
36 }

 

这样,如果实际被删除的节点x为红色,r为黑色,那么直接删除x,将r接入即可,可以等效视作抛弃w。如果x为黑色,r为红色,那么删除x后将r染黑接入即可。

不过删除的时候,可能造成双黑问题,即被删除的节点和接替的后继,都是黑色,这样会导致局部高度降低,从而不满足红黑树的条件。假设实际被删除节点为x,接替者为r,兄弟为s,兄弟的一个孩子为t,x的父亲为p,在这里把双黑分为四种情况:
(1)s有一个孩子t为红色,另外一个孩子不确定颜色,p的颜色也不确定。此时,可以考虑旋转操作(在B-树中为下溢操作),即s为轴的zig旋转,并且把p和t染黑,s继承p的颜色。这样,删除操作即宣告完成。

技术分享

(2)s及其两个孩子均为黑色,p为红色。此时,删除x后,可以将p“拉下来”,合并为一个新节点,并且交换p和s的颜色。因为从B-树的意义,p为红色,那么p的兄弟必然有一个为黑色,故不可能发生新的下溢。

技术分享

(3)s及其两个孩子均为黑色,p为黑色。这种情况,删除x后,同样将p下降一层,并把s染为红色。此时,子树整体黑高度下降,必然引发持续上层下溢,需要继续迭代。

技术分享

(4)s为红色,p必为黑色。此时,删除x仅造成以x为根的子树高度下降,故从红黑树角度,以p为轴进行一次旋转,并交换s和p的颜色即可。此时会有新的问题,即r的高度并没有恢复。但是,r有了新的兄弟s‘,而s‘必然是黑节点,此时问题转换为了(1)或者(2):如果s‘有红孩子,那么转换为(1),如果s‘没有红孩子,转换为(2)。因此,这种情况也不需要迭代,只需要4+1或者4+2。

技术分享

实现代码完全照抄的0 0以后有时间一定自己写一下

 1 template<typename T> void RedBlack<T>::solveDoubleBlack(BinNodePosi(T) r)//输入为用来替换x的后继
 2 {
 3     BinNodePosi(T) p = r ? r->parent : _hot; if (!p) return;//r的父亲,不存在可以直接退出
 4     BinNodePosi(T) s = (p->lc == r) ? p->rc : p->lc;//s为r的兄弟
 5     if (IsBlack(s))//s为黑
 6     {
 7         BinNodePosi(T) t = NULL;//s的红孩子(若左右均红,左优先)
 8         if (IsRed(s->rc)) t = s->rc;
 9         if (IsRed(s->lc)) t = s->lc;
10         if (t)//s为黑且有红孩子的时候(BB-1)
11         {
12             RBColor oldColor = p->color;//备份原子树根节点p的颜色
13             //进行旋转重平衡并将新子树的左右染黑
14             BinNodePosi(T) b = FromParentTo(*p) = rotateAt(t);//返回子树父亲节点
15             if (HasLChild(*b)) { b->lc->color = RB_BLACK; updateHeight(b->lc); }
16             if (HasRChild(*b)) { b->rc->color = RB_BLACK; updateHeight(b->rc); }
17             b->color = oldColor; updateHeight(b);
18         }
19         else//s为黑但是没有红孩子
20         {
21             s->color = RB_RED; s->height--;//s转红
22             if (IsRed(p))//BB-2R
23                 p->color = RB_BLACK;//p转黑但是高度不变
24             else//BB-2B
25             {
26                 p->height--;//p保持黑但是高度下降
27                 solveDoubleBlack(p);//递归上溯
28             }
29         }
30     }
31     else//s为红,p以及s的两个孩子必然都黑
32     {
33         s->color = RB_BLACK; p->color = RB_RED;//s转黑p转红,交换
34         BinNodePosi(T) t = IsLChild(*s) ? s->lc; s->rc;//取t与父亲s同侧
35         _hot = p; FromParentTo(*p) = rotateAt(t);//p原父亲的孩子为现在的根节点
36         solveDoubleBlack(r);//p为红,后续只能为BB-1或BB-2R
37     }
38 }

有问题可以参考邓俊辉大大的原书,虽然我觉得我解释地比原书好一些了,我看原书看了半天才看明白...主要是在二叉搜索树中的removeAt()函数,这个函数会执行一个比较聪明的对策:不真的删除节点后再连接节点,而是交换内部数值后,删除那个交换后的节点,这个实际被删除的节点是原节点的后继,再把子树接入被删除节点的父亲位置_hot。而在这里,x就是这个后继,而r是x的后继,可以说是后继的后继。一切p s r t都是从x来定义的,也就是说从来没有考虑被删除节点原位置的红黑关系,而是直接考虑交换后要删除节点的父亲、兄弟以及后继的红黑关系...这样精简了操作和代码,只需要分为四种情况就可以了,不过理解起来就变得复杂了。

红黑树