数据结构——B树及其基本操作

B树及其基本操作

在前面讨论的平衡二叉树和红黑树中,每个节点只能存储一个关键字,这在内存中进行查找时是高效的。然而,在实际的数据库系统和文件系统中,数据量往往非常庞大,无法全部装入内存,需要频繁地在磁盘等外存设备上进行读写操作。由于磁盘的访问速度远慢于内存,减少磁盘访问次数成为提高系统性能的关键。如果使用二叉树结构,树的高度较大,意味着需要多次磁盘访问才能找到目标数据。为了适应外存储器的特点,需要一种能够减少树高度、每个节点可以存储多个关键字的树形结构,B树正是为解决这一问题而设计的多路平衡查找树。

1. B树的定义

B树是一种平衡的多路查找树,它特别适合于外存储器环境下的查找操作。一棵mmm阶B树是一棵平衡的mmm路搜索树,它或者是一棵空树,或者是满足以下性质的树。

一棵mmm阶B树需要满足的性质可以归纳为以下几点。首先,树中每个节点至多有mmm棵子树,即至多包含m−1m-1m−1个关键字。其次,除根节点外,每个非叶节点至少有⌈m/2⌉\lceil m/2 \rceil⌈m/2⌉棵子树,即至少包含⌈m/2⌉−1\lceil m/2 \rceil - 1⌈m/2⌉−1个关键字。对于根节点,如果它不是叶子节点,则至少有两棵子树。第三,所有的叶子节点都出现在同一层次上,并且不带信息,这些叶子节点实际上是查找失败时的位置,通常称为外部节点或失败节点。第四,每个节点内的关键字从小到大排列,节点内的关键字之间以及关键字与子树指针之间满足特定的大小关系。具体来说,对于某个节点,如果其关键字从小到大依次为K1,K2,...,KnK_1, K_2, ..., K_nK1,K2,...,Kn,子树指针依次为P0,P1,...,PnP_0, P_1, ..., P_nP0,P1,...,Pn,那么P0P_0P0所指子树中所有关键字均小于K1K_1K1,Pi(1≤i≤n−1)P_i(1 \leq i \leq n-1)Pi(1≤i≤n−1)所指子树中所有关键字均大于KiK_iKi且小于Ki+1K_{i+1}Ki+1,PnP_nPn所指子树中所有关键字均大于KnK_nKn。

为了更直观地理解B树的结构,我们来看一棵具体的3阶B树的示例。

40\] \[10, 20\] \[60, 80\] \[5\] \[15\] \[25, 30\] \[50\] \[70\] \[90, 95

上图展示了一棵3阶B树的结构。根节点包含关键字40,将整棵树分为两个部分。左子树的根节点包含关键字10和20,右子树的根节点包含关键字60和80。可以看到,除了根节点外,其他非叶节点都至少包含⌈3/2⌉−1=1\lceil 3/2 \rceil - 1 = 1⌈3/2⌉−1=1个关键字,至多包含3−1=23-1=23−1=2个关键字。所有最底层的节点都在同一层次上,保证了树的平衡性。

B树的阶数mmm的选择对树的性能有重要影响。阶数越大,每个节点能存储的关键字越多,树的高度就越小,需要的磁盘访问次数就越少。在实际应用中,B树的阶数通常根据磁盘页面的大小来确定,使得一个节点的大小正好等于一个磁盘页面的大小,这样每次磁盘访问可以读取一个完整的节点。

B树的高度是影响查找效率的关键因素。对于包含nnn个关键字、阶数为mmm的B树,其最大高度hhh满足关系式h≤log⁡⌈m/2⌉n+12+1h \leq \log_{\lceil m/2 \rceil} \frac{n+1}{2} + 1h≤log⌈m/2⌉2n+1+1。这表明B树的高度随着阶数的增加而显著降低,当阶数足够大时,即使存储海量数据,树的高度也能保持在很小的范围内,从而大大减少了磁盘访问次数。

2. B树的查找操作

B树的查找过程是一个在节点内进行顺序或折半查找,在节点间进行多路分支的过程。从根节点开始,在当前节点中查找待查关键字。如果找到,则查找成功;如果当前关键字大于节点内所有关键字或小于某个关键字且大于前一个关键字,则沿着相应的子树指针继续向下查找,直到找到目标关键字或到达叶子节点位置表示查找失败。

B树查找操作的具体过程可以通过一个查找实例来说明。假设在上述3阶B树中查找关键字70,首先从根节点开始,70大于40,沿着根节点的右子树指针向下;到达节点[60,80],70大于60且小于80,沿着该节点中60与80之间的指针向下;最终到达节点[70],找到目标关键字,查找成功。整个查找过程访问了3个节点,也就是进行了3次磁盘访问。

B树查找的时间复杂度主要取决于树的高度。在节点内部的查找可以使用折半查找,时间复杂度为O(log⁡2m)O(\log_2 m)O(log2m),但由于mmm通常是固定的常数,这部分时间可以忽略不计。主要的时间开销在于磁盘访问,访问次数等于从根到目标节点的路径长度,不超过树的高度。因此,B树查找的时间复杂度可以表示为O(log⁡mn)O(\log_m n)O(logmn),其中nnn是关键字总数,mmm是B树的阶数。相比二叉树的O(log⁡2n)O(\log_2 n)O(log2n),当mmm较大时,B树的查找效率明显更高。

3. B树的插入操作

B树的插入操作相对复杂,因为插入新关键字后可能导致节点中关键字数量超过上限,需要进行节点分裂操作来维持B树的性质。插入操作的基本思路是先找到插入位置,将关键字插入到相应的叶子节点,然后检查节点是否溢出,如果溢出则进行分裂调整。

B树插入操作的详细过程可以分为以下几个步骤进行说明。

(1)查找插入位置

首先利用B树的查找过程,找到应该插入新关键字的最底层的非叶子节点。这个过程与查找失败的过程相同,最终会到达某个最底层节点。

(2)插入关键字

将新关键字插入到找到的节点中,并保持节点内关键字的有序性。插入后,节点中的关键字数量加1。

(3)检查节点是否溢出

如果插入后节点中的关键字数量达到mmm个(超过了m−1m-1m−1的上限),则该节点发生溢出,需要进行分裂。如果没有溢出,插入操作完成。

(4)节点分裂

当节点溢出时,需要将该节点分裂为两个节点。具体方法是,取该节点中间位置的关键字K⌈m/2⌉K_{\lceil m/2 \rceil}K⌈m/2⌉,将其提升到父节点中,原节点中小于该关键字的部分作为左节点,大于该关键字的部分作为右节点。这个过程可能导致父节点也发生溢出,需要继续向上分裂,直到根节点。

(5)根节点分裂

如果根节点发生溢出,则分裂根节点,原根节点的中间关键字成为新的根节点,分裂后的两部分成为新根的子树。这是B树高度增加的唯一方式。

下面通过具体的插入过程来说明节点分裂的情况。假设向一棵3阶B树中依次插入关键字。
分裂形成新树 插入50后溢出 初始状态 [40] [50] [60] [40, 50, 60]
溢出 [40, 60]

上图展示了向初始只有一个节点的3阶B树中插入关键字50导致溢出和分裂的过程。初始节点包含关键字40和60,插入50后节点包含3个关键字,超过了m−1=2m-1=2m−1=2的上限,发生溢出。分裂时取中间关键字50上升为新的根节点,40和60分别成为其左右子节点。

继续观察更复杂的插入分裂过程。
分裂后 向左子树插入30 插入前 [20] [30, 50] [40] [60, 70] [20, 30, 40]
溢出 [50] [60, 70] [20, 40] [50] [60, 70]

上图展示了插入导致非根节点溢出和分裂的情况。插入关键字30后,左子节点包含3个关键字发生溢出。分裂时取中间关键字30上升到父节点,20和40分别成为独立的子节点。分裂后根节点包含两个关键字30和50,有三棵子树,符合3阶B树的性质。

B树的插入操作最多需要沿着从根到叶的路径进行一次向上的分裂传播,时间复杂度为O(log⁡mn)O(\log_m n)O(logmn)。每次分裂操作都会减少树中某些节点的关键字数量,但不会影响整棵树的平衡性,所有叶子节点始终保持在同一层次上。

4. B树的删除操作

B树的删除操作是最复杂的操作,因为删除关键字后可能导致节点中关键字数量少于下限,需要通过借用兄弟节点的关键字或合并节点来维持B树的性质。删除操作需要考虑被删除关键字所在节点的位置以及删除后的调整策略。

B树删除操作的基本过程可以分为以下几种情况来讨论。

(1)删除叶子节点中的关键字

如果被删除的关键字在最底层的非叶子节点中,直接删除该关键字。删除后,如果该节点的关键字数量仍然不少于⌈m/2⌉−1\lceil m/2 \rceil - 1⌈m/2⌉−1,则删除操作完成。如果关键字数量少于下限,则需要进行调整。

(2)删除非叶子节点中的关键字

如果被删除的关键字在非叶子节点中,需要用该关键字的前驱或后继来替换它,然后删除该前驱或后继。前驱是该关键字左子树中最大的关键字,后继是右子树中最小的关键字,它们都位于最底层节点中,因此问题转化为情况(1)。

(3)节点关键字数量不足的调整

当删除导致节点关键字数量少于⌈m/2⌉−1\lceil m/2 \rceil - 1⌈m/2⌉−1时,需要进行调整。调整方法有两种:一是向兄弟节点借关键字,二是与兄弟节点合并。

如果相邻的兄弟节点中有节点的关键字数量大于⌈m/2⌉−1\lceil m/2 \rceil - 1⌈m/2⌉−1,可以从该兄弟节点借一个关键字。具体做法是,将父节点中分隔两个节点的关键字下移到关键字不足的节点,同时将兄弟节点中的相应关键字上移到父节点。
向右兄弟借关键字 删除30后不足 删除前 [20, 50] [60] [70, 80] [20]
不足 [50] [60, 70, 80] [20, 30] [50] [60, 70, 80]

上图展示了通过借用兄弟节点关键字来调整的过程。删除关键字30后,左子节点只剩一个关键字20,少于⌈3/2⌉−1=1\lceil 3/2 \rceil - 1 = 1⌈3/2⌉−1=1的下限。右兄弟节点有3个关键字,可以借用。将父节点的关键字50下移到左子节点,同时将右兄弟的最小关键字60上移到父节点,调整完成。

如果相邻的兄弟节点的关键字数量都等于⌈m/2⌉−1\lceil m/2 \rceil - 1⌈m/2⌉−1,无法借用关键字,则需要将关键字不足的节点与一个兄弟节点合并。合并时,将父节点中分隔两个节点的关键字下移,与两个子节点的关键字合并成一个节点。这个过程可能导致父节点的关键字数量减少,如果父节点也出现关键字不足,则需要继续向上调整。
与兄弟合并 删除20后不足 删除前 [30, 40, 50] [60] [70] 空
删除 [30, 60] [40, 50] [70] [20] [30, 60] [40, 50] [70]

上图展示了通过合并节点进行调整的过程。删除关键字20后,该节点变为空,需要删除。由于兄弟节点也只有最少数量的关键字,无法借用,因此将父节点中的关键字30下移,与兄弟节点[40,50]合并,形成新节点[30,40,50]。父节点关键字减少,但仍满足B树的性质。

B树的删除操作最多需要沿着从根到叶的路径进行一次向上的合并传播,时间复杂度为O(log⁡mn)O(\log_m n)O(logmn)。删除操作可能导致树的高度减小,这发生在根节点只剩一个子树时,此时删除根节点,其唯一的子树成为新的根。

5. B树的性能分析

B树作为一种专门为外存储器设计的数据结构,在大规模数据存储和检索方面具有显著优势。B树的核心优势在于通过增加节点容量来减少树的高度,从而减少磁盘访问次数。

从磁盘访问角度分析,一棵包含nnn个关键字的mmm阶B树,其高度不超过log⁡⌈m/2⌉n+12+1\log_{\lceil m/2 \rceil} \frac{n+1}{2} + 1log⌈m/2⌉2n+1+1。当mmm取较大值时,例如m=200m=200m=200,即使存储百万级的数据,树的高度也只有3到4层,这意味着查找任意关键字最多只需要3到4次磁盘访问。相比之下,如果使用红黑树存储相同数据,树的高度约为20层,需要20次磁盘访问,效率差距明显。

B树的空间利用率也值得关注。由于每个节点至少要包含⌈m/2⌉−1\lceil m/2 \rceil - 1⌈m/2⌉−1个关键字,最坏情况下每个节点只包含最少数量的关键字,此时空间利用率约为50%。但在实际应用中,通过合理的插入和删除策略,B树的空间利用率通常能达到69%左右,这是一个比较理想的水平。

B树的应用非常广泛,几乎所有的数据库管理系统都使用B树或其变种作为索引结构。文件系统如NTFS、ext4等也使用B树来组织目录结构和文件元数据。B树的成功在于它很好地平衡了查找效率、存储空间和维护代价,特别是它针对磁盘等外存储器的块访问特性进行了优化,使得在外存环境下的性能表现优异。

相关推荐
尘世中一位迷途小书童4 小时前
Vuetify Admin 后台管理系统
前端·前端框架·开源
伟大的车尔尼4 小时前
双指针的概念
数据结构·算法·双指针
2301_801252224 小时前
前端框架Vue(Vue 的挂载点与 data 数据对象)
java·前端·javascript·vue.js·前端框架
@卞4 小时前
排序算法(1)--- 插入排序
数据结构·算法·排序算法
zh_xuan5 小时前
LeeCode 74. 搜索二维矩阵
数据结构·算法·leecode
.ZGR.5 小时前
蓝桥杯题库——部分简单题题解(Java)
java·数据结构·算法
小李小李快乐不已6 小时前
图论理论基础(1)
数据结构·算法·leetcode·深度优先·图论·广度优先·宽度优先
熬了夜的程序员6 小时前
【LeetCode】80. 删除有序数组中的重复项 II
java·数据结构·算法·leetcode·职场和发展·排序算法·动态规划