B树:数据库索引的高效基石

引言

在前面的树系列中,我们学习的 BST、AVL 树、红黑树都是二叉树------每个节点最多两个子节点。当数据量小、能全部放进内存时,二叉树足够高效。

但现实是:数据库和文件系统的索引数据动辄几十 GB,远远超出内存容量,必须存储在磁盘上。磁盘 I/O 的速度比内存慢了几十万倍,传统的二叉树高度太高,一次查找需要访问几十个磁盘页,完全不可接受。

B 树 正是为此而生。它是一种多路平衡搜索树 ,每个节点可以存储多个键、拥有多个子节点,通过"矮胖"的结构大幅降低树的高度,从而减少磁盘 I/O 次数。

第一部分:B 树的基本概念

一、什么是 B 树

B 树(B-Tree)是一种自平衡的多路搜索树,由 Rudolf Bayer 和 Edward McCreight 于 1971 年提出。

B 树的定义(m 阶 B 树)

二、B 树节点的内部结构

三、B 树 vs 二叉树的高度对比

第二部分:B 树的查找

一、查找过程

B 树的查找和 BST 类似,区别在于每个节点内有多个键,需要在节点内找到正确的区间

二、查找代码

cpp 复制代码
#define M 5  // B 树的阶

typedef struct BTreeNode {
    int keys[M - 1];              // 键数组(最多 M-1 个键)
    struct BTreeNode* children[M]; // 子节点指针数组(最多 M 个)
    int n;                        // 当前键的数量
    int isLeaf;                   // 是否为叶子节点(1=叶子,0=内部节点)
} BTreeNode;

// 在节点 node 内查找 key
// 找到返回 1 并设置 *pos 为 key 的下标
// 未找到返回 0 并设置 *pos 为应该进入的子节点下标
int searchInNode(BTreeNode* node, int key, int* pos) {
    int i = 0;
    while (i < node->n && key > node->keys[i]) {
        i++;
    }
    
    if (i < node->n && key == node->keys[i]) {
        *pos = i;
        return 1;  // 找到了
    }
    
    *pos = i;  // 没找到,返回应进入的子节点下标
    return 0;
}

// 在 B 树中查找 key
BTreeNode* search(BTreeNode* root, int key) {
    if (root == NULL) return NULL;
    
    int pos;
    if (searchInNode(root, key, &pos)) {
        return root;  // 在当前节点找到了
    }
    
    if (root->isLeaf) {
        return NULL;  // 叶子节点,不存在
    }
    
    return search(root->children[pos], key);  // 进入子节点继续找
}

第三部分:B 树的插入

一、插入策略

B 树的插入比二叉树复杂很多,因为节点有容量上限。核心策略 :始终插入到叶子节点;如果节点满了就分裂

二、分裂过程(5 阶 B 树,M=5,每节点最多 4 键)

三、插入代码

cpp 复制代码
// 分裂节点 node 的第 childIndex 个子节点(该子节点已满)
void splitChild(BTreeNode* parent, int childIndex) {
    BTreeNode* child = parent->children[childIndex];
    BTreeNode* newChild = (BTreeNode*)malloc(sizeof(BTreeNode));
    newChild->isLeaf = child->isLeaf;
    
    int mid = (M - 1) / 2;  // 中间键的位置
    
    // 右半部分的键复制到新节点
    newChild->n = child->n - mid - 1;
    for (int j = 0; j < newChild->n; j++) {
        newChild->keys[j] = child->keys[mid + 1 + j];
    }
    
    // 如果不是叶子,复制子指针
    if (!child->isLeaf) {
        for (int j = 0; j <= newChild->n; j++) {
            newChild->children[j] = child->children[mid + 1 + j];
        }
    }
    
    child->n = mid;  // 左半部分保留在原节点
    
    // 父节点腾出位置,插入中间键
    for (int j = parent->n; j > childIndex; j--) {
        parent->keys[j] = parent->keys[j - 1];
        parent->children[j + 1] = parent->children[j];
    }
    parent->keys[childIndex] = child->keys[mid];
    parent->children[childIndex + 1] = newChild;
    parent->n++;
}

// 向非满节点插入 key(递归辅助函数)
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 == M - 1) {
            splitChild(node, i);
            if (key > node->keys[i]) i++;  // 确定分裂后进入哪个子节点
        }
        insertNonFull(node->children[i], key);
    }
}

// B 树插入主函数
BTreeNode* insert(BTreeNode* root, int key) {
    if (root == NULL) {
        BTreeNode* node = (BTreeNode*)malloc(sizeof(BTreeNode));
        node->keys[0] = key;
        node->n = 1;
        node->isLeaf = 1;
        return node;
    }
    
    if (root->n == M - 1) {
        // 根满了,创建新根
        BTreeNode* newRoot = (BTreeNode*)malloc(sizeof(BTreeNode));
        newRoot->isLeaf = 0;
        newRoot->n = 0;
        newRoot->children[0] = root;
        splitChild(newRoot, 0);
        insertNonFull(newRoot, key);
        return newRoot;
    }
    
    insertNonFull(root, key);
    return root;
}

第四部分:B 树的删除

B 树的删除是最复杂的操作,核心原则是:删除后每个节点仍满足最少键数的要求(根除外,内部节点至少 ⌈M/2⌉-1 个键)。

一、删除的三种情况

二、删除后的"借"与"合并"

当节点删除后键数不足最少要求时

第五部分:B 树的性能分析

操作 时间复杂度 说明
查找 O(log n) 每层在节点内二分查找 O(log m) + 树高 O(logₘ n) = O(log n)
插入 O(log n) 可能向上分裂,最坏到根
删除 O(log n) 可能借键或合并,最坏到根
空间 O(n) 每节点有 M-1 个键和 M 个指针

总结

一、B 树核心要点

要点 内容
核心思想 多路搜索,"矮胖"结构减少磁盘 I/O
节点结构 键 + 子指针,键在节点内有序
插入 总是插入叶子,满了就分裂,可能向上递归
删除 删内部节点用前后继替代,删叶子可能借或合并
平衡性 所有叶子在同一层
应用 数据库索引(B+ 树变体)、文件系统

二、B 树 vs 二叉树

对比 二叉树 B 树
子节点数 2 M 个
树高 高(log₂n) 矮(logₘn)
磁盘 I/O
适用场景 内存 磁盘

三、一句话记忆

B 树是多路平衡搜索树,每个节点存多个键、有多个子节点,通过"矮胖"结构大幅降低树高,从而减少磁盘 I/O 次数,是数据库和文件系统索引的底层基石。

相关推荐
fen_fen1 小时前
Oracle12,新增自增主键表和批量插入数据
数据库·sql·mysql
deepin_sir1 小时前
11 - 模块与包
前端·数据库·python
念恒123061 小时前
MySQL索引
数据库·mysql
Lao A(zhou liang)的菜园1 小时前
如何快速诊断Oracle性能问题?
数据库·oracle
铁皮哥1 小时前
【agent 开发】Claude Code 的 Skill 是怎么被加载的?从 name/description 到 SKILL.md 再到资源文件
java·服务器·数据库·python·gitee·github·软件工程
小糯米6011 小时前
C语言 自定义类型:结构体 与 联合体
c语言·开发语言·数据结构
一只fish2 小时前
Oracle官方文档翻译《Database Concepts 26ai》第14章-物理存储结构
数据库·oracle
hhb_6182 小时前
GraphQL实战避坑指南:性能与安全优化
数据库·安全·graphql
一 乐2 小时前
公交线路查询系统|基于Java+vue公交线路查询系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·公交线路查询系统