首先要感谢清华大学邓公的教材(数据结构c++语言版第三版)和视频
  邓公的视频网址如下:https://next.xuetangx.com/course/THU08091002048/1515966

  由于邓公实现b树是用自定义的vector实现,网上大部分也都是用数组实现。我尝试了一下用stl的vector实现。也能实现b树的功能。
  在此,为了方便,用了一些教材中的插图(侵删)

如果要看懂此代码,你需要拥有的基础为:

  • 对c++模板有基本了解
  • 对c++stl中的vector有一定了解
  • 熟悉stl迭代器,逆向迭代器的用法,在本代码的查找,插入和删除操作中,用了大量的迭代器操作,如果你不熟悉,可能会有点绕。
  • 对c++11的新特性,如auto ,lambdas,基于范围的for循环等有所了解

  本代码在vs2019版能运行,其他平台未测试,但应该能支持c++11的编译器都能运行。如果你没接触过b树,要理解本代码,你至少需要一个小时甚至以上,b树真的十分复杂。

此B树只适合存放不相同元素,如果想放相同的元素,请自行改进。

文章目录

  • 一.B树的定义~
    • 1.多路搜索树~
    • 2.多路平衡搜索树~
    • 3.B树的性质~
  • 二.B树节点类~
    • BTNode.h~
  • 三.B树类~
    • (一)定义变量和接口~
      • 1.需要的变量
      • 2.需要的接口
      • 3.必备的辅助函数
      • 4.BTree.h~
    • (二)B树查找~
      • 1.代码~
      • 2.效率分析~
    • (三)B树插入~
      • 1.简单插入图示~
      • 2.代码~
    • (四)上溢与分裂~
      • 1.只上溢一次,不会造成父亲上溢~
      • 2.上溢之后,父亲也会上溢~
      • 3.上溢到根节点~
      • 4.show you my code~
      • 5.复杂度分析~
    • (五)删除~
      • 1.节点为叶节点删除图示~
      • 2.节点不为叶节点删除图示~
      • 3.代码~
    • (六)下溢与合并~
      • 1.V的左兄弟L存在,左兄弟至少包含⌈m/2⌉个关键码(动图gif)~
      • 2.V的右兄弟R存在,右兄弟至少包含⌈m/2⌉个关键码(动图gif)~
      • 3.左兄弟为空,并且右兄弟没有足够孩子(动图gif)~
      • 4.右兄弟为空,并且左兄弟没有足够孩子(动图gif)~
      • 5.经过(3)(4)后,父亲发生下溢~
      • 6.show you my code~
      • 7.复杂度分析~
    • (七)前序遍历测试~
  • 四.结果测试~
    • 1.插入测试~
    • 2.删除测试~
  • 五.结语~

一.B树的定义~

1.多路搜索树~

比如以二叉搜索树(BST)的两层为间隔,将各节点与其左右孩子合并成一个大节点,就能得到一个四路搜索树

当以三层为间隔时,可以得到八路搜索树。因此可以推广到2^k路搜索树

接下来的代码,能创建任意路搜索树,如果创建的为4路搜索树,我们不妨称之为(2,4)树

2.多路平衡搜索树~

若树中所有叶节点的深度均相等,就可以称之为多路平衡搜索树
深度即从根节点到这个节点的长度。
如下图所示

3.B树的性质~

  1. 所有叶节点的深度均相等。
  2. B树的阶次:比如(2,4)树,其阶次为4。以及(2,3)树,其阶次为3。
  3. 关键码:B树中一个节点所存的数值即为关键码。
  4. 孩子个数:B树的一个节点可拥有的孩子个数。(如一个节点至少拥有的孩子个数为2,最多拥有的孩子个数为3,则为(2,3)树)。
  5. 阶次和孩子个数的关系:如果B树的阶次为m,那么其孩子的个数范围为⌈m/2⌉(取上界)到m之间。 此点对于B树而言至关重要
  6. 孩子个数和关键码的关系:简单观察不难发现,B树的关键码数等于并且必然等于孩子个数减一
  7. B树的根节点,不受此限制,在非空的B树中,根节点关键码数至少为1,孩子个数至少为2。
  8. 若B树为空,其根节点的关键码数为0,孩子个数为1。

举例说明:
即在一颗4阶的B树中,其孩子个数至少为2,最多为4.关键码个数至少为1,最多为3.
即在一颗3阶的B树中,其孩子个数至少为2,最多为3.关键码个数至少为1,最多为2.

二.B树节点类~

  1. 从B树的结构和性质来看,在B树节点中,至少需要一个存放关键码的容器和一个存放孩子节点的容器。

  2. 所以我们把同一节点的所有孩子组织为一个vector,各相邻孩子之间的关键码也组织为一个vector。当然,按照B-树的定义,孩子vector的实际长度总是比关键码vector多一

  3. 为了方便插入和删除,同样也需要一个parent指针,来指向父节点。

  4. 同样,为了规范,将节点定义包含在一个命名空间my_btree里面。

  5. 由于没有在堆区构造,所以不需要写析构函数来delete

BTNode.h~

#pragma once
#include <vector>
using std::vector;namespace my_btree {/*B-树节点模板类*/template<typename T = int>class BTNode {public:using BTNodePtr = BTNode<T>*;public:BTNodePtr _parent;//父节点vector<T> _key;//关键码vectorvector<BTNodePtr> _child;//孩子vector,其长度总比key多一private:int _order;//B树的order,方便扩容//可不要这个,让vector自动扩容public://用于创建根节点,初始时有0个关键码和一个空孩子指针BTNode(int order=3) :_order(order),_parent(nullptr), _key(0){_key.reserve(order);//并且给key和child的vector预留容量//用空间换时间。_child.reserve(order + 1);_child.push_back(nullptr);}};//class BTNode}//namespace my_btree

三.B树类~

(一)定义变量和接口~

1.需要的变量

 int _size;//存放的关键码总数int _order;//B-树的阶次,比如3阶b树,可以在构造的时候进行修改。BTNodePtr _root;//根节点BTNodePtr _hot;//BTree::search()最后访问的非空(除非树空)的节点位置

如果看过邓老师的课的同学,应该能明白_hot节点的作用。在这里我简单解释一下_hot的作用。即其对应查找之后,访问的最后一个非空节点位置。有了_hot,节点的插入和删除操作就能变得更加简单。

2.需要的接口

构造函数
析构函数
查找
插入
删除
遍历(前序遍历,方便看结果)
其他接口如 获取阶次,返回规模,判空,返回根节点等。

3.必备的辅助函数

因插入而造成节点上溢所需的解决上溢函数
因删除而造成节点下溢所需的解决下溢函数

4.BTree.h~

为了方便起见,以下均以3阶b树为基准

#pragma once
#include "BTNode.h"
#include <algorithm>
using std::cout;
using std::endl;namespace my_btree{template<typename T=int>class BTree {public:using BTNodePtr = BTNode<T>*;protected:int _size;//存放的关键码总数int _order;//B-树的阶次,比如3阶b树,可以在构造的时候进行修改。BTNodePtr _root;//根节点BTNodePtr _hot;//BTree::search()最后访问的非空(除非树空)的节点位置protected:void solveOverFlow(BTNode<T>*);//处理因插入而上溢之后的分裂处理void solveUnderFlow(BTNode<T>*);//处理因删除而下溢之后的合并处理public:BTree(int order=3):_order(order),_size(0),_root(new BTNode<T>(_order)),_hot(nullptr){}~BTree() {if (_root){delete _root;_root = nullptr;}}public:constexpr int order()const {//获取阶次return _order;}constexpr int size()const {return _size;}inline BTNodePtr root()const{//返回树根return _root;} constexpr bool empty()const {return !_root;}public:BTNode<T>* search(const T& data);//查找bool insert(const T& data);//插入bool remove(const T& data);//删除public:void show_BTree(BTNode<T>* BT)const;};//class BTree
}//namespace my_btree

(二)B树查找~

1.代码~

  从根节点开始,通过关键码的比较不断深入至下一层,直到某一关键码命中(查找成功),或者到达某一外部节点(查找失败)。

  此时各节点内通常都包含多个关键码,故有可能需要经过多次比较,才能确定应该转向下一层的哪个节点并继续查找。
  如果查找失败,返回不大于data(图中的key)的最大节点(由于节点数最多也不过几百个,故用顺序查找还是二分查找,都可以,这里用的是顺序查找)。从而转至下一层,直到找到或者到达叶子节点

  由于c++标准库并没有提供find_last的算法给vector使用。因此返回不大于data的最大节点,我用的是逆向迭代器结合find_if算法,并结合lambdas表达式作为仿函数来实现。

  并且由于标准库中的算法,需要传的是迭代器,返回的也是迭代器,因此,如果你对迭代器不是很懂,那么就建议你看看邓公原版的代码,那个稍微容量理解点。

 template<typename T>BTNode<T>* BTree<T>::search(const T& data){BTNode<T>* v = _root;//从根节点出发_hot = nullptr;//首先将_hot置空while (v) {//返回第一个不大于data的迭代器auto key_Iter = std::find_if(v->_key.rbegin(), v->_key.rend(),[data](T key_value){return key_value <= data;});if ((key_Iter != v->_key.rend()) && data == *key_Iter)//若找到,则返回return v;_hot = v;//否则就将当前节点设为_hot,并且去对应的孩子节点找//由于孩子的个数始终比关键码个数多一,所以不会越界v = v->_child.at(std::distance(key_Iter,v->_key.rend()));}return nullptr;//失败,抵达外部节点}

2.效率分析~

  vector的查找十分迅速,因此其耗时几乎可以忽略不计。
  b树的查找的耗时主要在于去其孩子节点找相应的值。并且在查找的过程中,在每一高度上,至多访问一个节点。因此,b树查找的复杂度即为b树的高度。
  在这里,证明略,查阅资料可得b树的平均高度为O(logmN)

  故b树查找的平均复杂度为O(logmN)

(三)B树插入~

  1. B树的插入与一般的插入一样,都要提前search,并判断是否存在这个节点。如果不存在,此时树中的_hot值也已经设定好了。(_hot的作用就体现出来了)
  2. 之后,对_hot的key的vector进行一次顺序查找,并返回应该插入的位置
  3. 并在该位置插入对应的key。并在孩子节点的下一个位置,创建一个空孩子指针
  4. 因为插入可能引发上溢,所以最后处理上溢

1.简单插入图示~

在上图中插入节点23

2.代码~

template<typename T>
bool BTree<T>::insert(const T& data)
{//搜寻了一遍之后,_hot节点所在的位置的关键码vector即为要插入的数值应该属于的vector。BTNode<T>* v = search(data);if (v)//确认目标不存在return false;//找到要插入的位置auto key_r_Iter = std::find_if(_hot->_key.rbegin(), _hot->_key.rend(), [data](T key_value) {return key_value <= data;});int distance = std::distance(key_r_Iter, _hot->_key.rend())+1; //提前算好距离并且必须要加1//由于insert只接受正向迭代器,所以要转成正向的,就是要插入到后一个位置_hot->_key.insert(key_r_Iter.base(), data); auto child_Iter = _hot->_child.begin();child_Iter += distance;//找到要插入的孩子节点的迭代器位置_hot->_child.insert(child_Iter, nullptr);//在对应孩子节点vector中,插入一个空指针_size++;//当此时_hot节点的孩子的个数大于原来的设定的B树的阶次时if(_order < static_cast<int>(_hot->_child.size()))solveOverFlow(_hot);//需做分裂,处理上溢return true;
}

(四)上溢与分裂~

  由b树的定义可知,刚发生上溢的节点,应恰好含有m个关键码。不妨取s=m/2的下界。则它们依次为:

{ k0, ..., ks-1; ks; ks+1, ..., km-1 }   可见,以ks为界,可将该节点分前、后两个子节点,且二者大致等长。

  于是,可令关键码ks上升一层,归入其父节点(若存在)中的适当位置,并分别以这两个子节点作为其左、右孩子。这一过程,称作节点的分裂(split)。

以下分三种情况进行考虑

1.只上溢一次,不会造成父亲上溢~

在上图中插入节点29


可以观察到,由于19 23 29有了三个key,超过了3阶b树的限制,3阶b树单个节点的关键码数最多为2
因此此时要发生上溢。

  我们之前设置的s的作用就体现出来了,由于m=3,所以s=1,因此,以下标为1的关键码为界限进行分裂,在19 23 29中,23的下标为1,所以23提升到父节点中,而19 和29 就成为其左右两个孩子。

  父亲节点36插入23之后,符合3阶b树的标准,所以上溢终止,调整结束。

2.上溢之后,父亲也会上溢~

在上图中插入节点45


  同理,插入了45之后,41 45 51三个节点由于有3个关键码,因此也会发生上溢,经过上溢方式1的调整。可得

这时候,不难观察到其父亲23 36 45此时也有了3个关键码,因此也会发生上溢,因此,可得。

这样就调整完毕。

3.上溢到根节点~

倘若此时我们插入87

此时会发生持续上溢

直到根节点

此时,还是跟之前一样,将key为1的关键码往上提,即把53往上提

至此就完成了上溢调整。 这也是b树长高的唯一可能。

4.show you my code~

注释写的很详细了,可以自己对照图加深理解。

template<typename T>
void BTree<T>::solveOverFlow(BTNode<T>* v)
{while (_order < static_cast<int>(v->_child.size())) {int s = _order / 2;//轴点(如果能到这一步,说明发生了上溢,此时,必然有key的size等于_order)int move_Number = _order - s - 1;//分裂后需要移动的关键码的个数//创建一个新节点,并且由于构造函数,这个节点必然有一个空孩子//堆区的数据,由vector来删除BTNode<T>* newNode = new BTNode<T>(_order);//将v的右侧的child的vector的数值转移到新的节点的孩子vector中//首先,因为这个新节点创建后,一定有一个空孩子,所以要将这个空孩子的值赋值成对应的值//而且v->_child的vector必然有两个元素以上(可能均为nullptr)newNode->_child.at(0) = *(v->_child.end() - move_Number - 1);//再在后面进行插入,此时是在begin()的下一个位置进行插入newNode->_child.insert(++newNode->_child.begin(), v->_child.end() - move_Number, v->_child.end());//删除v中对应child的vector的一部分v->_child.erase(v->_child.end() - move_Number - 1, v->_child.end());//将v的右侧的关键码vector的数值转移到新的节点的关键码vector中newNode->_key.insert(newNode->_key.begin(), v->_key.end() - move_Number, v->_key.end());//删除v中对应key的vector的一部分v->_key.erase(v->_key.end() - move_Number, v->_key.end());//如果新节点的第一个孩子不为空,如果一个孩子为空,则后面孩子必然为空if (newNode->_child.at(0)) {for (auto& child : newNode->_child) {//则令它们的父节点统一child->_parent = newNode;//即指向父节点}}BTNode<T>* p = v->_parent;//v节点的当前父亲if (p == nullptr) {//如果父亲为空//则说明此时根节点溢出,则需创建一个新的节点作为根节点_root = p = new BTNode<T>(_order);p->_child.at(0) = v;//就将这个根节点的孩子设为vv->_parent = p;//并更新v的父亲}//找到v在父节点对应的孩子的秩auto  p_r_Iter = std::find_if(p->_key.rbegin(), p->_key.rend(), [=](T key_value) {return key_value <= v->_key.at(0);});int distance = std::distance(p_r_Iter, p->_key.rend()) + 1;//提前算好距离并且必须要加1auto key_Iter = p_r_Iter.base();p->_key.insert(key_Iter, v->_key.at(s));//在对应位置插入轴点关键码v->_key.pop_back();//并且s,必然是v的key此时的最后一个,在v中删除这个关键码auto child_Iter = p->_child.begin();child_Iter += distance;p->_child.insert(child_Iter, newNode);//将这个新节点作为p的孩子newNode->_parent = p;//并且更新新节点的父亲v = p;//将v设成v的父亲,进入迭代循环}
}

5.复杂度分析~

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

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

(五)删除~

  1. 首先利用查找判断关键码是否存在,若存在,则在对应的大节点中,找到关键码所在的秩。
  2. 之后判断此大节点是否是叶节点。
  3. 如果是,直接删除其key和其中一个child。
  4. 如果不是,则找到关键码的直接后继。将这个要删除节点的值设为直接后继的关键码的值。再删除直接后继的key和其中一个child。
  5. 再更新规模,并且如有必要,进行下溢调整。

1.节点为叶节点删除图示~

在上图中删除节点41

2.节点不为叶节点删除图示~

在上图中删除节点53,首先将53与其直接后继64进行交换数值

然后当做叶节点删除53既可

3.代码~

template<typename T>
bool BTree<T>::remove(const T& data)
{BTNode<T>* v = search(data);if (v == nullptr)//如果没找到,就返回return false;//在v中确定data的逆向迭代器的位置//必然存在,不可能找不到auto key_r_Iter = std::find_if(v->_key.rbegin(), v->_key.rend(), [data](T key_value) {return key_value <= data;});//同search算法,不需要加1//distance必然不可能为0int distance = std::distance(key_r_Iter, v->_key.rend());if (v->_child.at(0)) {//如果v有孩子,就去找其直接后继BTNode<T>* u = v->_child.at(distance);//找到对应孩子节点的位置while (u->_child.at(0))//向左一直找,直到u没有孩子为止,说明u此时为叶节点u = u->_child.at(0);//此时的u即为原来v的直接后继,因此同BST,此时将v此时的值设为u的值既可v->_key.at(distance - 1) = u->_key.at(0);v = u;//让v去接替u的值,然后删掉其中的key和child。distance = 0;}if (distance == 0) {v->_key.erase(v->_key.begin());v->_child.erase(++(v->_child.begin()));//删第二个,也可以删第一个}else {auto key_Iter = key_r_Iter.base();v->_key.erase(--key_Iter);//此时删除必须要退一格进行删除auto child_Iter = v->_child.begin();child_Iter += distance;v->_child.erase(child_Iter);}_size--;//规模更新solveUnderFlow(v);//如果必要,需做旋转或合并return true;
}

(六)下溢与合并~

  通过B树的性质,不难发现,在m阶B-树中,刚发生下溢的节点V必恰好包含⌈m/2⌉(取上界)- 2个关键码和⌈m/2⌉- 1个孩子。以下将根据其左、右兄弟所含关键码的数目,分五种情况做相应的处理。

  还是以3阶b树来考虑,此时m=3,即m/2的上界为2.

1.V的左兄弟L存在,左兄弟至少包含⌈m/2⌉个关键码(动图gif)~

  即左兄弟至少有2个关键码。而v删除后此时只有0个关键码,此时,就将父节点的对应关键码给v,并从v的左兄弟中借最右边的那个关键码给父亲。同时也要对孩子进行一些删除和变换。

如删除下图中关键码3

2.V的右兄弟R存在,右兄弟至少包含⌈m/2⌉个关键码(动图gif)~

  即右兄弟至少有2个关键码。而v删除后此时只有0个关键码,此时,就将父节点的对应关键码给v,并从v的右兄弟中借最左边的那个关键码给父亲。同时也要对孩子进行一些删除和变换。

如删除下图中关键码1

3.左兄弟为空,并且右兄弟没有足够孩子(动图gif)~

  显然,由于一个节点至少有两个孩子,其父亲至少有两个孩子,因此,这个节点v至少存在一个兄弟,并且这个兄弟一定恰好包含⌈m/2⌉- 1个关键码,必然不可能为空

  对于3阶b树而言,其兄弟有且仅有1个关键码。不足以借它,所以可以向其父亲,借一个关键码,将两个节点合并成一个节点。同时对孩子进行调整。

如删除下图中关键码1

并且,借了一个关键码之后,这个新节点,必然不可能越界。

4.右兄弟为空,并且左兄弟没有足够孩子(动图gif)~

同(3)分析

如删除下图中关键码5

5.经过(3)(4)后,父亲发生下溢~

  但是从父亲借一个关键码,可能造成父亲的下溢,因此,只需对父亲再进行一次下溢的操作,即可修复父亲的下溢,当然,可能会引发连锁下溢,但最坏也不过下溢到根节点。

如删除下图中关键码1

  如果下溢到根节点,并且原来的根节点只有一个关键码,借了之后就没有了。那么此时就不妨删除这个根节点,以其孩子作为根节点,从而取代之。此时,b树的高度也必然下降一层

这也是b树变矮的唯一可能。

6.show you my code~

注释写的很详细了,可以自己对照图加深理解。

template<typename T>
void BTree<T>::solveUnderFlow(BTNode<T>* v)
{if ((_order + 1) / 2 <= static_cast<int>(v->_child.size()))//递归基,当前节点未下溢return;BTNode<T>* p = v->_parent;//当前节点的父亲if (p == nullptr) {//递归基,如果父亲为空节点,即说明此时到达了根节点。if (!v->_key.size() && v->_child.at(0)) {//如果v此时的key为空,但却有唯一的非空孩子_root = v->_child.at(0);//就将v的孩子作为根节点_root->_parent = nullptr;v->_child.at(0) = nullptr;//将v的这个孩子置为空delete v;//释放vv = nullptr;}//整树的高度降低一层return;//无论v此时是否为空,均达到递归基,若v不为空,则v就是根节点。}size_t rank = 0;while (p->_child.at(rank) != v)//找到v在其父亲中所属的孩子的位置rank++;/*1.向左兄弟借关键码*/if (0 < rank) {//如果v不是p的第一个孩子BTNode<T>* ls = p->_child.at(rank - 1);//则左兄弟必然存在if ((_order + 1) / 2 < static_cast<int>(ls->_child.size())) {//如果左兄弟有充足的孩子v->_key.insert(v->_key.begin(), p->_key.at(rank - 1));//将父亲插入v的最左边//将左兄弟的最右边的key设为父亲的这个key的值p->_key.at(rank - 1) = ls->_key.at(ls->_key.size() - 1);ls->_key.pop_back();//删除左兄弟的最右边的key//将左兄弟的最右边的孩子作为v的第一个孩子v->_child.insert(v->_child.begin(),ls->_child.back());ls->_child.pop_back();//删除左兄弟的最右边的child//如果左兄弟的右孩子不为空,即v此时的左孩子不为空,就重设左孩子的父亲。if (v->_child.front())v->_child.front()->_parent = v;return;//至此,就完成了当前层的下溢处理。同时,由于父亲的key没有减少,所以也完成了所有层的下溢处理。}}//如果执行到此,说明左兄弟没有充足的孩子,或者根本没进入if语句内,即rank=0,v没有左兄弟/*2.向右兄弟借关键码*/if (p->_child.size() - 1 > rank) {//如果v并非最后一个孩子,并且其左兄弟没有充足的孩子时BTNode<T>* rs = p->_child.at(rank + 1);//则右兄弟必然存在if ((_order + 1) / 2 < static_cast<int>(rs->_child.size())) {//如果右兄弟有充足的孩子v->_key.push_back(p->_key.at(rank));//将父亲插入v的最右边p->_key.at(rank) = rs->_key.front();//将右兄弟的最左边的key设为父亲的这个key的值rs->_key.erase(rs->_key.begin());//删除右兄弟的最左边的keyv->_child.push_back(rs->_child.front());//将右兄弟的最左边的孩子作为v的最后一个孩子rs->_child.erase(rs->_child.begin());//删除右兄弟的最左边的child//如果右兄弟的左孩子不为空,即v此时的右孩子不为空,就重设右孩子的父亲。if (v->_child.back())v->_child.back()->_parent = v;return;//至此,就完成了当前层的下溢处理。同时,由于父亲的key没有减少,所以也完成了所有层的下溢处理。}}//如果执行到此,说明右兄弟没有充足的孩子,或者根本没进入if语句中,即v没有右兄弟/*3.此时,要么左兄弟为空,并且右兄弟没有足够孩子;或者右兄弟为空,并且左兄弟没有足够孩子*//*左右兄弟不可能均为空*/if (0 < rank) {//与左兄弟和父亲合并BTNode<T>* ls = p->_child.at(rank - 1);//左兄弟必然存在,一定不为空,但没有足够孩子ls->_key.push_back(p->_key.at(rank - 1));//在左兄弟的最右边插入父亲的keyp->_key.erase(p->_key.begin() + rank - 1);//并删除父亲对应的节点//同时删除父亲这个节点的孩子,此时v不再是p的这个孩子p->_child.erase(p->_child.begin() + rank);ls->_child.push_back(v->_child.front());//将v的左孩子作为其左兄弟的最右孩子v->_child.erase(v->_child.begin());if (ls->_child.back())//并且重新设定父子关系ls->_child.back()->_parent = ls;//将v此时所有的key都插到其左兄弟的尾部ls->_key.insert(ls->_key.end(), v->_key.begin(), v->_key.end());v->_key.clear();//删除v的key的所有节点for (auto v_c_Iter : v->_child) {ls->_child.push_back(v_c_Iter);//将v此时所有的child都插到其左兄弟的尾部if (ls->_child.back())//如果不为空,就重新设定父子关系ls->_child.back()->_parent = ls;}v->_child.clear();//删除v的child的所有节点delete(v);//释放vv = nullptr;}else {//与右兄弟和父亲合并BTNode<T>* rs = p->_child.at(rank +1);//右兄弟必然存在,一定不为空,但没有足够孩子rs->_key.insert(rs->_key.begin(), p->_key.at(rank));//在右兄弟的最左边插入父亲的keyp->_key.erase(p->_key.begin() + rank);//并删除父亲对应的节点//同时删除父亲这个节点的孩子,此时v不再是p的这个孩子p->_child.erase(p->_child.begin() + rank);rs->_child.insert(rs->_child.begin(), v->_child.back());//将v的右孩子作为右兄弟的最左孩子v->_child.pop_back();if (rs->_child.front())//并且重新设定父子关系rs->_child.front()->_parent = rs;//将v此时所有的key都插到其右兄弟的头rs->_key.insert(rs->_key.begin(), v->_key.begin(), v->_key.end());v->_key.clear();//删除v的key的所有节点//将v此时所有的child都插到其右兄弟的头部rs->_child.insert(rs->_child.begin(), v->_child.begin(), v->_child.end());for (auto& rs_child : rs->_child) {if (rs_child) {//如果不为空,就重新设定父子关系//直到遍历到没有加v的child之前的原来的rs的child为止。if (rs_child->_parent == rs)break;rs_child->_parent = rs;}}v->_child.clear();//删除v的child的所有节点delete v;//释放vv = nullptr;}solveUnderFlow(p);//上升一层,如果有必要,继续分裂,至多递归至logn层
}

7.复杂度分析~

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

(七)前序遍历测试~

调用show_BTree函数。传一个root()作为参数既可。

template<typename T>
void BTree<T>::show_BTree(BTNode<T>* BT)const {//用的时候,传一个root()if (BT == nullptr) {cout << "BTree 不存在" << endl;return;}bool has_child = false;cout << "[";for (auto it = BT->_key.begin(); it != BT->_key.end(); ++it) {if (it != BT->_key.begin())cout << " ";cout << *it;}cout << "]";for (size_t i = 0; i <= BT->_key.size(); ++i) {if (BT->_child.at(i) != nullptr) {if (i == 0) {cout << "<";}else {cout << ",";}show_BTree(BT->_child.at(i));has_child = true;}}if (has_child)cout << ">";
}

四.结果测试~

1.插入测试~

#include<iostream>
#include "BTree.h"using namespace std;
using namespace my_btree;int main() {BTree b(3);for (int i = 0; i <7; i++) {b.insert(i);}b.show_BTree(b.root());return 0;
}

输出结果为

[3]<[1]<[0],[2]>,[5]<[4],[6]>>


更具体点为

2.删除测试~

#include<iostream>
#include "BTree.h"using namespace std;
using namespace my_btree;int main() {BTree b(3);for (int i = 0; i <7; i++) {b.insert(i);}b.show_BTree(b.root());cout << endl;for (int i = 0; i < 7; i++) {b.remove(i);b.show_BTree(b.root());cout << endl;}return 0;
}

输出结果为

[3]<[1]<[0],[2]>,[5]<[4],[6]>> [3 5]<[1 2],[4],[6]> [3 5]<[2],[4],[6]> [5]<[3 4],[6]> [5]<[4],[6]> [5 6] [6] []

五.结语~

  理解b树,特别是(2,4)树,是理解红黑树的基础,红黑树的代码见红黑树

  码字不易,此篇高达1w5千字,有任何疑问可以在下方留言;代码有任何纰漏之处,希望广大读者可以进行指正。

  如果你觉得对你有所帮助,可以点个赞

真c++创建B树(非c with class)相关推荐

  1. 2018南京网络赛 G. Lpl and Energy-saving Lamps (线段树非递归实现)

    线段树非递归实现.点修改下的区间查询. 注意预处理的时候要把一到十万的都进行处理一遍,因为输入的月份不一定在月份的个数之内. #include <iostream> #include &l ...

  2. 如何在vs中创建r树索引代码_线段树详解与实现

    此篇文章用于记录<玩转数据结构>课程的学习笔记 什么是线段树 线段树也被称为区间树,英文名为Segment Tree或者Interval tree,是一种高级的数据结构.这种数据结构更多出 ...

  3. 线段树线段树的创建线段树的查询单节点更新区间更新

    目录 线段树 什么是线段树? 线段树的创建 线段树的查询 单节点更新 区间更新 未完待续 线段树 实现问题:常用于求数组区间最小值 时间复杂度:(1).建树复杂度:nlogn.(2).线段树算法复杂度 ...

  4. php创建无限级树型菜单以及三级联动菜单

    http://www.php.cn/php-weizijiaocheng-373500.html 这篇文章主要介绍了php创建无限级树型菜单 ,主要使用的是递归函数,感兴趣的小伙伴们可以参考一下 写递 ...

  5. php动态创建菜单,php创建无限级树型菜单

    写递归函数,可考虑缓存,定义一些静态变量来存上一次运行的结果,多程序运行效率很有帮助.. 大概步骤如下: step1:到数据库取数据,放到一个数组, step2:把数据转化为一个树型状的数组, ste ...

  6. 浏览器创建render 树_如何为浏览器创建出色的游戏

    浏览器创建render 树 Do you get a rush when you discover a glitch for the first time? Or what about when yo ...

  7. 二叉树的创建——递归与非递归

    目录 一.知识铺垫 二.二叉树的创建之递归 三.二叉树的创建之非递归(迭代) 初学者对于二叉树的遍历(前序遍历,中序遍历,后序遍历,层序遍历),这些放在最早学习的内容,应该是非常熟悉的,但是渐渐的就会 ...

  8. vue教程2 【Vue组件化编程】 组件的创建和使用 非单文件组件和单文件组件的区别

    组件 实现局部功能的代码和资源的集合 非单文件组件 1.定义组件(创建组件) 2.注册组件 3.使用组件(写组件标签) 定义 使用Vue.extend(options)创建,其中options和new ...

  9. 【计算理论】图灵机 ( 非确定性图灵机 与 计算树 | 非确定性 | 非确定性图灵机 与 确定性图灵机 相互模仿 | 非确定性图灵机 -> 确定性图灵机 )

    文章目录 一.非确定性图灵机 与 计算树 二.非确定性 三.非确定性图灵机 与 确定性图灵机 相互模仿 四.非确定性图灵机 -> 确定性图灵机 一.非确定性图灵机 与 计算树 非确定性图灵机体现 ...

最新文章

  1. java字典序列化_Java对象序列化,Serialize Java Data Object,音标,读音,翻译,英文例句,英语词典...
  2. OMS SDK中OPhone应用与BAE JIL中Widget应用的区别
  3. 背水一战 Windows 10 (47) - 控件(ScrollViewer 特性): Chaining, Rail, Inertia, Snap, Zoom
  4. mvn 打包可执行包_如何用Maven打包可执行jar包
  5. 中英文怎么算 字符长度_钢材长度怎么算,只有1%的人才懂!
  6. php yanzhengm,ThinkPHP 在使用M方法(不创建模型类)时实现自动验证与自动填充
  7. ubuntu环境下一键切换python的virtualenv虚拟环境
  8. 如何查找业务用例和业务执行者
  9. linux导出mysql下ssl证书_Linux系统下生成证书 https证书
  10. 【Tensorflow】Tensorflow中的卷积函数(conv2d、slim.conv2d、depthwise_conv2d、conv2d_transpose)
  11. Oracle GoldenGate 12c 新特性
  12. [Diary]忧伤,止不住忧伤……
  13. (10)Node.js核心模块—fs文件系统之目录操作
  14. 避免编写解决不存在问题的代码
  15. 410. 分割数组的最大值
  16. 谈谈博客园和写博客,以及通过博客遇到的那些人
  17. 二维码中间嵌入logo
  18. 安装vs2010 sp1失败
  19. 前端 debugger
  20. 2019最新升级全能版vbox硬件级虚拟机系统 vm去虚拟化修改信息工具 批量启动克隆 virtualbox CPA网赚挂机电商

热门文章

  1. Linux连接redis数据库
  2. ecshop系统下载
  3. Python backtrader回测之布林带策略
  4. python模拟seo快排vps点击代码实操
  5. 上海职称不用考计算机和英语翻译,国家翻译专业资格考试将替代职称评定
  6. 一款高颜值开源知识管理工具
  7. dockers 拷贝
  8. [转贴]去除迅雷广告和弹出广告窗口
  9. html前端学习三:CSS
  10. java类中获取tomcat下的webap路径方法