首先要感谢清华大学邓公的教材(数据结构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树的定义~
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;//关键码vector
vector<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~
#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