B-树,,

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个元素,则此时释放原本的根节点,让此时的合并节点,作为新的根节点即可。

相关推荐
shy^-^cky2 小时前
文件的逻辑结构+ 物理结构
数据结构·操作系统·文件·数据·逻辑结构·物理结构·文件结构
睡觉就不困鸭2 小时前
第14天 四数之和
数据结构·算法
我不是懒洋洋2 小时前
手写一个线程安全的哈希表:从原理到实战
数据结构
云泽8082 小时前
二叉树高阶笔试算法题精讲(一):序列化、层序遍历、LCA 与 BST 转换
数据结构·c++·算法
嘻嘻哈哈樱桃2 小时前
牛客经典101题题解集--二叉树
java·数据结构·python·算法·leetcode·职场和发展
難釋懷3 小时前
Redis数据结构-Dict
数据结构·数据库·redis
望舒3293 小时前
KMP算法
数据结构·算法
cpp_25013 小时前
P2871 [USACO07DEC] Charm Bracelet S
数据结构·c++·算法·动态规划·题解·洛谷·背包dp
cpp_25014 小时前
P2722 [USACO3.1] 总分 Score Inflation
数据结构·c++·算法·动态规划·题解·洛谷·背包dp