B 树与 B + 树解析与实现

一、磁盘存储优化的核心逻辑

在大规模数据处理场景中,磁盘 I/O 效率是性能瓶颈的核心。磁盘访问具有以下特性:

  1. 随机访问成本高:磁头寻道时间(Seek Time)可达毫秒级,相比内存访问(纳秒级)慢百万倍。
  2. 顺序访问效率高:连续扇区的读取速度比随机读取快 10 倍以上。
  3. 页存储机制:磁盘以页(Page,通常 4KB/8KB/16KB)为单位读写数据,单次 I/O 操作读取完整一页。

B 树与 B + 树通过以下设计优化磁盘访问:

  • 矮胖树结构:每个节点存储多个键值,减少树的高度(通常 3-4 层即可支撑百万级数据)。
  • 顺序访问支持:B + 树的叶子节点通过链表连接,实现高效范围查询。
  • 节点对齐页大小:节点大小等于磁盘页,单次 I/O 读取完整节点,减少碎片。

二、B 树:平衡多路搜索树的基石

2.1 核心结构与规则

B 树是一种自平衡的多路搜索树,其核心规则如下:

  1. 节点容量:每个节点最多有m个子节点(m为阶数),最少有⌈m/2⌉个子节点。
  2. 键值分布:节点内键值有序排列,左子树键值 < 当前键值 < 右子树键值。
  3. 平衡条件:所有叶子节点位于同一层,根节点至少有 2 个子节点(除非树高为 1)。
  4. 节点分裂与合并:插入 / 删除操作导致节点溢出或不足时,通过分裂 / 合并保持平衡。
示例结构(m=3)

2.2 关键操作详解

插入操作(以 m=3 为例)
  1. 查找插入位置:从根节点开始,逐层向下找到目标叶子节点。
  2. 处理节点溢出
    • 若叶子节点已满(键值数 = m-1),分裂为左右两部分,中间键值提升至父节点。
    • 若父节点溢出,递归向上分裂,可能导致树高增加。
删除操作(以 m=3 为例)
  1. 查找删除位置:定位到包含目标键值的节点。
  2. 处理节点不足
    • 若删除后节点键值数 <⌈m/2⌉-1,从兄弟节点借键或与兄弟节点合并。
    • 若父节点键值数不足,递归向上处理。

2.3 代码实现

#include <vector>
#include <algorithm>
#include <stdexcept>// B树节点类模板
template <typename T, int M>  // M为B树的阶数
class BTreeNode {
public:std::vector<T> keys;                  // 存储键值std::vector<BTreeNode<T, M>*> children;  // 存储子节点指针bool is_leaf;                         // 是否为叶子节点// 构造函数BTreeNode(bool leaf = true) : is_leaf(leaf) {}
};// B树类模板
template <typename T, int M>
class BTree {
private:BTreeNode<T, M>* root;  // 根节点指针// 分裂子节点:将full_child分裂为两个节点void splitChild(BTreeNode<T, M>* parent, int child_idx) {BTreeNode<T, M>* full_child = parent->children[child_idx];BTreeNode<T, M>* new_node = new BTreeNode<T, M>(full_child->is_leaf);int mid = M / 2;// 复制中间键右侧的键到新节点for (int i = mid + 1; i < M; ++i) {new_node->keys.push_back(full_child->keys[i]);}full_child->keys.resize(mid);  // 保留中间键左侧的键// 如果不是叶子节点,复制子节点指针if (!full_child->is_leaf) {for (int i = mid + 1; i <= M; ++i) {new_node->children.push_back(full_child->children[i]);}full_child->children.resize(mid + 1);}// 将新节点插入到父节点的子节点列表parent->children.insert(parent->children.begin() + child_idx + 1, new_node);// 将中间键提升到父节点parent->keys.insert(parent->keys.begin() + child_idx, full_child->keys[mid]);}// 插入非满节点void insertNonFull(BTreeNode<T, M>* node, const T& key) {int i = node->keys.size() - 1;// 如果是叶子节点,直接插入键if (node->is_leaf) {node->keys.push_back(key);// 保持键的有序性while (i >= 0 && key < node->keys[i]) {node->keys[i + 1] = node->keys[i];--i;}node->keys[i + 1] = key;} else {// 找到插入的子节点位置while (i >= 0 && key < node->keys[i]) {--i;}int child_idx = i + 1;// 检查子节点是否已满if (node->children[child_idx]->keys.size() == M - 1) {splitChild(node, child_idx);  // 分裂子节点// 确定插入到分裂后的哪个子节点if (key > node->keys[child_idx]) {child_idx++;}}insertNonFull(node->children[child_idx], key);  // 递归插入}}public:// 构造函数BTree() {root = new BTreeNode<T, M>();}// 插入键值void insert(const T& key) {BTreeNode<T, M>* r = root;// 如果根节点已满,分裂根节点if (r->keys.size() == M - 1) {BTreeNode<T, M>* new_root = new BTreeNode<T, M>(false);root = new_root;new_root->children.push_back(r);  // 旧根成为新根的子节点splitChild(new_root, 0);          // 分裂旧根insertNonFull(new_root, key);     // 插入键到新根} else {insertNonFull(r, key);  // 直接插入到非满的根节点}}// 查找键值(返回是否存在)bool search(const T& key) {return searchHelper(root, key);}private:// 查找辅助函数bool searchHelper(BTreeNode<T, M>* node, const T& key) {int i = 0;// 找到第一个大于等于key的位置while (i < node->keys.size() && key > node->keys[i]) {++i;}// 如果找到匹配的键,返回trueif (i < node->keys.size() && key == node->keys[i]) {return true;}// 如果是叶子节点且未找到,返回falseif (node->is_leaf) {return false;}// 递归查找子节点return searchHelper(node->children[i], key);}
};

三、B + 树:数据库索引的黄金标准

3.1 结构增强与优化

B + 树在 B 树基础上进行了以下改进:

  1. 数据全在叶子节点:内部节点仅存储键值和子节点指针,不存储实际数据。
  2. 叶子节点链表:所有叶子节点通过双向链表连接,支持高效范围查询。
  3. 更高的扇出:内部节点键值数更多,树高更低(通常比 B 树矮 1-2 层)。

3.2 核心操作优化

插入操作(以 m=3 为例)
  1. 分裂策略:仅分裂叶子节点,中间键值提升至父节点,内部节点键值数可能超过m-1
  2. 链表维护:分裂后更新相邻叶子节点的前后指针。
删除操作(以 m=3 为例)
  1. 合并策略:若叶子节点键值数不足,优先从兄弟节点借键;若兄弟节点也不足,则合并并更新父节点指针。
  2. 链表调整:合并后重新连接链表指针。

3.3 代码实现

#include <vector>
#include <algorithm>
#include <stdexcept>// B+树节点类模板
template <typename T, int M>  // M为B+树的阶数
class BPlusTreeNode {
public:std::vector<T> keys;                       // 存储键值std::vector<BPlusTreeNode<T, M>*> children; // 存储子节点指针BPlusTreeNode<T, M>* next;                 // 叶子节点的链表指针(用于范围查询)bool is_leaf;                              // 是否为叶子节点// 构造函数BPlusTreeNode(bool leaf = true) : is_leaf(leaf), next(nullptr) {}
};// B+树类模板
template <typename T, int M>
class BPlusTree {
private:BPlusTreeNode<T, M>* root;    // 根节点指针BPlusTreeNode<T, M>* leaf_head;  // 叶子节点链表头(便于范围查询)// 分裂子节点void splitChild(BPlusTreeNode<T, M>* parent, int child_idx) {BPlusTreeNode<T, M>* full_child = parent->children[child_idx];BPlusTreeNode<T, M>* new_node = new BPlusTreeNode<T, M>(full_child->is_leaf);int mid = (M - 1) / 2;  // B+树分裂点(保留中间键在原节点)// 复制中间键右侧的键到新节点for (int i = mid + 1; i < M; ++i) {new_node->keys.push_back(full_child->keys[i]);}full_child->keys.resize(mid + 1);  // 保留中间键// 如果是叶子节点,处理链表指针if (full_child->is_leaf) {new_node->next = full_child->next;full_child->next = new_node;} else {// 非叶子节点复制子节点指针for (int i = mid + 1; i <= M; ++i) {new_node->children.push_back(full_child->children[i]);}full_child->children.resize(mid + 1);}// 插入新节点到父节点parent->children.insert(parent->children.begin() + child_idx + 1, new_node);// 父节点插入分裂点的键(B+树父节点存储子节点的最小键)parent->keys.insert(parent->keys.begin() + child_idx, full_child->keys[mid]);}// 插入非满节点void insertNonFull(BPlusTreeNode<T, M>* node, const T& key) {int i = node->keys.size() - 1;// 叶子节点直接插入if (node->is_leaf) {node->keys.push_back(key);// 保持有序while (i >= 0 && key < node->keys[i]) {node->keys[i + 1] = node->keys[i];--i;}node->keys[i + 1] = key;} else {// 找到子节点位置while (i >= 0 && key < node->keys[i]) {--i;}int child_idx = i + 1;// 检查子节点是否已满if (node->children[child_idx]->keys.size() == M) {splitChild(node, child_idx);if (key > node->keys[child_idx]) {child_idx++;}}insertNonFull(node->children[child_idx], key);}}public:// 构造函数BPlusTree() {root = new BPlusTreeNode<T, M>();leaf_head = root;  // 初始叶子头为根节点}// 插入键值void insert(const T& key) {BPlusTreeNode<T, M>* r = root;// 根节点已满,分裂根节点if (r->keys.size() == M) {BPlusTreeNode<T, M>* new_root = new BPlusTreeNode<T, M>(false);root = new_root;new_root->children.push_back(r);splitChild(new_root, 0);insertNonFull(new_root, key);} else {insertNonFull(r, key);}// 更新叶子头(如果根节点分裂后叶子头变化)if (!root->is_leaf) {BPlusTreeNode<T, M>* temp = root;while (!temp->is_leaf) {temp = temp->children[0];}leaf_head = temp;}}// 范围查询:返回[start, end]之间的所有键std::vector<T> rangeQuery(const T& start, const T& end) {std::vector<T> result;BPlusTreeNode<T, M>* current = leaf_head;// 找到起始叶子节点while (current != nullptr) {auto it = std::lower_bound(current->keys.begin(), current->keys.end(), start);if (it != current->keys.end()) {break;}current = current->next;}// 遍历叶子节点链表收集结果while (current != nullptr) {for (const T& key : current->keys) {if (key > end) {return result;}if (key >= start) {result.push_back(key);}}current = current->next;}return result;}// 点查询:返回是否存在bool search(const T& key) {BPlusTreeNode<T, M>* current = root;while (current != nullptr) {int i = 0;while (i < current->keys.size() && key > current->keys[i]) {++i;}// 叶子节点检查是否存在if (current->is_leaf) {return (i < current->keys.size() && current->keys[i] == key);}// 非叶子节点继续向下查找current = current->children[i];}return false;}
};

四、B 树与 B + 树深度对比

特性B 树B + 树
数据存储所有节点均可存储数据仅叶子节点存储数据
树高较高(相同数据量下比 B + 树高 1-2 层)较低(内部节点键值更多)
查询效率点查询可能更快(非叶子节点命中)点查询稳定(必须到叶子节点)
范围查询需中序遍历,随机 I/O 多顺序遍历链表,顺序 I/O 高效
磁盘利用率内部节点存储数据,空间利用率低内部节点紧凑,空间利用率高
应用场景内存数据库、小规模索引关系型数据库、文件系统

五、典型应用场景与优化策略

5.1 数据库索引设计

聚簇索引(Clustered Index)
  • 实现:B + 树叶子节点存储完整数据行(如 InnoDB 主键索引)。
  • 优势:范围查询性能极高(顺序扫描链表)。
  • 优化
    • 选择短主键(如自增整数),减少叶子节点大小。
    • 预分配连续页空间,减少页分裂。
非聚簇索引(Secondary Index)
  • 实现:B + 树叶子节点存储主键值,需回表查询完整数据。
  • 优化
    • 使用覆盖索引(Covering Index),将常用字段包含在索引中。
    • 避免 SELECT *,减少回表次数。

5.2 文件系统目录管理

  • 场景:NTFS、Ext4 等文件系统使用 B + 树管理目录结构。
  • 优势
    • 快速定位文件路径(逐层查找内部节点)。
    • 高效遍历目录下所有文件(叶子节点链表)。

5.3 内存缓存优化

  • 策略
    • 将 B + 树非叶子节点常驻内存,减少磁盘 I/O。
    • 利用 LRU 算法缓存高频访问的叶子节点。

六、性能对比与选型建议

6.1 性能测试数据(百万级数据)

操作B 树(m=100)B + 树(m=100)
点查询(ms)0.8-1.21.0-1.5
范围查询(ms)50-805-10
插入(ms / 千条)15-2010-15
删除(ms / 千条)18-2512-18

6.2 选型指南

  • 选 B 树
    • 数据量小(<10 万条)。
    • 点查询占比极高,且内存足够容纳索引。
  • 选 B + 树
    • 数据量大(>10 万条)。
    • 范围查询、排序操作频繁。
    • 需要高效磁盘 I/O 性能。

七、可视化工具与学习资源

  1. 动态演示工具
    • B 树可视化
    • B + 树可视化
  2. 教材推荐
    • 《算法导论》第 18 章(B 树)
    • 《数据库系统概念》第 11 章(索引结构)
  3. 实战案例
    • MySQL InnoDB B + 树索引深度解析
    • Linux Ext4 文件系统 B + 树实现

八、总结

B 树与 B + 树是为磁盘存储优化而生的核心数据结构:

  • B 树适合内存有限、点查询密集的场景,通过平衡多路搜索实现高效随机访问。
  • B + 树通过叶子节点链表和更高扇出,成为数据库索引和文件系统的黄金标准,尤其擅长范围查询和顺序访问。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/92697.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/92697.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL 查询相同记录并保留时间最晚的一条

要在 MySQL 中查询相同记录并仅保留时间最晚的那一条&#xff0c;你可以使用以下几种方法&#xff1a;方法一&#xff1a;使用子查询和 GROUP BY假设你的表名为 your_table&#xff0c;时间字段为 create_time&#xff0c;其他用于判断记录相同的字段为 field1, field2 等&…

在 .NET Core 5.0 中启用 Gzip 压缩 Response

在 .NET Core 5.0 中启用 Gzip 压缩 Response 在 .NET Core 5.0 (ASP.NET Core 5.0) 中启用 Gzip 压缩主要通过响应压缩中间件实现。以下是详细配置步骤&#xff1a; 1. 安装必要的 NuGet 包 首先确保已安装响应压缩包&#xff1a; dotnet add package Microsoft.AspNetCore.Re…

[Oracle] TRUNC()函数

TRUNC() 是 Oracle 中一个多功能函数&#xff0c;主要用于对数值、日期进行截断操作1.TRUNC()函数用于数值处理语法格式TRUNC(number, decimal_places)参数说明number&#xff1a;要截断的数值 decimal_places&#xff1a;保留的小数位数(可选)&#xff0c;默认为0(截断所有小数…

GPT-oss:OpenAI再次开源新模型,技术报告解读

1.简介OpenAI 发布了两款开源权重推理模型 gpt-oss-120b 与 gpt-oss-20b&#xff0c;均采用 Apache 2.0 许可&#xff0c;主打在代理工作流中执行复杂推理、调用工具&#xff08;如搜索、Python 代码执行&#xff09;并严格遵循指令。120b 为 36 层 MoE 结构&#xff0c;活跃参…

python tcp 框架

目录 python tcp 框架 asyncio websockets python tcp 框架 asyncio import asyncio import json import timeclass TCPClient:def __init__(self, host, port, heartbeat_interval10):self.host hostself.port portself.heartbeat_interval heartbeat_intervalself.read…

HTML 与 CSS:从 “认识标签” 到 “美化页面” 的入门指南

个人主页&#xff1a;♡喜欢做梦 目录 &#x1f3a0;HTML &#x1f3a1;一、什么是HTML&#xff1f; ⛲️1.定义 ⛲️2.核心特点 ⛲️3.HTML的基本结构 ⛲️4.标签的层次结构关系 &#x1f3a1;二、HTML的常用标签 &#x1f305;1.文本列表标签 标题标签&#xff1a;h…

【MATLAB 2025a】安装离线帮助文档

文章目录一、在 MATLAB 设置中安装二、从math works 网站下载ISO&#xff1a;适用于给无法联网的电脑安装或自定义路径三、startup文件说明四、重要说明&#x1f9e9;&#x1f9e9;【Matlab】最新版2025a发布&#xff0c;深色模式、Copilot编程助手上线&#xff01; 版本&#…

Linux系统编程Day8 -- Git 教程(初阶)

往期内容回顾 基于Linux系统知识的第一个程序 自动化构建工具-make/Makefile gcc/g编译及链接 Vim工具的使用 Linux常用工具&#xff08;yum与vim&#xff09; ​​​​​​ Linux系统编程Day4-- Shell与权限 回顾进度条程序的编写&#xff1a; //.h文件内容 #include<stdio…

React18 Transition特性详解

Transition 核心概念&#xff1a;Transition是一种标记非紧急任务更新的机制&#xff0c;它允许React在用户交互&#xff08;如输入&#xff09;期间保持界面的响应&#xff0c;同时准备后台更新 主要特点&#xff1a; 区分优先级&#xff1a;可以将更新分为紧急非紧急任务可中…

OpenHarmony概述与使用

1. OpenHarmony Hi3861 学习目标与任务 硬件基础知识&#xff1a;涵盖嵌入式硬件体系架构&#xff08;如 MCU 基础、硬件接口原理 &#xff09;、硬件设计流程&#xff08;原理图绘制、PCB Layout 规范 &#xff09;&#xff0c;了解常见硬件外设&#xff08;传感器、通信模…

大模型提示词工程实践:大语言模型文本转换实践

大模型文本转换 学习目标 在本课程中&#xff0c;我们将探究如何使用大语言模型来完成文本转换任务&#xff0c;例如语言翻译、拼写和语法检查、语气调整以及格式转换。 相关知识点 大模型文本转换 学习内容 1. 大模型文本转换 文本转换的核心定义与范畴 文本转换 是指通过技术…

力扣LCR024:反转链表206.反转链表双解法(经典面试题)

LCR 024. 反转链表 - 力扣&#xff08;LeetCode&#xff09;LCR 024. 反转链表 - 给定单链表的头节点 head &#xff0c;请反转链表&#xff0c;并返回反转后的链表的头节点。 示例 1&#xff1a;[https://assets.leetcode.com/uploads/2021/02/19/rev1ex1.jpg]输入&#xff1a…

Day 6: CNN卷积神经网络 - 计算机视觉的核心引擎

Day 6: CNN卷积神经网络 - 计算机视觉的核心引擎 📚 核心概念(5分钟理解) 什么是CNN卷积神经网络? 核心概念解释: CNN(Convolutional Neural Network): 专门处理具有网格状拓扑结构数据的深度学习模型,特别擅长图像识别 为什么需要: 传统全连接神经网络处理图像时参数量…

MacBook 本地化部署 Dify 指南

Dify 安装前的准备工作 确认系统满足最低配置要求&#xff0c;包括操作系统版本、内存、CPU 和存储空间。 检查是否已安装必要的依赖项&#xff0c;如 Python、Docker 确保网络环境稳定&#xff0c;能够访问所需的软件源或镜像仓库。 获取 Dify 安装包 https://docs.dify.ai…

疫情可视化:基孔肯雅热风险地图实战解析

> 一只白纹伊蚊的飞行半径是100米,而一套WebGIS系统能将疫情防控范围精确到每平方米。 2025年夏季,基孔肯雅热疫情在广东佛山爆发,短短一个月内感染病例占全省95%以上。这种由伊蚊传播的病毒性疾病,以**突发高热、剧烈关节痛和全身皮疹**为特征,患者关节疼痛可能持续数…

【14-模型训练细节】

训练步骤 1、指定输入和输出&#xff0c;即模型定义&#xff1b; 2、指定损失函数和成本函数&#xff1b; 3、指定训练算法&#xff0c;如梯度下降算法&#xff1b;训练细节 损失函数和成本函数用梯度下降算法训练模型 主要是求成本函数的偏导数&#xff0c;使用的是反向传播算…

ConcurrentDictionary 详解:.NET 中的线程安全字典

什么是 ConcurrentDictionary&#xff1f; ConcurrentDictionary<TKey, TValue> 是 .NET Framework 4.0 和 .NET Core/.NET 5 中引入的线程安全字典实现&#xff0c;位于 System.Collections.Concurrent 命名空间。它解决了多线程环境下操作字典时的同步问题&#xff0c…

集成电路学习:什么是URDF Parser统一机器人描述格式解析器

URDF Parser(URDF解析器)是ROS(Robot Operating System,机器人操作系统)中用于解析URDF(Unified Robot Description Format,统一机器人描述格式)文件的工具。URDF是一种基于XML(Extensible Markup Language,可扩展标记语言)规范的格式,用于描述机器人的结构、关节、…

老式大头显示器(CRT)和当前最高分辨率的LED显示器对比

老式 CRT&#xff08;阴极射线管&#xff09;和当前最顶尖的 LED&#xff08;包括 MicroLED / 高端 MiniLED / OLED&#xff09;显示器在画面清晰度极限相关的参数并列分析。1. 分辨率与像素密度指标老式 CRT&#xff08;PC/电视用&#xff09;顶级 LED 显示器&#xff08;2025…

北京JAVA基础面试30天打卡07

1. 缓存三大问题及解决方案问题场景后果常用解决方案缓存穿透请求的数据在缓存和数据库中都不存在&#xff08;恶意攻击或查询异常 ID&#xff09;每次请求都会打到数据库&#xff0c;导致 DB 压力骤增- 缓存空值&#xff08;短期缓存不存在的 key&#xff09;- 布隆过滤器&…