1. 引言
B树是一种自平衡的多路搜索树,广泛应用于数据库和文件系统中。它通过让每个节点容纳多个关键字和多个子节点,显著降低树的高度,从而减少磁盘I/O次数。本文将从B树的定义、性质、基本操作入手,结合C语言代码,帮助读者深入理解B树的原理与实现。
2. 什么是B树
B树是一种多路平衡搜索树,其每个节点可以拥有两个以上的子节点。与二叉搜索树不同,B树通过增加节点的分支数来降低树的高度,使得在大量数据场景下,查找、插入和删除操作所需的磁盘访问次数更少。
B树通常用于磁盘等块存储设备,因为一次磁盘I/O可以读取一个完整的节点,节点越大,单次I/O能处理的关键字越多,效率越高。
3. B树的性质
一棵阶数为 m 的B树满足以下性质:
- 每个节点最多包含 m 个子节点。
- 除根节点外,每个非叶子节点至少包含 ⌈m/2⌉ 个子节点。
- 根节点至少有两个子节点(除非它是叶子节点)。
- 有 k 个子节点的非叶子节点包含 k-1 个关键字。
- 所有叶子节点位于同一层,树始终保持平衡。
这些性质保证了B树的高度始终维持在 O(log_m n) 级别,其中 n 为关键字总数。
4. B树的基本操作
4.1 查找
B树的查找过程与二叉搜索树类似,但每个节点需要遍历多个关键字。从根节点开始,在节点内部顺序或二分查找目标关键字;若找到则返回,否则根据关键字大小进入对应的子树继续查找,直到叶子节点。
4.2 插入
插入操作首先执行查找,找到合适的叶子节点位置。若叶子节点未满,直接插入关键字;若节点已满,则需要进行节点分裂,将中间关键字提升到父节点,并递归处理父节点可能发生的溢出。
4.3 删除
删除操作较为复杂。若删除的关键字位于叶子节点,直接删除;若位于内部节点,则需要用前驱或后继关键字替换,然后递归删除。删除后若节点关键字数量低于下限,需要通过借位或合并操作恢复平衡。
5. C语言实现
下面给出一个简化的B树C语言实现,重点展示节点的定义、查找和插入逻辑。
c
#include <stdio.h>
#include <stdlib.h>
#define M 3 // B树的阶数
typedef struct BTreeNode {
int keys[M - 1]; // 关键字数组
struct BTreeNode *child[M]; // 子节点指针数组
int n; // 当前关键字数量
int leaf; // 是否为叶子节点
} BTreeNode;
// 创建新节点
BTreeNode* createNode(int leaf) {
BTreeNode *node = (BTreeNode*)malloc(sizeof(BTreeNode));
node->leaf = leaf;
node->n = 0;
for (int i = 0; i < M; i++) {
node->child[i] = NULL;
}
return node;
}
// 在节点内查找关键字位置
int searchKey(BTreeNode *node, int key) {
int idx = 0;
while (idx < node->n && key > node->keys[idx]) {
idx++;
}
return idx;
}
// 在B树中查找关键字
BTreeNode* search(BTreeNode *root, int key) {
if (root == NULL) return NULL;
int idx = searchKey(root, key);
if (idx < root->n && root->keys[idx] == key) {
return root;
}
if (root->leaf) return NULL;
return search(root->child[idx], key);
}
// 分裂满节点
void splitChild(BTreeNode *parent, int idx) {
BTreeNode *child = parent->child[idx];
BTreeNode *newNode = createNode(child->leaf);
newNode->n = M / 2 - 1;
for (int i = 0; i < M / 2 - 1; i++) {
newNode->keys[i] = child->keys[i + M / 2];
}
if (!child->leaf) {
for (int i = 0; i < M / 2; i++) {
newNode->child[i] = child->child[i + M / 2];
}
}
child->n = M / 2 - 1;
for (int i = parent->n; i > idx; i--) {
parent->child[i + 1] = parent->child[i];
}
parent->child[idx + 1] = newNode;
for (int i = parent->n - 1; i >= idx; i--) {
parent->keys[i + 1] = parent->keys[i];
}
parent->keys[idx] = child->keys[M / 2 - 1];
parent->n++;
}
// 向非满节点插入关键字
void insertNonFull(BTreeNode *node, int key) {
int idx = node->n - 1;
if (node->leaf) {
while (idx >= 0 && key < node->keys[idx]) {
node->keys[idx + 1] = node->keys[idx];
idx--;
}
node->keys[idx + 1] = key;
node->n++;
} else {
idx = searchKey(node, key);
if (node->child[idx]->n == M - 1) {
splitChild(node, idx);
if (key > node->keys[idx]) {
idx++;
}
}
insertNonFull(node->child[idx], key);
}
}
// B树插入入口
BTreeNode* insert(BTreeNode *root, int key) {
if (root == NULL) {
root = createNode(1);
root->keys[0] = key;
root->n = 1;
return root;
}
if (root->n == M - 1) {
BTreeNode *newRoot = createNode(0);
newRoot->child[0] = root;
splitChild(newRoot, 0);
insertNonFull(newRoot, key);
return newRoot;
}
insertNonFull(root, key);
return root;
}
// 中序遍历B树
void inorder(BTreeNode *root) {
if (root == NULL) return;
for (int i = 0; i < root->n; i++) {
if (!root->leaf) {
inorder(root->child[i]);
}
printf("%d ", root->keys[i]);
}
if (!root->leaf) {
inorder(root->child[root->n]);
}
}
int main() {
BTreeNode *root = NULL;
int keys[] = {10, 20, 5, 6, 12, 30, 7, 17};
int n = sizeof(keys) / sizeof(keys[0]);
for (int i = 0; i < n; i++) {
root = insert(root, keys[i]);
}
printf("中序遍历结果:");
inorder(root);
printf("\n");
int target = 12;
BTreeNode *result = search(root, target);
if (result != NULL) {
printf("找到关键字 %d\n", target);
} else {
printf("未找到关键字 %d\n", target);
}
return 0;
}
上述代码实现了B树的创建、查找、插入和中序遍历。其中 splitChild 负责在节点满时进行分裂,insertNonFull 处理向非满节点插入的逻辑,insert 作为入口处理根节点分裂的情况。
6. 代码说明
在实现中,M 表示B树的阶数,这里取 3,即每个节点最多有 3 个子节点、2 个关键字。当节点关键字数量达到 M-1 时,插入新关键字会触发分裂操作,中间关键字上移到父节点,从而维持B树的平衡性质。
查找操作的时间复杂度为 O(log_m n),插入操作在最坏情况下需要从叶子节点向上分裂到根节点,但整体仍保持对数级别。
7. 总结
B树通过多路分支和节点分裂机制,在保持树平衡的同时大幅降低树的高度,非常适合磁盘存储场景。本文介绍了B树的核心性质、基本操作,并给出了完整的C语言实现。读者可以在此基础上进一步扩展删除操作、磁盘页管理以及B+树等变体,以应对更复杂的实际需求。