B-树、B+树与B*树
1.B-树
B-树是一颗 m 阶的平衡树(Balance),其通常应用于文件索引系统的实现。一颗 m 阶的 B- 树需要满足如下条件:
- 树中每个节点最多含有 m 个孩子
- 除根节点和叶子节点之外,其它每个节点至少有 个孩子
- 除根节点之外的节点的关键字的个数 n 必须满足:,叶子节点也必须满足此条关于关键字数的性质
其中, 表示向上取整
1.1 插入操作
对于 B- 树的插入操作而言:
- 如果叶子节点空间足够,即该节点的关键字数小于 m-1,则直接插入在叶子结点的左边或右边;
- 如果空间满了以致于没有足够的空间去添加新的元素,即该节点的关键字数已经有 m 个了,则需要将该节点进行“分裂”,即将一半数量的关键字元素分裂到新的其相邻的右节点中,中间关键字元素上移到父节点中,而且当节点中关键元素向右移动了,相关的指针也需要向右移动;
- 此外,如果在上述中间关键字上移到父节点的过程中,导致根节点空间满了,那么根节点也要进行分裂操作,这样原来的根节点中的中间关键字元素向上移动到新的根节点中,因此导致树的高度增加一层;
比如,对于一颗 5 阶的 B- 树,根据 B- 树的性质可知,非根节点关键字的个数 n 需满足 ,每个节点最多含有 5 个孩子,除了根节点叶子节点之外,其它节点至少有 3 个孩子。
如下图所示,对于一颗空的 5 阶 B- 树,依次插入字符:C、N、G、A,所得到的结构如下:

然后继续插入字符 H,由于此时当前 B- 树的根节点的关键字个数已经达到最大,那么此时需要进行节点分裂,由于 A、C、G、H、N 中字符 G 是中间关键字元素,因此将其上移到父结点中,所形成的结构如下所示:

现在继续插入字符 E、K、Q,所得到的 B- 树如下:

继续插入字符 M,按照搜索树的性质,其会进入右子树中,但是此时,根节点的右孩子上的关键字元素已经是 4 个了,无法再继续插入,所以此时需要进行节点分裂,由于 H、K、M、N、Q 中关键字元素 M 为中间元素,因此将其上移到父节点中,所得到的结构如下:

继续插入字符 F、W、L、T,所得到的结构如下:

继续插入字符 Z,按照搜索树的性质,该字符会插入到 N、Q、T、W 所在的节点中,但是该节点已经拥有 4 个元素,所以此时需要进行节点分裂,由于 N、Q、T、W、Z 中元素 T 为中间元素,因此将其上移到父节点中,所得到的结构如下:

继续插入字符 D、P、R、X、Y,此时,A、C、E、F 节点需要进行分裂,将元素 D 上移到根节点中,所形成的结构如下:

最后,插入字符 S,那么需要分裂节点 N、P、Q、R,将字符 Q 上移到根节点中,但是由于此时根节点已经有 4 个元素了,那么需要对根节点进行分裂,将字符 M 上移形成新的根节点,B- 树的层数增加一层:

1.2 删除操作
对于 B- 树的删除操作而言,首先需要查找 B- 树中要删除的元素,如果元素存在,则进行删除,删除该元素后,需要判断该元素是否有左右孩子节点:
- 如果有,则上移孩子节点中的相邻元素(左孩子中最右边的节点或者右孩子中最左边的节点)到父节点中去;
- 如果没有,则直接进行删除;
删除元素并进行元素的移动之后,如果节点关键字的数目不满足条件,即其小于 ,那么此时就需要查看其相邻的兄弟节点是否丰满,即兄弟节点的关键字数目是否大于 :
- 如果丰满,则向父节点借一个元素来满足;
- 如果其相邻兄弟节点都不丰满,则该节点与其相邻的某一兄弟进行进行 “合并” 成为一个节点,以此来满足条件;
例如,对于插入操作最终所形成的 B- 树,删除其中的 H 节点,由于所删除的 H 节点没有孩子节点,因此直接删除即可,且删除后也满足 B- 树的特点,删除后的结构如下:

然后删除字符 T,由于字符 T 存在孩子元素,因此上移孩子节点中的相邻元素,如果上移 S 元素,那么 R、S 节点就会剩下一个元素,不满足 B- 树的性质,因此,上移 W 元素,所形成的结构如下:

然后删除元素 R,由于其不存在孩子节点,所以直接删除即可。但是删除后,R、S 节点就只剩下一个元素了,不满足 B- 树的性质,此时查看其相邻的兄弟节点是否丰满,由于左侧的 N、P 节点并不丰满,而右侧的 X、Y、Z 节点丰满,因此向父节点借来元素 W,然后让元素 X 补入到父节点中,所形成的结构如下:

最后,删除元素 E,由于其不存在孩子节点,所以直接删除即可,删除后节点 E、F 不满足 B- 树的性质,因此需要向临近的兄弟节点借元素,但是左右两侧的兄弟节点都不丰满,因此其需要和相邻的兄弟节点合并成为一个节点,合并后的结构如下:

但是可以看到,此时的节点 G 也不满足 B- 树的性质,且其兄弟节点也不丰满,因此,需要和相邻的兄弟节点合并成为一个节点,合并后的结构如下:

1.3 B-树的优势
B- 树通常应用于文件索引系统,例如数据库索引的使用。由于磁盘上的数据量非常大,无法一次性加载到内存中,因此需要事先给数据创建索引,给数据进行排序,加快数据搜索的速度,为此需要解决如下两个问题:
- 更少的磁盘 I/O;
- 更快的搜索算法;
比如,现在有 1000 0000 条索引需要从磁盘中读取并且进行搜索。如果采用 AVL 树,那么所形成的树的高度为 ,也就是说,使用 AVL 树最多需要进行 24 次磁盘 I/O;如果采用 300 阶的 B- 树,那么所形成的树的高度为 ,也就是说,使用 B- 树最多只需要 3 次磁盘 I/O 即可。
B- 树的高度就表示着需要花费的磁盘 I/O 次数,在节点内部查找目标元素时,也是使用二分搜索算法来进行查找的。
2.B+树
对于 B+ 树而言,它是 B- 树的一种变体形式,对于一颗 m 阶的 B- 树和 B+ 树,其区别如下:
- B- 树的每一个节点,存储了关键字和对应的数据地址,而 B+ 树的非叶子节点只存放关键字,不存放数据地址。因此 B+ 树的每一个非叶子节点存储的关键字是远多于 B- 树的,B+ 树的叶子节点存放关键字和数据,因此,从树的高度上来说,B+ 树的高度要小于 B- 树,使用的磁盘 I/O 次数较少,因此查询更快一些。
- B- 树由于每个节点都存储关键字和数据,因此距离根节点较近的数据,查询的就快,距离根节点较远的数据查的就慢;而 B+ 树所有的数据都在叶子节点上,因此在 B+ 树上搜索关键字,找寻对应数据的时间是比较平均的,没有快慢之分。
- 在 B- 树上如果做区间查找,遍历的节点是非常多的;而 B+ 树所有叶子节点都被连接成了有序的链表结构,因此做整表遍历和区间查找是非常容易的。
如下图所示,为一颗 B- 树的结构:

如下图所示,为一颗 B+ 树的结构:

B+ 树相比于 B- 树更适合操作系统的文件索引和数据库索引的原因如下:B+ 树的磁盘读写代价更低,B+ 树的内部节点没有指向关键字具体信息的指针,因此内部节点相比 B- 树更小。如果把所有同一内部节点的关键字放在同一块磁盘中,盘块所能容纳的关键字数量也就越多,一次性读入内存中的需要查找的关键字也就越多,相对 IO 读写次数也就降低了。其次,B+ 树的查询效率更加稳定,由于非终节点并不是最终指向文件内容的节点,而只是叶子节点中关键字的索引,所以任何关键字的查找都必须走一条从根节点到叶子节点的路。所有关键字的查询路径长度相同,导致每一个数据的查询效率都相同。
比如说,假设磁盘中的一个盘块容纳 16Bytes,而一个关键字需要 2Bytes,一个关键字具体信息的指针需要 2Bytes。那么一颗 9 阶的 B- 树的内部节点需要 2 个盘块,而 B+ 树的内部节点只需要 1 个盘块即可。当需要把内部节点读入内存中的时候,B- 树就比 B+ 树多一次盘块的查找时间(即磁盘中的盘片旋转时间)。
3.B*树
B* 树是 B+ 树的变体,在 B+ 树的非根和非叶子结点再增加指向兄弟节点的指针,其结构如下图所示:

B* 树定义了非叶子节点关键字的个数至少为 ,即块的最低使用率为 ,取代了 B+ 树的 ,其原因在于节点分裂的过程:
- B+树的分裂:当一个节点的关键字数量达到上限时,需要分配一个新的节点,并将原节点中的 数据复制到新的节点中,最后在父节点中增加新节点的指针,可见,B+ 树的分裂只会影响原节点和父节点,而不会影响兄弟节点,所以它不需要指向兄弟的指针;
- B*树的分裂:当一个节点的关键字数量达到上限时,如果它的下一个兄弟节点未满,那么将一部分数据转移到兄弟节点当中,再在原节点中插入关键字,最后修改父节点中兄弟节点的关键字,因为兄弟节点的关键字范围改变了;如果兄弟也满了,则在原节点和兄弟节点之间增加新的节点,并各复制 的数据到新的节点中,最后在父节点中增加新节点的指针;
所以,B* 树分配新节点的概率要低于 B+ 树,空间的使用率更高,其在 B+ 树的基础上,为非叶子节点也增加了链表指针,将节点的最低利用率从 提高到了 。
本文作者:Leaos
本文链接:https://www.cnblogs.com/tuilk/p/17062194.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器