B+树(B+ Tree)的原理与操作
字数 2857 2025-12-07 11:48:02
B+树(B+ Tree)的原理与操作
B+树是一种常用的平衡多路搜索树,广泛应用于数据库和文件系统的索引结构中。它是B树的一种变体,在设计上对范围查询和顺序访问进行了优化。B+树与B树的核心区别在于:B+树的所有数据记录都存储在叶子节点中,内部节点仅存储键值用于索引,且叶子节点之间通过指针连接成有序链表。
一、B+树的核心性质
- 平衡性:从根节点到每个叶子节点的路径长度相同。
- 节点容量:每个节点(除根节点)的关键字数量在预定范围内。设最小度为 \(t\)(通常 \(t \geq 2\)),则:
- 内部节点(非根、非叶)的关键字数 \(k\) 满足:\(t-1 \leq k \leq 2t-1\)。
- 根节点关键字数:\(1 \leq k \leq 2t-1\)。
- 叶子节点的关键字数通常与内部节点相同,但不同实现可能有差异。
- 关键字与指针:
- 内部节点:若包含 \(n\) 个关键字 \(K_1, K_2, \dots, K_n\)(按升序排列),则有 \(n+1\) 个子节点指针 \(P_1, P_2, \dots, P_{n+1}\)。对于任意关键字 \(X\):
- 指针 \(P_i\) 指向的子树中的所有关键字 \(< K_i\)(\(1 \leq i \leq n\))。
- 指针 \(P_{n+1}\) 指向的子树中的所有关键字 \(\geq K_n\)。
- 叶子节点:包含关键字和指向数据记录(或数据块)的指针。所有叶子节点通过指针链成双向(或单向)有序链表。
- 内部节点:若包含 \(n\) 个关键字 \(K_1, K_2, \dots, K_n\)(按升序排列),则有 \(n+1\) 个子节点指针 \(P_1, P_2, \dots, P_{n+1}\)。对于任意关键字 \(X\):
- 数据存储位置:所有数据记录仅存在于叶子节点。内部节点关键字是叶子节点关键字的“索引副本”,用于路由。
二、B+树的查找操作
查找从根节点开始,逐层向下,直到叶子节点。
步骤示例:查找关键字 \(key = 25\)。
- 从根节点开始,在节点内找到第一个大于等于 \(key\) 的关键字位置 \(i\)(若 \(key\) 小于所有关键字,则 \(i=1\))。
- 若当前节点是内部节点,沿指针 \(P_i\) 进入子节点。
- 重复步骤1-2,直到到达叶子节点。
- 在叶子节点中搜索 \(key\):
- 若找到,则通过叶子节点中的指针访问对应数据记录。
- 若未找到,则说明关键字不存在。
时间复杂度:树高 \(h = O(\log_t N)\),其中 \(N\) 为关键字总数。每次查找需访问 \(h\) 个节点,但通常 \(t\) 较大(如几百),使得树高很低,磁盘I/O次数少。
三、B+树的插入操作
插入操作需保持平衡,可能引起节点分裂。设插入关键字 \(key\) 及其对应数据。
步骤详解:
- 定位叶子节点:通过查找操作找到应插入的叶子节点 \(L\)。
- 插入叶子节点:
- 若 \(L\) 中关键字数 \(< 2t-1\),则直接按序插入 \(key\) 和数据指针,操作结束。
- 若 \(L\) 已满(关键字数 \(= 2t-1\)),需分裂 \(L\):
a. 创建新叶子节点 \(L'\)。
b. 将 \(L\) 中的关键字(包括新插入的)平均分配:原 \(L\) 保留前 \(t\) 个,\(L'\) 包含后 \(t\) 个(注意:这里分裂后每个节点关键字数可设计为 \(t\) 或 \(t-1\),常见为 \(t\))。
c. 更新叶子链表:将 \(L'\) 插入链表,使 \(L.next = L'\)。
d. 将 \(L'\) 的最小关键字复制到父节点作为索引(注意:B+树内部节点存储的是“副本”,不删除叶子中的关键字)。
- 更新父节点:
- 将指向 \(L'\) 的指针和新索引关键字插入父节点。
- 若父节点已满,则递归分裂父节点(分裂内部节点与叶子类似,但分裂时上提中间关键字到父节点,且不在子节点保留副本)。
- 根节点分裂:若根节点分裂,则创建新根,树高增加1。
示例:在最小度 \(t=2\) 的B+树中插入序列 [5, 10, 15, 20, 25]。
- 初始:插入5,10后,根节点(叶子)为 [5,10]。
- 插入15:根节点满,分裂为叶子节点 L1=[5,10] 和 L2=[15],创建新根节点 [15](指向L1和L2)。
- 插入20:找到L2,插入后为 [15,20]。
- 插入25:L2满,分裂为 L2=[15] 和 L3=[20,25],将20插入父节点(根),根变为 [15,20]。树结构稳定。
四、B+树的删除操作
删除可能导致节点关键字数过少,需合并或借关键字以保持平衡。
步骤详解:
- 定位叶子节点:查找包含关键字 \(key\) 的叶子节点 \(L\)。
- 删除关键字:从 \(L\) 中删除 \(key\) 及其数据指针。
- 处理下溢:若 \(L\) 中关键字数 \(\geq t-1\)(或满足实现规定的最小值),则结束。否则需调整:
- 借关键字:若相邻兄弟节点(左或右)有多余关键字(\(> t-1\)),则从兄弟借一个关键字,并更新父节点中的索引关键字。
- 借左兄弟:将左兄弟的最大关键字移到 \(L\) 的最小位置,父节点中对应索引更新为 \(L\) 的新最小关键字。
- 借右兄弟:将右兄弟的最小关键字移到 \(L\) 的最大位置,父节点中对应索引更新为右兄弟的新最小关键字。
- 合并节点:若兄弟节点无多余关键字,则将 \(L\) 与一个兄弟合并,并删除父节点中对应的索引关键字。
- 合并后,递归检查父节点是否下溢,直至根节点。若根节点只剩一个子节点,则可降低树高。
- 借关键字:若相邻兄弟节点(左或右)有多余关键字(\(> t-1\)),则从兄弟借一个关键字,并更新父节点中的索引关键字。
- 更新内部节点索引:若删除的关键字在内部节点中出现(作为索引),需替换为后继关键字(即叶子链表中的下一个最小关键字)。
示例:从上述树中删除20。
- 找到叶子节点 L3=[20,25],删除20后为 [25]。
- 检查下溢:\(t=2\),最小关键字数应为1,[25] 合法。但需更新父节点:原索引20被替换为25(因20是索引),根变为 [15,25]。
- 若删除25后 L3=[],则需与兄弟 L2=[15] 合并,形成 [15],父节点删除索引25,递归处理。
五、B+树与B树的对比优势
- 范围查询高效:叶子链表支持顺序遍历,无需回溯到内部节点。
- 更高扇出:内部节点仅存键,不存数据指针,可容纳更多关键字,降低树高。
- 查询稳定:所有查询均需到达叶子,时间复杂度恒定。
六、应用场景
- 数据库索引(如MySQL InnoDB)。
- 文件系统(如NTFS、ReiserFS)。
- 键值存储引擎。
通过以上步骤,你可以理解B+树如何通过平衡操作维护高效检索。实际实现中需注意磁盘页对齐、并发控制等工程细节。