B-tree B+tree
我们经常说做人要有B树,今天总结一下了解到的 B树和B+树,B-tree是B树,没有B-树。
首先我们来聊一聊,为什么要用B树,B树是一颗平衡查找树,旨在帮助我们减少磁盘I/O的开销,因为磁盘I/O的速度是很慢的,而内存中工作的速度就快很多。比如现在有 1M个4K 的文件,也就是4G, 如果构造一个1024叉B+树,只需要通过2次索引,就能找到我们要的文件的位置,也就是只需要做一次IO查找(磁盘寻址)操作。这无疑大大的提高了效率。
B树的性质
要学习B树,首先要了解一下B树的性质,B树是平衡多路搜索树。
一颗 M 阶 B树
1 每个结点上至多拥有M颗子树
2 根结点至少拥有两颗子树
3 除了根结点以外,其余每个分支结点至少拥有 M/2颗子树
4 所有的叶子节点都在同一层上 (这条性质就说明了B树是平衡的树)
5 有K颗子树的分支结点则存在 K-1 个关键字,关键字按照递增顺序进行排序
6 关键字数量满足 ceil(M/2)-1 <= n <= M-1 (ceil为上取整)
B数的层高为 logM(Num/M) M阶数,Num是结点总数量
B树和B+树的区别
B树是所有的结点都存储数据,B+树是只有叶子结点存储数据,内结点只做索引用。B树在访问内结点时要把磁盘上的数据读倒内存中,而B+树只需要内存中存放索引值即可,在内存中索引完了再做磁盘IO。
B树的操作
B树的定义
typedef int KEY_AVLUE;
struct _btree_node {
KEY_AVLUE *keys; // 根结点
struct _btree_node** childerns; // 子结点
int nums; // 现在存储了几个结点
int leaf; // 是否是叶子结点 用 int 是为了内存对齐
}btree_node ;
struct _btree
{
struct btree_node *root; // 一颗 b 树有很多结点
int t; // 2*t 阶 B树
}btree;
B树添加结点
添加结点 - 先分裂(子树满了),再添加
添加的时候,永远添加的都是叶子结点
分裂:(M应当选择一个偶数,则Key(M-1)就是一个奇数,奇数就便于找中间结点做分裂)
分裂有两种情况
- 只有根结点的时候 ,根分裂为 根左子树右子树
- 其他情况,把自己分裂为两个节点,并把中间结点给根结点
// 分配内存,创建结点
btree_node *btree_create_node(int t, int leaf) {
// 根结点
btree_node *node = (btree_node*)calloc(1, sizeof(btree_node)); // 使用calloc是为了让它自动清零
if (node == NULL) assert(0);
node->leaf = leaf;
// 有 2*t-1 个查找值
node->keys = (KEY_VALUE*)calloc(1, (2*t-1)*sizeof(KEY_VALUE));
// 可以插入的2*t个位置(子树)
node->childrens = (btree_node**)calloc(1, (2*t) * sizeof(btree_node*));
node->num = 0;
return node;
}
// 分裂 x 的 第 i 个子结点
void btree_split_child(btree *T, btree_node *x, int i) {
int t = T->t;
// y 是 x 的第 i 个子结点,要分裂的结点
btree_node *y = x->childrens[i];
// z 是分裂的新结点,若y为叶子结点,则分裂的也为叶子结点
btree_node *z = btree_create_node(t, y->leaf);
// 存放前 t-1个值,比如 t=3 就是6阶树,有5个关键字,分裂的时候,把后2个放到z中,中间的放父节点上
z->num = t - 1;
// z 放后面t-1 个
int j = 0;
for (j = 0;j < t-1;j ++) {
z->keys[j] = y->keys[j+t];
// 如果 y 不是叶子结点,则把 y 的孩子也拷贝
if (y->leaf == 0) {
for (j = 0;j < t;j ++) {
z->childrens[j] = y->childrens[j+t];
}
}
// 把 x 的子结点从 i 开始往后移一个
y->num = t - 1;
for (j = x->num;j >= i+1;j --) {
x->childrens[j+1] = x->childrens[j];
}
// 把空出来那个给新的分裂的结点
x->childrens[i+1] = z;
// 父结点的key也往后移一个
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 位置开始找,插入k
void btree_insert_nonfull(btree *T, btree_node *x, KEY_VALUE k) {
int i = x->num - 1;
if (x->leaf == 1) {// 如果 x 是叶子结点
// 找到比k小的位置i,i后的key 后移,k插到 i+1
while (i >= 0 && x->keys[i] > k) {
x->keys[i+1] = x->keys[i];
i --;
}
x->keys[i+1] = k;
x->num += 1;
} else {
while (i >= 0 && x->keys[i] > k) i --;
// 找位置,插到 i+1
// 如果要插入的树满了,就要先分裂
if (x->childrens[i+1]->num == (2*(T->t))-1) {
btree_split_child(T, x, i+1);
if (k > x->keys[i+1]) i++;
}
btree_insert_nonfull(T, x->childrens[i+1], k);
}
}
// 插入操作
void btree_insert(btree *T, KEY_VALUE key) {
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);
// 添加到分裂后的第 i 个位置
int i = 0;
if (node->keys[0] < key) i++;
btree_insert_nonfull(T, node->childrens[i], key);
} else {
btree_insert_nonfull(T, r, key);
}
}
B树删除结点
删除结点 -
删除叶子结点
先借位或者合并使树成为一个可以删除叶子结点的状态,再删除。
删除内结点
先把内结点下沉到叶子结点,再删除。
借位:因为第 idx 位置的 子树的个数要 >= ceil(M/2)-1 所以 要从
idx-1 借位
idx+1 借位
先从前面借,再向后面借
合并, 合并的是
{childs[idx].keys}, {keys[idx]}, {childs[idx+1].keys}
删除的时候,永远删除的都是叶子结点
// 释放结点内存
void btree_destroy_node(btree_node *node) {
assert(node);
free(node->childrens);
free(node->keys);
free(node);
}
// 合并
//{node->child[idx], node[idx], node->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;
//合并
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;
//删除 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;
// 把根结点合并到 左根右 了,就把这个子树提升为根结点,这种情况是 只有2个子结点和一个根结点的时候
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->key[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;
}