第八章:高级搜索树

第八章:高级搜索树

伸展树:逐层伸展

根据局部性原理,刚被访问过的节点,极有可能很快再次接受访问,所以节点一旦被访问,不妨立即调整至树根。

一旦v被访问,立刻对其父节点做旋转,使得v顶替其父节点的位置,这样自下而上,逐层单旋,直至v成为树根节点。

伸展树的效率取决于伸展树的初始形态以及节点的访问次序。

最坏情况是树的初始形态是一条单链,而每次访问的节点都是叶节点,这会导致每次要做的伸展操作成算术级数,因此访问完n个节点需要耗费n^2的时间,分摊下来也是线性的。

伸展树:双层伸展

为了解决逐层伸展的最坏情况,Tarjan提出了双层伸展的策略,反复考察祖孙三代,经过两次旋转使得v成为子树根。

对于zig-zag和zag-zig旋转而言,与两次逐层伸展别无二致,效果完全相同。

而对于zig-zig和zag-zag而言,效果完全不同。逐层伸展是先对v的父节点p做单旋,再对g做单旋。而这里的双层伸展则是先对v的祖父节点g做单旋,再对p做单旋。其结果都是使得v成为子树根,但是子树的结构却发生了改变。

这两种双旋会产生折叠效果,一旦访问了坏节点,对应路径的长度将随机减半。此时单次伸展的分摊时间仅为logn。

如果v没有祖父节点,这时只需做一次单旋即可,这种情况至多会发生一次,而且是最后一次伸展操作。

伸展树:算法实现

伸展树与之前的BST不同的是,查找操作也会引起树结构的调整。

伸展算法:

这里当v的父亲结点p和祖父节点g均存在时,反复做双层伸展,每次伸展后v都会以其原来曾祖父为父,故用gg记录下v曾祖父的位置,然后视情况做双层伸展,伸展完成后判断下g是gg的左孩子还是右孩子,并将以v为子树根的子树顶替掉g的位置。注意虽然在伸展 过程中g的引用可能发生改变,但gg的孩子节点仍指向了g,故无需担心伸展后判断g==gg->lc会存在逻辑错误。所有的双层伸展做完之后,如果v的父节点不为空(还未到达根节点位置),就最后再做一次单旋操作完成伸展。

下面需要考虑的就是四种情况的双层伸展应该如何实现了。

根据之前的分析,双层伸展zig-zag和zag-zig与单层伸展别无二致,均是进行下3 + 4重构,使得v成为新子树的根,p和g视情况成为v的两个孩子节点。但是zig-zig和zag-zag却不同了,单次对p的单次zig或者zag旋转相当于3+4重构,但是在双层伸展中,先对g做单旋,再对p做单旋,使得v成为子树根节点,这时候需要根据具体形态实现(比如上图)。

伸展操作的总代码如下:

template <typename NodePosi> inline //在节点*p与*lc(可能为空)之间建立父(左)子关系
void attachAsLChild ( NodePosi p, NodePosi lc ) { p->lc = lc; if ( lc ) lc->parent = p; }

template <typename NodePosi> inline //在节点*p与*rc(可能为空)之间建立父(右)子关系
void attachAsRChild ( NodePosi p, NodePosi rc ) { p->rc = 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;
} //调整之后新树根应为被伸展的节点,故返回该节点的位置以便上层函数更新树根

 

查找算法直接调用下searchin接口找到p,然后进行伸展操作即可,这里如果没找到p节点,就将其父节点hot伸展至根节点,否则将p伸展至根节点。

插入算法只需先调用search函数,找到待插入节点的父节点hot,经过伸展后hot已然是根节点,所以再按照上图的方法插入即可。

代码如下:

template <typename T> BinNodePosi(T) Splay<T>::insert ( const T& e ) { //将关键码e插入伸展树中
   if ( !_root ) { _size++; return _root = new BinNode<T> ( e ); } //处理原树为空的退化情况
   if ( e == search ( e )->data ) return _root; //确认目标节点不存在
   _size++; BinNodePosi(T) t = _root; //创建新节点。以下调整<=7个指针以完成局部重构
   if ( _root->data < e ) { //插入新根,以t和t->rc为左、右孩子
      t->parent = _root = new BinNode<T> ( e, NULL, t, t->rc ); //2 + 3个
      if ( HasRChild ( *t ) ) { t->rc->parent = _root; t->rc = NULL; } //<= 2个
   } else { //插入新根,以t->lc和t为左、右孩子
      t->parent = _root = new BinNode<T> ( e, NULL, t->lc, t ); //2 + 3个
      if ( HasLChild ( *t ) ) { t->lc->parent = _root; t->lc = NULL; } //<= 2个
   }
   updateHeightAbove ( t ); //更新t及其祖先(实际上只有_root一个)的高度
   return _root; //新节点必然置于树根,返回之
} //无论e是否存在于原树中,返回时总有_root->data == e

删除算法同样调用search函数返回了待删除元素的位置,同样是需要删除根节点,如果根节点只有左子树或者右子树,直接删除即可。如果根节点左右子树都存在,不妨以待删除元素中序下的后继节点作为新的根节点,这时不必再去查找中序后继,可以直接断开原根节点对左子树的引用,深入到右子树上做一次对原树根节点的查找操作,此时必然查找失败,hot作为失败节点的父结点(即原根节点中序后继)经过伸展到达了树根,此时再连上左子树即可。这里因为右子树中最小的节点被伸展至根,故必无左子树,可以直接连上原左子树。

template <typename T> bool Splay<T>::remove ( const T& e ) { //从伸展树中删除关键码e
   if ( !_root || ( e != search ( e )->data ) ) return false; //若树空或目标不存在,则无法删除
   BinNodePosi(T) w = _root; //assert: 经search()后节点e已被伸展至树根
   if ( !HasLChild ( *_root ) ) { //若无左子树,则直接删除
      _root = _root->rc; if ( _root ) _root->parent = NULL;
   } else if ( !HasRChild ( *_root ) ) { //若无右子树,也直接删除
      _root = _root->lc; if ( _root ) _root->parent = NULL;
   } else { //若左右子树同时存在,则
      BinNodePosi(T) lTree = _root->lc;
      lTree->parent = NULL; _root->lc = NULL; //暂时将左子树切除
      _root = _root->rc; _root->parent = NULL; //只保留右子树
      search ( w->data ); //以原树根为目标,做一次(必定失败的)查找
/ assert: 至此,右子树中最小节点必伸展至根,且(因无雷同节点)其左子树必空,于是
      _root->lc = lTree; lTree->parent = _root; //只需将原左子树接回原位即可
   }
   release ( w->data ); release ( w ); _size--; //释放节点,更新规模
   if ( _root ) updateHeight ( _root ); //此后,若树非空,则树根的高度需要更新
   return true; //返回成功标志
} //若目标节点存在且被删除,返回true;否则返回false

伸展树的优点是无须记录平衡因子等额外变量,并且分摊的时间复杂度与AVL树都是对数级别。更大的优势在于应用了局部性原理,如果连续的m次查找基本都集中在k个节点上,只需通过nlog的时间完成将k个节点伸展至splay树的上层,之后每次的查找都可以在logk时间内完成,故总的时间复杂度为O(mlogk + nlogn)。

伸展树的缺点在于尽管分摊下来复杂度不高,但是不能根本杜绝单次最坏情况的发生,因此不可以应用于效率敏感的场合。

B-树:动机

正因为I/O操作的时间正比于操作的次数,而与一次读多少内容关系并不大,所以为了一次可以多读取一点内容,就诞生了B-树。

B-树:结构

B-树可以视为一种平衡的多路搜索树。对一棵BST而言,如果每两层结点合并为一个超级节点,也就是每个超级节点都包含3个节点,4个分支;如果是三层节点合并为一个超级节点,每个超级节点就包含7个节点,8个分支;每d层合并为一个节点,每个超级节点就有m = 2^d个分支以及m – 1个关键码。

对于外部查找,使用B-树可以大大减少IO次数。如果利用AVL树来存储一个1G的记录,需要维护一个30层高的AVL树,每次查找需要30次IO操作,得不偿失。如果使用B-树,每次读取一组关键码,比如常见的磁盘数据块有1KB,而一块数据为4B,这样读入一个数据块就相对应的读取了256个数据,查找一次就只需要不到4次IO即可完成。

m阶B-树即m路平衡搜索树,规定其所有叶结点深度统一相等,外部节点是叶结点并不存在的孩子节点,深度自然也统一相等。这里B-树的高度定义为外部节点的深度。

m阶B树,即树中最多有m个分支,根据前面总结的,内部节点的关键码数n总是比分支数少1,所以内部节点关键码数不超过m – 1个。

另外规定:根节点关键码树的下限只要不小于1即可,分支数不少于2.对于其他内部节点,分枝数不得小于m / 2的上取整。所以m阶B-树也被称为(m/2,m)-树,比如3阶B树被称为(2,3)树。

如果需要画出完整的B-树,需要画出外部节点以及每个节点对孩子节点的引用,为了简化B-树,一般将引用简化为一个点,外部节点总是存在且深度统一,也可以省略不画,从而得到了上面最后一幅图的B-树的紧凑画法。

B-树节点

B-树节点需要存储父节点,以及用两个向量分别存储关键码和孩子,并提供了两个构造方法,一个生成空节点,另一个生成含一个关键码两个分支的节点。

B-树:查找

B-树的查找过程较为简单,根节点一般常驻内存,从根节点开始顺序搜索待查找节点,找到了就返回,找不到就根据返回的位置从外存调入其孩子节点,继续做顺序查找,直至查找到该节点或者未找到返回外部节点的引用。

鉴于I/O操作的时间远大于对节点内部进行查找的时间,所以节点内部的查找时间可以视为O(1)的。

这里向量的search接口是从后往前查找,返回不大于e的最大的元素位置,所以一旦查找失败,其孩子节点的位置在child向量中r + 1的位置,这里同样维持了一个hot引用始终指向正在查找节点的父节点。

这里首先区分两个概念,节点数指的是超级节点的个数,一个节点可能含多个关键码。

还要注意的是,每个节点的分支数不小于m / 2,不大于m,节点的关键码数=分支数 – 1.

含N个关键码的m阶B-树,要使高度最大,每层节点数应该尽可能的少,第0层1个节点,第1层2个节点,2 * (m/2)个分支,(以下用()表示上取整),第2层有2 * (m / 2)个节点,2 * (m/2)^2个分支,…,第k层有2 * (m/2)^(k-1)个节点。

内部节点一共N个对应于N种查找成功的情况,则外部节点一共N + 1个,对应N + 1种失败的查找。外部节点在第h层,节点数N + 1 >= 2 * (m/2)^(h-1)。则h <= 1 + log(m/2)(N + 1) / 2 = O(logmN).

如果与BBST做对比,差不多是其树高的1 / (log2m - 1)。

如果要求含N个关键码的m阶B-树的最小高度,则每个节点应该尽可能多的包含关键码。

首先第0层根节点自然只有1个 节点,包含m个分支,第1层m个节点,第h层m^h个节点,N + 1 <= m^h,h >= log2(N + 1),相对于BBST,树高约为BBST的1 / log2m。

B-树:插入

与其他BST类似,B-树的插入同样需要先对待插入节点e调用一次search函数,获得了查找失败时所处的叶子节点位置,此时,只需对该节点再做一次向量的search操作,便可以得到e应该插入的位置,插入e后,该节点需要增加一个分支,一种方法是直接在新插入关键码后面添加一个空分支,由于此时的节点都是叶节点,孩子都是空的,另一种方法就是直接在孩子向量的后面插入空分支。插入关键码e后,所处节点的分支数可能超过m ,节点发生了上溢,需要做分裂。

分裂操作是选取关键码向量的中位数(设向量的长度为n,取秩为n / 2的位置作为中位数),将中位数关键码上升一层合并到节点的父结点里,同时,分裂所得的左右两个节点作为其左右孩子。分裂所得的两个节点关键码数仍符合B-树的要求,证明见上图。

父节点在接纳一个新节点后依然可能发生上溢,可以继续采用之前的办法,继续分裂,最坏情况下会一直上溢的根节点,如果根节点也上溢,就需要在选取中位数关键码作为新的根节点了,此时树的高度增加了一个单位。

B-树处理上溢的代码如下:

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 ) ); //此策略可改进
   }
   u->child[_order - s - 1] = v->child.remove ( s + 1 ); //移动v最靠右的孩子
   if ( u->child[0] ) //若u的孩子们非空,则
      for ( Rank j = 0; j < _order - s; j++ ) //令它们的父节点统一
         u->child[j]->parent = u; //指向u
   BTNodePosi(T) p = v->parent; //v当前的父节点p
   if ( !p ) { _root = p = new BTNode<T>(); p->child[0] = v; v->parent = p; } //若p空则创建之
   Rank r = 1 + p->key.search ( v->key[0] ); //p中指向u的指针的秩
   p->key.insert ( r, v->key.remove ( s ) ); //轴点关键码上升
   p->child.insert ( r + 1, u );  u->parent = p; //新节点u与父节点p互联
   solveOverflow ( p ); //上升一层,如有必要则继续分裂——至多递归O(logn)层
}

B-树:删除

B-树的删除操作类似于BST的删除操作。与插入操作一样,需要先调用search函数找到待删除关键码的所在节点,然后再次调用向量的search函数找到其在向量中的位置。找到e的位置后,需要找到其中序的后继节点,只需先进入e的右孩子,然后找到其最小的关键码,之后一直向孩子节点的最小关键码深入,直至找到e右边的最小关键码,必然位于叶子节点,交换两个节点,再对交换后的e进行删除即可。此时,由于少了一个关键码,e所在节点的关键码树可能不再满足B-树要求,发生下溢。

删除关键码e后,节点v发生了下溢,需要先进行左顾右盼,看看其左右兄弟有没有多余的节点借出,如果有,为了维持顺序性,不能直接借出。假设其左兄弟的关键码数不少于m / 2.v的父节点中在其v与其左兄弟中间的关键码设为y,v的左兄弟最大的关键码设为x,不妨将父节点的y关键码借给v,再将左兄弟的x关键码补充给父节点,这种旋转操作即可修复下溢。

另一种情况是v的左右兄弟要么不存在,要么关键码刚刚够数,不足以借出。这时左右兄弟必存其一,不妨设L存在,且其关键码数恰好为(m / 2) - 1,这时需要进行合并,将父节点中关键码y作为L和v的粘合剂合并成新的一个节点,合并完成后,v的父节点少了一个关键码,v本身的关键码数也不至于超过m。但是v的父节点却因为借出了一个关键码可能发生下溢,解决办法与之前一样,继续旋转或者合并。最坏情况下根节点发生了下溢,即根节点只有一个关键码还借给了孩子节点,此时其孩子节点将成为新的树根,树高因此减1.

修复下溢的代码如下:

template <typename T> //关键码删除后若节点下溢,则做节点旋转或合并处理
void BTree<T>::solveUnderflow ( BTNodePosi(T) v ) {
   if ( ( _order + 1 ) / 2 <= v->child.size() ) return; //递归基:当前节点并未下溢
   BTNodePosi(T) p = v->parent;
   if ( !p ) { //递归基:已到根节点,没有孩子的下限
      if ( !v->key.size() && v->child[0] ) {
         //但倘若作为树根的v已不含关键码,却有(唯一的)非空孩子,则
         _root = v->child[0]; _root->parent = NULL; //这个节点可被跳过
         v->child[0] = NULL; release ( v ); //并因不再有用而被销毁
      } //整树高度降低一层
      return;
   }
   Rank r = 0; while ( p->child[r] != v ) r++;
   //确定v是p的第r个孩子——此时v可能不含关键码,故不能通过关键码查找
   //另外,在实现了孩子指针的判等器之后,也可直接调用Vector::find()定位
// 情况1:向左兄弟借关键码
   if ( 0 < r ) { //若v不是p的第一个孩子,则
      BTNodePosi(T) ls = p->child[r - 1]; //左兄弟必存在
      if ( ( _order + 1 ) / 2 < ls->child.size() ) { //若该兄弟足够“胖”,则
         v->key.insert ( 0, p->key[r - 1] ); //p借出一个关键码给v(作为最小关键码)
         p->key[r - 1] = ls->key.remove ( ls->key.size() - 1 ); //ls的最大关键码转入p
         v->child.insert ( 0, ls->child.remove ( ls->child.size() - 1 ) );
         //同时ls的最右侧孩子过继给v
         if ( v->child[0] ) v->child[0]->parent = v; //作为v的最左侧孩子
         return; //至此,通过右旋已完成当前层(以及所有层)的下溢处理
      }
   } //至此,左兄弟要么为空,要么太“瘦”
// 情况2:向右兄弟借关键码
   if ( p->child.size() - 1 > r ) { //若v不是p的最后一个孩子,则
      BTNodePosi(T) rs = p->child[r + 1]; //右兄弟必存在
      if ( ( _order + 1 ) / 2 < rs->child.size() ) { //若该兄弟足够“胖”,则
         v->key.insert ( v->key.size(), p->key[r] ); //p借出一个关键码给v(作为最大关键码)
         p->key[r] = rs->key.remove ( 0 ); //ls的最小关键码转入p
         v->child.insert ( v->child.size(), rs->child.remove ( 0 ) );
         //同时rs的最左侧孩子过继给v
         if ( v->child[v->child.size() - 1] ) //作为v的最右侧孩子
            v->child[v->child.size() - 1]->parent = v;
         return; //至此,通过左旋已完成当前层(以及所有层)的下溢处理
      }
   } //至此,右兄弟要么为空,要么太“瘦”
// 情况3:左、右兄弟要么为空(但不可能同时),要么都太“瘦”——合并
   if ( 0 < r ) { //与左兄弟合并
      BTNodePosi(T) ls = p->child[r - 1]; //左兄弟必存在
      ls->key.insert ( ls->key.size(), p->key.remove ( r - 1 ) ); p->child.remove ( r );
      //p的第r - 1个关键码转入ls,v不再是p的第r个孩子
      ls->child.insert ( ls->child.size(), v->child.remove ( 0 ) );
      if ( ls->child[ls->child.size() - 1] ) //v的最左侧孩子过继给ls做最右侧孩子
         ls->child[ls->child.size() - 1]->parent = ls;
      while ( !v->key.empty() ) { //v剩余的关键码和孩子,依次转入ls
         ls->key.insert ( ls->key.size(), v->key.remove ( 0 ) );
         ls->child.insert ( ls->child.size(), v->child.remove ( 0 ) );
         if ( ls->child[ls->child.size() - 1] ) ls->child[ls->child.size() - 1]->parent = ls;
      }
      release ( v ); //释放v
   } else { //与右兄弟合并
      BTNodePosi(T) rs = p->child[r + 1]; //右兄度必存在
      rs->key.insert ( 0, p->key.remove ( r ) ); p->child.remove ( r );
      //p的第r个关键码转入rs,v不再是p的第r个孩子
      rs->child.insert ( 0, v->child.remove ( v->child.size() - 1 ) );
      if ( rs->child[0] ) rs->child[0]->parent = rs; //v的最左侧孩子过继给ls做最右侧孩子
      while ( !v->key.empty() ) { //v剩余的关键码和孩子,依次转入rs
         rs->key.insert ( 0, v->key.remove ( v->key.size() - 1 ) );
         rs->child.insert ( 0, v->child.remove ( v->child.size() - 1 ) );
         if ( rs->child[0] ) rs->child[0]->parent = rs;
      }
      release ( v ); //释放v
   }
   solveUnderflow ( p ); //上升一层,如有必要则继续分裂——至多递归O(logn)层
   return;
}

红黑树:动机

支持对历史版本访问的数据结构称为一致性结构,蛮力算法虽然时间上尚可,但是空间上却随着版本的增加线性递增,时间因此也在递增,无法忍受。

如果采取大量共享少量更新的策略,将上个版本未进行改动的引用依旧保留给下一个版本,可以实现每个版本新增的复杂度仅为O(logn)。

我们希望两个版本间结构的差异不超过O(1),但是之前的一些BBST的旋转操作往往不能满足这个条件,但是红黑树在单次动态操作后版本的差异不会超过O(1)。

红黑树:结构

简单地说,红黑树即由红黑两类节点组成的BST,我们依旧对它增设外部节点NULL,使之成为真二叉树。

红黑树的规定:树根及外部节点均为黑色;其他节点若为红,则只能有黑孩子,这个性质保证了红之父、之子必黑,并且上下两层没有连续的红色节点;外部节点到根途中黑色节点数目相等(黑深度相等)。

自顶而下的考察红黑树的各节点,每遇到一个红节点,都将对应的子树整体提升一层,与其父节点水平对齐,,红黑树便转化成了一棵四阶B-树。(2,4)树中每个节点应包含且仅包含一个黑关键码,红色关键码也不会超过两个。

这时内部节点有四种情况,黑黑,黑红,红黑以及红红,就算是红红的情况中间也会有黑色关键码,不会出现两个相邻关键码都是红色的现象。

红黑树的高度及黑高度都是O(logn)级别的,且红高度不会超过黑高度。

红黑树:插入

在红黑树的某处插入关键码e,其必为叶子节点,设其父节点为p,将新插入节点x染红,则唯一可能不满足红黑树规则的就是其父节点p也可能是红色的,这时需要做双红修正。

需要考察x的祖父g以及p的兄弟u(x的叔父)。

如果u的颜色为黑色,则x的兄弟以及x的两个孩子黑高度均与u相等。考虑上图a的拓扑结构,先将x、p、g提升到一个超级节点内,发现是xp相邻且都为红色违法了规则,不妨将p与g交换颜色。

对(2,4)树相邻关键码进行重染色,为了不改变四棵子树的黑高度,需要对应的对红黑树做一次等价变换,也就是进行一次3+4重构,将g、p、x以及四棵子树按照中序顺序重新命名进行重构并重染色,如上图所示,最后的结果必然是b节点染黑,a,c节点染红。

如果u的颜色为红色,u的两个孩子非空且均为黑色,且与x的两个孩子以及x的兄弟黑高度相等。将红色节点上升至于黑色节点齐平,得到一个含四个关键码的超级节点,发生了上溢,对B-树而言,需要将g提升至父节点,进行分裂操作,同时g染成白色,p与u染成黑色。

如上图所示,b’经过分裂得到c’,再转化为红黑树成为了c,而b与c的拓扑结构并未发生改变。

总而言之,u为黑色时,双红修正对于对应的B-树而言,只需交换两个关键码的颜色,对于红黑树而言,需要重染色+一次3+4重构;u为红色时,双红修正对于对应的B-树而言需要重染色+分裂,而对于红黑树而言仅仅是颜色的改变,拓扑结构不变。

另外,由于节点的分类,可能导致g的父节点再次构成双红,则需要继续做双红修正,一旦g到达树根,则将g强行染为黑色,整树的黑高度加一。

红黑树:删除

这里首先要回顾的是removeat接口,当待删除元素只有一个分支时,直接删除e,并返回e的接替者,其孩子节点r;当待删除元素有两个分支时,找到e的中序后继(e的右子树的最左边节点),交换两个元素数据域,此时真正要删除的是元素x,并且x的左孩子w必是外部节点。这两种情况都可统一为最后实际删除的元素x,有父节点p,左孩子w(外部节点),右孩子r(可能为外部节点)。

首先考察x为红色的情况,继任者r要么是黑色的外部节点,要么是红色的节点(为了w黑深度的统一)。红色的r可以直接替代x,无须调整,如果r是外部节点,就相当于删除了红黑树中的叶子节点x,无须调整。

如果x是黑色,并且r也是黑色,删了x势必导致红黑树的规则被打破。一般情况下在删除前,x和r同为黑色只可能r是外部节点,否则r分支的外部节点和w的黑深度将不同。上图c画的双黑情况给r加了两个孩子,可能是想表达一般情况下,即双黑调整向上传播时的情况。

下面需要做的就是在x和r都是黑色时如何进行双黑修正。

BB-1:

x的兄弟节点s为黑色,且s至少有一个红色孩子,这时只需对t,s,p及其子树做3+4重构并重新染色即可。

这里分类的依据在于对红黑树对应的四阶B-树解决下溢方法的选择上。因为BB-1的情况是删掉x后,x所在的节点发生了下溢,这时,从x的兄弟节点中借一个关键码补给x的父节点,再从x的父节点中把p借给x所在节点来解决下溢,通过对红黑树的3+4重构和重染色可解决此问题。

BB-2R:

如果s为黑,且s的两个孩子均为黑;p为红。观察上图,这种情况同样是B-树中原x节点的下溢问题,不同的是,其兄弟节点s因为没有与之合并的红孩子,只包含一个关键码,无法再借出关键码了,此时,按照B-树删除的规则,需要从父节点中借出一个关键码给它的两个孩子节点作为粘合剂,因为父节点p是红色的,其左右必还有黑色的关键码,所以足以借出,将p借给孩子节点后再重染色,还原成红黑树仅相当于重染色操作。

BB-2B:

如果s和其两个孩子为黑,p也为黑,那么此时s和p都不足以借出关键码了,这时从p的父节点借出一个关键码,把s,p以及失去x的节点粘结到一起即可,转化为红黑树同样只是相当于重染色操作,但是此时p的父节点如果依然是黑色d,则可能因为借出了关键码再次下溢,便需要重复该过程了。

BB-3:

如果s节点为红,其两个孩子必黑,不妨对p做一次旋转,再染色,对于B-树的修改就是交换了s和p节点的颜色,此时还原成红黑树黑高度依然异常,不过此时x的兄弟节点已经是黑色的了,并且父节点s是红色,所以只会在做个BB-1或者BB-2R,这两种调整只需一轮便可恢复红黑树的平衡性。

双黑修正总的代码如下:

/******************************************************************************************
 * 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或BB2R
******************************************************************************************/
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->lc ) ? p->rc : p->lc; //r的兄弟
   if ( IsBlack ( s ) ) { //兄弟s为黑
      BinNodePosi(T) t = NULL; //s的红孩子(若左、右孩子皆红,左者优先;皆黑时为NULL)
      if ( IsRed ( s->rc ) ) t = s->rc; //右子
      if ( IsRed ( s->lc ) ) t = s->lc; //左子
      if ( t ) { //黑s有红孩子:BB-1
         RBColor oldColor = p->color; //备份原子树根节点p颜色,并对t及其父亲、祖父
      // 以下,通过旋转重平衡,并将新子树的左、右孩子染黑
         BinNodePosi(T) b = FromParentTo ( *p ) = rotateAt ( t ); //旋转
         if ( HasLChild ( *b ) ) { b->lc->color = RB_BLACK; updateHeight ( b->lc ); } //左子
         if ( HasRChild ( *b ) ) { b->rc->color = RB_BLACK; updateHeight ( b->rc ); } //右子
         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->lc : s->rc; //取t与其父s同侧
      _hot = p; FromParentTo ( *p ) = rotateAt ( t ); //对t及其父亲、祖父做平衡调整
      solveDoubleBlack ( r ); //继续修正r处双黑——此时的p已转红,故后续只能是BB-1或BB-2R
   }

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值