CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
  • C++语言面试问题集锦
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • 🔥C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 🔥从零用C语言写一个Redis
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • Go语言特性

    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 🔥使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • 使用Go从零开发一个数据库 说明
  • 1.文件与数据库
  • 2.索引
  • 3.B树:原理
    • 3.1 B树和二叉搜索树(BST)的直观理解
    • 3.2 B树和嵌套数组
    • 3.3 B树操作
    • 3.4 不可变数据结构
  • 4.B树:实践(第一部分)
  • 5.B树:实践(第二部分)
  • 6.持久化到磁盘
  • 7.空闲列表:重用页面
  • 8.行与列
  • 9.范围查询
  • 10.二级索引
  • 11.原子事务
  • 12.并发读写
  • 13.查询语言:解析器
  • 14.查询语言:执行
目录

3.B树:原理

# 03. B树:原理

# 3.1 B树和二叉搜索树(BST)的直观理解

我们对B树的初步理解源自平衡二叉搜索树(BST)。二叉树是存储有序数据的常用数据结构。在插入或删除键之后,保持树的良好形态就是 “平衡” 的含义。正如上一章所述,为了利用 “页”(IO的最小单位),应该使用n叉树而非二叉树。

B树可以看作是二叉搜索树的扩展。B树的每个节点包含多个键和多个指向子节点的链接。在节点中查找键时,会依据所有键来确定下一个子节点。

[1,   4,   9]
/        |        \
v      v      v
[1, 2, 3] [4, 6] [9, 11, 12]
1
2
3
4

B树的平衡方式与二叉搜索树不同。像红黑树(RB树)或AVL树这样常见的二叉搜索树,是通过旋转操作,基于子树高度来实现平衡的。而B树所有叶节点的高度相同,它是依据节点大小来实现平衡的:

  • 如果一个节点太大,无法存放在一个页中,就将其分裂为两个节点。这会增加父节点的大小,如果根节点分裂,还有可能增加树的高度。
  • 如果一个节点太小,尝试将其与兄弟节点合并。

如果你熟悉红黑树,可能也了解2 - 3树,2 - 3树很容易扩展为B树。

# 3.2 B树和嵌套数组

即便你不熟悉2 - 3树,也可以通过嵌套数组获得一些直观认识。

让我们从一个有序数组开始。查询操作可以通过二分查找来完成。但是,更新数组的时间复杂度是O(n),这是我们需要解决的问题。更新大数组效率很低,所以我们将其分割成较小的数组。假设我们把数组分成$\sqrt{n}$个部分,每个部分平均包含$\sqrt{n}$个键。

[[1,2,3], [4,6], [9,11,12]]
1

要查询一个键,我们必须先确定该键位于哪个部分,对$\sqrt{n}$个部分进行二分查找的时间复杂度是O(log(n))。之后,在该部分内对键进行二分查找,时间复杂度同样是O(log(n)),整体效率并不比之前差。而且更新操作的时间复杂度提升到了O($\sqrt{n}$)。

这是一个2层的有序嵌套数组,如果增加更多层级会怎样呢?这就是对B树的另一种直观理解。

# 3.3 B树操作

查询B树和查询二叉搜索树的方式相同。

更新B树则更为复杂。从现在起,我们将使用一种称为 “B + 树” 的B树变体,B + 树仅在叶节点存储值,内部节点只包含键。

插入键从叶节点开始。叶节点本质上就是一个有序的键列表。将键插入叶节点很简单,但插入操作可能会导致节点大小超过页大小。在这种情况下,我们需要将叶节点分裂为2个节点,每个节点包含一半的键,这样两个叶节点都能放入一个页中。

一个内部节点由以下部分组成:

  1. 指向子节点的指针列表。
  2. 与指针列表配对的键列表。每个键都是对应子节点的第一个键。

将叶节点分裂为2个节点后,父节点会用新的指针和键替换旧的指针和键。节点大小会增加,这可能会触发进一步的分裂。

parent              parent
/  |  \     =>         /  | |  \
L1   L2   L6          L1  L3 L4  L6
1
2
3

当根节点分裂时,会添加一个新的根节点。B树就是这样生长的。

new_root
/ \
root                 N1 N2
/  |  \     =>         /  | |  \
L1   L2   L6          L1  L3 L4  L6
1
2
3
4
5

删除键的操作与插入相反。节点永远不会为空,因为较小的节点会与其左兄弟节点或右兄弟节点合并。

当非叶根节点缩减为单个键时,根节点可以由其唯一的子节点替代。B树就是这样收缩的。

# 3.4 不可变数据结构

不可变(Immutable)意味着永远不在原地更新数据。一些类似的术语有 “只追加(append - only)”、“写时复制(copy - on - write)” 和 “持久化数据结构(persistent data structures)”(这里的 “持久化(persistent)” 与我们之前讨论的 “持久化(persistence)” 没有关系)。

例如,向叶节点插入键时,不要在原地修改该节点,而是创建一个新节点,包含要更新节点的所有键以及新插入的键。此时,父节点也必须更新,以指向新节点。

同样,父节点会带着新指针被复制。一直到根节点,整个路径都会被复制。这实际上创建了树的一个新版本,与旧版本共存。

不可变数据结构有几个优点:

  1. 避免数据损坏。不可变数据结构不会修改现有数据,只是添加新数据,所以即使更新操作被中断,旧版本的数据依然完好无损。
  2. 易于并发操作。读者可以与写者并发操作,因为读者可以不受影响地处理旧版本数据。

持久化和并发相关内容将在后面的章节介绍。目前,我们先编码实现一个不可变的B + 树。

上次更新: 2025/04/16, 02:04:00
2.索引
4.B树:实践(第一部分)

← 2.索引 4.B树:实践(第一部分)→

最近更新
01
第二章 关键字static及其不同用法
03-27
02
第一章 auto与类型推导
03-27
03
C++语言面试问题集锦 目录与说明
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式