Skip to content

B 树

为什么需要B 树

当数据量非常大时,数据无法全部存放在内存中,必须存储在磁盘上。磁盘的读写速度远慢于内存,每次磁盘 I/O 都是性能瓶颈。如果使用二叉查找树(如 AVL 树、红黑树),树的高度为 O(log₂n),查找一个关键字可能需要 log₂n 次磁盘访问——当 n 很大时,这是不可接受的。

B 树通过增加每个节点的分支数(多路查找),大幅降低树的高度,从而减少磁盘 I/O 次数。例如一棵 1001 阶 B 树,存储 10 亿个关键字只需要 3 层,最多 3 次磁盘访问即可完成查找。这就是数据库索引和文件系统广泛使用 B 树的原因。

核心思想

B 树是m 阶多路平衡查找树的典型代表。核心特点:

  • 多路分支:每个节点最多有 m 棵子树、m-1 个关键字,一次磁盘 I/O 读入一个节点后可以进行多次比较
  • 平衡:所有叶节点都在同一层,保证最坏情况下查找路径长度一致
  • 节点半满约束:除根节点外,每个节点至少有 ⌈m/2⌉ 棵子树,保证空间利用率

B 树节点的结构示意:

┌──┬────┬──┬────┬──┬─ ... ─┬──────┬──┐
│P₀│ K₁ │P₁│ K₂ │P₂│       │Kₙ    │Pₙ│
└──┴────┴──┴────┴──┴─ ... ─┴──────┴──┘

其中 Kᵢ 为关键字(K₁ < K₂ < ... < Kₙ),Pᵢ 为指向子树的指针。子树 Pᵢ 中所有关键字的值在 Kᵢ 和 Kᵢ₊₁ 之间。

交互可视化

通过下方的交互动画,你可以逐步观察B 树的执行过程:

加载可视化中...

操作详解

B 树的性质

一棵 m 阶 B 树满足以下性质:

性质描述
每个节点最多 m 棵子树即最多 m-1 个关键字
根节点子树数至少 2 棵(若非叶节点);关键字至少 1 个
非根非叶节点子树数至少 ⌈m/2⌉ 棵,即关键字至少 ⌈m/2⌉-1 个
所有叶节点在同一层叶节点是查找失败到达的空指针,不含任何信息
节点内关键字有序K₁ < K₂ < ... < Kₙ,节点内可用顺序/折半查找

B 树节点的 C 语言定义:

c
#define M 5  // 5 阶 B 树

typedef struct BTNode {
    int keyNum;             // 当前关键字个数
    int keys[M];            // 关键字数组(keys[0]不用,从1开始)
    struct BTNode *children[M + 1]; // 子树指针数组
    struct BTNode *parent;  // 指向父节点的指针
} BTNode;

⭐ 关键字个数范围(m 阶 B 树,非根节点):

$$⌈m/2⌉ - 1 \leq n \leq m - 1$$

⭐ 最小/最大关键字总数

  • 含 n 个关键字的 B 树,最小高度:每个节点尽可能满,每层 m-1 个关键字。各层节点数为 1, m, m², ...,因此 $n \leq m^h - 1$,即 $h \geq \log_m(n+1)$
  • 含 n 个关键字的 B 树,最大高度:每个节点尽可能少。根节点至少 1 个关键字、2 棵子树,其余节点至少 ⌈m/2⌉ 棵子树。第 h+1 层(叶节点层/失败层)至少有 $2⌈m/2⌉^{h-1}$ 个节点,而 n 个关键字对应 n+1 个失败节点,因此 $n + 1 \geq 2⌈m/2⌉^{h-1}$,即 $h \leq \log_{⌈m/2⌉}\frac{n+1}{2} + 1$

查找操作

B 树的查找是一个在节点间纵向移动(磁盘 I/O)与在节点内横向比较(内存操作)交替进行的过程。

关键步骤:

  1. 从根节点开始,将目标关键字 key 与当前节点中的关键字顺序比较(节点内关键字较少时用顺序查找,较多时可用折半查找)
  2. 若 key = Kᵢ,查找成功,返回该节点及位置
  3. 若 K ᵢ < key < Kᵢ₊₁,沿着指针 Pᵢ 进入对应子树,读取下一层节点(一次磁盘 I/O
  4. 若到达叶节点(空指针),查找失败
c
// B 树查找(返回关键字所在节点和位置)
typedef struct {
    BTNode *node;  // 关键字所在节点
    int index;     // 在节点中的位置
    int found;     // 是否找到:1-成功,0-失败
} SearchResult;

SearchResult BTreeSearch(BTNode *root, int key) {
    BTNode *p = root, *parent = NULL;
    int i;
    while (p != NULL) {
        // 在当前节点中顺序查找
        for (i = 1; i <= p->keyNum && key > p->keys[i]; i++);
        if (i <= p->keyNum && key == p->keys[i])
            return (SearchResult){p, i, 1};  // 查找成功
        parent = p;
        p = p->children[i - 1];  // 沿指针进入子树
    }
    return (SearchResult){parent, i, 0};  // 查找失败,返回插入位置
}

⭐ 查找过程中磁盘 I/O 次数 = 查找路径上的节点数 = 树的高度 h。节点内的比较在内存中完成,不涉及磁盘访问。

插入操作

B 树的插入总是发生在最底层的非叶节点(即终端节点)。

关键步骤:

  1. 先执行查找,确定关键字应插入的终端节点位置
  2. 若该节点插入后关键字数 ≤ m-1,直接插入,完成
  3. 若插入后关键字数 = m(溢出),需要进行节点分裂

⭐ 节点分裂过程

  1. 取该节点的中间关键字 K₍⌈m/2⌉₎
  2. 将中间关键字上提到父节点
  3. 原节点分裂为两个节点:中间关键字左边的关键字组成左节点,右边的关键字组成右节点
  4. 若父节点也溢出,继续向上分裂,最坏情况一直分裂到根节点,树高度增加 1

以 5 阶 B 树为例,每个节点最多 4 个关键字,插入导致 5 个关键字时触发分裂:

分裂前(溢出):  [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父节点关键字下移到当前节点,右兄弟最小关键字上移到父节点
合并左右兄弟关键字数都 = ⌈m/2⌉-1当前节点与一个兄弟合并,父节点中间隔关键字下移参与合并

合并操作可能导致父节点也下溢,需要逐层向上处理,最坏情况合并到根节点,根节点关键字被掏空后树高度减 1。

借右兄弟示例(5 阶 B 树,节点最少 2 个关键字):

        [..., 35, ...]          父节点
        /          \
    [20]          [40, 50, 60]   当前节点下溢,右兄弟富余

        [..., 40, ...]          35 下移,40 上提
        /          \
    [20, 35]      [50, 60]      恢复平衡
合并示例(5 阶 B 树):

        [..., 35, ...]          父节点
        /          \
    [20]          [40, 50]       当前节点下溢,右兄弟刚好 ⌈m/2⌉-1

        [...]                    35 下移参与合并
          |
    [20, 35, 40, 50]            合并为一个节点

复杂度分析

操作时间复杂度说明
查找O(log_m n) 次磁盘 I/O,节点内 O(m)树高 h = O(log_{⌈m/2⌉} n),每层一次磁盘访问
插入O(log_m n)查找 + 最坏情况逐层分裂到根
删除O(log_m n)查找 + 最坏情况逐层合并到根

磁盘 I/O 次数:查找过程中为 h 次;插入最坏需要 h 次查找 + h 次分裂写回;删除最坏需要 h 次查找 + h 次合并写回。实际应用中 m 取较大值(如几百到上千),使得 h 通常只有 3~4 层。

考研高频考点

  • ⭐ m 阶 B 树中非根节点关键字数范围 ⌈m/2⌉-1 ~ m-1(选择题/填空题高频)
  • ⭐ 含 n 个关键字的 m 阶 B 树的最大高度公式推导(大题常考)
  • ⭐ 插入时节点分裂的过程、中间关键字上提(手动建树题)
  • ⭐ 删除时借兄弟与合并操作的判断和执行(手动操作题)
  • ⭐ B 树 vs B+ 树的区别(简答题/选择题必考)
  • B 树所有叶节点在同一层的原因(概念题)
  • 磁盘 I/O 与 B 树阶数选择的关系(理解题)
  • 给定关键字序列,画出 B 树的构造过程(综合应用题)