4.1 二叉搜索树
BinNode* search (const T &e , BinNode* _hot, BinNode* x){
while(true){
if(!x) {return x;}
else if(e<x->data){
_hot=x;
x=x->lc;
}
else if(e>x->data){
_hot=x;
x=x->rc;
}
else if(e==x->data){
return x;
}
}
}
BinNode* search(const T &e){
return search(e, _root,_hot);
}
BinNode * insert(const T &e){
BinNode* x=search(e);
if(x) return x;//duplicated
x= new BinNode*(e,_hot);
_size++;
updateHeightAbove(x);
return x;
}
BinNode * remove(const T &e){
BinNode * x=search(e);
BinNode* succ;
if(!x) return false;
if(!x->rc){
succ=x=x->lc
}
else if((!x->lc)){
succ=x=x->rc;
}
else{
BinNode w=x;
w=w.succ();
swap(x->data,w->data);
BinNode *u =w->parent;
((u==x)?u->rc:u->lc)=succ=w->rc;//w only has rc
}
_hot=w->parent;//father of deleted elem;
if(succ) succ->parent =_hot;
delete w->data;
delete ->w;
_size--;
updateHeightAbove(_hot);
return succ;
}
4.2 控制树高 ,制造平衡二叉搜索树
zig右旋
zag左旋
不改变中序遍历顺序,
通过增加局部限制,保证平衡性,红黑树通过根节点到叶子节点黑节点数相同来保证。AVL树通过兄弟节点高度差不大于1来保证。
保证了单次动态修改后最多只有logn 出失去平衡,在logn时间内改回来。
4.2 AVL树
控制平衡因子<2保证(左右子树高度差)
Balanced (x){return stature(x->lc)==stature(x->rc);}//ideal
BalFac(x){ return stature(x->lc)-stature(x->rc);}
AVLBal(x){return (-2<BalFac(x))&&(BalFac(x)<2);}
remove(x)后仅单个节点失衡,insert后多个节点失衡
失衡节点集中UT(x),高度不低于x的祖父,从x向上搜索,首个失衡节点即为
既然g(x)由于x引入而失衡,所以g(x)的孩子p, p的孩子v都没有失衡,可由g(x)找到p和v
tallerChild(x) {stature(x->lc)>stature(x->rc)?(x->lc):(
stature(x->lc)<stature(x->rc)?x->rc:(
IsLChild(*(x))?x->lc:x->rc;//与父亲同侧者 zig/zag
)
)
}
p、v在一侧,采取单旋手段
单旋 g(x)插入右子树边高,zag(g(x))
g(x)插入左边子树高, zig(g(x)) 可调整平衡
双旋g(x)p v
左右,zag(p),zig(g)
右左,zig(p),zag(g)
insert(const T& e){
BinNode* x=search(e);
if(x) return x;
BinNode* xx= x =new BinNode(e, _hot); _size++;
//_hot增高,_hot的父亲有可能失衡
for (BinNode *g=_hot;g;g=g->parent){//从x之父向上搜索
if(!AVLBalanced(g)){//即找到g(x)//第一个失衡的祖先
//从g往下找两次较高孩子,若同样高,取与父亲同侧(单旋)
//g->parent->self
FromParentTo(*g)=rotateAt(tallerChild(tallerChild(g)));//采用3+4算法,使其恢复平衡
break;
}
else{
updateHeight(g);//g不失衡,高度也可能增加//插入调整局部,更新高度避免每次全局调整
}
}
return xx;//返回新节点
}
与插入操作不同,删除后,失衡节点仅一个g(x),其高度与失衡前相同,g(x)有可能是x父亲
沿着parent指针上行,可找到g(x),g(x)另一侧必有非空孩子p,-取p较高孩子为v(相同取同侧)//同侧调一次,不同侧调两次
-
单旋
设g(x)平衡因子为2(左侧高(-2对称)) fac(p)非负时,即同侧,调整g即可使得g恢复平衡
-
双旋
fac(p)为负时,即不同侧,需要先调整p,再调整g(双旋)
g调整完成后,g的高度有可能降低,导致祖先失衡(当g为高祖先的短分枝时)称为失衡传播,可沿着parent继续向前,重复调整
在此过程中的任一时刻, 至多只有一个失衡的节点;高层的某一节点由平衡转为失衡,只可能发生在下层失衡节点恢复平 衡之后。因此,可沿parent指针逐层遍历所有祖先,每找到一个失衡的祖先节点,即可套用以 上方法使之恢复平衡
remove(const T& e){
BinNode* x=search(e); if(!e) return false;
removeAt(x,_hot); _size--;//依然是二叉搜索树
for(BinNode* g=_hot;g;g=g->parent){
if(!AVLBalanced(*g)){
g=FromParentTo(g)=rotateAt(tallerChild(tallerChild(g)));
}
updateHeight(g);
}
return true;
}
统一平衡算法:
根据失衡节点调整其左右孩子位置关系,分别做单选和双旋操作,代码量大而复杂。将导致调试困难。
可将失衡节点子树统一划分为四个子树T0、T1、T2、T3
重新排列后中序遍历结果应该是T0 a T1 b T2 c T3,四颗子树彼此高度差不超过一层//实际上覆盖了单旋和双旋大所有情况。
重构过程仅涉及3个节点和4颗树。故称为3+4重构connnect34
connnect34(BinNode* a, BinNode *b, BinNode *c, BinNode *T1, BinNode * T2, BinNode* T3, BinNode* T4){
a->lc=T0;
if(T0) T0->parent=a;
a->rc=T1;
if(T1) T1->parent=a;
updateHeight(a);
c->lc=T2;
if(T2) T2->parent=c;
c->rc=T3;
if(T3) T3->parent=c;
updateHeight(c);
b->lc=a;
a->parent=b;
b->rc=c;
c->parent=b;
update(b);
return b;
}
旋转变换统一算法
rotateAt(BinNode *v){
BinNode* p=v->parent; BinNode *g=p->parent;
if(IsLChild(*p)){//zig (zig(g))
if(IsLChild(*v)){//zig-zig
p->parent=g->parent;
return connect34(v,p,g,v->lc,v->rc,p->rc,g->rc);
}
else{//zig-zag (zag(p))
v->parent=g->parent;
return connect(p,v,g,p->lc,v->lc,v->rc,g->rc);
}
}
else{//zag
if(IsRChild(*v)){//zag-zag
p->parent=g->parent;
connect34(g,p,v,g->lc,p->lc,v->lc,v->rc);
}
else{//zag-zig
v->parent=g->parent;
connect34(g,v,p,g->lc,v->lc,v->rc,p->rc);
}
}
}
4.2伸展树 Splay
无需维护树高、平衡因子,不需要严格保证平衡状态。分摊效率高
按照最常用优先,引入伸展树
根据数据局部性1.被访问过的节点更容易被访问 2.下一个被访问节点在上一个周围
只需将访问完成节点调整到根附近,保证前后等价即可。加快访问速度
一种直接的方法:访问一个节点后,以他的父亲节点为中心旋转,直到成为树根
无论查找或插 入,甚至“删除”都可通过3次旋转,将该树等价变换为以E为根的另一棵二叉搜索树。
-
最坏情况
不难验证,若从空树开始依次插入关键码{ 1, 2, 3, 4, 5 },且其间采用如上调整策略,
则可得到如图8.2(a)所示的二叉搜索树。接下来,若通过search()接口,再由小到大地依次访 问各节点一次,则该树在各次访问之后的结构形态将如图(b~f)所示。
随着节点E的逐层上升,两侧子树的结构也不断地调整,故这一过程也称作伸展(splaying), 而采用这一调整策略的二叉搜索树也因此得名。不过,为实现真正意义上的伸展树,还须对以上 策略做点微妙而本质的改进。之所以必须改进,是因为目前的策略仍存在致命的缺陷--对于很多访问序列,单次访问的分摊时间复杂度在极端情况下可能高达o(n)。
一般地,若节点总数为n,则旋转操作的总次数应为:
如此分摊下来,每次访问平均需要O(n)时间。很遗憾,这一效率不仅远远低于AVL树,而且甚至 与原始的二叉搜索树的最坏情况相当。而事实上,问题还远不止于此。
对于规模为任意n的伸展树, 只要按关键码单调的次序,周期性地反复进行查找,则无论总的访问次数m >> n有多大,就分 摊意义而言,每次访问都将需要O(n)时间!
那么,这类最坏的访问序列能否回避?具体地,又应该如何回避?
双层伸展:
为克服上述伸展调整策略的缺陷,一种简便且有效的方法就是:将逐层伸展改为双层伸展。 具体地,每次都从当前节点v向上追溯两层(而不是仅一层),并根据其父亲p以及祖父g的相对 位置,进行相应的旋转。以下分三类情况,分别介绍具体的处理方法。
-
zig-zig(zag-zag)
针对这种情况,首先以节点g为轴做顺时针旋转zig(g),其效果如图(b)所示。然后,再以p
为轴做顺时针旋转zig(p),其效果如图(c)所示。如此连续的两次zig旋转,合称zig-zig调整
-
zig-zag(zag-zig)
针对这种情况,首先以节点p为轴做顺时针旋转zig(p),其效果如(b)所示。然后,再以g
为轴做逆时针旋转zag(g),其效果如图(c)所示。如此zig旋转再加zag旋转,合称zig-zag调整。同样地,另一完全对称的情形--v是p的右孩子,而p是g的左孩子则可通过zag旋转再
加zig旋转实现调整,合称zag-zig操作
-zig/zag
若v最初的深度为奇数,则经 过若干次双层调整至最后一次调整时,v的父亲p即是 树根r。将v的左、右子树记作X和Y,节点p = r的另 一子树记作Z。
- 效果与效率
综合以上各种情况,每经过一次双层调整操作,节点v都会上升两层。若v的初始深度depth(v)为偶数,则最终v将上升至树根。若depth(v)为奇数,则当v上升至深度为1时,不妨最后再相应 地做一次zig或zag单旋操作。无论如何,经过depth(v)次旋转后,v最终总能成为树根。
重新审视图8.2的最坏实例不难发现,这一访问序列导致(n)平均单次访问时间的原因, 可以解释为:在这一可持续重复的过程中,二叉搜索树的高度始终不小于n/2;而且,至少有 一半的节点在接受访问时,不仅没有如最初设想的那样靠近树根,而且反过来恰恰处于最底层。 从树高的角度看,问题根源也可再进一步地解释为:在持续访问的过程中,树高依算术级数逐步 从n - 1递减至n/2,然后再逐步递增回到n - 1
-
效果
稍作对比不难看出,就调整之后的局部结构而言,zig-zag和zag-zig调整与此前的逐层伸 展完全一致(亦等效于AVL树的双旋调整),而zig-zig和zag-zag调整则有所不同。事实上, 后者才是双层伸展策略优于逐层伸展策略的关键所在。以如图8.6(b)所示的二叉搜索树为例, 在find(1)操作之后采用逐层调整策略与双层调整策略的效果,分别如图(a)和图(c)所示。
可见,最深节点(1)被访问之后再经过双层调整,不仅同样可将该节点伸展至树根,而且 同时可使树的高度接近于减半。就树的形态而言,双层伸展策略可“智能”地“折叠”被访问的 子树分支,从而有效地避免对长分支的连续访问
这就意味着,即便节点v的深度为O(n),双层 伸展策略既可将v推至树根,亦可令对应分支的长度以几何级数(大致折半)的速度收缩。
一旦这类“坏”节点 被“碰触”到,经过随后的双层伸展,其对应的分支都会收缩至长度大致折半。于是,即便每次 都“恶意地”试图访问最底层节点,最坏情况也不会持续发生
在改用“双 层伸展”策略之后,伸展树的单次操作均可在分摊的O(logn)时间内完成
#include "../BST/BST.h" //基亍BST实现Splay
template <typename T> class Splay : public BST<T> { //由BST派生癿Splay树模板类 protected:
BinNodePosi(T) splay(BinNodePosi(T) v); //将节点v伸展至根 public:
BinNodePosi(T) & search(const T& e); //查找(重写) BinNodePosi(T) insert(const T& e); //插入(重写) bool remove(const T& e); //初除(重写)
};
需强调的是,与一般的二叉搜索树不同,伸展
树的查找也会引起整树的结构调整,故search()操作也需重写
template <typename NodePosi> inline //在节点*p不*lc(可能为空)间建立父(左)子关系
void attachAsLChild(NodePosi p, NodePosi lc) { p->lChild = lc; if (lc) lc->parent = p; }
template <typename NodePosi> inline //在节点*p不*rc(可能为空)间建立父(右)子关系
void attachAsRChild(NodePosi p, NodePosi rc) { p->rChild = rc; if (rc) rc->parent = p; }
template <typename T> //Splay树伸展算法:从节点v出収逐局伸展
BinNodePosi(T) Splay<T>::splay(BinNodePosi(T) v) { //v为因最近访问而需伸展的节点位置
if ( !v ) return NULL; BinNodePosi(T) p; BinNodePosi(T) g; //*v的父亲与祖父
while ( ( p = v->parent ) && ( g = p->parent ) ) { //自下而上,反复对*v做双层伸展
BinNodePosi(T) gg = g->parent; //每轮之后*v都以原曾祖父(great-grand parent)为父
if ( IsLChild ( *v ) )
if ( IsLChild ( *p ) ) { //zig-zig
attachAsLChild ( g, p->rc ); attachAsLChild ( p, v->rc );
attachAsRChild ( p, g ); attachAsRChild ( v, p );
} else { //zig-zag
attachAsLChild ( p, v->rc ); attachAsRChild ( g, v->lc );
attachAsLChild ( v, g ); attachAsRChild ( v, p );
}
else if ( IsRChild ( *p ) ) { //zag-zag
attachAsRChild ( g, p->lc ); attachAsRChild ( p, v->lc );
attachAsLChild ( p, g ); attachAsLChild ( v, p );
} else { //zag-zig
attachAsRChild ( p, v->lc ); attachAsLChild ( g, v->rc );
attachAsRChild ( v, g ); attachAsLChild ( v, p );
}
if ( !gg ) v->parent = NULL; //若*v原先的曾祖父*gg不存在,则*v现在应为树根
else //否则,*gg此后应该以*v作为左或右孩子
( g == gg->lc ) ? attachAsLChild ( gg, v ) : attachAsRChild ( gg, v );
updateHeight ( g ); updateHeight ( p ); updateHeight ( v );
} //双层伸展结束时,必有g == NULL,但p可能非空
if ( p = v->parent ) { //若p果真非空,则额外再做一次单旋
if ( IsLChild ( *v ) ) { attachAsLChild ( p, v->rc ); attachAsRChild ( v, p ); }
else { attachAsRChild ( p, v->lc ); attachAsLChild ( v, p ); }
updateHeight ( p ); updateHeight ( v );
}
v->parent = NULL; return v;
} //调整之后新树根应为被伸展的节点,故返回该节点的位置以便上层函数更新树根
- 查找算法的实现 在伸展树中查找任一关键码e的过程
template <typename T> BinNodePosi(T) & Splay<T>::search(const T& e) { //在伸展树中查找兲键码e BinNodePosi(T) p = searchIn(_root, e, _hot = NULL);
_root = splay((p ? p : _hot)); //将最后一个被讵问癿节点伸展至根
return _root;
} //不其它BST不同,无讳查找成功不否,_root都指向最后被讵问癿节点
首先,调用二叉搜索树的通用算法searchIn()(代码7.3)尝试查找具有关键码e的节点。 无论查找是否成功,都继而调用splay()算法,将查找终止位置处的节点伸展到树根。
-
插入
为将节点插至伸展树中,固然可以调用二叉搜索树的标准插入算法BST::insert(),再通过双层伸展,将新插入的节点提升至树根。
然而,以上接口Splay::search()已集成了splay()伸展功能,故查找返回后,树根节点 要么等于查找目标(查找成功),要么就是_hot,而且恰为拟插入节点的直接前驱或直接后继 (查找失败)。因此,不妨改用如下方法实现Splay::insert()接口。
-
删除
为从伸展树中删除节点,固然也可以调用二叉搜索树标准的节点删除算法BST::remove() (190页代码7.6),再通过双层伸展,将该节点此前的父节点提升至树根。
然而,同样鉴于Splay::search()已集成了splay()伸展功能,且在成功返回后,树根节 点恰好就是待删除节点。因此,亦不妨改用如下方法实现Splay::remove()接口。
4.4 B-树
实践证明,分级存储才是行之有效的方法。在由内存与外存(磁盘)组成的二级存储系统中, 数据全集往往存放于外存中,计算过程中则可将内存作为外存的高速缓存,存放最常用数据项的 复本。借助高效的调度算法,如此便可将内存的“高速度”与外存的“大容量”结合起来。
- 多路搜索树
当数据规模大到内存已不足以容纳时,常规平衡二叉搜索树的效率将大打折扣。其原因在于,
查找过程对外存的访问次数过多。
比如可以两层为间隔,将各节点与其左、右孩子合并为“大节点”: 原节点及其孩子的共三个关键码予以保留;孩子节点原有的四个分支也予以保留并按中序遍历次 序排列;节点到左、右孩子的分支转化为“大节点”内部的搜索,在图中表示为水平分支。如此 改造之后,每个“大节点”拥有四个分支,故称作四路搜索树。
这一策略还可进一步推广,比如以三层为间隔,将各节点及其两个孩子、四个孙子合并为含 有七个关键码、八个分支的“大节点”,进而得到八路搜索树。一般地,以k层为间隔如此重组, 可将二叉搜索树转化为等价的2^k路搜索树,统称多路搜索树(multi-way search tree)。
实际上,在此时的搜索每下降一层,都以“大节点” 为单位从外存读取一组(而不再是单个)关键码。更为重要的是,这组关键码在逻辑上与物理上 都彼此相邻,故可以批量方式从外存一次性读出,且所需时间与读取单个关键码几乎一样。
-
多路平衡搜索树
m阶B -tree,所谓m阶B-树4(B-tree),即m路平衡搜索树(m >2)
#include "../vector/vector.h"
#define BTNodePosi(T) BTNode<T>* //B-树节点位置
template <typename T> struct BTNode { //B-树节点模板类 // 成员(为简化描述起见统一开放,读者可根据需要迕一步封装)
BTNodePosi(T) parent; //父节点
Vector<T> key; //兲键码向量
Vector<BTNodePosi(T)> child; //孩子向量(其长度总比key夗一)
// 极造函数(注意:BTNode叧能作为根节点创建,而且刜始时有0个兲键码和1个空孩子指针) BTNode() { parent = NULL; child.insert(0, NULL); }
BTNode(T e, BTNodePosi(T) lc = NULL, BTNodePosi(T) rc = NULL) {
parent = NULL; //作为根节点,而且刜始时
key.insert(0, e); //叧有一个关键码,以及
child.insert(0, lc); child.insert(1, rc); //两个孩子
if (lc) lc->parent = this; if (rc) rc->parent = this;
}
};
#include "BTNode.h" //引入B-树节点类 2
3 template <typename T> class BTree { //B-树模板类
4 protected:
5 int _size; //存放癿兲键码总数
6 int _order; //B-树癿阶次,至少为3——创建时指定,一般丌能修改
7 BTNodePosi(T) _root; //根节点
8 BTNodePosi(T) _hot; //BTree::search()最后讵问癿非空(除非树空)癿节点位置
9 void solveOverflow(BTNodePosi(T)); //因揑入而上溢乀后癿分裂处理
10 void solveUnderflow(BTNodePosi(T)); //因初除而下溢乀后癿合幵处理
11 public:
12 BTree(int order = 3) : _order(order), _size(0) //极造函数:默讣为最低癿3阶
13 { _root = new BTNode<T>(); }
14 ~BTree() { if (_root) release(_root); } //枂极函数:释放所有节点
15 int const order() { return _order; } //阶次
16 int const size() { return _size; } //觃模
17 BTNodePosi(T) & root() { return _root; } //树根
18 bool empty() const { return !_root; } //刞空
19 BTNodePosi(T) search(const T& e); //查找
20 bool insert(const T& e); //揑入
21 bool remove(const T& e); //初除
22 }; //BTree
- 关键码查找
template <typename T> BTNodePosi(T) BTree<T>::search(const T& e) { //在B-树中查找兲键码e BTNodePosi(T) v = _root; _hot = NULL; //从根节点出収
while (v) { //逐局查找
Rank r = v->key.search(e); //在弼前节点中,找刡丌大亍e癿最大兲键码
if ((0 <= r) && (e == v->key[r])) return v; //成功:在弼前节点中命中目标兲键码
_hot = v; v = v->child[r + 1]; //否则,转入对应子树(_hot指向其父)——需做I/O,最费时间
} //返里在向量内是二分查找,但对通常癿_order可直接顸序查找 return NULL; //失败:最终抵达外部节点
}
与二叉搜索树类似,B-树的每一次查找过程中,在每一高度上至多访问 一个节点。这就意味着,对于高度为h的B-树,外存访问不超过O(h - 1)次。
存有N个关键码的m阶B-树的高度h = O(logmN)
-
上溢与分裂
一般地,刚发生上溢的节点,应恰好含有m个关键码。若取s = m/2,则它们依次为:
{ k0, ..., ks-1; ks; ks+1, ..., km-1 } 可见,以ks为界,可将该节点分前、后两个子节点,且二者大致等长。于是,可令关键码ks
上升一层,归入其父节点(若存在)中的适当位置,并分别以这两个子节点作为其左、右孩子。 这一过程,称作节点的分裂(split)。
template <typename T> //兲键码揑入后若节点上溢,则做节点分裂处理 void BTree<T>::solveOverflow(BTNodePosi(T) v) {
if (_order >= v->child.size()) return; //逑弻基:弼前节点幵未上溢
Rank s = _order / 2; //轴点(此时应有_order = key.size() = child.size() - 1) BTNodePosi(T) u = new BTNode<T>(); //注意:新节点已有一个空孩子
for (Rank j = 0; j < _order - s - 1; j++) { //v右侧癿_order-s-1个孩子及兲键码分裂为右侧节点u
u->child.insert(j, v->child.remove(s + 1)); //逐个秱劢效率低 u->key.insert(j, v->key.remove(s + 1)); //此策略可改迕
9}
10 u->child[_order - s - 1] = v->child.remove(s + 1); //秱劢v最靠右癿孩子
- 删除
为从B-树中删除关键码e,也首先需要调用search(e)查找e所属的节点。倘若查找失败, 则说明关键码e尚不存在,删除操作即告完成
template <typename T> bool BTree<T>::remove(const T& e) { //从BTree树中初除关键码e
BTNodePosi(T) v = search(e); if (!v) return false; //确讣目标兲键码存在
Rank r = v->key.search(e); //确定目标兲键码在节点v中的秩(由上,肯定合法)
if (v->child[0]) { //若v非叶子,则e的后继必然为叶子节点
BTNodePosi(T) u = v->child[r+1]; //在右子树中一直向左,即可
while (u->child[0]) u = u->child[0]; //找出e的后继
v->key[r] = u->key[0]; v = u; r = 0; //并与之交换位置
} //至此,v必然位于最底局,且其中第r个兲键码就是待初除者
v->key.remove(r); v->child.remove(r + 1); _size--; //初除e,以及其下两个外部节点solveUnderflow(v); //如有必要,需做旋转戒合幵
return true;
-
下溢与合并
由上,在m阶B-树中,刚发生下溢的节点V必恰好包含m/2 - 2个关键码和m/2 - 1个分 支。以下将根据其左、右兄弟所含关键码的数目,分三种情况做相应的处置。
在经如此合并而得新节点中,关键码总数应为:
(m/2 -1)+1+(m/2 -2) = 2*(m/2) -2<= m-1 故原节点V的下溢缺陷得以修复,而且同时也不致于反过来引发上溢。
接下来,还须检查父节点P关键码y的删除可能致使该节点出现下溢。好在,即便如此, 也尽可套用上述三种方法继续修复节点P。当然,修复之后仍可能导致祖父节点以及更高层节点 的下溢这种现象称作下溢的传递。特别地,当下溢传递至根节点且其中不再含有任何关键码 时,即可将其删除并代之以其唯一的孩子节点,全树高度也随之下降一层。
与上溢传递类似地,每经过一次下溢修复,新下溢节点的高度都必然上升一层
如前所述,若下溢现象持续传播至树根,且树根当时仅含一个关键码。于是,在其仅有的两 个孩子被合并、仅有的一个关键码被借出之后,原树根将退化为单分支节点。对这一特殊情况, 需删除该树根,并以刚合并而成的节点作为新的树根整树高度也随之降低一层。
4.5红黑树
伸展树实现简便、无需修改节点 结构、分摊复杂度低,但可惜最坏情况下的单次操作需要O(n)时间,故难以适用于核电站、医 院等对可靠性和稳定性要求极高的场合。反之,7.4节的AVL树尽管可以保证最坏情况下的单次 操作速度,但需在节点中嵌入平衡因子等标识;更重要的是,删除操作之后的重平衡可能需做多 达O(logn)次旋转,从而频繁地导致全树整体拓扑结构的大幅度变化。
红黑树即是针对后一不足的改进。通过为节点指定颜色,并巧妙地动态调整,红黑树可保证: 在每次插入或删除操作之后的重平衡过程中,全树拓扑结构的更新仅涉及常数个节点。尽管最坏 情况下需对多达O(logn)个节点重染色,但就分摊意义而言仅为O(1)个
当然,为此首先需在AVL树“适度平衡”标准的基础上,进一步放宽条件。实际上,红黑树 所采用的“适度平衡”标准,可大致表述为:任一节点左、右子树的高度,相差不得超过两倍。
由红、黑两色节点组成的二叉搜索树若满足以下条件,即为红黑树5(red-black tree):
其中,条件(1)和(2)意味着红节点均为内部节点,且其父节点及左、右孩子必然存在。另 外,条件(3)意味着红节点之父必为黑色,因此树中任一通路都不含相邻的红节点。
由此可知,在从根节点通往任一节点的沿途,黑节点都不少于红节点。除去根节点本身,沿 途所经黑节点的总数
称作该节点的黑深度(black depth)
--根节点的黑深度为0,其余依此 类推。故条件(4)亦可等效地理解和描述为“所有外部节点的黑深度统一”。
由条件(4)可进一步推知,在从任一节点通往其任一后代外部节点的沿途,黑节点的总数亦 必相等。除去(黑色)外部节点,沿途所经黑节点的总数称作该节点的黑高度(black height)。 如此,所有外部节点的黑高度均为0,其余依此类推。
- (2,4)-树
红黑树与四阶B-经适当转换之后,二者相互等价
此时,若将原红黑树的节点视作关键码,沿水平方向相 邻的每一组(父子至多三个)节点即恰好构成4阶B-树的一个节点。
关键码也被赋予了对应的颜色。对照红黑 树的条件,(2,4)-树中的每个节点应包含且仅包含一个黑关键码,同时红关键码不得超过两个。 而且,若某个节点果真包含两个红关键码,则黑关键码的位置必然居中。
包含n个内部节点的红黑树T的高度h也不致超 过O(logn)。更严格地有:
log2(n+1) <=h <=2∙log2(n+1)
#include "../BST/BST.h" //基亍BST实现RedBlack
template <typename T> class RedBlack : public BST<T> { //RedBlack树模板类 protected:
void solveDoubleRed(BinNodePosi(T) x); //双红修正
void solveDoubleBlack(BinNodePosi(T) x); //双黑修正 int updateHeight(BinNodePosi(T) x); //更新节点x癿高度
public:
BinNodePosi(T) insert(const T& e); //揑入(重写) bool remove(const T& e); //初除(重写)
}
可见,这里直接沿用了二叉搜索树标准的查找算法search(),并根据红黑树的重平衡规则 与算法,重写了insert()和remove()接口;新加的两个内部功能接口solveDoubleRed()和 solveDoubleBlack(),分别用于在节点插入或删除之后恢复全树平衡。其具体实现稍后介绍。
预留的两个成员 变量height和color
可借助辅助宏来检查节点的颜 色以及判定是否需要更新(黑)高度记录,如此可大大简化相关算法的描述。
#define IsBlack(p) (!(p) || (RB_BLACK == (p)->color)) //外部节点也规作黑节点 #define IsRed(p) (!IsBlack(p)) //非黑即红
#define BlackHeightUpdated(x) ( \
(stature((x).lChild) == stature((x).rChild)) && \
((x).height == (IsRed(&x) ? stature((x).lChild) : stature((x).lChild) + 1)) \ ) //RedBlack高度更新条件
1 template <typename T> int RedBlack<T>::updateHeight(BinNodePosi(T) x) { //更新红黑树节点高度
2 x->height = max(stature(x->lChild), stature(x->rChild)); //孩子一般黑高度相等,除非出现双黑
3 return IsBlack(x) ? x->height++ : x->height; //若弼前节点为黑,则计入黑深度
4 } //因统一定stature(NULL) = -1,故height比黑高度少一,好在丌致影响刡各种算法中癿比较刞断
此处的height已不再是指常规的树高,而是红黑树的黑高度。故如代码8.15所示,节点黑 高度需要更新的情况共分三种:或者左、右孩子的黑高度不等;或者作为红节点,黑高度与其孩 子不相等;或者作为黑节点,黑高度不等于孩子的黑高度加一。
因新节点的引入,而导致父子节点同为红色的此类情况,称作“双红”(double red)。 为修正双红缺陷,可调用solveDoubleRed(x)接口。每引入一个关键码,该接口都可能迭代地 调用多次。在此过程中,当前节点x的兄弟及两个孩子(初始时都是外部节点),始终均为黑色。
-
双红修正RR1
首先,考查u为黑色的情况。此时,x的兄弟、两个孩子的黑高度,均与u相等。图8.25(a) 和(b)即为此类情况的两种可能(另两种对称情况,请读者独立补充)。
此时红黑树条件(3)的违反,从B-树角度等效地看,即同一节点不应包含紧邻的红色关键码。 故如图8.25(c')所示,只需令黑色关键码与紧邻的红色关键码互换颜色。从图(c)红黑树的角度 看,这等效于按中序遍历次序,对节点x、p和g及其四棵子树,做一次局部“3 + 4”重构。
不难验证,如此调整之后,局部子树的黑高度将复原,这意味着全树的平衡也必然得以恢复。 同时,新子树的根节点b为黑色,也不致引发新的双红现象。至此,整个插入操作遂告完成。
-
双红修正(RR-2)
再考查节点u为红色的情况。此时,u的左、右孩子非空且均为黑色,其黑高度必与x的兄弟
以及两个孩子相等。图8.26(a)和(b)给出了两种可能的此类情况(另两种对称情况,请读者独 立补充)。此时红黑树条件(3)的违反,从B-树角度等效地看,即该节点因超过4度而发生上溢。
不难验证,如此调整之后局部子树的黑高度复原。然而,子树根节点g转为红色之后,有可 能在更高层再次引发双红现象。从图8.26(c')B-树的角度来看,对应于在关键码g被移出并归入 上层节点之后,进而导致上层节点的上溢即上溢的向上传播。
若果真如此,可以等效地将g视作新插入的节点,同样地分以上两类情况如法处置。请注意, 每经过一次这样的迭代,节点g都将在B-树中(作为关键码)上升一层,而在红黑树中存在双红 缺陷的位置也将相应地上升两层,故累计至多迭代O(logn)次。
/****************************************************************************************** *RedBlack双红调整算法:解决节点x不其父均为红色癿问题。分为两大类情冴:
* RR-1:2次颜色翻转,2次黑高度更新,1~2次旋转,丌再逑弻
* RR-2:3次颜色翻转,3次黑高度更新,0次旋转,需要逑弻 ******************************************************************************************/
template <typename T> void RedBlack<T>::solveDoubleRed(BinNodePosi(T) x) { //x弼前必为红 if (IsRoot(*x)) //若已(逑弻)转至树根,则将其转黑,整树黑高度也随乀逑增
{ _root->color = RB_BLACK; _root->height++; return; } //否则,x的父亲p必存在 BinNodePosi(T) p = x->parent; if (IsBlack(p)) return; //若p为黑,则可终止调整。否则 BinNodePosi(T) g = p->parent; //既然p为红,则x癿祖父必存在,且必为黑色 BinNodePosi(T) u = uncle(x); //以下,规x叔父u癿颜色分删处理
if (IsBlack(u)) { //u为黑色(含NULL)时
if (IsLChild(*x) == IsLChild(*p)) //若x不p同侧(即zIg-zIg戒zAg-zAg),则
///// /////
p->color = RB_BLACK; //p由红转黑,x保持红 else //若x不p异侧(即zIg-zAg戒zAg-zIg),则
x->color = RB_BLACK; //x由红转黑,p保持红
g->color = RB_RED; //g必定由黑转红 以上虽保证总共两次染色,但因增加了刞断而得丌偿失 在旋转后将根置黑、孩子置红,虽需三次染色但效率更高
BinNodePosi(T) gg = g->parent; //曾祖父(great-grand parent)
BinNodePosi(T) r = FromParentTo(*g) = rotateAt(x); //调整后癿子树根节点 r->parent = gg; //不原曾祖父联接
} }
} else { //若u为红色
p->color = RB_BLACK; p->height++; //p由红转黑
u->color = RB_BLACK; u->height++; //u由红转黑
if (!IsRoot(*g)) g->color = RB_RED; //g若非根,则转红
solveDoubleRed(g); //继续调整g(类似亍尾逑弻,可优化为迭代形式)
- 节点删除算法
节点删除与双黑现象
template <typename T> bool RedBlack<T>::remove(const T& e) { //从红黑树中初除关键码e
BinNodePosi(T) & x = search(e); if (!x) return false; //确定目标节点存在(留意对_hot的位置)
BinNodePosi(T) r = removeAt(x, _hot); if (0 >= --_size) return true; //实施删除
// assert: _hot某一孩子刚被初除,且被r所指节点(可能是NULL)接替。以下检查是否失衡,幵做必要调整
if (!_hot) //若刚被初除癿是根节点,则将其置黑,并更新黑高度
{ _root->color = RB_BLACK; updateHeight(_root); return true; } // assert: 以下,原x(现r)必非根,_hot必非空
if (BlackHeightUpdated(*(_hot))) //若所有祖先癿黑深度依然平衡,则无需调整
return true;
if (IsRed(r)) //否则,若r为红,则叧需令其转黑
{ r->color = RB_BLACK; r->height++; return true; }
// assert: 以下,原x(现r)均为黑色
solveDoubleBlack(r); return true; //经双黑调整后迒回
} //若目标节点存在且被初除,返回true;否则迒回false
不难验证,此时红黑树的前两个条件继续满足,但后两个条件却未必依然满足。
如图8.28所示,除了其接替者r,x 还应有另一个孩子w。既然x是实际被删除 者,故w必为(黑色的)外部节点NULL。
如图(a)和(a')所示,若x为红色, 则在删除x并代之以r后,条件(3~4)依然 满足;反之,若x为黑色,则要看其替代 者r的颜色。
如图(b)和(b')所示,若r为红色, 则只需将其翻转为黑色,即可使条件 (3~4)重新满足。
然而如图(c)和(c')所示,若x和r均 为黑色,则为使条件(3~4)重新成立,还 需要做略微复杂一些的处理。
因某一无红色孩子的黑节点被删除,而导致的此类复杂情况,称作“双黑”(double black) 现象。此时,需从r出发调用solveDoubleBlack(r)算法予以修正。
自然,原黑节点x的兄弟必然非空,将其记作s;x的父亲记作p,其颜色不确定(故在图中 以八角形示意)。以下视s和p颜色的不同组合,按四种情况分别处置。
- 双黑修正(BB-1)
既然节点x的另一孩子w=NULL,故从B-树角度(图8.29(a'))看节点x被删除之后的情况,
可以等效地理解为:关键码x原所属的节点发生下溢;此时,t和s必然属于B-树的同一节点,且 该节点就是下溢节点的兄弟。故可参照B-树的调整方法,下溢节点从父节点借出一个关键码(p), 然后父节点从向下溢节点的兄弟节点借出一个关键码(s),调整后的效果如图(b')。
从红黑树的角度(图(b))来看, 上述调整过程等效于,对节点t、s和p 实施“3 + 4”重构。
此外,根据红黑树与B-树的对应 关系不难理解,若这三个节点按中序遍 历次序重命名为a、b和c,则还需将a 和c染成黑色,b则继承p此前的颜色。 就图8.29的具体实例而言,也就是将t 和p染成黑色,s继承p此前的颜色。注 意,整个过程中节点r保持黑色不变。
由图8.29(b)(及其对称情况)不 难验证,经以上处理之后,红黑树的所 有条件,都在这一局部以及全局得到恢 复,故删除操作遂告完成。
- 双黑修正(BB-2-R)
节点s及其两个孩子均为黑色时,视节点p颜色的不同,又可进一步分为两种情况。
先考虑p为红色的情况。如图8.30(a)所示,即为一种典型的此类情况
与BB-1类似,在对应的B-树中,关键码x的删除导 致其所属的节点下溢。但因此时关键码s所在节点只有两 个分支,故下溢节点无法从父节点借出关键码(p)。
按照8.2.8节的B-树平衡算法,此时应如图(b')所 示,将关键码p取出并下降一层,然后以之为“粘合剂”, 将原左、右孩子合并为一个节点。从红黑树角度看,这 一过程可如图(b)所示等效地理解为:s和p颜色互换。
由图8.30(b)(及其对称情况)可知,经过以上处 理,红黑树的所有条件都在此局部得以恢复。另外,由 于关键码p原为红色,故如图8.30(a')所示,在关键码p 所属节点中,其左或右必然还有一个黑色关键码(当然, 不可能左、右兼有)--这意味着,在关键码p从其中取 出之后,不致引发新的下溢。至此,红黑树条件亦必在 全局得以恢复,删除操作即告完成。
- 双黑修正(BB-2-B)
接下来,再考虑节点s、s的两个孩子以及节点p均为黑色的情况。 如图8.31(a)所示,即为一种典型的此类情况(与之对称的情况,请读者独立补充)。此时
与BB-2-R类似,在对应的B-树中,因关键码x的删除,导致其所属节点发生下溢。
因此可如图(b')所示,将下溢 节点与其兄弟合并。从红黑树的角 度来看,这一过程可如图(b)所示等 效地理解为:节点s由黑转红。
由图8.31(b)(及其对称情况) 可知,经以上处理,红黑树所有条 件都在此局部得到恢复。
然而,因s和x在此之前均为黑 色,故如图8.31(a')所示,p原所 属的B-树节点必然仅含p这一个关 键码。于是在p被借出之后,该节点 必将继而发生下溢,故有待于后续 进一步修正。
从红黑树的角度来看,此时的状态可等效地理解为:节点p的父节点刚被删除。当然,可以 按照本节所介绍的算法,视具体的情况继续调整。
实际上稍后总结时将会看出,这也是双黑修正过程中,需要再次迭代的唯一可能。幸运的是, 尽管此类情况可能持续发生,下溢的位置也必然不断上升,故至多迭代O(logn)次后必然终止。
- 双黑修正(BB-3)
最后,考虑节点s为红色的情
况。如图8.32(a)所示,即为一种典 型的此类情况(与之对称的情况, 请读者独立补充)。此时,作为红 节点s的父亲,节点p必为黑色;同 时,s的两个孩子也应均为黑色。
于是从B-树的角度来看,只需 如图(b')所示,令关键码s与p互换 颜色,即可得到一棵与之完全等价 的B-树。而从红黑树的角度来看, 这一转换对应于以节点p为轴做一 次旋转,并交换节点s与p的颜色。
读者可能会发现,经过如此处理之后,双黑缺陷依然存在,而且缺陷位置的高度也并未上升。 既然如此,这一步调整的意义又何在呢?
实际上,经过这一转换之后,情况已经发生了微妙而本质的变化。仔细观察图(b)不难发现, 在转换之后的红黑树中,被删除节点x(及其替代者节点r)有了一个新的兄弟s'--与此前的 兄弟s不同,s'必然是黑的!这就意味着,接下来可以套用此前所介绍其它情况的处置方法,继 续并最终完成双黑修正。
还有一处本质的变化,同样需要注意:现在的节点p,也已经黑色转为红色。因此接下来即 便需要继续调整,必然既不可能转换回此前的情况BB-3,也不可能转入可能需要反复迭代的情 况BB-2-B。实际上反过来,此后只可能转入更早讨论过的两类情况BB-1或BB-2-R。这就意 味着,接下来至多再做一步迭代调整,整个双黑修正的任务即可大功告成。
一旦在某步迭代中做过节点的旋转调整,整个修复过程便会随即 完成。因此与双红修正一样,双黑修正的整个过程,也仅涉及常数次的拓扑结构调整操作。
这一有趣的特性同时也意味着,在每此插入操作之后,拓扑联接关系有所变化的节点绝不会 超过常数个这一点与AVL树(的删除操作)完全不同,也是二者之间最本质的一项差异。
/****************************************************************************************** *RedBlack双黑调整算法:解决节点x不被其替代癿节点均为黑色癿问题
分为三大类共四种情冴:
BB-1 :2次颜色翻转,2次黑高度更新,1~2次旋转,不再迭代
BB-2R:2次颜色翻转,2次黑高度更新,0次旋转,不再更新
BB-2B:1次颜色翻转,1次黑高度更新,0次旋转,需要迭代
BB-3 :2次颜色翻转,2次黑高度更新,1次旋转,转为BB-1或者BB-2-R
template <typename T> void RedBlack<T>::solveDoubleBlack(BinNodePosi(T) r) { BinNodePosi(T) p = r ? r->parent : _hot; if (!p) return; //r父亲
BinNodePosi(T) s = (r == p->lChild) ? p->rChild : p->lChild; //r的兄弟
if(IsBlack(s)) { //兄弟s为黑
BinNodePosi(T) t = NULL; //s的红孩子(若左、右孩子都是红,左者优先;皆黑时为NULL)
if (HasLChild(*s) && IsRed(s->lChild)) t = s->lChild;
else if (HasRChild(*s) && IsRed(s->rChild)) t = s->rChild;
if(t) { //黑s有红孩子:BB-1
RBColor oldColor = p->color; //备份原子树根节点p颜色,并对t及其父亲、祖父
BinNodePosi(T) b = FromParentTo(*p) = rotateAt(t); //重平衡,并将新子树的左、右孩子染黑
if (HasLChild(*b)) b->lChild->color = RB_BLACK; updateHeight(b->lChild); //左孩子
if (HasRChild(*b)) b->rChild->color = RB_BLACK; updateHeight(b->rChild); //右孩子
b->color = oldColor; updateHeight(b); //新子树根节点继承原根节点的颜色
} else { //黑s无红孩子
s->color = RB_RED; s->height--; //s转红
if (IsRed(p)) { //BB-2R
p->color = RB_BLACK; //p转黑,但黑高度不变 }
else { //BB-2B
p->height--; //p保持黑,但黑高度下降
solveDoubleBlack(p); }
}
} else { //兄弟s为红:BB-3
s->color = RB_BLACK; p->color = RB_RED; //s转黑,p转红
BinNodePosi(T) t = IsLChild(*s) ? s->lChild : s->rChild; //取t不其父s同侧
_hot = p; FromParentTo(*p) = rotateAt(t); //对t及其父亲、祖父做平衡调整
solveDoubleBlack(r); //继续修正r处双黑——此时的p已转红,故后续叧能是BB-1或者BB-2R
} }