B树学习笔记
为什么在二叉树的基础上需要有多叉树这个数据结构呢
多叉树可以降低树的高度, 那降低高度的好处是什么?
减少查找次数?其实遇上最坏的情况, 也不会少的log(n/N) * N, 但是在内存管理中多叉树是发挥不出他的作用的; 但是对于读取磁盘中的数据多叉树能极大的减少读取磁盘这个操作的次数, 因为树的层数变少了
多叉树和b树之间的关系
-
多叉树没有约束关于树高也就是平衡 (b树约束了所有叶子节点在一层)
-
多叉树没有约束子树的数量 (b树每一个节点至少有多个子树)
-
b树的节点数据是有规律的
2-3-4树 3-4-5树 基本都在b树的场景下被替代了
b树
一颗M阶b树T, 必须满足的条件:
- 每个节点至多有
M颗子树 - 根节点至少拥有两颗子树
- 除了根节点以外, 其余每个分支节点至少拥有
M/2颗子树 - 所有的叶子节点都在同一层上
- 有
k颗子树的分支节点, 则存在k - 1个关键字, 关键字按照递增顺序进行排序 - 关键字满足
ceil(M/2) - 1 <= n <= M - 1
b树不适合做范围查询, 因为没有办法快速的去查找范围里的开始和结束的位置, 因此引出了b+树
b+树中所有的叶子节点之间是链表结构
b树和b+树的区别
- b+树所有的数据存储到叶子节点
- 叶子节点通过前后指针链接起来
结点定义
typedef int KEY_TYPE;
struct btree_node {
struct btree_node** children; // 1. 每个节点至多有M颗子树
KEY_TYPE* keys; // M * 2是为了方便树做分裂
int num; // 节点的key的数量
int leaf; // 是否为叶子节点
};
struct btree {
struct btree_node* root;
int t; // 树的degree
};
b树不需要指向父节点, 因为会做分裂
实现
结点创建
// 1 是叶子结点 0 不是
struct btree_node* btree_create_node(int t, int leaf)
{
struct btree_node* node = (struct btree_node*)calloc(1, sizeof(struct btree_node));
// 创建失败
if(node == NULL)
return NULL;
node->num = 0;
node->keys = (KEY_TYPE*)calloc(1, (2 * t - 1) * sizeof(KEY_TYPE));
node->children = (struct btree_node**)calloc(1, 2 * t * sizeof(struct btree_node*));
node->leaf = leaf;
return node;
}
结点销毁
void btree_destroy_node(struct btree_node* node)
{
if(node) {
if(node->keys)
free(node->keys);
if(node->children)
free(node->children);
free(node);
}
}
树的创建
void btree_create(struct btree* T, int t)
{
T->t = t;
struct btree_node* x = btree_create_node(t, 1);
T->root = x;
}
结点插入
不论什么值, 插入的时候一定是插入在叶子结点上然后再根据树的情况下做分裂
- 找到对应的结点并且未满
- 找到的结点已满 a. 找内结点已满, 内结点分裂 b. 找叶子结点已满, 叶子结点分裂
分裂
在我们开始插入之前我们要先理解是如何分裂然后插入结点的
先分裂, 后插入
实现的方式应该是: 先分裂出来, 再找到F插入的位置并插入
/**
* @brief 结点分裂
* @param T b树
* @param x 结点
* @param i 第几颗子树
*/
void btree_split_child(struct btree* T, struct btree_node* x, int i)
{
struct btree_node* y = x->children[i]; // 第i个子树
struct btree_node* z = btree_create_node(T->t, y->leaf); // 创建的新结点用作复制
// Z结点的修改
// 复制
for (int j = 0; j < T->t - 1; j++) {
z->keys[j] = y->keys[j + T->t];
}
// 如果有结点, 把结点下的子树一起复制过来
if (y->leaf) {
for (int j = 0; j < T->t; j++) {
z->children[j] = z->children[j + T->t];
}
}
// Y的修改
y->num = T->t - 1;
// X 的修改
// 选择插入位置, 并把指针后移
for (int j = x->num; j >= i + 1; j--) {
x->children[j + 1] = x->children[j];
}
x->children[i + 1] = z;
for (j = x->num - 1; j >= i; j--) {
x->keys[j + 1] = x->keys[j];
}
x->keys[i] = y->keys[T->t - 1];
x->num += 1;
}
其实画个图就能很快明白这是怎么操作的
插入实现
1. 如果是根节点并且根节点已经满了的情况
生成一个空的父节点, 然后这个结点的第1个子树(index是0)
struct btree_node* root = T->root;
// 根节点数量满了
if (root->num == 2 * T->t - 1) {
struct btree_node* node = btree_create_node(T->t, 0);
T->root = node;
node->children[0] = root;
btree_split_child(T, node, 0);
}
2. 插入的是一个不满的结点
- 如果是叶子结点, 寻找插入位置插入即可
如果是插入到最后简单, 直接插入就行
其他位置就是后移指针
// 当前x结点最后一个key的索引
int i = x->num - 1;
if (x->leaf) {
// 寻找插入位置, 如果不是最后一个后移指针
while (i >= 0 && x->keys[i] > key) {
x->keys[i + 1] = x->keys[i];
i--;
}
// 插入key
x->keys[i + 1] = key;
x->num += 1;
}
- 如果不是叶子结点是个内结点(有其他子树)
// 判断是插入在哪个子树上
while (i >= 0 && x->keys[i] > key) i--;
// 对比子树
// 如果满了就进行分裂
if (x->children[i + 1]->num == 2 * T->t - 1) {
btree_split_child(T, x, i + 1);
// 这个真的非常绝, 就知道是插入在分裂之后的哪个子树上
if (key > x->keys[i + 1]) i++;
}
btree_insert_not_full(T, x->children[i + 1], key);
向下找子树
删除结点
删除是个合并的过程
删除也是类似的, 先合并在做删除
添加是不可能出现合并的
删除是比其他麻烦的不管啥树
判断key数量是M / 2 - 1
1. 相邻两颗子树都是M / 2 - 1, 合并
2. 如果左边子树大于 M / 2 - 1, 借一个结点过来
借结点:
父节点的复制到其中一个子树中(不是借位的那个)
借位的结点提升到父节点
和3是一个情况图画在了3中
3. 如果右边子树大于 M / 2 - 1, 借一个结点过来
b树的线程安全
-
锁
root, 锁粒度过大 -
锁
子树, 是比较可行的方案比起 b+树, b树更向一个学术产品
完整的源码: lingshen_edu/btree.c at master · Vik1ang/lingshen_edu (github.com)
参考资料: