B 树与 B+ 树
含AI生成内容
B 树与 B+ 树
概述
B 树和 B+ 树是多路平衡查找树(Multi-way Balanced Search Tree),专为磁盘 I/O 设计的数据结构。折半查找和 BST/AVL 都是内存中的查找结构,而数据库索引动辄存储百万条记录,不可能全放内存。B 树/B+ 树的核心优势是:每个结点存储多个关键字,一次磁盘 I/O 读一个结点,大幅减少磁盘访问次数。
例如一棵 1001 阶 B 树,存储 10 亿个关键字只需要 3 层,最多 3 次磁盘访问即可完成查找。
相关概念页面:二叉排序树与 AVL 树、红黑树、查找算法。
m 阶 B 树的定义
B 树是 m 阶多路平衡查找树,其结点结构如下:
┌──┬────┬──┬────┬──┬─ ... ─┬──────┬──┐│P₀│ K₁ │P₁│ K₂ │P₂│ │Kₙ │Pₙ│└──┴────┴──┴────┴──┴─ ... ─┴──────┴──┘其中 Kᵢ 为关键字(K₁ < K₂ < … < Kₙ),Pᵢ 为指向子树的指针,子树 Pᵢ 中所有关键字的值在 Kᵢ 和 Kᵢ₊₁ 之间。
B 树的性质
一棵 m 阶 B 树满足以下性质:
| 性质 | 描述 |
|---|---|
| 每个结点最多 m 棵子树 | 即最多 m-1 个关键字 |
| 根结点子树数 | 至少 2 棵(若非叶结点);关键字至少 1 个 |
| 非根非叶结点子树数 | 至少 ⌈m/2⌉ 棵,即关键字至少 ⌈m/2⌉ - 1 个 |
| 所有叶结点在同一层 | 叶结点是查找失败到达的空指针,不含任何信息 |
| 结点内关键字有序 | K₁ < K₂ < … < Kₙ |
关键字个数范围(m 阶 B 树)
| 结点类型 | 子树个数 | 关键字个数 |
|---|---|---|
| 根结点 | 2 ~ m | 1 ~ m-1 |
| 非根非叶结点 | ⌈m/2⌉ ~ m | ⌈m/2⌉ - 1 ~ m-1 |
B 树的高度
含 n 个关键字的 m 阶 B 树:
- 最小高度(每个结点尽可能满,每层 m-1 个关键字):h ≤ log_m(n+1)
- 最大高度(每个结点尽可能少,根 1 关键字,其余 ⌈m/2⌉ - 1):h ≥ log_{⌈m/2⌉}((n+1)/2) + 1
易错:B 树的”叶结点”指最底层的外部空结点(NULL 指针),不含信息。最底层有关键字的结点称为”终端结点”。B 树”所有叶结点在同一层”指的是这些 NULL 空结点在同一层。
B 树的查找
B 树的查找是在结点间纵向移动(磁盘 I/O)与在结点内横向比较(内存操作)交替进行的过程:
- 从根结点开始,将目标 key 与当前结点中的关键字顺序比较
- 若 key = Kᵢ,查找成功
- 若 Kᵢ < key < Kᵢ₊₁,沿指针 Pᵢ 进入子树(一次磁盘 I/O)
- 若到达叶结点(空指针),查找失败
磁盘 I/O 次数 = 查找路径上的结点数 = 树的高度 h。结点内比较在内存中完成,不涉及磁盘访问。
B 树的插入
插入总是发生在最底层的非叶结点(终端结点):
- 查找确定插入位置
- 若插入后关键字数 ≤ m-1,直接插入,完成
- 若插入后关键字数 = m(溢出),进行结点分裂
结点分裂过程
- 取中间关键字 K_{⌈m/2⌉}
- 将中间关键字上提到父结点
- 原结点分裂为左右两个结点(中间关键字左右两侧的关键字各成一结点)
- 若父结点也溢出,继续向上分裂
- 最坏情况分裂到根结点,树高度增加 1
分裂前(溢出): [K₁ K₂ K₃ K₄ K₅] ↑ 中间关键字 K₃ 上提
分裂后: 父结点 ← [..., K₃, ...] / \ [K₁ K₂] [K₄ K₅]B 树长高的唯一方式是根结点分裂——这保证了所有叶结点始终在同一层。
B 树的删除
删除后需保证每个结点的关键字数不低于 ⌈m/2⌉ - 1:
删除位置
| 情况 | 处理方式 |
|---|---|
| 删除关键字在终端结点 | 直接删除,然后检查是否下溢 |
| 删除关键字在非终端结点 | 用直接前驱(左子树最大值)或直接后继(右子树最小值)替换,转化为删除终端结点中的关键字 |
下溢处理
当终端结点关键字数 < ⌈m/2⌉ - 1 时:
| 处理策略 | 条件 | 操作 |
|---|---|---|
| 借左兄弟 | 左兄弟关键字数 > ⌈m/2⌉ - 1 | 父结点关键字下移,左兄弟最大关键字上移 |
| 借右兄弟 | 右兄弟关键字数 > ⌈m/2⌉ - 1 | 父结点关键字下移,右兄弟最小关键字上移 |
| 合并 | 左右兄弟都 = ⌈m/2⌉ - 1 | 当前结点与一个兄弟合并,父结点中间关键字下移参与合并 |
合并可能导致父结点也下溢,需逐层向上处理,最坏情况合并到根结点,树高度减 1。
借右兄弟示例(5 阶 B 树)
[..., 35, ...] / \ [20] [40, 50, 60] 当前结点下溢,右兄弟富余 ↓ [..., 40, ...] 35 下移,40 上提 / \ [20, 35] [50, 60] 恢复平衡B+ 树
B+ 树的特点
B+ 树是 B 树的变体,专为范围查询优化。其核心特点:
- 所有数据存储在叶结点:内部结点仅保存索引(关键字副本),不存放实际数据
- 叶结点通过指针依次链接:形成一个有序链表,支持顺序遍历和范围查询
- 查找必须走到叶结点:无论目标是否在内部结点出现,都要查到叶子才能获取数据
- 内部结点的关键字个数 = 子树个数(B 树中是关键字个数 = 子树个数 - 1)
m 阶 B+ 树的结构
[30 | 60] ← 内部结点(仅索引) / | \ [10|20] [30|50] [60|80] ← 内部结点(仅索引) / | \ / | \ / | \ [叶] → [叶] → [叶] → [叶] ← 叶结点(存数据,链表相连)B 树与 B+ 树对比
| 对比项 | B 树 | B+ 树 |
|---|---|---|
| 数据存储位置 | 所有结点都存数据 | 仅叶结点存数据 |
| 内部结点作用 | 既是索引又存数据 | 纯索引,不存数据 |
| 关键字个数 | n 个关键字对应 n+1 棵子树 | n 个关键字对应 n 棵子树 |
| 关键字是否重复 | 关键字不重复 | 内部结点的关键字在叶子中再次出现 |
| 查找路径 | 可能在任意层命中 | 必须查到叶结点 |
| 查找性能 | 不稳定(最好 O(1)) | 稳定(每次都 O(log n)) |
| 叶结点链表 | 无 | 有,叶结点按顺序链接 |
| 范围查询 | 需中序遍历,效率低 | 沿叶结点链表扫描,效率高 |
| 内部结点扇出 | 较小(同时存数据) | 较大(纯索引,容纳更多关键字) |
为什么数据库索引用 B+ 树
- 磁盘 I/O 更少:内部结点不存数据,同样大小的磁盘页可容纳更多关键字,树高更低
- 范围查询高效:定位到起始叶结点后,沿链表顺序扫描即可
- 查询性能稳定:每次查询都走到叶结点,路径长度一致
- 全表扫描友好:遍历叶结点链表即可完成全表顺序扫描
在 MySQL InnoDB 中,聚簇索引的叶结点直接存放整行数据,非聚簇索引的叶结点存放主键值(需回表查询)。
复杂度分析
| 操作 | B 树 | B+ 树 |
|---|---|---|
| 查找(磁盘 I/O) | O(log_m n) | O(log_m n) |
| 插入 | O(log_m n) | O(log_m n) |
| 删除 | O(log_m n) | O(log_m n) |
| 范围查询 | O(log n + k)(中序遍历) | O(log n + k)(链表扫描,更优) |
实际应用中 m 通常取较大值(几百到上千),使得树高 h 通常只有 3~4 层。
考研高频考点
- m 阶 B 树非根结点关键字数范围:⌈m/2⌉ - 1 ~ m-1(选择题/填空题高频)
- 含 n 个关键字的 m 阶 B 树的最大高度公式推导(大题常考)
- 插入时结点分裂过程、中间关键字上提(手动建树题)
- 删除时借兄弟与合并操作的判断和执行(手动操作题)
- B 树 vs B+ 树的区别(简答题/选择题必考)
- B+ 树查找必须到叶结点(判断题高频陷阱)
- B+ 树中内部结点关键字个数 = 子树个数(与 B 树不同)
- B 树所有叶结点在同一层的原因(概念题)
- 磁盘 I/O 与 B 树阶数选择的关系(理解题)
- 给定关键字序列画出 B 树构造过程(综合应用题)
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!










