1. 为什么要学 B-树
B-树是一种多路平衡搜索树,它不是为了在内存里跟红黑树比拼常数,而是为了解决一个更现实的问题:当数据量大到无法完全放进内存时,如何尽量减少磁盘访问次数。
磁盘的读写有天然的机械延迟(寻道、旋转),一次磁盘 IO 的代价远高于内存比较操作。因此,评价外存索引结构好坏的核心指标不是"算法复杂度的常数项",而是完成一次查找需要访问磁盘多少次。B-树通过让每个节点存储多个关键字、拥有多棵子树,把树的高度压得非常低,让大多数操作只需要访问两到四层的节点就能完成。
B-树最典型的应用场景有两个:
- 数据库索引:关系型数据库常用树形结构组织索引,按关键字快速定位记录所在的磁盘页,减少随机读写。
- 文件系统:操作系统的目录结构和元数据管理也大量使用类似 B-树的多路平衡结构,保证在海量文件中快速寻址。
学习 B-树的价值不在于默写复杂的删除细节,而在于理解它背后的设计取舍:为什么多路比二叉更省磁盘 IO、为什么自底向上的平衡方式更适合外存、为什么节点分裂和合并是维持平衡的核心手段。这些思想在后续学习 B+ 树、LSM 树、数据库存储引擎时都会反复出现。
2. B-树的定义与阶数约束
正式讨论前先约定一个概念:B-树的阶数 m,表示一棵 B-树中每个节点最多可以拥有的子树数量。m 越大,节点越"胖",树越矮。通常讨论的 B-树要求 m 大于等于 3。
一棵 m 阶 B-树需要同时满足以下约束:
- 关键字数量上限:每个节点最多有 m-1 个关键字,因此最多有 m 棵子树。一个非叶子节点若有 k 个关键字,就有 k+1 棵子树。
- 内部节点下限:除根节点外,每个内部节点至少有 ceil(m/2)-1 个关键字,即至少有 ceil(m/2) 棵子树。这里的 ceil 表示向上取整。
- 根节点下限:根节点如果是非叶子节点,至少要有 1 个关键字、2 棵子树;根节点如果是叶子节点,可以没有关键字(此时树为空树,也可以特殊处理为只有 0 个节点的空树)。
- 完全平衡:所有叶子节点都位于同一层。也就是说,从根节点到任意一个叶子节点的路径长度完全相同,树的高度严格一致。
- 节点内有序:每个节点内的关键字从左到右递增排列。对于关键字 Ki,它左边子树中的所有关键字都小于 Ki,右边子树中的所有关键字都大于 Ki。
以最常见的 5 阶 B-树为例:每个节点最多 4 个关键字、最多 5 棵子树;除根外每个节点最少 ceil(5/2)-1 = 2 个关键字、至少 3 棵子树。记住这个"4 上 2 下"的边界,后续看插入删除时会清晰很多。
这里特别容易与二叉搜索树混淆:二叉搜索树每个节点只存 1 个关键字、最多 2 棵子树,而 B-树每个节点可以存多个关键字、有多棵子树。它们都满足"左小右大"的搜索树性质,但 B-树是"节点内也有序"的多路结构,这是本质区别。
3. 核心操作分步拆解
下面用一个 5 阶 B-树的口径来讲解,即每个节点最多 4 个关键字,除根外至少 2 个关键字。所有操作都可以拆成"局部判断 + 必要时向上回溯"的套路。
3.1 查找
查找是 B-树最简单也最核心的操作,因为它直接决定了磁盘访问次数。查找某个关键字 key 的过程如下:
- 从根节点开始,在当前节点的关键字序列中顺序或二分查找 key。
- 如果找到,返回该关键字及其所在位置。
- 如果没找到,确定 key 应该落在哪个子区间,沿着对应的子树继续查找。
- 如果到达叶子节点仍没找到,说明 key 不存在。
对比二叉搜索树,B-树查找只多了"在节点内部定位关键字"这一步。真正省磁盘 IO 的关键在于:每个节点通常对应一个磁盘页,一次读取就能拿到大量关键字,树高远低于二叉树。
3.2 插入与节点分裂
插入始终发生在叶子节点。整体思路是:先按查找路径走到叶子,插入关键字;如果插入后超过上限,就"分裂"。
以一个 5 阶 B-树为例,依次执行以下步骤:
- 定位叶子:沿查找路径找到 key 应插入的叶子节点。
- 插入:把 key 插到该叶子节点的有序位置上。如果节点关键字数仍小于等于 4(m-1),插入结束。
- 上溢分裂 :如果插入后节点关键字数变成 5(等于 m),说明节点"上溢"了,必须分裂。
- 取节点中间位置的关键字(第 3 个关键字,下标为 2)作为上升关键字,插入到父节点中。
- 节点本身以该关键字为界分成左右两部分:左边 2 个关键字成为原节点,右边 2 个关键字成为一个新节点。
- 如果该节点不是叶子,子树也要按同样边界分给两个节点。
- 向上传播:父节点插入上升关键字后也可能超过上限,此时继续对父节点执行同样的分裂。最坏情况下分裂一直传播到根节点,根节点分裂后树的高度增加 1。
举个例子,假设当前叶子节点关键字为 10, 20, 30, 40(已满),再插入 25,节点变成 10, 20, 25, 30, 40,超过上限 4。分裂时中间关键字 25 上升,左右分成 10, 20 和 30, 40,25 被插入父节点。这个过程的直观感受是:每次分裂都把一个过胖的节点从中间"一切两半",中间值向上顶。
3.3 删除
删除是 B-树操作里最容易记混的部分。它的总体思路是:先统一转化成"删除叶子节点中的关键字",再按情况处理下溢。
以 5 阶 B-树(除根外每个节点至少 2 个关键字)为例,删除分三步:
- 定位关键字:先查找目标关键字所在节点。
- 若是内部节点关键字,先"替换" :如果目标关键字在内部节点,不能直接删掉它,否则子树结构会断裂。正确做法是:用它左子树中的最大关键字 (前驱)或右子树中的最小关键字(后继)替换它。由于前驱和后继一定在叶子节点中,问题就转化为"删除叶子节点中的那个替换来源关键字"。
- 删除叶子关键字并处理下溢 :在叶子节点中删除目标关键字后,检查该节点关键字数是否小于下限 2(ceil(m/2)-1)。
- 情况 A:不违反下限。删除后节点关键字数仍大于等于 2,直接结束。
- 情况 B:向兄弟"借"。如果该节点的某个相邻兄弟节点关键字数大于下限 2(即借走一个后兄弟仍不违反最小约束),则通过父节点关键字进行"旋转":把父节点中分隔两兄弟的关键字下移到当前节点,兄弟中相邻的边界关键字上移到父节点。本质是重新分配关键字,不改变树的高度。
- 情况 C:合并。如果相邻兄弟也只有下限 2 个关键字,无法再借,就把当前节点、父节点中分隔它们的关键字、兄弟节点合并成一个节点。合并后父节点少了一个关键字,可能又触发父节点的下溢,这时继续向上回溯处理。最坏情况下合并传播到根节点,根节点只剩一个关键字并被删除时,树的高度减 1。
举个例子:某个叶子节点删除后只剩 5,低于下限 2。检查它的左兄弟为 10, 15, 20(有 3 个关键字,大于下限 2),此时可以"借"。父节点分隔关键字为 8。旋转后:父节点分隔关键字变为 10,当前节点变成 5, 8,左兄弟变成 15, 20。所有约束都重新满足。
如果左兄弟也只有 10, 15(正好下限 2),无法借,则把当前节点 5、父节点分隔关键字 8、左兄弟 10, 15 合并成一个节点 5, 8, 10, 15,父节点少一个关键字,继续向上检查。
记忆时抓住一句话:删除内部键先替换,删除叶子键看下限,能借就借,不能借就合并,合并可能一路传到根。
4. B-树与其他树结构的对比
学完 B-树后,最常见的问题是:它跟二叉平衡树、B+ 树到底差在哪里?下面用一个表格做横向对比,帮助区分适用场景。
| 对比维度 | 二叉搜索树 / 平衡二叉树(AVL、红黑树) | B-树 | B+ 树 |
|---|---|---|---|
| 节点结构 | 每个节点 1 个关键字,最多 2 棵子树 | 每个节点最多 m-1 个关键字,最多 m 棵子树 | 内部节点只存索引,关键字数量上限与 B-树类似 |
| 树的形态 | 二叉,树高较高 | 多路,树矮且胖 | 多路,比 B-树更矮(内部节点不含数据) |
| 数据存放位置 | 关键字与数据在每个节点都出现 | 关键字与数据可以同时存在于内部节点和叶子节点 | 数据全部存放在叶子节点,内部节点只作导航 |
| 查找特性 | 适合内存中的普通查找 | 适合外存单点查找,减少磁盘 IO | 适合外存单点查找和范围查找 |
| 范围查询能力 | 中序遍历可完成,但跨磁盘页效率一般 | 范围查询仍需回溯,效率不如 B+ 树 | 叶子节点用链表串联,范围查询直接顺序遍历,效率最高 |
| 典型场景 | 内存数据结构、语言标准库容器 | 文件系统目录索引、需要外存索引但不强调范围查询的场景 | 数据库索引(如 MySQL InnoDB 的主键索引) |
一句最简概括:平衡二叉树用于内存,B-树和 B+ 树用于外存;B-树单点访问强,B+ 树胜在范围查询和更高扇出。实际数据库引擎几乎都选择 B+ 树,原因就是范围扫描和叶子链表带来的顺序访问优势。
5. 易错点与记忆技巧
复习 B-树时,以下五个点是最容易混淆的地方,建议对照前面内容反复确认:
- 易错点一:键树数量记反。m 阶 B-树最多有 m-1 个关键字、m 棵子树。不要记成"最多 m 个关键字"。口诀:"键少一,孩满 m"。
- 易错点二:忽略根节点下限特殊。除根外的内部节点最少 ceil(m/2)-1 个关键字,而根节点可以只有 1 个关键字、2 棵子树。判断下溢时如果节点是根,规则不同。
- 易错点三:删除内部关键字直接删。内部节点关键字不能直接删除,必须先用前驱或后继替换,把问题转化成删除叶子关键字。直接删会导致子树结构断裂。
- 易错点四:分裂与合并的触发条件。插入后关键字数大于 m-1(达到 m)才分裂;删除后小于 ceil(m/2)-1 才借或合并。上下限的"边界值"要算清楚。
- 易错点五:B-树与 B+ 树的数据存储。B-树内部节点可以存储完整数据,B+ 树内部节点只存索引、数据全部在叶子。这个区别直接决定了 B+ 树更适合范围查询。
记忆技巧可以总结成几句顺口溜:
- 结构记忆:"m 阶键减一,孩子多达 m;除根有下限,上取半减一;所有叶子同一层,节点内部递增排。"
- 操作记忆:"插入必到叶子,满了就裂中间;删除先换前驱,叶子少了可借或并。"
- 设计动机记忆:"树矮磁盘访次少,多路是为省 IO。"所有复杂规则最终都服务于这一目标,理解动机后很多细节更容易推导。
6. 核心知识点总结清单
最后附上一份速查清单,方便后续快速复盘。复习时可以按"定义 → 操作 → 对比 → 易错点"顺序自测。
- 定位:B-树是多路自平衡搜索树,核心目标是降低树高、减少磁盘 IO。
- 阶数 m 的含义:每个节点最多 m-1 个关键字、m 棵子树;除根外最少 ceil(m/2)-1 个关键字;根节点非叶子时至少有 1 个关键字、2 棵子树。
- 平衡性:所有叶子节点在同一层,B-树从根到叶高度严格一致。
- 查找:从根开始,在节点内定位关键字,未命中则进入对应子树;查找成本与树高成正比,树高是对数级别。
- 插入:总是插入叶子节点;超过 m-1 个关键字时分裂,中间关键字上移到父节点;分裂可能递归到根,根分裂时树高加 1。
- 删除:内部节点关键字先用前驱或后继替换,统一转为删除叶子关键字;删除后低于下限时优先向兄弟"借"(旋转),借不到则与兄弟、父节点关键字合并;合并可能递归到根,根只剩一个孩子时树高减 1。
- 与平衡二叉树对比:平衡二叉树节点瘦、树高,适合内存;B-树节点胖、树矮,适合外存,省磁盘访问。
- 与 B+ 树对比:B-树数据可存内部节点;B+ 树数据全部在叶子,内部只存索引,叶子用链表串联,范围查询更高效,是数据库索引的主流选择。
- 易错焦点:阶数与键数差一、根节点下限特殊、内部删除需先替换、分裂合并边界、B/B+ 树数据存储位置差异。
- 记忆主线:"矮胖省 IO"是设计本质;"插入满则裂,删除少则借或并"是操作本质。
如果这份清单都能不看前文默写出来,说明 B-树的核心知识已经掌握;卡住的条目就是下次复习的重点。