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则上升一层,并以分裂出来的两个子节点作为左、右孩子。
被提升的关键码,可能有三种进一步的处置方式:
- 如图(a1)所示,原上溢节点的父节点存在,且足以接纳一个关键码,只需将被提升的关键码按次序插入父节点中,即修复完成。
- 如图(b1)所示,原上溢节点的父节点存在,但已处于饱和状态,强行提升后,尽管得到修复,但导致父节点继而发生上溢——上溢的向上传递现象。
- 如图(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 -
实例
备注:根节点的分裂是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⌉个关键码
-
V的右兄弟R存在,且至少包含⌈m/2⌉个关键码
-
V的左、右兄弟L和R或者存在,或者其包含的关键码均不足⌈m/2⌉个
新节点关键码总数为:(⌈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 -
实例
-
复杂度
与插入操作同理,在存有N个关键码的m阶B-树中的每次关键码删除操作,都可以在O(logmN)时间内完成。另外同样地,因某一关键码的删除而导致Ω(logmN)次合并操作的情况也极为罕见,单次删除操作过程中平均只需做常数次节点的合并。
“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 15 天,点击查看活动详情”