B-树和B+树
一、B-树
1、认识B-树
B-树,有时又写为B_树(其中的“-”或者“_”只是连字符,并不读作“B减树”)
一颗 m 阶的 B-树,或者本身是空树,否则必须满足以下特性:
- 树中每个结点至多有 m 棵子树;(即至多含有m-1个关键字)
- 若根结点不是叶子结点,则至少有两棵子树;(至少一个关键字)
- 除根之外的所有非终端结点至少有
⌈m/2⌉
棵子树;(即至少含有⌈m/2⌉-1个关键字) - 所有的非终端结点(非叶子结点)中包含下列信息数据:(n,A0,K1,A1,K2,A2,…,Kn,An);
n 表示结点中包含的关键字的个数,取值范围是:⌈m/2⌉-1≤ n ≤m-1
。Ki (i 从 1 到 n)为关键字,且 Ki
< Ki+1
;Ai
代表指向子树根结点的指针,且指针 Ai-1
所指的子树中所有结点的关键字都小于 Ki
,An
所指子树中所有的结点的关键字都大于 Kn
。
如上图所示,当前结点中有 4 个关键字,之间的关系为:K1<K2<k3<K4
。同时对于 A0
指针指向的子树中的所有关键字来说,其值都要比 K1
小;而 A1
指向的子树中的所有的关键字的值,都比 K1
大,但是都要比 K2
小。
所有的叶子结点都出现在同一层次,实际上这些结点都不存在,指向这些结点的指针都为 NULL
2、B-树查找操作
对比二叉排序树是二路查找,而B-树是多路查找,因为B-树结点内的关键字是有序的,在结点内进行查找时除了顺序查找外,还可以用折半查找来提升效率。
如下图所示就是一棵 4 阶的 B-树,这棵树的深度为 4 :
在使用 B-树进行查找操作时,例如在如上图所示的 B-树中查找关键字 47 的过程为:
- 从整棵树的根结点开始,由于根结点只有一个关键字 35,且 35 < 47 ,所以如果 47 存在于这棵树中,肯定位于
A1
指针指向的右子树中; - 然后顺着指针找到存有关键字 43 和 78 的结点,由于 43 < 47 < 78,所以如果 47 存在,肯定位于
A1
所指的子树中; - 然后找到存有 47、53 和 64 三个关键字的结点,最终找到 47 ,查找操作结束;
若查找到深度为 3 的结点还没结束,则会进入叶子结点,但是由于叶子结点本身不存储任何信息,全部为 NULL,所以查找失败。
3、B-树插入操作(构建B-树)
B-树也是从空树开始,通过不断地插入新的数据元素构建的。但是 B-树构建的过程不同于二叉排序树和平衡树二叉树。B-树在插入新的数据元素时并不是每次都向树中插入新的结点。
因为对于 m 阶的 B-树来说,在定义中规定所有的非终端结点(终端结点即叶子结点,其关键字个数为 0)中包含关键字的个数的范围是[⌈m/2⌉-1,m-1]
,所以在插入新的数据元素时,首先向最底层的某个非终端结点中添加,如果该结点中的关键字个数没有超过 m-1,则直接插入成功,否则还需要继续对该结点进行处理。
(1)模拟插入
假设现在在下图的3阶B-树(深度为4,省略了叶子结点)基础上插入 4 个关键字 30、26、85 和 7:
插入关键字 30 :从根结点开始,由于 30 < 45,所以要插入到以 b 结点为根结点的子树中,再由于 24 < 30,插入到以 d 结点为根结点的子树中,由于 d 结点中的关键字个数小于 m-1=2,所以可以将关键字 30 直接插入到 d 结点中。结果如下图所示:
插入关键字 26:从根结点开始,经过逐个比较,最终判定 26 还是插入到 d 结点中,但是由于 d 结点中关键字的个数超过了 2,所以需要做如下操作:
- 关键字 37 及其左右两个指针存储到新的结点中,假设为 d’ 结点;
- 关键字 30 存储到其双亲结点 b 中,同时设置关键字 30 右侧的指针指向 d’;
经过以上操作后,插入 26 后的B-树为:
插入关键字 85:从根结点开始,经过逐个比较,最终判定插入到 g 结点中,同样需要对 g 做分裂操作:
- 关键字 85 及其左右两个指针存储到新的结点中,假设为 g’ 结点;
- 关键字 70 存储到其双亲结点 e 中,同时设置 70 的右侧指针指向 g’ ;
经过以上操作后,插入 85 后的结果图为:
上图中,由于关键字 70 调整到其双亲结点中,使得其 e 结点中的关键字个数超过了 2,所以还需进一步调整:
- 将 90 及其左右指针存储到一个新的结点中,假设为 e’ 结点;
- 关键字 70 存储到其双亲结点 a 中,同时其右侧指针指向 e’ ;
最终插入关键字 85 后的 B-树为:
插入关键字 7:从根结点开始依次做判断,最终判定在 c 结点中添加,添加后发现 c 结点需要分裂,分裂规则同上面的方式一样,结果导致关键字 7 存储到其双亲结点 b 中;后 b 结点分裂,关键字 24 存储到结点 a 中;结点 a 同样需要做分裂操作,最终 B-树为:
实现完模拟操作后,我们可以总结出以下结论:在构建 B-树的过程中,假设 p 结点中已经有 m-1 个关键字,当再插入一个关键字之后,此结点分裂为两个结点,如下图所示:
提示:结点分裂为两个结点的同时,还分裂出来一个关键字 K⌈m/2⌉,存储到其双亲结点中。
(2)一道例题
4、B-树中删除操作
在 B-树种删除关键字时,首先前提是找到该关键字所在结点,在做删除操作的时候分为两种情况,
- 第一种情况是删除结点为 B-树的非终端结点(不处在最后一层);
- 第二种情况是删除结点为 B-树最后一层的非终端结点。
(1)第一种情况
以上图(3阶B-树(深度为4,省略了叶子结点))为例,关键字 24、45、53、90属于不处在最后一层的非终端结点,关键字 3、12、37等同属于最后一层的非终端结点。
如果该结点为非终端结点且不处在最后一层,假设用 Ki
表示,则只需要找到指针 Ai
所指子树中最小的一个关键字代替 Ki
,同时将该最小的关键字删除即可。
还如上图中,如果要删除关键字 45 ,只需要使用关键字 50 代替 45 ,同时删除 f 结点中的 50 即可。
(2)第二种情况
如果该结点为最后一层的非终端结点,有下列 3 种可能:
- 被删关键字所在结点中的关键字数目不小于
⌈m/2⌉
,则只需从该结点删除该关键字Ki
以及相应的指针Ai
。
例如,在上图(3阶B-树(深度为4,省略了叶子结点))中,删除关键字 12 ,只需要删除该关键字 12以及右侧指向 NULL 指针即可。
- 被删关键字所在结点中的关键字数目等于
⌈m/2⌉-1
,而与该结点相邻的右(左)兄弟结点中的关键字数目大于⌈m/2⌉-1
,只需将该兄弟结点中的最小(或者最大)的关键字上移到双亲结点中,然后将双亲结点中小于(或者大于)且紧靠该上移关键字的关键字移动到被删关键字所在的结点中。
如在上图中删除关键字 50,其右兄弟结点 g 中的关键字大于2,所以需要将结点 g 中最小的关键字 61 上移到其双亲结点 e 中(由此 e 中结点有:53,61,90),然后将小于 61 且紧靠 61 的关键字 53 下移到结点 f 中,最终删除后的 B-树如下所示。
- 被删除关键字所在的结点如果和其相邻的兄弟结点中的关键字数目都正好等于
⌈m/2⌉-1
,假设其有右兄弟结点,且其右兄弟结点是由双亲结点中的指针Ai
所指,则需要在删除该关键字的同时,将剩余的关键字和指针连同双亲结点中的Ki
一起合并到右兄弟结点中。
例如,在上中 B-树中删除关键字 53,由于其有右兄弟,且右兄弟结点中只有 1 个关键字。在删除关键字 53 后,结点 f 中只剩指向叶子结点的空指针,连同双亲结点中的 61(因为 61 右侧指针指向的兄弟结点 g)一同合并到结点 g 中,最终删除 53 后的 B-树如下所示:
在合并的同时,由于从双亲结点中删除一个关键字,若导致双亲结点中关键字数目小于
⌈m/2⌉-1
,则继续按照该规律进行合并。例如上中 B-树的情况下删除关键字 12 时,结点 c 中只有一个关键字,然后做删除关键字 37 的操作。此时在删除关键字 37 的同时,结点 d 中的剩余信息(空指针)同双亲结点中的关键字 24 一同合并到结点 c 中,效果图如下:
由于结点 b 中一个关键字也没有,所以破坏了B-树的结构,继续整合。在删除结点 b 的同时,由于 b 中仅剩指向结点 c 的指针,所以连同其双亲结点中的 45 一同合并到其兄弟结点 e 中,最终的B-树如下:
(3)一道例题
由于 B-树具有分支多层数少的特点,使得它更多的是应用在数据库系统中。
二、B+树(学习了解)
该部分只做兴趣了解。
学习推荐 | 作者 |
---|---|
B+树介绍 | 博客园-wade&luffy |
B+树的几点总结 | 简书-VernonVan |
B+树的插入、删除图文详解 | 博客园-nullzx |
1、认识B+树
一颗 m 阶的 B+树和 m 阶的 B-树的差异在于:
- 有 n 棵子树的结点中含有 n 个关键字;
在上面有说,在 B-树中的每个结点关键字个数 n 的取值范围为
⌈m/2⌉ -1≤n≤m-1
,而在 B+树中每个结点中关键字个数 n 的取值范围为:⌈m/2⌉≤n≤m
。
- 所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
- 所有的非终端结点(非叶子结点)可以看成是索引部分,结点中仅含有其子树(根结点)中的最大(或最小)关键字。
例如,下图所示的就是一棵深度为 4 的 3 阶 B+树:
如上图所示,B+树中含有两个头指针,一个指向整棵树的根结点,另一个指向关键字最小的叶子结点。同时所有的叶子结点依据其关键字的大小自小而大顺序链接,所有的叶子结点构成了一个 sqt
指针为头指针的链表。
2、B+树查找
所以,B+树可以进行两种查找运算:
-
一种是利用
sqt
链表做顺序查找 -
另一种是从树的根结点开始,进行类似于二分查找的查找方式。
在 B+树中,所有非终端结点都相当于是终端结点的索引,而所有的关键字都存放在终端结点中,所有在从根结点出发做查找操作时,如果非终端结点上的关键字恰好等于给定值,此时并不算查找完成,而是要继续向下直到叶子结点。
B+树的查找操作,无论查找成功与否,每次查找操作都是走了一条从根结点到叶子结点的路径。
3、B+树中插入关键字
在B+树中插入关键字时,需要注意以下几点:
- 插入的操作全部都在叶子结点上进行,且不能破坏关键字自小而大的顺序;
- 由于 B+树中各结点中存储的关键字的个数有明确的范围,做插入操作可能会出现结点中关键字个数超过阶数的情况,此时需要将该结点进行“分裂”;
B+树中做插入关键字的操作,有以下 3 种情况:
一棵初始深度为 4 的 3 阶 B+树:
(1)第一种情况
若被插入关键字所在的结点,其含有关键字数目小于阶数 M,则直接插入结束;
例如,在上面的初始B+树中插入关键字13,其结果如下图所示:
(2)第二种情况
若被插入关键字所在的结点,其含有关键字数目等于阶数 M,则需要将该结点分裂为两个结点,一个结点包含⌊M/2⌋
,另一个结点包含⌈M/2⌉
。同时,将⌈M/2⌉
的关键字上移至其双亲结点。假设其双亲结点中包含的关键字个数小于 M,则插入操作完成。
例如,在上面的初始B+树中插入关键字 95,其插入后的 B+树如下图所示:
(3)第三种情况
在第 2 情况中,如果上移操作导致其双亲结点中关键字个数大于 M,则应继续分裂其双亲结点。
例如,在上面的初始B+树插入关键字 40,则插入后的 B+树如下所示:
注意:如果插入的关键字比当前结点中的最大值还大,破坏了B+树中从根结点到当前结点的所有索引值,此时需要及时修正后,再做其他操作。例如,上面初始的B+树中插入关键字 100,由于其值比 97 还大,插入之后,从根结点到该结点经过的所有结点中的所有值都要由 97 改为 100。改完之后再做分裂操作。
4、B+树中删除关键字
在 B+树中删除关键字时,有以下几种情况:
一棵初始深度为 4 的 3 阶 B+树:
(1)第一种情况
找到存储有该关键字所在的结点时,由于该结点中关键字个数大于⌈M/2⌉
,做删除操作不会破坏 B+树,则可以直接删除。
例如,在上面初始的B+树中删除关键字 91,删除后的 B+树如下所示:
(2)第二种情况
当删除某结点中最大或者最小的关键字,就会涉及到更改其双亲结点一直到根结点中所有索引值的更改。
例如,在上面初始的B+树中删除关键字 97,删除后的 B+树如下所示:
(3)第三种情况
当删除该关键字,导致当前结点中关键字个数小于⌈M/2⌉
,若其兄弟结点中含有多余的关键字,可以从兄弟结点中借关键字完成删除操作。
例如,在上面初始的B+树中删除关键字 51,由于其兄弟结点中含有 3 个关键字,所以可以选择借一个关键字,同时修改双亲结点中的索引值,删除之后的 B+树如下所示:
(4)第四种情况
第 3 种情况中,如果其兄弟结点没有多余的关键字,则需要同其兄弟结点进行合并。
例如,在第三种情况中的 B+树中删除关键字 59,删除后的 B+树如下所示:
(5)第五种情况
当进行合并时,可能会产生因合并使其双亲结点破坏 B+树的结构,需要依照以上规律处理其双亲结点。
例如,在第二种情况中的 B+树中删除关键字 63,当删除后该结点中只剩关键字 72,且其兄弟结点中只有 2 个关键字,无法实现借的操作,只能进行合并。但是合并后,合并后的效果图如下所示:
如上图所示,其双亲结点中只有一个关键字,而其兄弟结点中有 3 个关键字,所以可以通过借的操作,来满足 B+树的性质,最终的 B+树如下所示:
总之,在 B+树中做删除关键字的操作,采取如下的步骤:
-
删除该关键字,如果不破坏 B+树本身的性质,直接完成操作;
-
如果删除操作导致其该结点中最大(或最小)值改变,则应相应改动其父结点中的索引值;
-
在删除关键字后,如果导致其结点中关键字个数不足,有两种方法:一种是向兄弟结点去借,另外一种是同兄弟结点合并。(注意这两种方式有时需要更改其父结点中的索引值。)
由于B+树更多的是用于文件索引系统,只需要了解实现过程即可。