B树
B树概念
B树,现在博主还没有写完,我会把我搞定的每个操作都分享给大家。
B树是一个多路查找二叉树,是从2-3 这样的数据结构变化而来的。B树的优点是:能够较好的保持树的高度,尽量地缩小树的深度,从而有利于减少查找键值所消耗的时间。
B树定义
一棵B树,该B树,在以下简称为T
具有以下性质的有根树。其中根命名为T.root
:
B树分为三种类型的节点,这里先给大家奉上一个图示:
如上图所示:边框为黑色的,我们称它为内部节点;而灰色的节点,我们叫做终端节点。因为B树在各种数据库引擎(MySQL,oracle ,SQLserver…)等都有所涉及,且这些都是与硬盘有直接联系的。为了节省查找所消耗的时间,每个节点都记录了该文件的具体路径。而每个节点所形成的父子关系,也就是说子节点所记录的路径,在它的父节点都是具有包含关系的。终端节点就是文件所在的具体位置。
内部节点,终端(叶子)节点这里前者简称:inside,后者简称:leafNode
,每个节点(含T.root inside outside )都具有以下性质(孩子指针数量x.d
x.k
:
- 每个节点都有d个指向孩子的指针,leafNode除外,原因在于保持B树高度的一致性;
- 每个节点都含有k=d-1个关键字,当且仅当,k=d时,需要对当前节点进行分裂,B树的分裂,也是B树唯一增长高度的情况,每个叶节点具有相同的深度,即树的高度h;;
- 每个节点x,它所具有的关键字本身和孩子指针的关系:x.k1,x.k2,x.k3…x.kn以升序存放到x中,使得x.k1<=x.k2<=x.k3<=…<=x.kn;
- 每个节点x,它的孩子x.d 具有以下性质:x.d1<=x.k1<=x.d2<=x.k2<=…<=x.dn<=x.kn<=x.dn+1。也是以升序的形式存放指针的;
- 每个节点所包含的关键字个数有上、下界。
- 除了T.root以外,每个节点必须至少有,[x.d/2]个关键字,换言之,x.d是每个节点在规定度数D的限制下,都具有最多的孩子;
- 每个节点的某一个节点满足x.k=x.d-1时,也就意味着该节点,需要进行分裂。对应这定义2;
根据定义的结构。它的结构雏形就是这个样子:
代码块1
struct my_B_tree
{
size_t degress;//当前节点的度数(键数量)
size_t key[DEGRESS];//关键字,与私有成员中的dictArr下标所映射(表现方式为:数组).
my_B_tree* parent;//当前节点的前驱指针。
my_B_tree* sonPt[SONNODECOUNT];//孩子指针的数量,
my_B_tree();//节点的构造函数
};
结构体里面有指针,我们就需要要一个针对这个结构体的构造函数
this->degress = 0;//初始化当前节点的关键字数量为0
memset(this->key, 0, sizeof(size_t)*DEGRESS);//对存储的键进行初始化
for (size_t i = 0; i < SONNODECOUNT; ++i)//孩子指针赋空值
{
sonPt[i] = nullptr;
}
this->parent = nullptr;//父指针初始化
定义介绍完了,我们开始思考更一般的情况,也就是B树的节点x.d=4的情况,那么B树节点关键字上界就是x.k=3。
以下操作都是按照B树的x.k=3(即关键字的数量为3,则每个节点最多的孩子数量为4)的情况来进行代码实现的。
上面的n种定义,看了稀里糊涂。来一起体验B树的插入过程吧!
B树的插入
在初始化一个B树的根时(注意:根是临时的,会随着树的高度增加时而变化)。
所以,就有了这几个变量,稍后会给大家一一解释。
代码块2
enum Insert_Location { _first, _second, _third, _forth };//后面揭晓
my_B_tree* tempRootNode;//插入过程中,根节点永远不是固定的。
my_B_tree* rootNode;//分裂后产生的节点
bool B_Tree_isSplit;//b树是否分裂过?
Insert_Location istLocation;
由于tempRootNode
是一个指针,不对它进行初始化就是一个野指针。
为防止这个问题,就需要有一个构造函数,对里面的值进行初始化
this->tempRootNode = nullptr;//设置临时根节点为空
B_Tree_isSplit = false;//第一次的B树是没有分裂过的
ok,我们就开始进行插入。先上几张图(以度数=3)的b树为例
- 首次插入的时候,tempRoot为空,所以,我们需要为tempRoot创建节点
如上图所示:
- 键值是有序的,按照从小到大的顺序来
- 这个节点的键值数量已经达到了度数=3,需要分裂
分裂后:
这个时候,不要想该怎么把这些键给它扣出来,不可能的,编译器给这个结构体分配的内存是整体的。
你还记得吗?当一个节点的度超过规定的度数时,就要将该节点分裂。
在首次分裂之前,节点的关键字个数是满足3个
时就要分裂,对吧?
所以,分裂出来的2个节点都平均拿到了一个关键字。而现在为根的节点,它所保存的键是45
它的左子树的节点比当前节点小。相反,右节点的键比他大。那么我们更要考虑一般的情况,当前仅当,B树中某一个节点的键数达到规定,就需要将其分裂。设:关键字数量为N,左子树和右子树分别取得(N-1)/2,下标为:(N-1)/2的键值作为分割这两个子树的键值,它独自在根节点。
下面,分别向该B树中有序地插入25 31
发现了什么,插入的节点都在叶节点,并且,这些待插入的键值始终满足<根节点的键值。并且,那个节点的数量也达到了规定的数量。需要分裂,分裂后,它应该是这样。
还记得,B树的定义吗?一个节点有n个键值数量,那么他就要有n+1个孩子,所以,我们要对红色箭头所指向的节点进行分裂
得到如下图
是不是两个相邻的子节点都满足分裂这两个子节点的键值的大小关系,没错,是的。并且,还发现了65
所在的节点向后移动了。(有木有?)。没错,除21所在的节点没有移动外,其他的节点都向后面移动了。
按部就班,我们再插入几个数值,有序地插入52 55
如下图
又一次糟糕的情况发生了,需要分裂节点,是的,那你在纸上画画,看能画出来这幅图吗?如下图所示:
是这个样子吧,根节点地键值数量为3,有4个孩子。呃,挺好的,对称美。这时候,出现了一个问题,那就是,父节点的键值数量达到规定,继续分裂它。
树的高度又增加了,还发现在左图,分裂红色箭头指向的位置,还只是仅仅创建节点把(N-1)/2个节点分给他吗?不是,如果是这样的话,叶节点就失踪了。该咋办?我们就要想办法从待分裂节点的孩子指针数组中,将这些指针也按照(N-1)/2分给这两个节点。
那么,在进行最后的一此插入操作:有序插入:54
首先,从根节点开始,先判断根节点的键值与待插入
的键值进行比对,发现大于。我们就要走右子树键值为:55
,走到右子树,再次发现还是没有到到达叶节点,继续判断,小于55。然后进入52的节点,然后,在此比较大于,走右子树,发现,没有孩子了。那么现在就将这个键值数量插入到这个节点中去。当然,这里的每个节点的键值数量都是1,如果某个键值的数量有很多,我们就要一直比较,知道找到具体的节点位置。
来,我们对这个操作进行一次总结
- 在首次插入键值的时候,初始化树是空的,需要我们创建;
- (没分裂)插入键值,一定要按照小->大的顺序进行插入。(分离后),插入的键值总是在叶子节点,并且在搜索的过程中,是以待插入节点与当前节点的每个键值进行比较,最终插入到叶节点;
- 当且仅当,某个节点的键值数量一旦达标,就需要将其分裂,避免内存泄漏、丢失数据就要有浅拷贝带分裂节点的孩子指针。
const Insert_Location indexSelection(my_B_tree* curNode, size_t key)
{
size_t i = 0;
//分裂后,根节点关键字个数为1的情况
for (; i < curNode->degress; ++i)
{
if (key <= curNode->key[i])//待插入的键值与当前节点的每个键值进行比较。如果小于,就return对应的枚举变量
{
//如果满足条件,就根据当前的索引i值,返回指定的指针进行插入
return (Insert_Location)i;
}
//否则就继续循环
}
return (Insert_Location)(i);
}
就比如现在还是在B树中插入54,首先,45所在的节点就1个键,54>45(看上面代码)继续循环,i=1,已经等于这个节点的键值数量,退出循环,所以返回1,强制转换为枚举变量,方便移动指针。
switch (location)
{
case _first:
trackNode = trackNode->sonPt[0];
break;
case _second:
trackNode = trackNode->sonPt[1];
break;
case _third:
trackNode = trackNode->sonPt[2];
break;
case _forth:
trackNode = trackNode->sonPt[3];
break;
//……这里还有很多变量,当B树不是3阶B树,就这么多枚举会出Bug
}
根据传入的location,进行移动。
bool isLocation = false;//路径是否改变。
my_B_tree* backupsRootNode = tempRootNode;//备份(当前)根节点地址
//1.判断当前父节点是否为空
if (!tempRootNode)
{
tempRootNode = createNode();
}
if (B_Tree_isSplit)//已经分裂
{
my_B_tree* trackNode = tempRootNode;
while (trackNode)
{
//2.选择合适的指针进行插入键值
Insert_Location locatiton = indexSelection(trackNode, key);//对键值进行合适的路径选择(需要修改)
selectLocation(trackNode, locatiton);
isLocation = true;
if (trackNode)
{
tempRootNode = trackNode;
}
}
}
tempRootNode->key[tempRootNode->degress++] = key;
_insert_shellMode_Sort(tempRootNode, key);//对节点内的关键字进行排序
if (tempRootNode->degress >= DEGRESS)
{
if (!B_Tree_isSplit)
{
firstSplitNode(tempRootNode);//首次分裂节点。
tempRootNode = rootNode;
return;
}
//分裂节点
splitNode(tempRootNode, backupsRootNode);
tempRootNode = backupsRootNode;//根节点新创建的节点
return;
}
if (isLocation)
tempRootNode=backupsRootNode;
现在对上面的代码进行解释:
我定义了一个bool类型的isLocation
变量,B树的根是不确定的,所以,从插入开始时,就要分为两种情况。在这里也就表现在bool B_Tree_isSplit
,因为在首次创建B树,它也就只有一个节点。仅当B树在分裂之后,这个B_Tree_isSplit=true
,这也就意味着,每次插入键,就要从根到叶,都要进行比较。
所以,就有了my_B_tree* trackNode = tempRootNode;
这个变量就是探路的,下面的while循环基本上就是这个操作。trackNode
进行寻路。若trackNode
可以走,则tempRootNode
就跟上trackNode
的步伐,仅当trackNode
为NULL时,也就意味着tempRootNode
到达了目的节点。然后就直接有序的插入。函数_insert_shellMode_Sort
就是对插入后的键值进行排序。
这还不算完,因为B树的键值≥3时,就要分裂了,也就是tempRootNode->degress >= DEGRESS
那就开始分裂吧。
解释一下这段代码。!B_Tree_isSplit
就是取反操作:true->false false->true。在构造函数里面,这个变量就是false,能进入到这里面,也就是节点满了。以下是firstSplitNode函数
最终,tempRootNode重新回到临时根节点。
//firstSplitNode函数
B_Tree_isSplit = true;
my_B_tree* leftNode = createNode();
my_B_tree* rightNode = createNode();
my_B_tree* root = createNode();
root->key[root->degress++] = firstSplit->key[DEGRESS >> 1];
splitNodeValue(leftNode, rightNode, firstSplit);
root->sonPt[0] = leftNode;
root->sonPt[1] = rightNode;
leftNode->parent = rightNode->parent = root;
FREE_MEMORY(firstSplit);//释放首次创建B树的节点
rootNode = root;
设置,B_Tree_isSplit
为true,然后创建3个节点,然后将待分裂节点从中键切开,分别向left right 赋键值,注意:节点的键数要++。以下是splitNodeValue函数
主要就是将键值平均分给left和right 。其次,就是将left和right与之关联,最终释放掉首次创建的B树节点
for (size_t _leftIndex = 0; _leftIndex < (DEGRESS >> 1); ++_leftIndex)
{
left->key[left->degress++] = spliting_Node->key[_leftIndex];
}
for (size_t _rightIndex = (DEGRESS >> 1)+1; _rightIndex < DEGRESS ; ++_rightIndex)
{
right->key[right->degress++] = spliting_Node->key[_rightIndex];
}
}
剩下的操作,就是这样,因为分裂的节点不仅在叶节点上,也存在内部节点分裂,这个时候一定要注意把待分裂的节点的孩子与
这个时候,就要分裂第2步根对把。如下图
上图中,红色的待分裂节点,最终分裂成了蓝色和紫色,他们两个分别有红色节点的一般孩子,切记,孩子们也要修改parent。然后25在与这个节点相连,最终完成。
有更好的想法,希望大家在评论区留言,我会认真与大家进行交流。以后还会继续更新数据结构算法的文章,感谢大家关注我哦!!!