B-tree(B树)是一种自平衡的多路搜索树,广泛用于文件系统、数据库索引、键值存储系统等对大规模数据的高效插入、查找和删除有高要求的场景。相比于二叉搜索树(BST),B-tree 可以减少磁盘I/O次数,提升整体性能。
🧠 一、B-tree 原理简述
1.1 特点
- 每个节点最多有
m
个孩子(m
是阶数)。 - 每个节点有
k
个关键字(k
满足⌈m/2⌉ - 1 ≤ k ≤ m - 1
)。 - 所有叶子节点在同一层,保证树的平衡性。
- 每个节点的关键字按升序排列,满足多叉搜索树特性。
- 所有关键字存储在节点中(与 B+ Tree 不同)。
1.2 查找逻辑
- 从根节点开始,对关键字做有序比较。
- 若匹配则返回;否则进入对应的子节点继续查找,直到找到或进入叶子节点。
1.3 插入逻辑
- 插入时先在叶子节点找到插入位置;
- 若插入后关键字个数超出限制,节点将分裂,中间值上移;
- 若上移导致父节点也超限,则可能触发递归分裂,甚至根节点分裂导致整棵树高度增加。
1.4 删除逻辑
- 删除叶子节点关键字较简单;
- 若删除内部节点关键字,则需要用前驱或后继节点的关键字替代;
- 删除可能导致节点关键字数量过少,需要借位或合并处理。
🔍 二、源码实现(Java 简化版)
下面是一个简化的 B-tree 实现,支持插入和查找(不含删除,实际删除较复杂,可按需补充):
2.1 BTreeNode 类(节点结构)
class BTreeNode {
int t; // 阶数(最小度数)
int n; // 当前关键字数
int[] keys;
BTreeNode[] children;
boolean isLeaf;
BTreeNode(int t, boolean isLeaf) {
this.t = t;
this.isLeaf = isLeaf;
this.keys = new int[2 * t - 1];
this.children = new BTreeNode[2 * t];
this.n = 0;
}
// 查找某个 key
BTreeNode search(int key) {
int i = 0;
while (i < n && key > keys[i]) i++;
if (i < n && keys[i] == key) return this;
if (isLeaf) return null;
return children[i].search(key);
}
}
2.2 BTree 类(操作接口)
public class BTree {
BTreeNode root;
int t; // 阶数
public BTree(int t) {
this.root = new BTreeNode(t, true);
this.t = t;
}
// 查找
public BTreeNode search(int key) {
return root == null ? null : root.search(key);
}
// 插入主入口
public void insert(int key) {
BTreeNode r = root;
if (r.n == 2 * t - 1) {
BTreeNode s = new BTreeNode(t, false);
s.children[0] = r;
splitChild(s, 0, r);
insertNonFull(s, key);
root = s;
} else {
insertNonFull(r, key);
}
}
// 插入到非满节点
private void insertNonFull(BTreeNode node, int key) {
int i = node.n - 1;
if (node.isLeaf) {
while (i >= 0 && key < node.keys[i]) {
node.keys[i + 1] = node.keys[i];
i--;
}
node.keys[i + 1] = key;
node.n++;
} else {
while (i >= 0 && key < node.keys[i]) i--;
i++;
if (node.children[i].n == 2 * t - 1) {
splitChild(node, i, node.children[i]);
if (key > node.keys[i]) i++;
}
insertNonFull(node.children[i], key);
}
}
// 节点分裂
private void splitChild(BTreeNode parent, int i, BTreeNode fullChild) {
BTreeNode newNode = new BTreeNode(t, fullChild.isLeaf);
newNode.n = t - 1;
for (int j = 0; j < t - 1; j++)
newNode.keys[j] = fullChild.keys[j + t];
if (!fullChild.isLeaf) {
for (int j = 0; j < t; j++)
newNode.children[j] = fullChild.children[j + t];
}
fullChild.n = t - 1;
for (int j = parent.n; j >= i + 1; j--)
parent.children[j + 1] = parent.children[j];
parent.children[i + 1] = newNode;
for (int j = parent.n - 1; j >= i; j--)
parent.keys[j + 1] = parent.keys[j];
parent.keys[i] = fullChild.keys[t - 1];
parent.n++;
}
}
📦 三、B-tree 应用场景
✅ 1. 数据库索引(如 MySQL、PostgreSQL)
- MySQL 中的 InnoDB 默认使用 B+ Tree 实现聚簇索引。
- 非聚簇索引(辅助索引)也是 B+ Tree 的变种。
- 支持范围查找、前缀匹配,性能远胜哈希索引。
✅ 2. 文件系统(如 NTFS、HFS+、ext4)
- 许多文件系统利用 B-tree/B+Tree 管理文件名、inode 或数据块索引。
- 高效支持插入、删除和范围查询。
✅ 3. 操作系统中的页表(延伸)
- 虽然操作系统页表更常见的是树形结构,但某些设计中也用到了 B-tree 来提升页映射效率。
✅ 4. 键值存储系统(如 RocksDB、LevelDB)
- 虽以 LSM Tree 为主,但历史上也有很多使用 B-tree 的存储引擎,如 BerkeleyDB。
✅ 5. 其他:
- Lucene 倒排索引的部分结构
- 大型路由表查找(例如 SDN 中的路径匹配)
- 日志系统、时间序列数据库的区块索引结构
📈 四、与其他树的对比
特性 | BST | AVL | Red-Black | B-tree | B+Tree |
---|---|---|---|---|---|
平衡性 | 低 | 高 | 中等 | 高 | 高 |
子节点数 | 2 | 2 | 2 | 多 | 多 |
查找效率 | O(log n) | O(log n) | O(log n) | O(logₘ n) | O(logₘ n) |
应用场景 | 内存数据 | 内存数据 | 内存数据 | 磁盘/数据库 | 数据库/文件系统 |
叶子节点结构 | N/A | N/A | N/A | 含数据 | 存所有数据,链式连接 |
我们来继续深入分析 B-tree 的相关内容,尤其是以下几个方面:
🧠 五、B+ Tree 与 B Tree 的区别与优劣分析
虽然 B-tree 应用广泛,但在数据库和文件系统中更常见的是 B+ Tree。我们先对比一下这两者:
特性 | B-tree | B+ Tree |
---|---|---|
关键字存储位置 | 所有节点 | 仅叶子节点 |
叶子节点是否链表连接 | 否 | 是(双向链表) |
范围查找效率 | 一般 | 高效(叶子链表) |
内部节点是否存储数据 | 是 | 否(只做导航) |
I/O 友好程度 | 一般 | 更优(分支因子更高) |
插入/删除复杂度 | 中 | 高,但维护顺序更好 |
✅ 为什么数据库系统更喜欢 B+ Tree?
- B+ Tree 所有数据都在叶子节点,结构更加扁平,查找路径更短。
- 非叶子节点只用于索引,因此可以容纳更多关键字,降低树高,减少磁盘访问次数。
- 叶子节点链表使得范围扫描非常高效,适合数据库的
BETWEEN
/LIKE 'prefix%'
等操作。
🔄 六、B-tree 删除操作(进阶)
B-tree 的删除相较于插入要复杂得多,主要面临节点可能“下溢”的问题。大致分为以下几种情况:
情况一:删除叶子节点的 key,节点关键字仍 ≥ t-1
- 直接删除,无需额外操作。
情况二:删除叶子节点的 key,导致关键字数 < t-1
- 若兄弟节点有多于
t-1
个关键字,则“向兄弟借位”。 - 否则,合并兄弟节点并下移父节点 key。
情况三:删除内部节点的 key
- 用左子树中最大 key 或右子树中最小 key 替换(称为“合适的替代 key”),然后在子树中递归删除替代 key。
- 若替代过程中发生下溢,再执行合并或借位。
💡这就是为什么很多数据库引擎在面对删除时采用“懒删除”或“标记删除”,等待后台合并处理,避免频繁触发结构调整。
🧪 七、磁盘 I/O 模型下 B-tree 的优势
传统的算法复杂度分析常基于内存访问,而 B-tree 最初的设计目标正是降低磁盘访问成本。
假设场景:
- 每个磁盘页面为 4KB。
- 每个 key 占用 16 字节,则每个节点最多可容纳
4096 / 16 = 256
个 key。 - B-tree 的**分支因子(fan-out)**为 257(256 key + 257 child pointers)。
📌 高度估算(插入 1000 万个 key):
- 若 fan-out = 256,树高为:
log₂₅₆(10^7) ≈ 3.3
,即最多只需 4 次磁盘访问。
相比之下,二叉树的高度为 log₂(10^7) ≈ 24
,访问 24 次,磁盘延迟约为 5~10 毫秒级,代价非常高。
✅ 结论:B-tree 在磁盘或大型缓存页上能极大地减少树高,降低磁盘 I/O,是数据库索引结构的理想选择。
🔧 八、真实系统中的 B-tree 应用举例
1️⃣ MySQL(InnoDB)
- 默认采用 B+ Tree 聚簇索引(Clustered Index)。
- 主键即为叶子节点中保存的记录的物理位置。
- 非主键索引(Secondary Index)也用 B+ Tree 存储,叶子节点保存的是主键的值。
- 支持高效的范围查找、排序与前缀查询。
2️⃣ PostgreSQL
- 支持多种索引类型,其中默认也是 B+ Tree。
- 优化器通过 cost model 判断是否使用索引扫描(index scan)。
3️⃣ Lucene 倒排索引(间接使用)
- 虽然主结构是倒排表,但对 term dictionary 的实现可借助 B-tree 或 FST(有限状态转移机)加速 term 前缀匹配。
4️⃣ HDFS NameNode 文件目录结构(早期版本)
- 使用基于 B-tree 的目录结构快速索引百万级文件路径。
📚 九、补充学习资源推荐
类型 | 名称 |
---|---|
官方文档 | MySQL InnoDB Storage Format |
算法书籍 | 《算法导论》第 18 章:B树 |
源码阅读 | Apache Derby BTree 实现 |
博客推荐 | Stanford CS: B-Trees and External Memory Data Structures |
动画演示 | VisuAlgo: B-Tree 可视化演示 |