B-树概念
很多文章把B树和B-tree理解成了两种不同类别的树,其实这两个是同一种树。
B树和平衡二叉树稍有不同的是B树属于多叉树又名平衡多路查找树(查找路径不只两个),数据库索引技术里大量使用者B树和B+树的数据结构。
使用背景
适合用在磁盘里,而不是内存里。我们通过cpu指令(汇编代码)可访问内存时,若不存在内存中,就回去磁盘中寻址。而磁盘寻址是很慢的(磁盘慢在寻址),需要通过磁头找到磁道对应的扇区,才可拿到对应的数据。(计算机组成原理)
磁盘图:
数据结点很多时,比如1024个结点,使用二叉树时其就会有10层,我们寻找数据时最差就要10次寻址;这种二叉树结构在内存使用是没有问题的,但在磁盘中使用就非常的耗时,所以我们用多叉树来降低层级,减少寻找次数,也就是使用B-tree。
规则
一颗M阶B树T,满足以下条件((注:M阶代表一个树节点最多有多少个查找路径,M=M路,当M=2则是2叉树,M=3则是3叉))
- 每个结点至多拥有M课子树
- 根结点至少拥有两颗子树
- 除了根结点以外,其余每个分支结点至少拥有M/2课子树
- 所有的叶结点都在同一层上
- 有k颗子树的分支结点则存在k-1个关键字,关键字按照递增顺序进行排序
- 关键字数量满足ceil(M/2)-1 <= n <= M-1(其中ceil(x)是一个取上限的函数)
建议创建B-tree时,M选择偶数,后期分裂是恰好找到中间值
B树的一个节点对应的就是磁盘中的一个扇区。
代码
typedef int KEY_VALUE;
#define DEGREE 3 //定义阶数
typedef struct _btree_node {
KEY_VALUE *keys; //关键字数组,结点中存储的数
struct _btree_node **childrens; //子节点指针(数组指针),指向下一层的指数
int num; //当前结点里存储了多少个关键字
int leaf; //是否为叶子结点
} btree_node; //B树的结点定义
typedef struct _btree {
btree_node *root;
int t; //阶数,自己控制,感觉是整个树阶数的一半
} btree; //B树
btree_node *btree_create_node(int t, int leaf)
{//t阶数,这个结点多大
btree_node *node = (btree_node*)calloc(1, sizeof(btree_node));//在内存的动态存储区中分配1个长度为size的连续空间,函数返回一个指向分配起始地址的指针
if (node == NULL) assert(0);
node->leaf = leaf;
node->keys = (KEY_VALUE*)calloc(1, (2*t-1)*sizeof(KEY_VALUE));
node->childrens = (btree_node**)calloc(1, (2*t) * sizeof(btree_node));
node->num = 0;
return node;
}
void btree_destroy_node(btree_node *node)
{
assert(node);
free(node->childrens);
free(node->keys);
free(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-树的分裂
背景
M阶B-树中结点关键字数>M-1时,就要进行结点拆分。
例如:当前是一个5路查找树,那么此时m=5,关键字数必须<=5-1(这里关键字数>4就要进行节点拆分)
拆分流程
从中间位置把关键字(不包括中间位置的关键字)分成两部分。左部分所含关键字放在旧结点中,右部分所含关键字放在新结点中,中间位置的关键字连同新结点的存储位置插入到父结点中。如果父结点的关键字个数也超过(m-1),则要再分裂,再往上插。直至这个过程传到根结点为止。
上图是5阶B-tree,可以看到最右子节点关键字数>(5-1),应该对其分裂。将中间关键字L提起插入到父节点中,左边仍放置在旧结点中,右部分放入新结点中,且新结点也要和父结点关联。
代码
//往某棵树添加某值:T指定的数,key准备插入的指定的值
//根节点单独处理出入分裂
void btree_insert(btree *T, KEY_VALUE key) {
//int t = T->t;
//创建一个空结点,其的第0个孩子指向根节点,就可以用以前的方式分裂了
btree_node *r = T->root;
if (r->num == 2 * T->t - 1) { //若插入的位置时根节点
//思路:先创建一个空的结点,然其成为原来根节点的父结点,然后再对原来的根节点也就是现在的子节点进行分裂
btree_node *node = btree_create_node(T->t, 0);
T->root = node;
node->childrens[0] = r;
btree_split_child(T, node, 0); //这样就分裂成T->t个结点
//如何插到想要的位置:找到对应的位置;插进去
int i = 0;
if (node->keys[0] < key) i++;
btree_insert_nonfull(T, node->childrens[i], key);
} else {
btree_insert_nonfull(T, r, key);
}
}
//T代表树, x代表结点, i代表x结点的第几个子节点
//x的第几个儿子进行分裂
//T为树,x为待插的结点,i代表x节点的第几个儿子
//T树X节点的第几个儿子进行分裂,因为没有父指针,所以传父结点和子节点的位置
void btree_split_child(btree *T, btree_node *x, int i) {
int t = T->t; //t
//1、创建空间以存储分裂的元素
btree_node *y = x->childrens[i]; //待分裂的子节点
btree_node *z = btree_create_node(t, y->leaf);
z->num = t - 1; //num为当前结点的key个数
//2、拷贝
int j = 0;
for (j = 0;j < t-1;j ++) {
z->keys[j] = y->keys[j+t]; //将子节点右部分的关键字存储在新结点Z中 //拷贝
}
if (y->leaf == 0) { //若不为叶子节点,将其指向的子树一起拷贝过去
for (j = 0;j < t;j ++) {
z->childrens[j] = y->childrens[j+t];
}
}
y->num = t - 1; //num为子树的个数,为原来左边部分的大小
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 --) {
x->keys[j+1] = x->keys[j];
}
x->keys[i] = y->keys[t-1]; //将中间关键字付给父结点
x->num += 1;
}
//插入是一种递归的方法,x是未满的节点,
//x指的时未满的结点
//插入的两个步骤:
//1、找到对应的结点;
//2、对结点的key对比,找到合适的位置进行插入;
//插入的数据都是插在叶子结点上
void btree_insert_nonfull(btree *T, btree_node *x, KEY_VALUE k) {
//最终都是插入到叶子节点中
int i = x->num - 1; //num为当前结点中key的个数,从右往左来比较结点的关键字,此时i是关键字的下标。//最后一个key值开始找
if (x->leaf == 1) { //若为叶子节点时,对结点的key对比,找到合适的位置,进行插入。//只有叶子节点才插入
while (i >= 0 && x->keys[i] > k) {
x->keys[i+1] = x->keys[i];
i --; //将结点的key值移位,给新key腾位置。也就是找到对应的位置,从后往前找
}
x->keys[i+1] = k;//插入新的key值
x->num += 1; num为key的个数,要符合=m-1
} else { 不是叶子节点,就递归找叶子节点
while (i >= 0 && x->keys[i] > k) i --; //通过比较关键字找到对应关键字位置,从而找到对应的子树.//找到对应的子树,key值
//找到位置后,发现子树满了,就先进行分裂,分裂完(不是满的,不用再判断了)再插
if (x->childrens[i+1]->num == (2*(T->t))-1) { //i+1对应子树的下标,若子树结点中的关键字数
btree_split_child(T, x, i+1); //若子树是满的就分裂
if (k > x->keys[i+1]) i++; //
}
btree_insert_nonfull(T, x->childrens[i+1], k);//找到对应的子树
}
}
B-树的删除
删除某个Key时,在查找过程中右子树的Key个数=ceil(M/2)-1时,要进行借位操作
删除的情况:
1、idx子树,key个数=ceil(M/2)-1时:
A.借位:
a.从idx-1借位,当其key个数>ceil(m/2)-1;
b.从idx+1借位,当期key个数>ceil(m/2)-1;
B.合并
{children[idx].keys} {keys[idx]} {childs[idx+1].keys}
代码
//{child[idx], key[idx], child[idx+1]}
void btree_merge(btree *T, btree_node *node, int idx) {
btree_node *left = node->childrens[idx];
btree_node *right = node->childrens[idx+1];
int i = 0;
/data merge
left->keys[T->t-1] = node->keys[idx];
for (i = 0;i < T->t-1;i ++) {
left->keys[T->t+i] = right->keys[i];
}
if (!left->leaf) {
for (i = 0;i < T->t;i ++) {
left->childrens[T->t+i] = right->childrens[i];
}
}
left->num += T->t;
//destroy right
btree_destroy_node(right);
//node
for (i = idx+1;i < node->num;i ++) {
node->keys[i-1] = node->keys[i];
node->childrens[i] = node->childrens[i+1];
}
node->childrens[i+1] = NULL;
node->num -= 1;
if (node->num == 0) {
T->root = left;
btree_destroy_node(node);
}
}
//删除过程,删除的节点也会是在叶子节点上面
void btree_delete_key(btree *T, btree_node *node, KEY_VALUE key) {
if (node == NULL) return ;
int idx = 0, i;
while (idx < node->num && key > node->keys[idx]) {
idx ++;
}
if (idx < node->num && key == node->keys[idx]) {
if (node->leaf) { //如果是叶子节点,直接删除
for (i = idx;i < node->num-1;i ++) {
node->keys[i] = node->keys[i+1];
}
node->keys[node->num - 1] = 0;
node->num--;
if (node->num == 0) { //root
free(node);
T->root = NULL;
}
return ;
} else if (node->childrens[idx]->num >= T->t) {//向前一位借
btree_node *left = node->childrens[idx];
node->keys[idx] = left->keys[left->num - 1];
btree_delete_key(T, left, left->keys[left->num - 1]);
} else if (node->childrens[idx+1]->num >= T->t) { //向后一位借
btree_node *right = node->childrens[idx+1];
node->keys[idx] = right->keys[0];
btree_delete_key(T, right, right->keys[0]);
} else { //合并
btree_merge(T, node, idx);
btree_delete_key(T, node->childrens[idx], key);
}
} else {
btree_node *child = node->childrens[idx];
if (child == NULL) {
printf("Cannot del key = %d\n", key);
return ;
}
if (child->num == T->t - 1) {
btree_node *left = NULL;
btree_node *right = NULL;
if (idx - 1 >= 0)
left = node->childrens[idx-1];
if (idx + 1 <= node->num)
right = node->childrens[idx+1];
if ((left && left->num >= T->t) ||
(right && right->num >= T->t)) {
int richR = 0;
if (right) richR = 1;
if (left && right) richR = (right->num > left->num) ? 1 : 0;
if (right && right->num >= T->t && richR) { //borrow from next
child->keys[child->num] = node->keys[idx];
child->childrens[child->num+1] = right->childrens[0];
child->num ++;
node->keys[idx] = right->keys[0];
for (i = 0;i < right->num - 1;i ++) {
right->keys[i] = right->keys[i+1];
right->childrens[i] = right->childrens[i+1];
}
right->keys[right->num-1] = 0;
right->childrens[right->num-1] = right->childrens[right->num];
right->childrens[right->num] = NULL;
right->num --;
} else { //borrow from prev
for (i = child->num;i > 0;i --) {
child->keys[i] = child->keys[i-1];
child->childrens[i+1] = child->childrens[i];
}
child->childrens[1] = child->childrens[0];
child->childrens[0] = left->childrens[left->num];
child->keys[0] = node->keys[idx-1];
child->num ++;
node->keys[idx-1] = left->keys[left->num-1];
left->keys[left->num-1] = 0;
left->childrens[left->num] = NULL;
left->num --;
}
} else if ((!left || (left->num == T->t - 1))
&& (!right || (right->num == T->t - 1))) {
if (left && left->num == T->t - 1) {
btree_merge(T, node, idx-1);
child = left;
} else if (right && right->num == T->t - 1) {
btree_merge(T, node, idx);
}
}
}
btree_delete_key(T, child, key);
}
}
int btree_delete(btree *T, KEY_VALUE key) {
if (!T->root) return -1;
btree_delete_key(T, T->root, key);
return 0;
}