数据结构 | 第8章 - B-树(下)

82 阅读11分钟

8.2.5 关键码插入

 0001 template <typename T> bool BTree<T>::insert ( const T& e ) { //将关键码e插入B树中
 0002    BTNodePosi<T> v = search ( e ); if ( v ) return false; //确认目标节点不存在
 0003    Rank r = _hot->key.search ( e ); //在节点_hot的有序关键码向量中查找合适的插入位置
 0004    _hot->key.insert ( r + 1, e ); //将新关键码插至对应的位置
 0005    _hot->child.insert ( r + 2, NULL ); //创建一个空子树指针
 0006    _size++; //更新全树规模
 0007    solveOverflow ( _hot ); //如有必要,需做分裂
 0008    return true; //插入成功
 0009 }

插入失败:首先调用search(e)在树中查找该关键码。若查找成功,则不予插入,操作完成并返回false。

插入成功:查找过程必然终止于某一外部节点v,且其父节点由变量 _hot指示,且必然指向某一叶节点(可能同时也是根节点)。接下来,在该节点中再次查找目标关键码e。此次注定查找失败,却可以确定e在其中的正确插入位置r。最后,只需将e插至这一位置。

上溢情况:至此,_hot所指的节点中增加了一个关键码。若该节点内关键码的总数依然合法(即不超过m - 1个),则插入操作随即完成。否则,称该节点发生了一次上溢(overflow)。

8.2.6 上溢与分裂

  • 算法

    一般地,刚发生上溢的节点,应恰好含有m个关键码。若取s = ⌊m/2⌋,则它们依次为:

    { k0, ..., ks-1; ks; ks+1, ..., km-1 }(下标从0开始,直至n-1,故向下取整

    可见,以ks为界,可将该节点分前、后两个子节点,且二者大致等长。于是,可令关键码ks上升一层,归入其父节点(若存在)中的适当位置,并分别以这两个子节点作为其左、右孩子。这一过程,称作节点的分裂(split)。

  • 可能的情况

    以如图8.14(a1)所示的6阶B-树局部为例,其中节点{ 17, 20, 31, 37, 41, 56 },因所含关键码增至6个而发生上溢。为完成修复,可以关键码37为界,将该节点分裂为{ 17, 20, 31 }和{ 41, 56 };关键码37则上升一层,并以分裂出来的两个子节点作为左、右孩子。

    image-20220722150250415.png

    被提升的关键码,可能有三种进一步的处置方式:

    1. 如图(a1)所示,原上溢节点的父节点存在,且足以接纳一个关键码,只需将被提升的关键码按次序插入父节点中,即修复完成。
    2. 如图(b1)所示,原上溢节点的父节点存在,但已处于饱和状态,强行提升后,尽管得到修复,但导致父节点继而发生上溢——上溢的向上传递现象。
    3. 如图(c1)所示,若上溢传递至根节点。则可令被提升的关键码自成一个节点,并作为新的树根。至此上溢修复完毕,全树增高一层。
  • 实现

     0001 template <typename T> //关键码插入后若节点上溢,则做节点分裂处理
     0002 void BTree<T>::solveOverflow ( BTNodePosi<T> v ) {
     0003    while ( _m <= v->key.size() ) { //除非当前节点并未上溢
     0004       Rank s = _m / 2; //轴点(此时应有_m = key.size() = child.size() - 1)
     0005       BTNodePosi<T> u = new BTNode<T>(); //注意:新节点已有一个空孩子
     0006       for ( Rank j = 0; j < _m - s - 1; j++ ) { //v右侧_m-s-1个孩子及关键码分裂为右侧节点u
     0007          u->child.insert ( j, v->child.remove ( s + 1 ) ); //逐个移动效率低
     0008          u->key.insert ( j, v->key.remove ( s + 1 ) ); //此策略可改进
     0009       }
     0010       u->child[_m - s - 1] = v->child.remove ( s + 1 ); //移动v最靠右的孩子
     0011       if ( u->child[0] ) //若u的孩子们非空,则
     0012          for ( Rank j = 0; j < _m - s; j++ ) //令它们的父节点统一
     0013             u->child[j]->parent = u; //指向u
     0014       BTNodePosi<T> p = v->parent; //v当前的父节点p
     0015       if ( !p ) { _root = p = new BTNode<T>(); p->child[0] = v; v->parent = p; } //若p空则创建之
     0016       Rank r = 1 + p->key.search ( v->key[0] ); //p中指向v的指针的秩
     0017       p->key.insert ( r, v->key.remove ( s ) ); //轴点关键码上升
     0018       p->child.insert ( r + 1, u );  u->parent = p; //新节点u与父节点p互联
     0019       v = p; //上升一层,如有必要则继续分裂——至多O(logn)层
     0020    } //while
     0021 } //solveOverflow
    
  • 实例

    image-20220722153844829.pngimage-20220722153918538.png

    备注:根节点的分裂是B-树长高的唯一可能。

  • 复杂度

    若将B-树的阶次m视作为常数,则关键码的移动和复制操作所需的时间都可以忽略。至于solveOverflow()算法,其每一递归实例均只需常数时间,递归层数不超过B-树高度。由此可知,对于存有N个关键码的m阶B-树,每次插入操作都可在O(logmN)时间内完成

    实际上,因插入操作而导致Ω(logmN)次分裂的情况极为罕见,单次插入操作平均引发的分裂次数,远远低于这一估计,故时间通常主要消耗于对目标关键码的查找。

8.2.7 关键码删除

 0001 template <typename T> bool BTree<T>::remove ( const T& e ) { //从BTree树中删除关键码e
 0002    BTNodePosi<T> v = search ( e ); if ( !v ) return false; //确认目标关键码存在
 0003    Rank r = v->key.search ( e ); //确定目标关键码在节点v中的秩(由上,肯定合法)
 0004    if ( v->child[0] ) { //若v非叶子,则e的后继必属于某叶节点
 0005       BTNodePosi<T> u = v->child[r+1]; //在右子树中一直向左,即可
 0006       while ( u->child[0] ) u = u->child[0]; //找出e的后继
 0007       v->key[r] = u->key[0]; v = u; r = 0; //并与之交换位置
 0008    } //至此,v必然位于最底层,且其中第r个关键码就是待删除者
 0009    v->key.remove ( r ); v->child.remove ( r + 1 ); _size--; //删除e,以及其下两个外部节点之一
 0010    solveUnderflow ( v ); //如有必要,需做旋转或合并
 0011    return true;
 0012 }

为从B-树中删除关键码e,也首先需要调用search(e)查找e所属的节点。

删除失败:倘若查找失败,则说明关键码e不存在,操作结束。

删除成功:查找到目标节点v。若v是叶节点,则进一步确定e在节点v中的秩r,将e从v中删去。若v不是叶节点,将e与其直接后继关键码互换后删去即可。( 根据搜索树的性质可知,e的直接后继关键码所属的节点u必为叶节点,且关键码就是其中的最小者u[0] )

下溢情况:删除后,若该节点所含关键码的总数依然合法(即不少于⌈m/2⌉ - 1),则删除操作随即完成。否则,称该节点发生了下溢(underflow) ,并需要通过适当的处置,使该节点以及整树重新满足B-树的条件。

8.2.8 下溢与合并

由上,在m阶B-树中,刚发生下溢的节点V必恰好包含⌈m/2⌉ - 2个关键码和⌈m/2⌉ - 1个分支。以下将根据其左、右兄弟所含关键码的数目,分三种情况做相应的处置:

  • V的左兄弟L存在,且至少包含⌈m/2⌉个关键码

    image-20220722163842837.png

  • V的右兄弟R存在,且至少包含⌈m/2⌉个关键码

    image-20220722163908416.png

  • V的左、右兄弟L和R或者存在,或者其包含的关键码均不足⌈m/2⌉个

    image-20220722164142115.png

    新节点关键码总数为:(⌈m/2⌉ - 1) + 1 + (⌈m/2⌉ - 2) = 2 × ⌈m/2⌉ - 2 ≤ m -1

    故原节点V的下溢缺陷得以修复,而且同时也不致于反过来引起上溢。

    下溢传递现象:关键码y的删除可能致使该节点出现下溢。好在,即便如此,也尽可套用上述三种方法继续修复节点P。当然,修复之后仍可能导致祖父节点以及更高层节点的下溢——这种现象称作下溢的传递。

    特别地,当下溢传递至根节点且其中不再含有任何关键码时,即可将其删除并代之以其唯一的孩子节点,全树高度也随之下降一层

  • 实现

     0001 template <typename T> //关键码删除后若节点下溢,则做节点旋转或合并处理
     0002 void BTree<T>::solveUnderflow ( BTNodePosi<T> v ) {
     0003    while ( ( _m + 1 ) / 2 > v->child.size() ) { //除非当前节点并未下溢
     0004       BTNodePosi<T> p = v->parent;
     0005       if ( !p ) { //递归基:已到根节点,没有孩子的下限
     0006          if ( !v->key.size() && v->child[0] ) {
     0007             //但倘若作为树根的v已不含关键码,却有(唯一的)非空孩子,则
     0008             _root = v->child[0]; _root->parent = NULL; //这个节点可被跳过
     0009             v->child[0] = NULL; release ( v ); //并因不再有用而被销毁
     0010          } //整树高度降低一层
     0011          return;
     0012       }
     0013       Rank r = 0; while ( p->child[r] != v ) r++;
     0014       //确定v是p的第r个孩子——此时v可能不含关键码,故不能通过关键码查找
     0015       //另外,在实现了孩子指针的判等器之后,也可直接调用Vector::find()定位
     0016    // 情况1:向左兄弟借关键码
     0017       if ( 0 < r ) { //若v不是p的第一个孩子,则
     0018          BTNodePosi<T> ls = p->child[r - 1]; //左兄弟必存在
     0019          if ( ( _m + 1 ) / 2 < ls->child.size() ) { //若该兄弟足够“胖”,则
     0020             v->key.insert ( 0, p->key[r - 1] ); //p借出一个关键码给v(作为最小关键码)
     0021             p->key[r - 1] = ls->key.remove ( ls->key.size() - 1 ); //ls的最大关键码转入p
     0022             v->child.insert ( 0, ls->child.remove ( ls->child.size() - 1 ) );
     0023             //同时ls的最右侧孩子过继给v
     0024             if ( v->child[0] ) v->child[0]->parent = v; //作为v的最左侧孩子
     0025             return; //至此,通过右旋已完成当前层(以及所有层)的下溢处理
     0026          }
     0027       } //至此,左兄弟要么为空,要么太“瘦”
     0028    // 情况2:向右兄弟借关键码
     0029       if ( p->child.size() - 1 > r ) { //若v不是p的最后一个孩子,则
     0030          BTNodePosi<T> rs = p->child[r + 1]; //右兄弟必存在
     0031          if ( ( _m + 1 ) / 2 < rs->child.size() ) { //若该兄弟足够“胖”,则
     0032             v->key.insert ( v->key.size(), p->key[r] ); //p借出一个关键码给v(作为最大关键码)
     0033             p->key[r] = rs->key.remove ( 0 ); //rs的最小关键码转入p
     0034             v->child.insert ( v->child.size(), rs->child.remove ( 0 ) );
     0035             //同时rs的最左侧孩子过继给v
     0036             if ( v->child[v->child.size() - 1] ) //作为v的最右侧孩子
     0037                v->child[v->child.size() - 1]->parent = v;
     0038             return; //至此,通过左旋已完成当前层(以及所有层)的下溢处理
     0039          }
     0040       } //至此,右兄弟要么为空,要么太“瘦”
     0041    // 情况3:左、右兄弟要么为空(但不可能同时),要么都太“瘦”——合并
     0042       if ( 0 < r ) { //与左兄弟合并
     0043          BTNodePosi<T> ls = p->child[r - 1]; //左兄弟必存在
     0044          ls->key.insert ( ls->key.size(), p->key.remove ( r - 1 ) ); p->child.remove ( r );
     0045          //p的第r - 1个关键码转入ls,v不再是p的第r个孩子
     0046          ls->child.insert ( ls->child.size(), v->child.remove ( 0 ) );
     0047          if ( ls->child[ls->child.size() - 1] ) //v的最左侧孩子过继给ls做最右侧孩子
     0048             ls->child[ls->child.size() - 1]->parent = ls;
     0049          while ( !v->key.empty() ) { //v剩余的关键码和孩子,依次转入ls
     0050             ls->key.insert ( ls->key.size(), v->key.remove ( 0 ) );
     0051             ls->child.insert ( ls->child.size(), v->child.remove ( 0 ) );
     0052             if ( ls->child[ls->child.size() - 1] ) ls->child[ls->child.size() - 1]->parent = ls;
     0053          }
     0054          release ( v ); //释放v
     0055       } else { //与右兄弟合并
     0056          BTNodePosi<T> rs = p->child[r + 1]; //右兄弟必存在
     0057          rs->key.insert ( 0, p->key.remove ( r ) ); p->child.remove ( r );
     0058          //p的第r个关键码转入rs,v不再是p的第r个孩子
     0059          rs->child.insert ( 0, v->child.remove ( v->child.size() - 1 ) );
     0060          if ( rs->child[0] ) rs->child[0]->parent = rs; //v的最右侧孩子过继给rs做最左侧孩子
     0061          while ( !v->key.empty() ) { //v剩余的关键码和孩子,依次转入rs
     0062             rs->key.insert ( 0, v->key.remove ( v->key.size() - 1 ) );
     0063             rs->child.insert ( 0, v->child.remove ( v->child.size() - 1 ) );
     0064             if ( rs->child[0] ) rs->child[0]->parent = rs;
     0065          }
     0066          release ( v ); //释放v
     0067       }
     0068       v = p; //上升一层,如有必要则继续旋转或合并——至多O(logn)层
     0069    } //while
     0070 } //solveUnderflow
    
  • 实例

    image-20220722165802978.pngimage-20220722165826217.png

  • 复杂度

    与插入操作同理,在存有N个关键码的m阶B-树中的每次关键码删除操作,都可以在O(logmN)时间内完成。另外同样地,因某一关键码的删除而导致Ω(logmN)次合并操作的情况也极为罕见,单次删除操作过程中平均只需做常数次节点的合并

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 15 天,点击查看活动详情