一、红黑树的存在意义
平衡二叉搜索树的形式多样,各具特色,如伸展树实现简便,不需要修改节点的数据结构即可使分摊复杂度降低到O(logn)的量级,但是在最坏情况下单次搜索操作需要Ω(n)的时间,因此其难以被使用在核电站、医院等对可靠性和稳定性要求极高的场合。而AVL树,虽然其可以保证在最坏情况下的单次操作速度,但是其需要在节点中嵌入平衡因子标识,而更加严重的是,其在单次删除操作之后可能导致Ω(logn)次旋转,这种现象导致全树整体拓扑结构的频繁的大幅度的变化,因此这限制了其在实际中的实用性。
红黑树是在1972年由RudolfBayer发明的,它在普通二叉搜索树的基础上增加了一些约束条件,从而使红黑树的单次最坏操作的复杂度仅需O(logn),且每次恢复红黑树结构在最多只需要O(logn)个节点进行重染色,需要不超过3次的树旋转(删除是3次,插入是2次),所以其为持久性结构(一致性结构)。可见红黑树在时间复杂度和结构变换方面具有很强的稳定性,而且与AVL树相比,其只是略微地放松了对高度的要求(任一节点左右子树的高度,相差不得超过2倍),在别的方面并没有很大的牺牲,所以这种变化是绝对值得的。
红黑树和AVL树一样都对插入时间、删除时间和查找时间提供了最好可能的最坏情况担保。这不只是使它们在时间敏感的应用如即时应用(real time application)中有价值,而且使它们有在提供最坏情况担保的其他数据结构中作为建造板块的价值;例如,在计算机几何中使用的很多数据结构都可以基于红黑树。
红黑树在函数式编程中也特别有用,在这里它们是最常用的持久数据结构之一,它们用来构造关联数组和集合,在突变之后它们能保持为以前的版本。除了O(logn)的时间之外,红黑树的持久版本对每次插入或删除需要O(log n)的空间。
值得一提的是,红黑树确实是很复杂的,但其复杂性不会使其性能作任何的牺牲,应该说,这种复杂性其实是红黑树设计巧妙性的体现。
二、红黑树的定义
红黑树是每个节点都带有颜色属性的二叉搜索树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
(1) 根节点为黑色;
(2) 外部节点为黑色;
(3) 其余节点若为红色,则其孩子节点必为黑色;(红节点的父亲和孩子都必须为黑色)
(4) 从任一外部节点到根节点的沿途,黑节点的数目相对。(黑深度)
这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长,导致这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。
平衡性:
注意到性质3导致了高度的约束:路径不能有两个相邻的红色节点,则最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质4所有最长的路径都有相同数目的黑色节点,这就表明了任一节点左右子树的高度,相差不得超过2倍,所以树是平衡的,树高h=O(logn)。
红黑树的具体高度:
若T高度为h,黑高度为H,则 h = R + H <= 2H
对于一个有n个内部节点的红黑树,log2(n+1)<=h<=2*log2(n+1) (推导:最小高度为黑高度为4阶B-树的高度,最大高度为黑高度的2倍)
如下为隐藏了外部节点(虚拟节点)的一颗红黑树。
三、4阶B-树角度下的红黑树
红黑树的定义晦涩难懂,而幸运的是,对红黑树进行简单的转换,即可转换为4阶B-树。
具体的转换为:自顶向下逐层检查红黑树各个节点,每遇到一个红色节点,则将以该红色节点为根的子树整体提升一层,从而红色节点与其父黑节点水平,两者的联边相应地调整为横向。
例如:
提升红子树......
将黑节点与其红孩子视作(关键码合并为)超级节点。无非四种组合,分别对应于4阶B-树的一类内部节点。
四、红黑树的动态调整算法(search同普通的二叉树)
直接观察红黑树的变换过程难以理解,所以往往以B-树的角度进行理解。
(1) 插入
(a) 拟插入关键码e (设T中本来不含e)。
(b)按BST的常规算法插入,x=insert(e) ,x必定为末端节点。
(c)将x染红 (除非其为根节点,根节点强制染黑) ,染红是因为使条件1~4尽快能都满足,这时1,2,4依然满足,而3不见得,若x的父节点本身为红,则会出现双红。
(d) 若出现双红,则考察x的叔父 u (x的父节点的兄弟 ),根据u 的颜色,分2种情况处理。
双红修正:
(i) 情况1:叔父节点u的颜色为黑(2次染色,2次黑度更新,1~2次旋转,不再递归)
对于祖孙三代节点,共有4种情况:如下的 a,b 2种及其对称的2种。
此时的问题不严重,对应于B树,各个节点都没有发生上溢,只有颜色不对,所以从b-树角度而言,只需要交换p和g或者x和g的颜色即可,而这种在B-树角度下的变换在红黑树的角度来看即为结构重构为如下所示的结构。
策略:采用AVL树算法,进行局部的3+4重构,将节点x、p、g及其四颗子树,按照中序组合为 T0<a<T1<b<T2<c<T3。
结果:因为未发生上溢,B树的结构不变,所以是局部调整,不用递归。
(ii) 情况2:叔父节点u的颜色为红(3次染色,0次黑度更新,0次旋转,O(logn)次递归)
对于祖孙三代节点,共有4种情况:如下的 a,b 2种及其对称的2种。
此类情况比较严重,这种情况以B-树的角度看是超级节点发生了上溢,红黑树修复此类的双红问题相当于B-树修复大节点的上溢。找到中轴节点上移到父节点,原大节点分裂为左右小节点。
为方便分析,可以先将有问题的祖孙三代转换为对应的B-树节点,然后按照B-树节点修正上溢的方法进行分裂,然后将分裂好的B树局部转换为实际的红黑树。如下图对于上图的(b)情况,有(b)->(b')->(b')->(c)。
策略:p与u转黑,而g转红。
结果:即使从B-树的角度看节点的拓扑结构发生了变化,但是从红黑树的角度看局部到整树的拓扑结构并没有发生变化,不需要选择,只需要进行节点颜色的修改,但是由于红节点的提升,祖父节点仍可能发生双红,所以需要对祖先节点进行遍历检查,不过注意每次迭代都上升2层,总体时间也是O(logn)。
双红修正总结:
(a) 总体流程
所以对于插入操作,虽然可能会执行多达O(logn)次的重染色,但是最多发生2次旋转(1次3+4重构),而O(1)次的重构操作是很重要的特点,因为这类操作对持久性结构使至关重要的性能。
(2) 删除
(a) 执行二叉树的常规删除算法 r=removeAt(x,_hot) (若x只含单分支则直接删除,有两个孩子则找到中序遍历下的直接后继,将x和直接后继进行交换后,删除掉x),这里x一直认为是实际被删除的节点(可能为转换后实际被删除的),而r为最终的顶替者(必为其孩子)。
(b) a操作导致x由孩子r替代,x的另一孩子为w (黑色外部节点),因为x必定被转换成单分支节点或者无分支节点。
(c) 删除操作之后红黑树的拓扑结构依然是完整的,且红黑树的1~2条件依然满足,但3~4却不一定满足,如可能出现两个连续的红色节点(条件3),更严重的是在被删除节点所在的通路上,黑节点的数目可能发生变化(条件4)。
第一大类:
有一大类情况容易解决:被删除节点及其替换节点二者之一为红色(不可能都是红的),则只需要将替代者r染为黑色即可 (被删除分支的黑高度维持原状),如下图。
第二大类:若x和r均为黑色(双黑修正)
若x和r均为黑色,则经过删除之后,被删除节点的响应分支的黑高度会被减1,则导致红黑树不满足条件(4)。从4阶B-树的角度理解这个情况。因为x和r均为黑色,则x所在的大节点只包含x1个关键码,4阶B-树除根节点外每个节点内的关键码为[1,3],所以x被删除,这肯定会导致x所在大节点的下溢,所以红黑树双黑修复就对应着B-树的下溢修复。在新树中,针对r的父亲p及r的兄弟s的不同情况(共4种),分别进行处理。
(i) 情况1:兄弟节点s为黑,且其至少有一个红孩子t(BB1)
如下图a为其中的一种局部结构,而其余的结构与之对称或者与之相似,处理方法相同。
从4阶B-树的角度进行观察,此时下溢节点的左兄弟恰有足够的关键码(红孩子t被提升提升),所以按照B-树的策略,下溢节点向父节点借关键码,而父节点向左兄弟节点借关键码,形成(b'),B-树即修复完成,由于父节点的关键码数不会发生变化,所以不会发生下溢传播。最终的B-树节点转换得到的红黑树,即为此情况下要转换成的实际红黑树结果。从(a)向(b)的转换,可以通过一次3+4重构解决,并相应地进行重染色。
策略:一次3+4重构:t,s,p重命名为a,b,c;r保持黑色,a和c染黑,b继承p的原色。
结果:红黑树的性质在全局得以恢复,不会发生迭代。
(ii) 情况2-1:兄弟节点s为黑,且其两个孩子均为黑色;p为红色(BB-2R)
如下图a为其中的一种局部结构,而其余的结构与之对称或者与之相似,处理方法相同。
为方便分析,依然从B-树的角度进行查看,可见下溢节点的左兄弟并没有足够多的关键码外借,所以按照B-树的策略,需要将父节点中的关键码下移作为粘合剂,将下溢节点及其兄弟节点进行合并成(b'),而由于p节点为红色,则其原所在的大节点中必定有且仅有一个其他黑色节点(p被提升作为其水平节点),所以父节点还剩一个关键码,所以父节点不会发生下溢。最终的B-树节点转换得到的红黑树,即为此情况下要转换成的实际红黑树结果。现在从红黑树的角度观察从(a)到(b)的转变,可见只需要修改部分节点颜色即可,而不需要进行结构上的调整。
策略:不需要结构上的重构,r保持黑色,兄弟节点s转为黑,p转黑。
结果:红黑树的性质在全局得以恢复,不会发生迭代。
(ii) 情况2-2:兄弟节点s为黑,且其两个孩子均为黑色;p为黑色(BB-2B)
如下图a为其中的一种局部结构,而其余的结构与之对称或者与之相似,处理方法相同。
同样从B-树的角度进行分析,下溢节点的左兄弟并没有足够多的关键码外借,所以按照B-树的策略,需要将父节点中的关键码下移作为粘合剂,将下溢节点及其兄弟节点进行合并成(b'),而由于p节点为黑色,则其原所在的大节点中必定只有其自身一个关键码,所以父节点必定会发生下溢,向上传播一层,这个过程至多会出现 logn 次。幸运的是,回到红黑树的角度观察从(a)到(b)的变化,可以发现红黑树的拓扑结构未发生任何实际的变化,只是部分节点的颜色发生了改变,即虽然发生了迭代,红黑树的结构变化仍然不超过O(1)。
策略:不需要结构上的重构,s转红,r与p保持黑色。
结果:红黑树的性质只在局部做了恢复,由于发生下溢传播,需向上迭代检查,最多可能出现logn次,但是迭代不涉及结构上的重构,红黑树的结构变化仍然不超过O(1)。
(iii) 情况3:兄弟节点s为红色(BB-3)
如下图a为其中的一种局部结构,而其余的结构与之对称或者与之相似,处理方法相同。
实际上对于这种情况,只需要通过简单的变换将其转换为前述的其中2种情况之一即可进一步处理。从B-树的角度上,只需将s和p所对应的关键码的颜色互换即可。而B-树中的这一颜色互换,在实际的红黑树中就是一次结构调整,即以p为轴做一次顺时针旋转或者逆时针旋转,然后翻转s和p的颜色。此时可以看到,经过此次调整,r有了一个新的兄弟s'(黑色),所以跳出了BB-3情况,而由于p已经转红,所以只可能转到BB-1或者BB-2R,而不会是BB-2B(需要迭代)。所以经过此次调整之后,红黑树只需要进行一次调整即可。
策略:zag(p)或zig(p),红s转黑,黑p转红。
结果:红黑树的在经过1次旋转和2次染色后转换为BB1或是BB-2R,此后只需要进行一次调整即可恢复整树,两次加起来最多发生3次旋转,所以红黑树的结构变化仍然不超过O(1)。
双黑修正总结:
(a) 总体流程
红黑树的每次删除操作,在每一层的操作都可在常数次完成,所以总体时间为O(logn),而从树的结构变化次数这一方面进行考察,每次删除最多需要O(logn)次重染色,1次3+4重构,1次单旋,总共常数次的结构调整(持久性,或称一致性),这也是红黑树优于AVL树的关键点(AVL树的插入也是常数次,它的插入操作是个好孩子,虽然一次闯祸会导致很多问题,但是一次就能改正,而它的删除操作是个坏孩子,每次闯祸问题很小,但是屡教不改,需要迭代)。
五、红黑树的实现
红黑树redBlack类由普通二叉搜索树类bst继承而来,沿袭了其中的search()二分查找,在此基础上增加了黑高度更新,及双红修正及双黑修正策略。(二叉搜索树的bst类的实现见https://blog.csdn.net/qq_18108083/article/details/84927274)
操作 | 功能 | 对象 |
solveDoubleRed(binNode<T>* x) | 双红修正 | 红黑树 |
solveDoubleBlack(binNode<T>* r) | 双黑修正 | 红黑树 |
updateHeight(binNode<T>* x) | 更新节点x的黑高度 | 红黑树 |
insert(const T& e) | 插入元素 | 红黑树 |
remove(const T& e) | 删除元素 | 红黑树 |
(1) redBlack.h
#pragma once
#include"bst.h"
#define IsRoot(x) (!((x).parent))
#define IsLChild(x) (!IsRoot(x) && (&(x) == (x).parent->lc))
#define IsRChild(x) (!IsRoot(x) && (&(x) == (x).parent->rc))
#define HasParent(x) (!IsRoot(x))
#define HasLChild(x) ((x).lc)
#define HasRChild(x) ((x).rc)
#define HasChild(x) (HasLChild(x) || HasRChild(x)) //至少拥有一个孩子
#define HasBothChild(x) (HasLChild(x) && HasRChild(x)) //同时拥有两个孩子
#define IsLeaf(x) (!HasChild(x))
#define sibling(p) /*兄弟*/ \
( IsLChild( * (p) ) ? (p)->parent->rc : (p)->parent->lc )
#define uncle(x) /*叔叔*/ \
( IsLChild( * ( (x)->parent ) ) ? (x)->parent->parent->rc : (x)->parent->parent->lc )
#define FromParentTo(x) /*来自父亲的引用*/ \
( IsRoot(x) ? _root : ( IsLChild(x) ? (x).parent->lc : (x).parent->rc ) )
#define IsBlack(p) (!(p) || (RB_BLACK == (p)->color)) //外部节点也视作黑节点
#define IsRed(p) (!IsBlack(p)) //非黑即红
#define BlackHeightUpdated(x) ( /*RedBlack高度更新条件*/ \
( stature( (x).lc ) == stature( (x).rc ) ) && \
( (x).height == ( IsRed(& x) ? stature( (x).lc ) : stature( (x).lc ) + 1 ) ) \
)
/*判断节点的高度是否要更新,正常情况下左右孩子的高度相同,且如果为红色,则自身黑高度是孩子节点的高度加1*/
template<typename T> class redBlack :public bst<T>
{
protected:
void solveDoubleRed(binNode<T>* x); //双红修正
void solveDoubleBlack(binNode<T>* r); //双黑修正
int updateHeight(binNode<T>* x); //更新节点x的高度
public:
binNode<T>* insert(const T& e); //插入元素
bool remove(const T& e); //删除元素
//search()沿用bst中的search
};
template<typename T> int redBlack<T>::updateHeight(binNode<T>* x)
{
x->height = max(stature(x->lc), stature(x->rc));
return IsBlack(x) ? x->height++ : x->height; //只有自身是黑节点才加1
}
template<typename T> binNode<T>* redBlack<T>::insert(const T& e)
{
binNode<T>* &x = search(e);
if (x) return x; //若已经存在则返回
x = new binNode<T>(e, _hot, nullptr, nullptr, -1); //创建红节点,以_hot为父,黑高度-1
_size++;
solveDoubleRed(x); //双红修正
return x ? x : _hot->parent;
}
template<typename T> void redBlack<T>::solveDoubleRed(binNode<T>* x)
{
if(IsRoot(*x)) //若x为根节点,强行改为黑色
{
_root->color = RB_BLACK; _root->height++; return;
}
binNode<T>* p = x->parent;
if (IsBlack(p))
return; //未发生双红
binNode<T>* g = p->parent; //爷节点g 父节点p 本节点x
binNode<T>* u = uncle(x); //视叔父的情况分别处理
// case 1:叔父节点为黑色时(外部节点也是黑色的)
if (IsBlack(u))
{
//case 1-1:p和g同侧,交换p和g的颜色然后3+4重构(以B树角度更加方便看)
if (IsLChild(*x) == IsLChild(*p))
p->color = RB_BLACK; //p由红转黑,x保持红
//case 1-2:p和g不同侧,交换x和g的颜色然后3+4重构(以B树角度更加方便看)
else
x->color = RB_BLACK; //x由红转黑,p保持红
g->color = RB_RED; //g一定变红
binNode<T>* gg = g->parent; //爷爷的爸爸
binNode<T>* r = FromParentTo(*g) = rotateAt(x); //进行旋转(3+4重构)
r->parent = gg;
}
//case 2:叔父节点为红色时,只需要改变颜色即可(可能发生上溢迭代,每次两层)
else
{
p->color = RB_BLACK; p->height++;
u->color = RB_BLACK; u->height++;
if (!IsRoot(*g))
g->color = RB_RED; //变红从B树的角度看就是分裂
solveDoubleRed(g); //从x直接到g,可见每次迭代都是两层
}
}
template<typename T> bool redBlack<T>::remove(const T& e)
{
binNode<T>* &x = search(e);
if (!x) return false; //若不存在则直接返回
binNode<T>* r = removeAt(x, _hot);
if (!(--_size)) return true; //若树变空则不用双黑修正
if (!_hot) //若只剩下根节点,则强制其为黑
{
_root->color = RB_BLACK;
updateHeight(_root);
return true;
}
if (BlackHeightUpdated(*_hot)) //若被删节点的父节点仍然处于平衡状态则不需要双黑修正
{
return true;
}
//case 1:若被删除节点的后继为红色,则直接将其转换成黑色即可
if (IsRed(r))
{
r->color = RB_BLACK;
r->height++;
return true;
}
//case 2:若被删除节点及其直接后继均为黑色,则进行双黑修正
solveDoubleBlack(r);
return true;
}
template<typename T> void redBlack<T>::solveDoubleBlack(binNode<T>* r)
{
binNode<T>* p = r ? r->parent : _hot;
if (!p) return; //如果r为根节点则直接返回
binNode<T>* s = (r == p->lc) ? p->rc : p->lc; //r的兄弟
if (IsBlack(s)) //兄弟s为黑
{
binNode<T>* t = nullptr;
if (IsRed(s->rc)) t = s->rc; //s的红孩子(若左、右孩子皆红,左者优先,皆黑时为nullptr)
if (IsRed(s->lc)) t = s->lc;
if (t) //黑s有红孩子
{
RBColor oldColor = p->color; //备份原子树根节点p颜色
//通过旋转重平衡,并将新子树的左右孩子染黑
binNode<T>* b = FromParentTo(*p) = rotateAt(t); //旋转
if (b->lc) //左子
{
b->lc->color = RB_BLACK; updateHeight(b->lc);
}
if (b->rc)
{
b->rc->color = RB_BLACK; updateHeight(b->rc);
}
b->color = oldColor; updateHeight(b); //新子树根节点继承原节点的颜色
}
else //黑s无红孩子
{
s->color = RB_RED;
s->height--; //转红
if (IsRed(p))
{
p->color = RB_BLACK; //p转黑,但黑高度不变
}
else
{
p->height--; //p保持黑,但黑高度下降
solveDoubleBlack(p); //递归迭代
}
}
}
else //兄弟s为红
{
s->color = RB_BLACK; //s转黑
p->color = RB_RED; //p转红
binNode<T>* t = IsLChild(*s) ? s->lc : s->rc; //取t与其父s同侧
_hot = p;
FromParentTo(*p) = rotateAt(t); //取t及其父亲、祖父做平衡调整
solveDoubleBlack(r); //继续修正r处双黑,此时的p已经转红,故后续只能是之前已经处理的情况
}
}