【数据结构】考研408 | B树收官:插入与删除的平衡艺术——分裂、合并与借位

B树的基本操作

  • 导读
  • 一、插入
    • [1.1 基本概念](#1.1 基本概念)
      • [1.1.1 体系一:严蔚敏版教材](#1.1.1 体系一:严蔚敏版教材)
      • [1.1.2 体系二:通用体系](#1.1.2 体系二:通用体系)
      • [1.1.3 二者的区别](#1.1.3 二者的区别)
    • [1.2 插入过程](#1.2 插入过程)
    • [1.3 实例说明](#1.3 实例说明)
  • 二、删除
    • [2.1 删除过程](#2.1 删除过程)
    • [2.2 结点的转换](#2.2 结点的转换)
    • [2.3 终端结点的删除](#2.3 终端结点的删除)
      • [2.3.1 删除的三种情况](#2.3.1 删除的三种情况)
      • [2.3.2 借](#2.3.2 借)
      • [2.3.3 合并](#2.3.3 合并)
  • 三、插入与删除小结
  • 结语

导读

大家好,很高兴又和大家见面啦!!!

在上一篇内容中,我们深入探讨了B树的 查找操作树高特性 ,揭示了 B树 如何通过多路平衡结构显著降低树高,从而优化大规模数据存储场景下的查询效率。

我们特别分析了B树的查找思想具体过程 (包括成功与失败的场景),并推导出B树高度与关键字数量及阶数的数学关系:
l o g m ( n + 1 ) ≤ h ≤ l o g ⌈ m 2 ⌉ n + 1 2 + 1 log_m(n+1) \leq h \leq log_{\lceil \frac{m}{2} \rceil}\frac{n+1}{2}+1 logm(n+1)≤h≤log⌈2m⌉2n+1+1

这一公式确保了B树在动态数据环境下仍能保持高效检索能力。

然而,高效的查找仅是 B树 价值的 冰山一角B树 的真正强大之处在于其动态平衡能力 ------当插入或删除关键字时,B树 能通过分裂合并等操作自动调整结构,维持稳定的树高与性能。

无论是数据库索引还是文件系统,B树 都需要频繁处理数据的增删操作。那么,B树 如何在不破坏平衡的前提下实现这些操作?其背后的分裂与合并机制又是如何运作的?

今天,让我们直接进入 B树 的核心动态操作环节,探索其插入与删除的完整流程与底层逻辑。

一、插入

在说明其具体的插入过程之前,我们需要先认识一下 B树 中的一些概念,

1.1 基本概念

目前存在着两种体系,这里我们以一棵 3阶B树 为例,分别说明这两种体系;

1.1.1 体系一:严蔚敏版教材

外部结点 内部结点 终端结点 NULL NULL NULL NULL n, p0, key1, p1, key2, p2 n, p0, key1, p1, key2, p2 n, p0, key1, p1, key2, p2 n, p0, key1, p1, key2, p2 n, p0, key1, p1, key2, p2 n, p0, key1, p1, key2, p2 n, p0, key1, p1, key2, p2 n, p0, key1, p1, key2, p2

这里是将 B树 分为了 内部结点外部结点 两大部分,而内部结点又将其细分出了 终端结点

  • 内部结点:树中存储着关键字,且实际存在的结点
  • 外部结点 :也称为 叶子结点/空叶结点 ,结点本身不存储任何关键字,仅代表查找失败时的情况,一般为空指针
  • 终端结点空叶结点 的直接父结点,同时也是树中 内部结点 的最底层结点;

1.1.2 体系二:通用体系

终端结点/叶子结点 内部结点 n, p0, key1, p1, key2, p2 n, p0, key1, p1, key2, p2 n, p0, key1, p1, key2, p2 n, p0, key1, p1, key2, p2 n, p0, key1, p1, key2, p2 n, p0, key1, p1, key2, p2 n, p0, key1, p1, key2, p2 n, p0, key1, p1, key2, p2

在该体系中,通用将 B树 分为:内部结点叶子结点 两部分,只不过此时的 终端结点 等同于 叶子结点 ,它是存储数据的最后一层结点,也是查找路径的终点,下方连接着查找失败时的外部结点;而 内部结点 是指所有的 非终端结点

1.1.3 二者的区别

不管是 体系一 还是 体系二 ,均是采用的 内部结点 + 叶子结点 来构建一棵 B树 ,这二者的区别就是 终端结点 的归属:

  • 体系一 中,终端结点内部结点最底层结点 , 也是 叶子结点直接父结点
  • 体系二 中,终端结点叶子结点 ,也是查找路径的终点 ,其下方连接着查找失败时的 外部结点

体系二 (内部结点/终端结点二分法)在绝大多数现代数据库、文件系统等领域的资料和工程实践中,是绝对的主流和标准;

但是我们目前所使用的是基于 严蔚敏版的《数据结构》教材 所编写的《数据结构》教材,因此这里我们以 体系一 为准。

1.2 插入过程

BST 中执行插入操作时,我们是将待插入的关键字作为一个新的结点插入到树中:

  • 在插入前,我们会通过查找操作,确定该结点的插入位置:
    • 查找失败,则该失败位置为新结点的插入位置
    • 查找成功,则不需执行插入操作
  • 当查找失败时,该结点会作为新的叶结点插入到树中
  • 当查找成功时,说明该树中已经存在该关键字,因此无需执行插入操作

B树 的插入操作相比与 BST ,其具体过程要复杂的多。其主要原因是因为 m阶B树中的结点最多可以存储 m − 1 m - 1 m−1 个关键字 ,因此 B树 的插入操作不再是将关键字作为新的结点插入到树中,而是直接在树的结点中插入新的关键字;

BST 的插入操作相同的是,新结点的插入一定是发送在 终端结点 中;但是在 B树 中查找到插入的位置后,并不能简单地将新关键字添加到 终端结点 中,这是因为当该结点中已经存在 m − 1 m - 1 m−1 个关键字时,此时的插入操作就会导致整棵树不再满足 m阶B树 的要求。

因此 B树 的具体过程如下所示:

  • 定位 :通过 查找 算法找到 新关键字 的插入的 终端结点(该结点一定是查找失败时的叶结点的直接父结点);
  • 插入 :每个非根结点的关键字个数均在 [ ⌈ m 2 ⌉ − 1 , m − 1 ] [\lceil \frac{m}{2} \rceil - 1, m - 1] [⌈2m⌉−1,m−1] 这个范围内,因此,在插入的过程中会存在两种情况:
    • 在结点中插入新的关键字后,该结点关键字的总数 n n n 满足 n < m n < m n<m ,此时直接进行插入;
    • 在结点中插入新的关键字后,该结点关键字的总数 n n n 满足 n = = m n == m n==m,此时需要通过 分裂 操作使结点中的关键字数量再一次满足 [ ⌈ m 2 ⌉ − 1 , m − 1 ] [\lceil \frac{m}{2} \rceil - 1, m - 1] [⌈2m⌉−1,m−1];

可以看到,B树 的插入操作中会涉及一个十分重要的操作------分裂。该操作的具体方法是:

  • 取一个新结点插入到当前结点的父结点中,作为父结点的新子树
  • 将插入了 k e y key key 后的原结点从中间位置 ⌈ m 2 ⌉ \lceil \frac{m}{2} \rceil ⌈2m⌉ 处将结点中的关键字分为三部分:
    • 左侧的全部关键字放入原结点中
    • 右侧的全部关键字放入新结点中
    • ⌈ m 2 ⌉ \lceil \frac{m}{2} \rceil ⌈2m⌉ 处的关键字插入到原结点的父结点中
  • 若向父结点中插入关键字时导致父结点的关键字个数也超过了上限,则继续对父结点进行分裂操作;
  • 分裂的过程会从 终端结点 向上传递,若传递到了根结点,此时 B树 的高度会增加 1 1 1

1.3 实例说明

下面我们以一棵 3阶B树 为例,来说明 B树 的具体插入过程。现在我们需要在一棵 空树 中插入 [5,7,1,3,5,6,4],其具体过程如下所示:

  • 插入关键字 5

由于该 3阶B树 是一棵空树,此时我们需要创建一个新的结点,并将该结点作为根结点插入到树中:
n=1, p0, 5, p1, key2, p2

此时我们就完成了第一步插入操作;

  • 插入关键字 7

通过查找操作,我们确定了此时的失败结点的直接父结点为根结点,因此我们需要将该关键字插入到根结点中:
n=2, p0, 5, p1, 7, p2

由于此时的关键字个数 n = 2 < m = 3 n = 2 < m = 3 n=2<m=3 因此完成插入;

  • 插入关键字 1

通过查找操作,我们确定此时的失败结点的直接父结点为根结点,因此我们需要将该关键字插入到根结点中:
n=3, 1, p0, 5, p1, 7, p2

由于此时的关键字个数 n = 3 = = m = 3 n = 3 == m = 3 n=3==m=3 因此我们需要对该结点进行分裂操作:

  1. 取一个新结点插入到树中

n=3, 1, p0, 5, p1, 7, p2 n, p0, key1, p1, key2, p2

由于当前结点为根结点,因此我们需要再为该结点创建一个父结点作为新的根结点:
n, p0, key1, p1, key2, p2 n=3, 1, p0, 5, p1, 7, p2 n, p0, key1, p1, key2, p2

  1. 以 ⌈ 3 2 ⌉ = 2 \lceil \frac{3}{2} \rceil = 2 ⌈23⌉=2 处的关键字为分界线将当前结点分为三部分:
    • 左侧关键字放入原结点中
    • 右侧关键字放入新结点中
    • ⌈ 3 2 ⌉ = 2 \lceil \frac{3}{2} \rceil = 2 ⌈23⌉=2 处的关键字放入到父结点中

这里我们需要注意的是 ⌈ 3 2 ⌉ = 2 \lceil \frac{3}{2} \rceil = 2 ⌈23⌉=2 处的关键字是[1, 5, 7] 这三个关键字的 位序 其对应的下标应该是 i = 1,也就是关键字 5,因此通过分裂操作后的新树如下所示:
n=1, p0, 5, p1, key2, p2 n=1, p0, 1, p1, key2, p2 n=1, p0, 7, p1, key2, p2

此时我们就完成了第一次分裂操作,由于此时是直接对 根节点 执行的分裂操作,因此 B树 的树高需要增加 1 1 1;

  • 插入关键字 3

通过查找操作,我们确定此时的失败结点的直接父结点为关键字 1 所在的 终端结点,因此我们需要将该关键字插入到该结点中:
n=1, p0, 5, p1, key2, p2 n=2, p0, 1, p1, 3, p2 n=1, p0, 7, p1, key2, p2

由于此时该结点的关键字个数 n = 2 < m = 3 n = 2 < m = 3 n=2<m=3 ,因此完成插入;

  • 插入关键字 5

通过查找操作,我们发现 5 已经存在于根结点中,因此不执行任何操作;

  • 插入关键字 6

通过查找操作,我们确定此时的失败结点的直接父结点为关键字 7 所在的 终端结点,因此我们需要将该关键字插入到该结点中:
n=1, p0, 5, p1, key2, p2 n=2, p0, 1, p1, 3, p2 n=2, p0, 6, p1, 7, p2

由于此时该结点的关键字个数 n = 2 < m = 3 n = 2 < m = 3 n=2<m=3 ,因此完成插入;

  • 插入关键字 4

通过查找操作,我们确定此时的失败结点的直接父结点为关键字 7 所在的 终端结点,因此我们需要将该关键字插入到该结点中:
n=1, p0, 5, p1, key2, p2 n=3, p0, 1, p1, 3, p2, 4 n=2, p0, 6, p1, 7, p2

由于此时的关键字个数 n = 3 = = m = 3 n = 3 == m = 3 n=3==m=3 因此我们需要对该结点进行分裂操作:

  1. 取一个新结点插入到树中

n=1, p0, 5, p1, key2, p2 n=3, p0, 1, p1, 3, p2, 4 n=0, p0, key1, p1, key2, p2 n=2, p0, 6, p1, 7, p2

  1. 以 ⌈ 3 2 ⌉ = 2 \lceil \frac{3}{2} \rceil = 2 ⌈23⌉=2 处的关键字为分界线将当前结点分为三部分:
    • 左侧关键字放入原结点中
    • 右侧关键字放入新结点中
    • ⌈ 3 2 ⌉ = 2 \lceil \frac{3}{2} \rceil = 2 ⌈23⌉=2 处的关键字放入到父结点中

n=2, p0, 3, p1, 5, p2 n=1, p0, 1, p1, key2, p2 n=1, p0, 4, p1, key2, p2 n=2, p0, 6, p1, 7, p2

此时我们完成了第一次分裂操作,接下来我们需要检查该结点的父结点------ 根结点 中的关键字个数是否满足 1 ≤ n ≤ 2 1 \leq n \leq 2 1≤n≤2;

可以看到,此时的根节点的关键字个数满足要求,因此完成插入;

二、删除

B树 的删除操作与插入操作类似,但是会稍微复杂一点:

  • B树 的插入操作只需要保证当前的插入结点中关键字的数量:
    • 完成插入后,当前结点的关键字数量满足 n < m n < m n<m ,则直接插入即可
    • 完成插入后,当前结点的关键字数量满足 n = = m n == m n==m ,则需要 分裂 操作使其恢复 B树 的性质------ B树 中除根结点外,各结点中关键字的数量在 [ ⌈ m 2 ⌉ − 1 , m − 1 ] [\lceil \frac{m}{2} \rceil - 1, m - 1] [⌈2m⌉−1,m−1] 之间
  • B树 的删除操作不仅需要关注当前结点中的关键字数量,还需要关注其兄弟结点中关键字的数量

接下来我们就来一起看一下 B树 中的删除操作是如何实现的;

2.1 删除过程

B树 的删除过程可以总结为以下步骤:

  • 定位 :通过查找操作,找到待删除的目标关键字所在的结点:
    • 若找到了该结点,则将该结点中的目标关键字进行删除
    • 若未找到该结点,则表示树中不存在目标关键字,因此无需执行任何操作
  • 删除 :在进行删除操作时,需要根据目标关键字的具体位置来执行相应的操作:
    • 目标关键字 k e y key key 不在 终端结点 中,则将其转化为删除 终端结点 中的 k e y ′ key' key′
    • 目标关键字 k e y key key 在 终端结点 中,则需要根据具体的情况进行删除操作

简单的理解就是 B树 中的删除操作与插入操作一样,一定是发生在 终端结点 中;

那我们应该如何在 终端结点 中进行删除操作?对于不在 终端结点 中的关键字,我们又应该如何删除?

接下来我们就围绕这两个问题继续深入 B树 的删除操作;

2.2 结点的转换

当我们通过查找发现目标关键字 k e y key key 不在 终端结点 中时,我们需要通过其直接前驱(或直接后继) k e y ′ key' key′ 来替代目标关键字 k e y key key ,而该直接前驱(或直接后继) k e y ′ key' key′ 一定是位于 终端结点 中;

  • 直接前驱:目标关键字 k e y key key 的左侧子树中 最右下 的元素
  • 直接后继:目标关键字 k e y key key 的右侧子树中 最左下 的元素

22 5, 11 36, 45 1, 3 6, 8, 9 13, 15 30, 35 40, 42 47, 48, 50, 56

在这棵 5阶B树 中,位于 非终端结点 的关键字有 5 5 5 个:5, 11, 22, 36, 45。这些关键字的直接前驱与直接后继如下所示:

  • 关键字 5 的直接前驱为关键字 3 ,直接后继为关键字 6
  • 关键字 11 的直接前驱为关键字 9 ,直接后继为关键字 13
  • 关键字 22 的直接前驱为关键字 15 ,直接后继为关键字 30
  • 关键字 36 的直接前驱为关键字 35 ,直接后继为关键字 40
  • 关键字 45 的直接前驱为关键字 42 ,直接后继为关键字 47

这里我们以关键字 22 为例,其直接前驱与直接后继所在结点我们分别以红色和蓝色进行标注:
22 5, 11 36, 45 1, 3 6, 8, 9 13, 15 30, 35 40, 42 47, 48, 50, 56

可以看到,其直接前驱位于其左侧子树的最右侧的 终端结点 中,其具体位置为该结点中的 最右侧元素

其直接后继位于其右侧子树的最左侧 终端结点 中,其具体位置为该结点中的 最左侧元素

明确了关键字 k e y key key 的替代关键字 k e y ′ key' key′ 的具体位置后,下面我们就来看一下我们应该如何执行从 非终端结点终端结点 的转换操作,这里我们还是以删除关键字 22 为例:

  • 当我们通过直接前驱来完成转换时,我们需要通过其直接前驱 15 来直接替代关键字 22

转换后 15 5, 11 36, 45 1, 3 6, 8, 9 13, 15 30, 35 40, 42 47, 48, 50, 56 转换前 22 5, 11 36, 45 1, 3 6, 8, 9 13, 15 30, 35 40, 42 47, 48, 50, 56

  • 当我们通过直接后继来完成转换时,我们需要通过其直接前驱 30 来直接替代关键字 22

转换后 30 5, 11 36, 45 1, 3 6, 8, 9 13, 15 30, 35 40, 42 47, 48, 50, 56 转换前 22 5, 11 36, 45 1, 3 6, 8, 9 13, 15 30, 35 40, 42 47, 48, 50, 56

从上图中我们不难发现,不管是使用直接前驱,还是直接后继,实际上我们只需要将对应的值存储到目标结点中来替代原关键字 k e y key key 即可;

若无法理解 替代 这一过程,那我们也可以理解为,先删除原关键字 k e y key key 再插入其直接前驱或者直接后继 k e y ′ key' key′ 到该结点中;

由于执行的是一次删除 + 一次插入,因此整个 替代 过程不会破坏 B树 的性质;

2.3 终端结点的删除

当我们对 B树 执行删除操作时,不管目标关键字 k e y key key 是否位于 终端结点 中,最终都会在 终端结点 中完成删除操作;

由于删除操作会直接减少对应结点中的一个关键字,这就有可能导致结点中的关键字个数少于 ⌈ m 2 ⌉ − 1 \lceil \frac{m}{2} \rceil - 1 ⌈2m⌉−1 ,这时就破坏了 B树 的性质,因此为了保证删除操作后不破坏 B树 的性质,这时我们就需要通过 合并 操作来维持 B树 的性质;

2.3.1 删除的三种情况

B树 中,对 终端结点 进行删除操作时,会存在 3 3 3 种情况:

  • 直接删除关键字 :当被删除的关键字所在的结点中的关键字个数满足 n ≥ ⌈ m 2 ⌉ n \geq \lceil \frac{m}{2} \rceil n≥⌈2m⌉ 时,这就表明即使删除了一个关键字,该结点中的关键字总数仍满足 [ ⌈ m 2 ⌉ − 1 , m − 1 ] [\lceil \frac{m}{2} \rceil - 1, m - 1] [⌈2m⌉−1,m−1],因此我们可以直接在该结点中删除目标关键字 k e y key key

  • 兄弟够借 :当被删除的关键字所在的结点中的关键字个数恰好为 ⌈ m 2 ⌉ − 1 \lceil \frac{m}{2} \rceil - 1 ⌈2m⌉−1 时,这就表明当前结点删除一个关键字后,就不再满足 B树 的性质,若此时与该结点相邻的兄弟结点中的关键字个数满足 n ≥ ⌈ m 2 ⌉ n \geq \lceil \frac{m}{2} \rceil n≥⌈2m⌉,这时我们就可以向该兄弟结点 一个关键字,以确保在执行删除操作后,仍保持 B树 的性质;

  • 兄弟不够借 :当被删除的关键字所在结点中的关键字个数恰好为 ⌈ m 2 ⌉ − 1 \lceil \frac{m}{2} \rceil - 1 ⌈2m⌉−1 ,且与该结点相邻的兄弟结点中的关键字个数均为 ⌈ m 2 ⌉ − 1 \lceil \frac{m}{2} \rceil -1 ⌈2m⌉−1 ,这就表明,当该结点删除一个关键字后,会破坏 B树 的性质,并且我们向其兄弟结点 了一个关键字后仍会破坏 B树 的性质,在这种情况下,我们需要通过 合并 操作来保证 B树 的性质不会被破坏

那么我们应该如何 ?当不够 时,我们又应该如何 合并 呢?下面我们继续来深入探讨;

2.3.2 借

所谓的 实际上是将兄弟结点中的关键字 k e y 1 key1 key1 删除后插入到父结点中,并将父结点中的关键字 k e y 2 key2 key2 删除后,插入到当前结点中。下面我们就以一棵 5阶B树 的删除操作来说明整个借的过程:
n = 2, key1, key2 n = 2, key3, key4 n = 3, key5, key6, key7

在上述这棵 5阶B树 中,根据 B树 的性质,除了根结点外,其余结点中的结点数在 [ 2 , 4 ] [2, 4] [2,4] 这个范围中,当我们要删除 k e y 4 key4 key4 时,其具体过程如下所示:

  • 直接删除 k e y 4 key4 key4

n = 2, key1, key2 n = 1, key3 n = 3, key5, key6, key7

此时结点中只存在一个关键字,而与其相邻的右兄弟结点中有 3 3 3 个关键字,满足 3 > 2 3 > 2 3>2 ,因此我们可以从右兄弟中借其最左侧的关键字 k e y 5 key5 key5;

  • 向右兄弟 一个关键字,其具体过程分为两步:
    • 从右兄弟中删除关键字 k e y 5 key5 key5,并将其插入到父结点中
    • 从父结点中删除最左侧的关键字 k e y 1 key1 key1 ,并将其插入到当前结点中

第二步: 删除 key1,并将其插入到当前结点中 将关键字key1插入到当前结点中 n = 2, key2, key5 n = 2, key3, key1 n = 2, key6, key7 第一步: 删除 key5,并将其插入到父结点中 将关键字key5 插入到父结点 n = 3, key1, key2, key5 n = 1, key3 n = 2, key6, key7

也就是说,虽然 操作是从相邻的兄弟结点中 一个关键字,但是实际上我们是执行了两次 操作:

  • 从父结点中 一个关键字插入到当前结点中
  • 从相邻结点中 的关键字填补到父结点中

而这个 的关键字也是有要求的:

  • 从左兄弟 ,则需要 左兄弟中最右侧的关键字
  • 从右兄弟 ,则需要 右兄弟中最左侧的关键字

这里就以上图中的关键字为例:

  • 在删除 k e y 4 key4 key4 之前,各关键字的大小为: k e y 3 < k e y 4 < k e y 1 < k e y 2 < k e y 5 < k e y 6 < k e y 7 key3 < key4 < key1 < key2 < key5 < key6 < key7 key3<key4<key1<key2<key5<key6<key7
  • 在删除 k e y 4 key4 key4 后,关键字的大小顺序为: k e y 3 < k e y 1 < k e y 2 < k e y 5 < k e y 6 < k e y 7 key3 < key1 < key2 < key5 < key6 < key7 key3<key1<key2<key5<key6<key7

而这些关键字对应的结点为:

k e y 3 ⏟ 当前结点 < k e y 1 < k e y 2 ⏟ 根结点 < k e y 5 < k e y 6 < k e y 7 ⏟ 右兄弟结点 \underbrace{key3}{当前结点} < \underbrace{key1 < key2}{根结点} < \underbrace{key5 < key6 < key7}_{右兄弟结点} \\ 当前结点 key3<根结点 key1<key2<右兄弟结点 key5<key6<key7

当我们向右兄弟 一个结点并将其插入到根结点中时,我们只能够找根结点中最大关键字 k e y 2 key2 key2 的直接后继,也就是 k e y 5 key5 key5 ,因此完成第一次 后,各关键字的位置做出了如下调整:

k e y 3 ⏟ 当前结点 < k e y 1 < k e y 2 ⏟ 根结点 < k e y 5 < k e y 6 < k e y 7 ⏟ 右兄弟结点 k e y 3 ⏟ 当前结点 < k e y 1 < k e y 2 < k e y 5 ⏟ 根结点 < k e y 6 < k e y 7 ⏟ 右兄弟结点 \underbrace{key3}{当前结点} < \underbrace{key1 < key2}{根结点} < \underbrace{\textcolor{red}{key5} < key6 < key7}{右兄弟结点} \\\ \\ \underbrace{key3}{当前结点} < \underbrace{key1 < key2 < \textcolor{red}{key 5}}{根结点} < \underbrace{key6 < key7}{右兄弟结点} 当前结点 key3<根结点 key1<key2<右兄弟结点 key5<key6<key7 当前结点 key3<根结点 key1<key2<key5<右兄弟结点 key6<key7

同理,当我们要从根结点中 一个关键字插入到当前结点中,我们也只能找 k e y 3 key3 key3 的直接后继 k e y 1 key1 key1,因此完成第二次 后,各关键字的位置做出了如下调整:

k e y 3 ⏟ 当前结点 < k e y 1 < k e y 2 < k e y 5 ⏟ 根结点 < k e y 6 < k e y 7 ⏟ 右兄弟结点 k e y 3 < k e y 1 ⏟ 当前结点 < k e y 2 < k e y 5 ⏟ 根结点 < k e y 6 < k e y 7 ⏟ 右兄弟结点 \underbrace{key3}{当前结点} < \underbrace{\textcolor{red}{key1} < key2 < key 5}{根结点} < \underbrace{key6 < key7}{右兄弟结点} \\\ \\ \underbrace{key3 < \textcolor{red}{key1}}{当前结点} < \underbrace{key2 < key 5}{根结点} < \underbrace{key6 < key7}{右兄弟结点} \\ 当前结点 key3<根结点 key1<key2<key5<右兄弟结点 key6<key7 当前结点 key3<key1<根结点 key2<key5<右兄弟结点 key6<key7

现在大家应该能够理解为什么我们是借的 k e y 5 key5 key5 与 k e y 1 key1 key1 了。若大家在后续的实战中,还是不太明白如何 ,那可以像我这样将相关结点中的关键字按照大小顺序进行排序,并对排序后的各关键字进行结点划分,这样就能一清二楚了;

若各位不想这么麻烦,这里我们也给大家做一个 的规则总结:

  • 向右兄弟
    • 右兄弟中的最小关键字,并将其插入到父结点中
    • 父结点中的最小关键字,并将其插入到当前结点中
  • 向左兄弟
    • 左兄弟中的最大关键字,并将其插入到父结点中
    • 父结点中的最大关键字,并将其插入到当前结点中

因此我们可以将 的总结为六个字:左借大,右借小

2.3.3 合并

当相邻的左右兄弟均不够借时,我们就需要通过 合并 来保证 B树 的性质。

合并 顾名思义,就是将两个结点 合并 成一个新的结点,但是,该合并过程并不是直接合并两个结点,而是在不改变各关键字的先后顺序的前提下,将当前结点、兄弟结点以及根结点中的关键字进行合并,组成新的结点,之后再对结点进行处理,使其继续保持 B树 的性质;

下面我们就以一棵 5阶B树 为例,说明 合并 的具体过程;
22 5, 11 36, 45 1, 3 6, 8, 9 13, 15 30, 35 40, 42 47, 48, 50, 56

在这棵 5阶B树 中,除根节点外,其余结点中的关键字数量需要保证在 [ 2 , 4 ] [2, 4] [2,4] 这个范围内;

假设我们需要删除关键字 35 。从图中我们不难发现,关键字 35 所在的结点以及与其相邻的两个兄弟结点中关键字的数量均为 2 2 2 ;

也就是说,当我们删除 35 这个关键字后,当前结点中的关键字数量就已经不满足 B树 的要求,并且我们也无法从兄弟结点中

在这种情况下,我们就需要通过 合并 操作来保证删除后的 B树 仍满足 B树 的性质。其具体过程如下:

  • 删除关键字 35

22 5, 11 36, 45 1, 3 6, 8, 9 13, 15 30 40, 42 47, 48, 50, 56

此时的树已经不再满足 B树 的性质,因此我们需要选择当前结点与其兄弟结点进行 合并

  • 合并 兄弟结点

合并 操作中的合并对象一定是 相同父结点的兄弟结点 ,因此,对于当前结点而言,与其相邻的且父结点相同的兄弟结点只有一个右兄弟结点,因此我们进行合并的对象一定是 右兄弟

在合并的过程中,我们需要将当前结点、右兄弟结点,以及这两个结点中间的位于父结点中的关键字 36 进行合并,形成一个新的结点:
22 5, 11 45 1, 3 6, 8, 9 13, 15 30, 36, 40, 42 47, 48, 50, 56

可以看到,合并后得到的新结点满足 B树 的性质: n ≤ m − 1 n \leq m - 1 n≤m−1 ,因此该结点无需做出任何调整,合并完成;

但是我们会发现,此时该结点的父结点中的关键字个数不再满足 B树 的性质,因此我们需要将 合并 操作向上传递,也就是对当前结点的父结点进行合并操作:
22 5, 11 45 1, 3 6, 8, 9 13, 15 30, 36, 40, 42 47, 48, 50, 56

从图中我们可以看到,45 所在的结点的具有相同父结点的兄弟结点只有该结点的左兄弟结点,因此我们需要将当前结点、左兄弟结点,以及位于父结点的两个结点中间的关键字进行合并,组成新的结点:
NULL 5, 11, 22, 45 1, 3 6, 8, 9 13, 15 30, 36, 40, 42 47, 48, 50, 56

在此次的 合并 操作完成后,我们发现新结点的父结点中不存在任何关键字,此时我们只需要将其删除,使新结点成为新的根结点即可:
5, 11, 22, 45 1, 3 6, 8, 9 13, 15 30, 36, 40, 42 47, 48, 50, 56

在完成本次的处理后,此时该棵 5路查找树 再一次恢复了 B树 的性质,并且树的高度由原来的 h = 3 h = 3 h=3 降至 h = 2 h = 2 h=2;

三、插入与删除小结

m阶B树 中,不管是 插入 还是 删除 均有以下共同点:

  • 最终的操作均是在 终端结点 上执行
  • 完成 终端结点 的操作后,还需要继续向上传递,直至树中的所有结点中的关键字个数均满足 [ ⌈ m 2 ⌉ − 1 , m − 1 ] [\lceil \frac{m}{2} \rceil - 1, m - 1] [⌈2m⌉−1,m−1]

不过这两种操作还是会存在不同之处:

  • 树高的变化不同:
    • 插入 操作可能会导致树高 + 1 + 1 +1
    • 删除 操作可能会导致树高 − 1 - 1 −1
  • 保持平衡的操作不同:
    • 插入 操作通过 分裂 保持 B树绝对平衡
    • 删除 操作通过 合并 保持 B树绝对平衡

插入 操作中,分裂 是将一个结点分裂为两个结点,并且可能会产生新的根结点;

删除 操作中, 的规则是 左借大,右借小合并 则是将两个结点以及位于父结点中的两个结点中间的关键字 合并 成一个新的结点,并且可能会直接删除原先的根结点;

结语

通过本文的详细探讨,我们系统性地掌握了B树两大核心动态操作------插入与删除的完整机制。让我们回顾一下其中的关键知识点:

🔄 核心操作流程回顾

B树 的插入与删除操作均遵循严格的平衡维护原则。

  • 插入操作 始于 终端结点 的精准定位,通过 结点分裂 机制(以 ⌈ m 2 ⌉ \lceil \frac{m}{2} \rceil ⌈2m⌉ 为分界点将关键字分为三部分)维持B树的平衡特性。当分裂向上传播至根节点时,会导致树高增加
  • 删除操作 则通过关键字替换 (将非终端结点的删除转化为终端结点处理)、借位操作 (遵循"左借大,右借小"原则)以及 结点合并 三种策略应对不同情况,确保删除后仍满足 B树 的严格定义。

⚖️ 平衡机制对比分析

插入与删除操作在维持B树平衡方面展现出不同的特性:

  • 插入操作通过主动分裂来预防节点溢出,是向上生长的过程;
  • 而删除操作则通过借位与合并来修复下溢,是向下调整的过程。

这种差异直接体现在树高变化上:

  • 插入可能导致树高增加
  • 删除则可能导致树高降低

值得注意的是,所有操作最终都在 终端结点 完成,并通过向上传递的链式反应确保整棵树的平衡性。

💡 实际应用价值
B树通过这些精心设计的操作机制,在外存数据管理(如数据库索引和文件系统)中展现出巨大优势。

其低树高特性显著减少了磁盘I/O次数,而动态平衡能力则确保了数据操作的高效稳定。

理解这些底层机制,对于设计高性能存储系统和进行数据库优化具有重要指导意义。

📚 知识体系整合

本文与前期讨论的B树查找操作和树高特性形成了完整知识体系。

从静态查找到动态维护,我们已全面掌握了B树这一重要数据结构的核心原理,为后续学习B+树等衍生结构奠定了坚实基础。

互动与分享

  • 点赞👍 - 您的认可是我持续创作的最大动力

  • 收藏⭐ - 方便随时回顾这些重要的基础概念

  • 转发↗️ - 分享给更多可能需要的朋友

  • 评论💬 - 欢迎留下您的宝贵意见或想讨论的话题

感谢您的耐心阅读! 关注博主,不错过更多技术干货。我们下一篇再见!

相关推荐
Queenie_Charlie4 小时前
小明统计数组
数据结构·c++·set
是毛毛吧4 小时前
数据结构与算法11种排序算法全面对比分析
数据结构·算法
长安er4 小时前
LeetCode 102/103/513 二叉树层序遍历(BFS)三类经典题解题总结
数据结构·算法·leetcode·二叉树·bfs·层序遍历
wregjru4 小时前
【C++进阶】1.C++ 模板进阶
数据结构·算法
暴风游侠8 小时前
linux知识点-内核参数相关
linux·运维·服务器·笔记
Galloping-Vijay12 小时前
Claude Code 使用笔记
笔记
极市平台14 小时前
骁龙大赛-技术分享第5期(上)
人工智能·经验分享·笔记·后端·个人开发
kupeThinkPoem14 小时前
计算机算法导论第三版算法视频讲解
数据结构·算法
啄缘之间14 小时前
11. UVM Test [uvm_test]
经验分享·笔记·学习·uvm·总结