前面学习了平衡二叉树( A V L AVL AVL 树),我们知道了平衡的概念就是让树尽量的 “胖” ,让它的高度不会线性增长,那么这篇笔记主要是分享 B B B 树和 B + B+ B+ 树的原理、应用(不涉及代码实现,也许有一天会补上,谁知道呢)
B B B 树(又称 B − B- B− 树)和 B + B+ B+ 树其实差别不是很大,所以我会着重介绍 B B B 树,然后最后指出 B + B+ B+ 树的不同点
那么什么是 B B B 树呢?
B B B 树,又称 多路平衡查找树 , B B B 树中所有结点的孩子个数的最大值称为 B B B 树的 阶 ,通常用 m m m 表示。
这里的多路其实就是指这一颗树可能不只是最多有两颗子树,具体多少颗是由 B B B 树的阶决定的,作为一颗 B B B 树应该满足一下要求:
上面提到的关键字就是对于每一个结点可能会存在多个值的情况,这些值按照升(降)序排列,用于划分子树区间,之前的二叉平衡树我们对于每一个结点只有一个关键字,我们就有两个分支,显然若是有 m m m 个关键字则会有 m + 1 m+1 m+1 个分支,其实这就是一个简单的区间划分(相邻关键字划分一个区间、前后俩结点单独一个分区)
PS:这里需要注意一下,在王道的书上是将叶结点(P285页)定义成了不存在的一层,但是在严蔚敏的书上定义的就是最后一层有关键字的结点,比如下图中,王道将第四层定义为叶子节点,严蔚敏的书上认为叶子结点(终端节点)是第三层,而笔者也认为第三层才是叶子节点或者称为终端节点

假设一颗二叉树包含 n ( n > = 1 ) n(n>=1) n(n>=1) 个关键字、高度为 h h h ,阶数为 m m m 的 B B B 树
∵ n < = ( m − 1 ) ( 1 + m + m 2 + … … + m h − 1 ) n < = m h − 1 ∴ h > = l o g m ( n + 1 ) ∵ n <=(m-1)(1+m+m^2+……+m^{h-1}) \\ n <= m^h - 1 \\ ∴ h>=log_m(n+1) ∵n<=(m−1)(1+m+m2+……+mh−1)n<=mh−1∴h>=logm(n+1)
既然是至多,那么我们尽可能希望每一个结点的关键字都是最少的即 ⌈ m 2 ⌉ \left \lceil \frac{m}{2} \right \rceil ⌈2m⌉ ,前面也提到了根节点可以特殊一下 最少一个关键字
那么我们会发现,第一层至少有 1 1 1 个结点,第二层至少有 2 2 2 个结点,第三层至少有 2 ⌈ m 2 ⌉ 2\left \lceil \frac{m}{2} \right \rceil 2⌈2m⌉ 个结点……第 h + 1 h+1 h+1 层至少有 2 ⌈ m 2 ⌉ h − 2 2\left \lceil \frac{m}{2} \right \rceil ^{h-2} 2⌈2m⌉h−2 这一层就是不包含关键字的一层,即查找失败的一层,又因为关键字的个数为 n n n 那么说明第 h + 1 h+1 h+1 层最少 n + 1 n+1 n+1 个结点,于是我们得到:
∵ n + 1 > = 2 ( ⌈ m 2 ⌉ ) h − 1 ∴ h < = l o g ⌈ m 2 ⌉ n + 1 2 + 1 ∵n+1>=2(\left \lceil \frac{m}{2} \right \rceil)^{h-1} \\ \\ ∴ h <= log_{\left \lceil \frac{m}{2} \right \rceil}\frac{n+1}{2} + 1 ∵n+1>=2(⌈2m⌉)h−1∴h<=log⌈2m⌉2n+1+1
假设一颗 3 3 3 阶 B B B 树一共有 8 8 8 个关键字,那么其树的高度范围为: 2 < = h < = 3.17 2<=h<=3.17 2<=h<=3.17
前面也说了 B B B 树的每一个结点在内部都是有序排列的,并且结点与结点之间也是有一定关系的,在之前的平衡二叉树中,我们有两条路的分支,但是在 B B B 树的查找中我们需要根据结点的多路分支做决定,那么其实查找操作和前面的平衡二叉树|二叉查找树是一样的啦
先通过上面的定位操作定位到一个查找失败的结点,然后检查该节点的父结点的关键字个数,若是关键字个数小于 m − 1 m-1 m−1 那么说明可以直接插入到该节点(叶子节点),否则的话插入后会引起结点的分裂,因为要维护平衡的关系
分裂的方法:
例如下面的分裂操作:

我们可以来观察这个过程:

实际上就是将 ⌈ m 2 ⌉ \left \lceil \frac{m}{2} \right \rceil ⌈2m⌉ 位置的关键字直接变为其左边和右边关键字的父节点,然后因为要往上贡献一个关键字,这可能会导致父节点层分裂,然后继续向上贡献一个关键字,以此类推,直到停止贡献,或者到达根节点,分裂出新的根,比如在上面的插入 52 52 52 后我们再插入 61 、 62 61、62 61、62 就会让这棵树的高度拔高一层,流程如下:

删除操作要更加复杂一点,大体分为两种情况:
那么我们直接将该结点的前驱(后驱)关键字拿来顶替就行,例如下图删除
80
80
80 的情况
主要分为三种情况:

我们可以注意到在合并的过程中双亲结点的关键字会减一,那么就会出现下列的情况:
B + B+ B+ 树是从 B B B 树改进过来的,他和 B B B 树的主要区别在于:
- B + B+ B+ 树中保存元素的结点都在叶子结点上, 非叶子结点只起到索引 的作用
- B + B+ B+ 树的所有叶子结点都通过指针链接起来了
- B + B+ B+ 树每一个关键字对应一棵子树,假设一个结点最多有 n n n 个关键字,那么其结点只有 n n n 颗子树
- 一个 m m m 阶 B + B+ B+ 树除跟节点外,其余结点的关键字个数 n n n 的范围为 ⌈ m 2 ⌉ < = n < = m \left \lceil \frac{m}{2} \right \rceil<=n<=m ⌈2m⌉<=n<=m ,根节点的关键字区间为: 1 < = n < = m 1<=n<=m 1<=n<=m
一个 m m m 阶的 B + B+ B+ 树满足如下条件:
比如下面就是一颗
B
+
B+
B+ 树:

为什么B+树的叶子节点要用指针连接起来?
这个是和业务场景有关的。 B + B+ B+ 树常用来保存数据库索引,数据库select数据,不一定只选一条,很多时候会选多条。如果选多条的话, B B B 树需要进行局部的中序遍历,才能获取到所有符合条件的元素,而且很可能要跨层访问。而 B + B+ B+ 树因为所有元素都在叶子节点上,且不同子树的叶子节点已经用指针连接起来了,当需要选择多条数据时, B + B+ B+ 树就会有很大的优势。
B+树主要用于 磁盘IO 的读取,数据库的索引
众所周知磁盘 I O IO IO 的读取相比 C P U CPU CPU 的处理是非常慢的,所以我们需要尽可能一次性多读取一些数据到内存,然后进行处理,那么我们就希望尽可能减少文件树的高度,但是并不是越低越好,因为假如这颗 B + B+ B+ 树的关键字就等于所有的元素树,那么就变成了一个线性表了,反而会拖慢检索时间,并且我们磁盘每次的读写是由一定限制的,一般磁盘块和页内存都是 4 k b 4kb 4kb
磁盘有预读机制,每次读的时候都是加载一个磁盘页到内存里面,所以我们的每一个结点关键字的总大小应该是尽可能的贴近磁盘页的大小,这样以来可以达到最好的读写效率也充分的利用了磁盘cache的预读机制
参考文章: