B-tree
一、B树的性质
一颗M阶B树T,满足以下条件
1. 每个结点至多拥有M课子树;
2. 根结点至少拥有两颗子树;
3. 除了根结点以外,其余每个分支结点至少拥有M/2课子树;
4. 所有的叶结点都在同一层上;
5. 有k课子树的分支结点则存在k-1个关键字,关键字按照递增顺序进行排序;
6. 关键字数量满足ceil(M/2)-1 <= n <= M-1。
例子:关键字为26英文字母,阶数M为6,按顺序插入得到的B-tree如下所示。
二、B树的创建
2.1 节点的定义
/**
* B-tree
* create by marco
**/
#include <iostream>
#include<vector>
using namespace std;
typedef int KEY_VALUE;
/* 节点数据结构 */
struct btree_node
{
KEY_VALUE *keys; // 节点中的关键字数组
struct btree_node **children; // 指向子节点的数组
int num; // 节点中关键字个数
int leaf; // 是否是叶子节点
};
/* 创建B树节点 */
btree_node* btree_node_create(int dgree, int leaf)
{
btree_node *node = (btree_node*)calloc(1, sizeof(btree_node));
if (node == nullptr) {
printf("btree_node_create failed!\n");
return nullptr;
}
node->leaf = leaf;
node->keys = (KEY_VALUE*)calloc(1, (dgree * 2 - 1) * sizeof(KEY_VALUE));
node->children = (btree_node**)calloc(1, (dgree * 2) * sizeof (btree_node));
node->num = 0;
return node;
}
/* 释放B树节点 */
void btree_node_destory(btree_node *node)
{
if (node == nullptr)
return;
free(node->children);
free(node->keys);
free(node);
}
2.2 B树的定义
struct btree
{
struct btree_node *root; // 指向B树的根节点
int dgree; // B树的阶数的一半, 如此设置方便之后判断节点是否需要操作
};
/* 创建B树 */
btree* btree_create(int dgree)
{
btree *T = new btree;
T->dgree = dgree;
btree_node *node = btree_node_create(dgree, 1);
T->root = node;
return T;
}
三、B树的插入操作
3.1 B树插入关键字的主要思想
- 首先二叉遍历找到待插入的叶子节点。注意,B树关键字的插入,必然是插入到叶子节点,可以这么理解,不遍历到叶子节点是无法判断关键字是否已存在的。
- 判断叶子节点的关键字个数是否小于阶数M-1,若小于即可直接判断插入的具体位置。插入关键字B、C、D如下图所示。
- 如果关键字个数等于阶数M-1,则表示该节点已满,需要分裂节点。分裂叶子节点的具体规则为:将中间关键字(索引为阶数M/2 - 1)放入父节点,新建节点存入被分裂叶子节点的后一半数据,并将新建节点插入到父节点中,排在被分裂叶子节点的分支之后。记得更新被分裂的叶子节点,已经被瓜分啦。插入关键字F如下图所示。
- 注意此时需要递归判断父节点的关键字个数,如果等于阶数M-1,则需继续分裂。
- 分裂结束即可插入关键字。
3.2 具体实现代码如下
/* 分裂B树节点 */
/**
* 参数列表:
* btree *T : B树指针
* btree_node *x : 待分裂节点的父节点指针
* int index : 待分裂节点在父节点中的孩子指针索引
* */
void btree_node_split(btree *T, btree_node *x, int index)
{
int dgree = T->dgree;
btree_node * y = x->children[index];
/* 新建节点z,将待分裂节点的后一半关键字放入新节点z */
btree_node *z = btree_node_create(dgree, y->leaf); // 创建节点,传入参 数为:节点的阶数+是否叶子节点
z->num = dgree - 1; // 设置新节点z的关键字个数
for (int k = dgree; k < 2 * dgree - 1; ++k)
{
z->keys[k - dgree] = y->keys[k];
}
if (y->leaf == 0) // 若待分裂节点为非叶子节点,则拥有的子树也需要复制到 新节点z
{
for (int k = dgree; k < 2 * dgree; ++k )
{
z->children[k - dgree] = y->children[k];
}
}
/* 将分裂出的关键字放入父节点x */
for (int k = x->num - 1; k >= index; k--) // 将后半关键字往后移动位 置,腾出空间,以便存放分裂节点的中间关键字
{
x->keys[k + 1] = x->keys[k];
}
x->keys[index] = y->keys[dgree - 1];
x->num += 1;
y->num = dgree - 1;
/* 将新建节点z作为子树插入到x节点 */
for (int k = x->num; k > index; --k) // 将父节点中部分子树往后移动一 个单位,以便插入新建节点z
{
x->children[k + 1] = x->children[k];
}
x->children[index + 1] = z;
}
/* 插入关键字---根节点非满 */
void btree_insert_key_nodeNonFull(btree *T, btree_node *x, KEY_VALUE key)
{
int index = x->num - 1;
if (x->leaf == 1)
{
while (index >= 0 && x->keys[index] > key )
{
x->keys[index + 1] = x->keys[index];
index--;
}
x->keys[index + 1] = key;
x->num += 1;
}
else // 非叶子节点,则该节点必然不会是待插入节点,继续往下探索,探索过程中判断即将探索的下层节点关键字是否满,满了需要分裂
{
while (index >= 0 && x->keys[index] > key)
index--;
if (x->children[index + 1]->num == (2 * T->dgree - 1))
{
btree_node_split(T, x, index + 1);
if (x->keys[index + 1] < key) // 更新待探索的子节点索引,因为 分裂节点会向x节点插入新的关键字,所以要重新判断该关键字是否小于key
index++;
}
btree_insert_key_nodeNonFull(T, x->children[index + 1], key);
}
}
/* 插入关键字 */
void btree_insert_key(btree *T, KEY_VALUE key)
{
btree_node *r = T->root;
if (r->num == 2 * T->dgree - 1) // 特殊情况,根节点关键字满,需要分裂并且 重新设置根节点
{
btree_node *node = btree_node_create(T->dgree, 0);
T->root = node;
node->children[0] = r;
btree_node_split(T, node, 0);
int i = 0;
if (node->keys[0] < key) // 判断节点插入的位置
++i;
btree_insert_key_nodeNonFull(T, node->children[i], key);
}
else
{
btree_insert_key_nodeNonFull(T, r, key);
}
}
四、B树的删除操作
4.1 B树删除关键字的主要思想
- 首先二叉遍历找到关键字所在的节点。
- 判断节点的关键字个数删除之后是否满足:ceil(M/2)-1 <= n <= M-1。满足即可直接删除啦。删除关键字A如下如所示。
- 不满足的话,首先判断是否可以向左右兄弟节点借关键字。判断规则:a.左右兄弟节点关键字被借走一个之后个数n仍然满足ceil(M/2)-1 <= n <= M-1;b.当左右兄弟都满足时,选关键字个数多的兄弟节点。
- 具体借的规则:首先向父节点借一个左/右关键字(与借的节点方位保持一致)放入待删除关键字的节点中,然后将左/右兄弟节点中的最大/最小关键字复制到父节点中。相应的孩子指针也要相应的移动。删除关键字S如下图所示。
- 如果左右子节点都无法借出关键字时,需要进行合并操作,即将左/右兄弟节点和当前待删除关键字的节点,以及最后一个小于该关键字的父节点中的关键字合并。删除关键字M如下图所示。
- 每个节点中相应的孩子指针的实际操作和关键字类似。
4.2 具体实现代码如下
/* 合并节点 */
/**
* 参数列表:
* btree *T : B树指针
* btree_node *node : 待合并节点的上一次父节点指针
* int index : 合并需要三个部分---父节点关键字,父节点关键字前后的子节点。 index为父节点关键字索引
* */
void btree_node_merge(btree *T, btree_node *node, int index)
{
btree_node *left = node->children[index]; // 合并的左子节点
btree_node *right = node->children[index + 1]; // 合并的右子节点
/* 将父节点关键字以及右子节点都存入左子节点中 */
left->keys[T->dgree - 1] = node->keys[index];
for (int i = 0; i < right->num; ++i) // 移动right中的关键字
{
left->keys[T->dgree + i] = right->keys[i];
}
if (left->leaf == 0) // 移动right中的子节点指针
{
for (int i = 0; i <= right->num; ++i)
{
left->children[T->dgree + i] = right->children[i];
}
}
left->num += right->num + 1;
btree_node_destory(right); // 释放右子节点
for (int i = index + 1; i < node->num; ++i) // 更新父节点关键字和指向子 节点的指针位置
{
node->keys[i - 1] = node->keys[i];
node->children[i] = node->children[i + 1];
}
node->children[node->num + 1] = nullptr;
node->num -= 1;
if (node->num == 0) // 如果父节点个数为0,这种情况只可能是父节点为根节点
{
T->root = left;
btree_node_destory(node);
}
}
/* 删除节点中关键字 */
void btree_node_delete_key(btree *T, btree_node *node, KEY_VALUE key)
{
if (node == nullptr)
return;
int index = 0;
// 找到节点中第一个大于等于key的关键字
while (index < node->num && key > node->keys[index])
index++;
if (index < node->num && node->keys[index] == key) { // 在该节点中找 到等于key的关键字, 考虑具体的删除操作
if (node->leaf == 1) // 该节点为叶子节点
{
for (int i = index; i < node->num - 1; ++i)
{
node->keys[i] = node->keys[i + 1];
}
node->keys[node->num - 1] = 0;
node->num -= 1;
if (node->num == 0)
{
free(node);
T->root = nullptr;
}
} else if (node->children[index]->num >= T->dgree) { // 借用左 子节点的关键字进行覆盖
btree_node * left = node->children[index];
node->keys[index] = left->keys[left->num - 1];
btree_node_delete_key(T, left, left->keys[left->num - 1]); / / 递归删除子节点中用来覆盖上层节点的关键字
} else if (node->children[index + 1]->num >= T->dgree) { // 借 用左子节点的关键字进行覆盖
btree_node *right = node->children[index + 1];
node->keys[index] = right->keys[0];
btree_node_delete_key(T, right, right->keys[0]); // 递归删 除子节点中用来覆盖上层节点的关键字
} else {
btree_node_merge(T, node, index);
btree_node_delete_key(T, node->children[index], key);
}
} else { // 继续往children[index]子节点找
btree_node *child = node->children[index];
if (child == nullptr) {
printf("Cannot find key : %d\n", key);
return;
}
/* 找关键字所在的节点过程中,遇到关键字个数==dgree-1的节点,进行丰满处 理 */
if (child->num == T->dgree - 1) {
btree_node *left = nullptr, *right = nullptr;
if (index > 0)
left = node->children[index - 1];
if (index < node->num)
right = node->children[index + 1];
if ((left && left->num >= T->dgree) || (right && right->num >= T->dgree)) {
int select = 0;
if (left && right)
select = (right->num > left->num) ? 1 : 0;
if (select) { // 选右子树
/* 将父节点node的第一个大于key的关键字移给child */
child->keys[child->num] = node->keys[index];
child->num += 1;
child->children[child->num] = right->children[0];
/* 将child右兄弟节点的第一个关键字移给父节点node */
node->keys[index] = right->keys[0];
/* 更新child右兄弟节点中关键字和子节点指针的位置 */
for (int i = 0; i < right->num - 1; ++i) {
right->keys[i] = right->keys[i + 1];
right->children[i] = right->children[i + 1];
}
right->keys[right->num - 1] = 0;
right->children[right->num - 1] = right->children [right->num];
right->children[right->num] = nullptr;
right->num -= 1;
} else { // 选左子树
child->num++;
child->children[child->num] = child->children [child->num - 1];
for (int i = child->num - 2; i >= 0; --i) {
child->keys[i + 1] = child->keys[i];
child->children[i + 1] = child->children[i];
}
child->keys[0] = node->keys[index - 1];
child->children[0] = left->children[left->num];
node->keys[index - 1] = left->keys[left->num - 1];
left->keys[left->num - 1] = 0;
left->children[left->num] = nullptr;
left->num -= 1;
}
} else {
if (left && left->num == T->dgree - 1) {
btree_node_merge(T, node, index - 1);
child = left;
} else if (right && right->num == T->dgree - 1) {
btree_node_merge(T, node, index);
}
}
}
btree_node_delete_key(T, child, key);
}
}
/* 删除B树关键字 */
void btree_delete_key(btree *T, KEY_VALUE key) {
if (T->root == nullptr)
return ;
btree_node_delete_key(T, T->root, key);
}
五、B树的应用案例
案例一、设计一个图片存储索引组件
1. 图片的数量是巨大的。
2. 可以添加图片,可以删除图片
3. 可以通过图片名称进行查找
4. 添加,删除,查找都是对单一图片进行操作
六、Tips
下面代码为打印函数和主函数,所有代码段可组成完整的B树创建、添加、删除代码。
void btree_print(btree *T, btree_node *node, int layer)
{
btree_node *p = node;
if (p) {
printf("\nlayer = %d keynum = %d is_leaf = %d\n", layer, p->num, p->leaf);
for(int i = 0; i < node->num; i++)
printf("%C ", p->keys[i]);
printf("\n");
layer++;
for(int i = 0; i <= p->num; i++)
if(p->children[i])
btree_print(T, p->children[i], layer);
}
else
printf("the tree is empty\n");
}
int main() {
btree *T = btree_create(3);
char key[27] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
for (int i = 0; i < 26; i++) {
printf("%C ", key[i]);
btree_insert_key(T, key[i]);
btree_print(T, T->root, 0);
}
btree_print(T, T->root, 0);
for (int i = 0; i < 26; i++) {
printf("\n---------------------------------\n");
btree_delete_key(T, key[25-i]);
btree_print(T, T->root, 0);
}
return 0;
}
七、 源代码下载