B树是非常重要的数据结构,它是一种自平衡树, 能够保持数据有序。并且这种结构可允许查找数据、顺序访问、插入数据以及删除数据。这种数据可以用来描述外部存储。该结构经常用在数据库和文件系统的实现上。
B树有如下3条规则:
- B树中每个节点的元素数量和子树的数量都是有限的,除了根节点外,所有节点最多拥有M-1个元素,所有非叶子非根节点最多拥有M个子树(称为M阶B树)
- 根节点至少拥有两个子树,除了根节点之外的非叶子节点拥有K个子树以及K-1个元素((M+1/2) < K < M),元素按照递增或递减顺序排列
- 所有叶子节点属于同一层
现在我将一一介绍B树的源码,来讲解B树结构的过程。
1. B树和B树节点的定义
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int KEY_VALUE;
typedef struct _btree_node
{
KEY_VALUE *keys;
struct _btree_node **childrens;
int num;
int leaf;
} btree_node;
// definition of b-tree
// root: the pointer to the root node, t: minimum degree
typedef struct _btree {
btree_node *root;
int t;
} btree;
首先,我们定义B树的节点为结构体_btree_node
。成员如上面的代码片段所示。keys是一堆索引,KEY_VALUE
表示key的数据类型。实际编译的时候,KEY_VALUE
会将该变量定义为int
类型。childrens
为子节点序列。num
为*keys
数组中有效子节点的个数, leaf
判断该节点是否是叶子节点的变量。
有了节点定义之后,我们就可以定义整棵树_btree
。在上面的代码片段里,我们在树结构体内定义树的根节点为root
, 以及树的最小阶(degree)t
。
2. B树节点的创建和B树的创建
static btree_node *btree_create_node(int t, int leaf) {
// create a node in a b-tree
btree_node *node = (btree_node *)calloc(1, sizeof(btree_node));
if (node == NULL) return NULL;
node->leaf = leaf;
node->keys = (KEY_VALUE *)calloc(1, (2*t-1)*sizeof(KEY_VALUE));
if (node->keys == NULL) {
free(node);
return NULL;
}
node->childrens = (btree_node **)calloc(1, (2*t) * sizeof(btree_node*));
if (node->childrens == NULL) {
free(node->keys);
free(node);
return NULL;
}
node->num = 0;
return node;
}
void btree_create(btree *T, int t) {
T->t = t;
btree_node *x = btree_create_node(t, 1);
T->root = x;
}
接下来,有了B树的定义,我们可以定义如何创建B树的节点以及B树。首先,对于创建B树的节点的创建,我们传递参数t和leaf。t就是被创建的节点的编号,leaf就是被创建节点是否是叶节点。1就是叶节点,0就是内节点。节点一共有4个变量,leaf
,keys
,childrens
以及num
。leaf直接从传参的leaf获取值;key代表的索引编号,并赋予初始空间(2*t-1)*sizeof(KEY_VALUE)
。2*t-1
就是上述定义中的多元素个数(2t阶子树)。接着,给childrens
变量分配空间(2*t)*sizeof(btree_node*)
。同样也是由定义给出的。然后我们node的数目为num。因为是初始化节点,所以key的有效子节点*个数是0,进而num=0
。
我们除了要创建树的节点,更不要忘记创建树!关于树的创建,我们只需要给树结构体赋予变量t和创建树的根节点。源码实现如上所示。
3. B树节点的销毁和B树的销毁
#节点的销毁
void btree_destroy_node(btree_node *node) {
assert(node);
free(node->childrens);
free(node->keys);
free(node);
}
#树的销毁
void btree_destroy(btree *T) {
if (!T->root) {
return;
}
btree_delete_tree(T->root);
T->root = NULL;
}
static void btree_delete_tree(btree_node *node) {
if (node->leaf) {
btree_destroy_node(node);
return;
}
int i = 0;
for (i = 0; i <= node->num; i++) {
btree_delete_tree(node->childrens[i]);
}
btree_destroy_node(node);
}
-
btree_destroy_node
:删除节点node
,不仅仅需要将节点删除,我们还需要把节点里面给childrens
和keys
分配的空间也要free
掉。如果需要把代码写的更鲁棒(robust)一点,我们使用断言函数assert()
判断需要删除的节点node
是否存在。 -
btree_destroy
andbtree_delete_tree
。关于删除树的实现,稍微有点复杂。我们可以采用递归的方法实现。具体来说,由于B树都是由高层节点指向底层节点,直到指向叶节点
。所以,我们删除的顺序,是自底向上,从叶节点开始删除。我先实现了递归删除函数btree_delete_tree
。在这个函数里,当节点是叶节点,则用节点删除函数btree_destroy_node
来删除,然后回归上一轮函数。否则,我们会对这个node的每一个子节点做递归删除遍历。遍历结束后,我们才能够把这个节点删除。
实现该函数后,则可以写出删除树的函数。简单来说就是对树的根节点做递归删除即可。
4. B树子树的分裂。
static void btree_split_child(btree *T, btree_node *x, int i) {
int t = T->t;
btree_node *y = x->childrens[i];
btree_node *z = btree_create_node(t, y->leaf);
z->num = t - 1;
int j = 0;
for (j = 0; j < t-1; j++) {
z->keys[j] = y->keys[j+t];
}
if (y->leaf == 0) {
for (j = 0; j < t; j++) {
z->childrens[j] = y->childrens[j+t];
}
}
y->num = t-1;
for (j = x->num; j >= i+1; j--) {
x->childrens[j+1] = x->childrens[j];
}
x->childrens[i+1] = z;
for (j = x->num-1; j >= i; j--) {