真c++创建B树(非c with class)
首先要感谢清华大学邓公的教材(数据结构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树的性质~
- 所有叶节点的深度均相等。
- B树的阶次:比如(2,4)树,其阶次为4。以及(2,3)树,其阶次为3。
- 关键码:B树中一个节点所存的数值即为关键码。
- 孩子个数:B树的一个节点可拥有的孩子个数。(如一个节点至少拥有的孩子个数为2,最多拥有的孩子个数为3,则为(2,3)树)。
- 阶次和孩子个数的关系:如果B树的阶次为m,那么其孩子的个数范围为⌈m/2⌉(取上界)到m之间。 此点对于B树而言至关重要。
- 孩子个数和关键码的关系:简单观察不难发现,B树的关键码数等于并且必然等于孩子个数减一。
- B树的根节点,不受此限制,在非空的B树中,根节点关键码数至少为1,孩子个数至少为2。
- 若B树为空,其根节点的关键码数为0,孩子个数为1。
举例说明:
即在一颗4阶的B树中,其孩子个数至少为2,最多为4.关键码个数至少为1,最多为3.
即在一颗3阶的B树中,其孩子个数至少为2,最多为3.关键码个数至少为1,最多为2.
二.B树节点类~
从B树的结构和性质来看,在B树节点中,至少需要一个存放关键码的容器和一个存放孩子节点的容器。
所以我们把同一节点的所有孩子组织为一个vector,各相邻孩子之间的关键码也组织为一个vector。当然,按照B-树的定义,孩子vector的实际长度总是比关键码vector多一。
为了方便插入和删除,同样也需要一个parent指针,来指向父节点。
同样,为了规范,将节点定义包含在一个命名空间my_btree里面。
由于没有在堆区构造,所以不需要写析构函数来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树插入~
- B树的插入与一般的插入一样,都要提前search,并判断是否存在这个节点。如果不存在,此时树中的_hot值也已经设定好了。(_hot的作用就体现出来了)
- 之后,对_hot的key的vector进行一次顺序查找,并返回应该插入的位置。
- 并在该位置插入对应的key。并在孩子节点的下一个位置,创建一个空孩子指针。
- 因为插入可能引发上溢,所以最后处理上溢。
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)次分裂的情况极为罕见,单次插入操作平均引发的分裂次数,远远低于这一估计,故时间通常主要消耗于对目标关键码的查找。
(五)删除~
- 首先利用查找判断关键码是否存在,若存在,则在对应的大节点中,找到关键码所在的秩。
- 之后判断此大节点是否是叶节点。
- 如果是,直接删除其key和其中一个child。
- 如果不是,则找到关键码的直接后继。将这个要删除节点的值设为直接后继的关键码的值。再删除直接后继的key和其中一个child。
- 再更新规模,并且如有必要,进行下溢调整。
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)相关推荐
- 2018南京网络赛 G. Lpl and Energy-saving Lamps (线段树非递归实现)
线段树非递归实现.点修改下的区间查询. 注意预处理的时候要把一到十万的都进行处理一遍,因为输入的月份不一定在月份的个数之内. #include <iostream> #include &l ...
- 如何在vs中创建r树索引代码_线段树详解与实现
此篇文章用于记录<玩转数据结构>课程的学习笔记 什么是线段树 线段树也被称为区间树,英文名为Segment Tree或者Interval tree,是一种高级的数据结构.这种数据结构更多出 ...
- 线段树线段树的创建线段树的查询单节点更新区间更新
目录 线段树 什么是线段树? 线段树的创建 线段树的查询 单节点更新 区间更新 未完待续 线段树 实现问题:常用于求数组区间最小值 时间复杂度:(1).建树复杂度:nlogn.(2).线段树算法复杂度 ...
- php创建无限级树型菜单以及三级联动菜单
http://www.php.cn/php-weizijiaocheng-373500.html 这篇文章主要介绍了php创建无限级树型菜单 ,主要使用的是递归函数,感兴趣的小伙伴们可以参考一下 写递 ...
- php动态创建菜单,php创建无限级树型菜单
写递归函数,可考虑缓存,定义一些静态变量来存上一次运行的结果,多程序运行效率很有帮助.. 大概步骤如下: step1:到数据库取数据,放到一个数组, step2:把数据转化为一个树型状的数组, ste ...
- 浏览器创建render 树_如何为浏览器创建出色的游戏
浏览器创建render 树 Do you get a rush when you discover a glitch for the first time? Or what about when yo ...
- 二叉树的创建——递归与非递归
目录 一.知识铺垫 二.二叉树的创建之递归 三.二叉树的创建之非递归(迭代) 初学者对于二叉树的遍历(前序遍历,中序遍历,后序遍历,层序遍历),这些放在最早学习的内容,应该是非常熟悉的,但是渐渐的就会 ...
- vue教程2 【Vue组件化编程】 组件的创建和使用 非单文件组件和单文件组件的区别
组件 实现局部功能的代码和资源的集合 非单文件组件 1.定义组件(创建组件) 2.注册组件 3.使用组件(写组件标签) 定义 使用Vue.extend(options)创建,其中options和new ...
- 【计算理论】图灵机 ( 非确定性图灵机 与 计算树 | 非确定性 | 非确定性图灵机 与 确定性图灵机 相互模仿 | 非确定性图灵机 -> 确定性图灵机 )
文章目录 一.非确定性图灵机 与 计算树 二.非确定性 三.非确定性图灵机 与 确定性图灵机 相互模仿 四.非确定性图灵机 -> 确定性图灵机 一.非确定性图灵机 与 计算树 非确定性图灵机体现 ...
最新文章
- java字典序列化_Java对象序列化,Serialize Java Data Object,音标,读音,翻译,英文例句,英语词典...
- OMS SDK中OPhone应用与BAE JIL中Widget应用的区别
- 背水一战 Windows 10 (47) - 控件(ScrollViewer 特性): Chaining, Rail, Inertia, Snap, Zoom
- mvn 打包可执行包_如何用Maven打包可执行jar包
- 中英文怎么算 字符长度_钢材长度怎么算,只有1%的人才懂!
- php yanzhengm,ThinkPHP 在使用M方法(不创建模型类)时实现自动验证与自动填充
- ubuntu环境下一键切换python的virtualenv虚拟环境
- 如何查找业务用例和业务执行者
- linux导出mysql下ssl证书_Linux系统下生成证书 https证书
- 【Tensorflow】Tensorflow中的卷积函数(conv2d、slim.conv2d、depthwise_conv2d、conv2d_transpose)
- Oracle GoldenGate 12c 新特性
- [Diary]忧伤,止不住忧伤……
- (10)Node.js核心模块—fs文件系统之目录操作
- 避免编写解决不存在问题的代码
- 410. 分割数组的最大值
- 谈谈博客园和写博客,以及通过博客遇到的那些人
- 二维码中间嵌入logo
- 安装vs2010 sp1失败
- 前端 debugger
- 2019最新升级全能版vbox硬件级虚拟机系统 vm去虚拟化修改信息工具 批量启动克隆 virtualbox CPA网赚挂机电商