B-树的由来
解决大规模数据在磁盘等外部存储上的高效索引与读写问题。用 "少层、多分支" 把磁盘 I/O 压到最低。
磁盘和内存的处理数据的速度差距太大了,最大差距可能会达到100万倍。所以,同等数据量如果用AVL树来存,或者用红黑树来存,他们的树的高度都比较高,这样会导致我们进行插入,删除,以及查找操作,从上往下遍历的时候,每经过一层,都会有一个节点从磁盘中读取到内存里面,会有一次内存和磁盘的交互,导致效率比较低。
如何在数据量不变的情况下,让其树的高度降低?
让一个节点存储若干个有效值即可(这就是B-树)

三大性质
平衡、有序、多路
平衡
B-树所有的叶子节点都在同一层
有序
单个节点内部是有序的,整体来看也是有序的
多路
M阶B-树,每一个节点最多可以伸出M个分支
根节点和非根节点
根节点
元素最少1个就行,最多M+1个(M是奇数/偶数)
非根节点
元素最少[M/2]-1(向上取整的意思)个就行,最多M-1个
初始化
cpp
void Init_BTree(BTree* pTree) {
pTree->root=NULL;
}
打印
cpp
Result* Search_BT(BNode* root, ElemType key) {
BNode* p = root;
BNode* pp = NULL;
int i ;
Result* res = (Result*)malloc(sizeof(Result));
if (res == NULL)
exit(EXIT_FAILURE);
while (p != NULL)
{
i = 1;
while( i <= p->key_num) {
if (p->key_arr[i] < key)
{
i++;
}
else if (i > p->key_arr[i])
{
pp = p;
p = p->ptr_arr[p->key_num];
}
else
{
res->tag = true;
res->pNode = p;
res->index = i;
return res;
}
}
pp = p;
p = p->ptr_arr[p->key_num];
}
if (NULL == root)
{
res->tag = false;
res->pNode=pp;
res->index=i;
return res;
}
}
搜索
思路:
一个节点内部有若干个有效元素,所以对其所有元素从左到右遍历,当前指向的值和我要找的值进行比较,只会有三种情况:
情况一:当前值如果小于要找的值,则跳过,继续判断下一个
情况二:如果当前值等于要找的值,则找到了,购买result赋值结束
情况三:如果当前值大于要找的值,则跳转到当前值的左子树里去判断
特殊情况:如果所有的有效值都小于要找的值,则跳转到最后一个有效元素的右子树上
cpp
Result* Search_BT(BNode* root, ElemType key) {
BNode* p = root;
BNode* pp = NULL;
int i ;
Result* res = (Result*)malloc(sizeof(Result));
if (res == NULL)
exit(EXIT_FAILURE);
while (p != NULL)
{
i = 1;
while( i <= p->key_num) {
if (p->key_arr[i] < key)
{
i++;
}
else if (i > p->key_arr[i])
{
pp = p;
p = p->ptr_arr[p->key_num];
}
else
{
res->tag = true;
res->pNode = p;
res->index = i;
return res;
}
}
pp = p;
p = p->ptr_arr[p->key_num];
}
if (NULL == root)
{
res->tag = false;
res->pNode=pp;
res->index=i;
return res;
}
}
插入
插入第一个值:1


插入第二个值:5

插入第三个值:7

插入第四个值:4

插入第五个值:16

插入16之后,元素个数变成了五个,5阶树最多容纳4个,超过了上限4个,则劈开/分裂,满了就分裂。
分裂操作具体实现步骤:
1.一旦确定了该节点要分裂,立马申请一个临时节点,放在待分裂节点的右侧
2.找到待分裂节点的中位数
3.将中位数右边的所有元素,统一挪动到RightNode里面
4.再将自身上移到父结点处
5.更新一下这三个结点的有效元素个数
注意事项:
1.中位数上移时,如果中位数不存在,则购买一个新的根节点newroot来存放这个中位数,并将其孩子指针指向待分裂节点和RightNode。
1.
2.
3.
4.
插入第六个值:35

插入第七个值:24

插入第八个值:42

上溢出了

插入第九个值:21

插入第十个值:17

插入第十一个值:18

插入18后上溢出,则寻找中位数,分裂,中位数上移




注意事项:
2.中位数上移时,如果父结点中有元素大于中位数,需要将父结点中大于中位数的元素向右挪动,注意别忘了将其连接的右孩子指针也向后挪动。
插入第十二个值:19

插入第十三个值:20

插入第十四个值:22

溢出,需要分裂,寻找中位数,把中位数的右侧全部拷贝到新节点,然后中位数上移

插入第十五个值:23

插入第十六个值:6

再插入第十七个值:8

插入第十八个值:9

上溢出,分裂,购买一个新节点放在他的右侧

中位数上移

父节点又上溢出了,造成了连续上溢出,需要再分裂一次,购买一个新节点放到他的右侧,中位数右边的元素挪动到新节点的时候,别忘了把该元素的指针带过去。

接下来该中位数17上移了,但是发现中位数上移没有节点来接收,则立马在申请一个新的根节点newroot用来接收这个没有人管的中位数。

注意事项:
3.待分裂节点如果不是叶子节点,则将其中位数右侧元素挪动到RightNode的时候,将其连接的孩子也挪动到RightNode中
元素全部插入成功,最终结果就是这样。

删除

删除第一个值:45

首先45存在,且不是叶子节点。是双分支,狸猫换太子。

再删除
46被后续元素整体向前挪动覆盖掉,此时该节点的元素个数3->2,但是2没有突破下限
删除第二个值:68

68存在,且存在于叶子节点,则直接删除

68删除完之后,该节点元素个数从2->1,且其不是根节点,则1个元素怒突破下限,则先去问其兄弟节点是否能借。
其左右兄弟都存在,则都够借,则自己任选一个兄弟节点执行借取操作。(这里我们选右兄弟)
借取操作口诀:父下来,兄上去
借取操作不会导致父节点缺少元素,换句话说,借取操作不会导致父节点连续的下溢出

删除第三个值:86


86存在,且存在于叶子节点上,则直接删除。删除完之后,该节点元素个数从2->1,则下溢出,先去问兄弟节点去借。
但是其左右兄弟都存在,但是元素都只有2个,都不够借,则直接执行合并操作。并且此时左右兄弟都存在且都不够借,则任选其一进行合并
合并操作口诀:父下左,右靠左

合并操作特别注意:父节点少一个元素,则需要将这个元素后面的元素及其所带的右孩子指针也向前挪动。

合并操作执行结束之后,发现父节点永久少了一个元素,所以要特别注意,父节点是否出现了连续的下溢出。这里的操作,父节点少一个,也才从4->3,没有出现下溢出,直接结束
删除第四个值:30
30存在,但是存在在非叶子节点,直接狸猫换太子。这里采用直接前驱代替。

删除完30之后,发现该节点元素个数从2->1,则下溢出了,先去借。发现其左兄弟不存在,则右兄弟一定存在,但右兄弟不够借,这里没得挑,只能和有兄弟合并。
和右兄弟合并口诀:父下左,右靠左

合并完之后,因为父节点下移了一个元素,所以需要将父节点中下移的元素的后续所有元素向前挪动一个格子。

合并操作需要小心一点,需要再观察父节点有没有连续下溢出。
父节点只剩下42了,出发了连续的下溢出。所以需要同样的处理方案,先借,借不了再和兄弟合并。其左兄弟不存在,则一定存在右兄弟,且发现右兄弟够借。
借操作:父下来,兄上去


现在执行了父下来,兄上去,但是,特别注意,兄弟节点如果是非叶子节点(说明其孩子指针是非NULL的),则肯行会有一个孩子指针没有人来接收,则需要借取的节点来接收。后续元素向前覆盖的时候,别忘了将其附带的孩子指针也向前挪动。

删除第五个值:57
57存在,存在于叶子节点上,直接删除,并且删除之后,没有下溢出,无需调整。

删除第六个值:47
47存在,是叶子节点,直接删

但是删除完之后,发现该节点下溢出了,则先去问兄弟借。
有左兄弟,但是不够借,并且右兄弟不存在,所以借取不了,只能执行合并操作

合并口诀:父下左,右靠左

此时,合并操作完成后,发现其父节点出现了连续的下溢出,则需要同样的处理方法,先去问兄弟借,借不了再合并。
但是发现此时下溢出节点(父节点)它左兄弟不存在,且其右兄弟存在但是不够借,只能和右兄弟合并。

合并口诀:父下左,右靠左

此时,右靠左相当于让其左边的节点将其右兄弟全部吸收掉,但是右兄弟自身不仅有若干个元素,还有若干个指针域,所以右靠左的时候不仅说的是元素靠左,还要孩子指针向左。如果右兄弟是叶子节点,则其孩子指针域都是NULL,可以省略。

此时父节点进行了合并操作,那么父节点的父节点有没有下溢出呢?父节点的父节点从1->0了,则其肯定是根节点,也下溢出了。
则根节点下溢出比较特殊,单独处理:
释放当前没有元素的原根节点,并将此时合并的那个节点作为新根存在。


总结:
1.先去查看key值是否存在,如果不存在,不用删直接结束
2.存在的情况下,进一步去判断该元素所在节点是叶子节点还是非叶子节点
3.如果是非叶子节点,先狸猫换太子(用直接前驱或者直接后继元素顶替)
4.如果是叶节点的话(狸猫换太子,顶替的狸猫也是叶节点),则可以直接删除这个值
5.删除完之后,更新节点元素个数,然后判断是否是下溢出
6.如果没有下溢出,则无需调整
7.如果有下溢出现象,则需要借取调整
B-树的删除调整总结
1.去观察其亲兄弟节点(左右兄弟)是否能给他借取元素
2.如果兄弟节点能借,则直接执行借取操作
3.借取操作完之后,可以直接结束(因为借取操作不会导致父节点缺少元素,所以借取操作不存在所谓的父节点连续下溢出)
4.但是如果兄弟节点要么不存在,要么存在不够借,也就是没有条件执行借取操作,则只能执行合并操作
5.执行合并操作之后,要小心其父节点可能会出现连续的下溢出现象,如果父节点没有连续下溢出,则可以调整结束
6.如果父节点真的出现连续下溢出,则还是先问兄弟借,借不了再合并
注意事项:
1.借取操作时,如果兄弟节点是非叶子节点,则其孩子指针与都是非空,则兄弟节点替你还给父节点一个元素,那么兄弟节点的这个元素的后续元素整体向前挪动的时候,要将其附带的孩子的指针域也要向前挪(并且兄弟节点肯定会有一个孩子没有人管,你得替你兄弟接收一下)
2.合并操作中,右靠左时,右边的节点如果是非叶子,则右靠左的时候,左边的节点不仅要吸收右边节点的所有元素,还要将其右边节点的所有孩子指针域也吸收掉。
3.合并操作会导致父节点少一个元素,那么如果父节点本身是仅有一个元素的根节点,则此时合并操作会导致这个原本根节点缺少一个元素,变成0个元素,则此时释放原本的根节点,让此时的合并节点,作为新的根节点即可。