提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
一、B树的用途
B树在用途是很普遍的,常见的数据库mysql,sqlite都用到了B树。总的来讲,B树主要是用于大规模的搜索,其层高固定的性质,提高了搜索的效率,降低了复杂度。B树是一种外存数据结构,也就是用到外部磁盘的时候,会用到B树。因此磁盘的每一次寻址都是一次时间消耗,常规的二叉查找树如果数据量很大的话,树的层高就很高,从而增加了磁盘寻址的时间消耗,B树使层高固定在横向添加关键字索引方便查找,并且也降低了查找树时磁盘寻址的次数。
B树的性质
一个m阶的B树需要满足以下性质:
每个结点最多包含m个孩子(m>=2)
叶子结点的层高都一致
除了根结点和子结点外,每个结点的元素必须大于等于m/2个
如果根结点不是叶子结点,则至少有两个孩子
每个非终端结点中包含n个关键字:
(1)ki为关键字,从头到尾按照升序排序
(2)pi为关键字之间的接点,指向子树根,pi指向的子树上的元素大小在k(i-1)到k(i)之间
(3)结点上的n个关键字,n必须满足m/2-1 <= n <= m-1
还有一种是以度(一个结点能包含的最少的关键字数)表示的B-树。那么每个结点最多就可以包含2m-1个关键字,最多包含2m个孩子结点。
B树的定义
我在代码中经常这样定义B树的结点:
#define KEY_VALUE int
typedef struct btree_node {
KEY_VALUE* keys; // 结点的关键字的集合
struct btree_node* childrens[]; // 结点的子结点的指针数组
int num; // 结点的关键字的个数
int leaf; // 是否是叶子结点 1:叶子结点 0:非叶子结点
} btree_node;
typedef struct btree {
btree_node* root;
int degree; // B树的度
} btree;
结点的定义相对来说比较简单,主要包含关键字的集合,指向孩子的指针数组,关键字的个数以及是否是叶子结点。其中是否是叶子结点这个参数比较重要,因为需要带入来判断我们是否是叶子结点,叶子结点和非叶子结点的操作是不一样的
二、B树结点的创建
创建一个结点是比较简单的,有了上面的数据结构,我们只要对每一个值进行初始化就可以
btree_node* btree_create_node(int t, int leaf)
{
btree_node* node = (btree_nbode*)calloc(1, sizeof(btree_node));
if (node == NULL) {
return;
}
node->keys = (KEY_VALUE*)calloc(1, (2 * t -1) * sizeof(KEY_VALUE));
if (node->keys == NULL) {
free(node);
return;
}
node->childrens = (btree_node**)calloc(1, 2 * t * sizeof(btree_node*));
if (node->keys == NULL) {
free(node->keys);
free(node);
return;
}
node->num = 0;
node->leaf = leaf;
return node;
}
三、B树的删除
有创建的话就会有删除
void btree_delete_node(btree_node* node)
{
if (node == NULL) {
return;
}
if (node->keys) {
free(node->keys);
}
if (node->childrens != NULL) {
free(node->childrens);
}
free(node);
return;
}
四、B树结点的插入
接下来的就是比较重点的部分了。B树的插入和删除。
在现有的B树中插入一个元素到子结点,其实此操作和二叉树的插入类似,从树的根开始遍历到适合当前元素插入的位置(比较元素和结点的key的大小)。但是B树的性质仍然需要满足,因此插入之前需要现在满足B树的性质,保证插入元素之后能够也能满足B树的性质。
那么我们怎样保证插入元素之前,B树的性质不被破坏呢?
这时候就需要采用我们传说中的拆分操作,将即将满的结点拆成两部分,前半部分保留在该结点中,后半部分放入创建的新结点中,中间的结点上移到父结点指针的后一个元素之前。如下图:
那么具体的步骤如下:
- 初始化node作为根结点
- 如果node不是叶子结点,找到node的下一个孩子y;如果y没满的话,将它作为新的结点x,找下一个孩子;如果满了就进行分裂操作,结点node指向结点y的两部分,如果key的值大于y结点的中间元素的值就插在右半边,key小于中间值则插在左半边。
- 如果插入到叶子结点的话,由于之前从根结点一路走来肯定是满足B树的性质的,所以直接插入可以
void btree_insert_notfull(btree* T, btree_node* node, KEY_VALUE key)
{
if (node == NULL || T == NULL) {
return;
}
int i = node->num - 1;
if (node->leaf) { // 插入到叶子结点上
while (i >= 0 && node->keys[i] > key) {
node->keys[i] = node->keys[i - 1];
i--;
} // 找到第一个比key小的值,插到他的它一个位置
node->keys[i++] = key;
node->num++; // 数量加1,由于加的树叶子结点,所以不用管子结点的指针数组,因为就是NULL;
} else {
while (i >= 0 && node->keys[i] > key) i--; // 找到当前的结点
if (node->childrens[i + 1]->num == 2 * T->degree - 1) {
btree_splite_node(T, node, i); // 如果当前的结点已经满了,则分裂
if (key > node->keys[i + 1]) i++; // 分裂完之后,如果子结点上移的结点小于key,那么就插到下一个子结点;如果大于key就插到当前的子结点
}
btree_insert_notfull(T, node->childrens[i + 1], key);
}
}
void btree_insert(btree* T, int key)
{
if (node == NULL || T == NULL) {
return;
}
btree_node* node = T->root;
if (node->num != 2 * T->degree - 1) {
btree_insert_notfull(T, node, key);
} else {
// 如果根结点满的话,我们创建一个新的结点,将新的结点作为根结点
btree_node* x = btree_create_node(T->degree, 0);
T->root = x;
x->childrens[0] = node;
btree_splite_node(T, x, 0);
// 分裂之后如果key小于上移的值,则到当前的子结点下面找,如果大于的话就到下一个子结点去找key的值。
int j = 0;
if (key > x->keys[0]) j++;
btree_insert_notfull(T, x->childrens[j], key);
}
}
void btree_splite_node(btree* T, btree_node* x, int key)
{
int t = T->degree;
btree_node* y = x->childrens[i];
btree_node* z = btree_create_node(t, y->leaf);
z->num = t - 1;
int j = 0;
// 创建z结点,将原结点的右半部分给z
for (j = 0; j < t - 1; j++) {
z->keys[j] = y->keys[j + t];
}
if (!z->leaf) {
for (j = 0; j < t; j++) {
z->childrens[j] = y->childrens[j + t];
}
}
y->num = t - 1;
// 将x结点的子结点指针以及关键字从后往前移,给上移的结点让出空间
for (j = x->num; j >= i + 1; j--) {
x->childrens[j + 1] = x->childrens[j];
}
x->chiledrens[i + 1] = z;
for (j = x->num - 1; j >= i; j--) {
x->keys[j + 1] = x->keys[j];
}
x->keys[j] = y->keys[t - 1];
x->num++;
}
五、B树的删除
删除的操作比插入的操作要更加复杂,当然如果只是在叶子结点上进行删除,那就比较简单了。但如果在飞叶子结点上进行删除呢?我们需要考虑以下几种情况:
- 如果删除的关键字在叶子结点,那么直接删除k关键字
- 如果删除的关键字在当前的结点中,这是有需要分为3种情况:
(1)如果位于要删除的关键字之前的孩子结点上至少有num个关键字,那么就将前一个子结点的最后一个关键字上移,然后删掉该关键字所占的内存
(2)如果位于要删除的关键字之后的孩子结点至少有num个关键字,那么就将后一个子结点的第一个关键字上移,覆盖要删除的关键字,然后将子结点的该关键字删除
(3)如果位于要删除关键字的前后两边的子结点的关键字个数没有t个,那么就需要合并两个子结点和父结点要删除的这个结点到左边的子结点,删除右边的子结点,然后在左边的子结点中删除关键字结点
- 如果删除的关键字在不在当前的结点中,则确定包含关键字的子树的根结点。
(1)如果当前结点的兄弟结点都只包含t-1个关键字,将x的一个关键字移动至新的结点,使之合并为一个新的结点。图中描述的是和右兄弟结点合并,也可以和左兄弟结点合并
(2)如果当前结点仅包含t-1个关键字,但是兄弟结点至少有t个关键字,那么久先将父结点的某个关键字下移到当前的结点,然后将兄弟结点的一个关键字上移到父结点。图中表示的是右兄弟结点借,也可以向左兄弟结点借
void btree_merge_key(btree* T, btree_node* node, int idx)
{
if (T->root == NULL || node == NULL) {
return;
}
btree_node* left = node->childrens[idx];
btree_node* right = node->childrens[idx + 1];
left->keys[T->degree - 1] = node->keys[idx];
for (int i = 0; i < T->degree - 1; i++) {
left->keys[i + T->degree] = right->keys[i];
}
if (!left->leaf) {
for (int i = 0; i < T->degree; i++) {
left->childrens[i + T->degree] = right->keys[i];
}
}
left->num += T->degree;
btree_destory_node(right);
for (int j = idx + 1; j < node->num; j++) {
node->keys[j - 1] = node->keys[j];
node->childrens[i] = node->childrens[i + 1];
}
node->keys[node->num - 1] = 0;
node->childrens[node->num] = NULL;
node->num--;
if (!node->num) {
T->root = left;
btree_destory_node(node);
}
}
void btree_delete_key(btree* T, btree_node* node, KEY_VALUE key)
{
if (node == NULL) {
return;
}
int idx = 0;
while (idx < node->num && key > node->keys[idx]) { // 找到第一个大于key的值
idx++;
}
if (idx < num && key == node->keys[idx]) { // 如果找到了key
if (node->leaf) { // 所在结点为叶子结点
for (int 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) { // 如果为根结点的话直接删除
free(node);
T->root = 0;
}
return;
} else {
if (node->childrens[idx]->num >= T->degree) { // 如果子树可以借一个的话就问子树借
node->keys[idx] = node->keys[node->num - 1];
btree_delete_key(T, node->childrens[idx], node->keys[node->num - 1]);
} else if (node->childrens[idx + 1]->num >= T->degree) { // 没有的话问下一个子树借
node->keys[idx] = node->childrens[idx + 1]->keys[0];
btree_delete_key(T, node->childrens[idx + 1], node->childrens[idx + 1]->keys[0]);
} else { // 两边子树的关键字个数都没到T->degree的话,只能合并
// 合并
btree_merge_key(T, node, idx);
btree_delete_key(T, node->childrens[idx], key);
}
}
} else {
btree_node* child = node->childrens[idx];
if (node == NULL) {
printf("no result\n");
return;
}
if (child->num == T->degree - 1) { // 如果当前子结点不够借位
btree_node* left = node->keys[idx - 1];
btree_node* right = node->keys[idx + 1];
if ((left && left->num >= T->degree) || // 问兄弟结点借,然后加到子结点的上面,再进行删除
(right && right->num >= T->degree)) {
int richR = 0;
if (right) {
richR = 1;
}
if (left && right) {
richR = (right->num - left->num) ? 1 : 0;
}
if (right && right->num >= T->degree && richR) {
child->keys[child->num] = node->keys[idx];
child->childrens[child->num + 1] = right->childrens[0];
child->num += 1;
node->keys[idx] = right->keys[0];
for (int i = 0; i < right->num - 1; i++) {
right->keys[i] = right->keys[i + 1];
right->childrens[i] = right->childrens[i + 1];
}
right->keys[num - 1] = 0;
right->childrens[num - 1] = right->childrens[num];
right->childrens[num] = NULL;
right->num--;
} else {
for (int j = child->num; j > 0; j--) {
child->keys[j] = child->keys[j - 1];
child->childrens[j + 1] = child->childrens[j];
}
child->keys[0] = left->keys[left->num - 1];
child->childrens[1] = child->childrens[0];
child->childrens[0] = left->childrens[left->num];
child->num++;
left->childrens[left->num] = NULL;
left->keys[left->num - 1] = 0;
left->num--;
}
} else if ((!left || left->num == T->degree - 1) ||
(!right || right->num == T->degree - 1)) {
if (left && left->num == T->degree - 1) {
btree_merge_key(T, node, idx -1);
child = left;
} else if (right && right->num == T->degree - 1) {
btree_merge_key(T, node, idx);
}
}
}
btree_delete_key(T, child, key);
}
}
int btree_delete(btree_node* T, KEY_VALUE key)
{
if (!T->root) {
return -1;
}
btree_delete_key(T, T->root, key);
return 0;
}
六、B树的查找
B树的查找思路其实和二叉树的查找是一致的,从根结点开始遍历,对每个结点进行遍历,如果找到关键字则直接返回,在当前子结点未找到的话则找到关键字所在的子树继续向下查找,最后如果遍历到空结点还是未找到的话,说明没有该关键字
int btree_node_search(btree_node* node, int key)
{
if (T->root == NULL) {
return -1;
}
int i = 0;
while (i < n && key > node->keys[i]) {
i++;
}
if (node->keys[i] == key) {
return key;
}
if (node->leaf) {
return -1;
}
return btree_node_search(node->childrens[i], key);
}
七、B树的中序遍历
B树的中序遍历也是和二叉树类似,遵循左->中->右的顺序进行
void travelse(btree_node* x)
{
int i = 0;
for (i = 0; i < x->num; i++) { // 非叶子结点,先遍历左边子树的结点
if (x->leaf == 0) {
travelse(x->childrens[i]);
}
print("%d ", x->keys[i]);
}
if (x->leaf == 0) { // 打印以最后一个孩子为根的子树
travelse(x->keys[i]);
}
}