1 二叉树、多叉树、B树、B+树
- 多叉树与二叉树对比:
树需要加载到内存中,构建树时,需要进行多次I/O操作。
如果节点非常多,会造成树的高度非常大,降低操作速度。
多叉树:降层高,结点数量变少,查找结点的次数就变少。(磁盘查找)
- 多叉树与B树的关系
1 多叉树没有约束平衡
2 没有约束每个节点子树的数量
3 遍历的时候数据是有顺序的
- B+树与B树
1 所有数据存储到叶子节点
2 所有叶子节点通过前后指针链接起来
- 数据库用B+树不用B树
1 B树只适合随机检索,而B+树同时支持随机检索和顺序检索
2 B+树内部节点不存储数据,只存储索引值,因为B+树一个节点可以存储更多的索引值,降低层数,减少了I/O次数,磁盘读写代价更低,I/O读写次数是影响索引检索效率的最大因素
3 B+树的查询效率更加稳定。B树搜索有可能会在非叶子节点阶数,约靠近根节点的记录查找时间越短,其性能等价于在关键字全集内做一次二分查找。而在B+树中,顺序检索比较明显,随机检索时,任何关键字的查找都必须走一条从根节点到叶节点的路,所有关键字的查找路径相同,导致每一个关键字的查询效率相当
4 B树在在基于范围查询的操作上的性能没有B+树好,因为B+树的叶子节点使用了指针顺序的链接在一起,只要遍历叶子节点就可以实现整棵树的遍历,相比较B+树来说,由于B树的叶子节点是相互独立的,所以对于范围查询,需要从根节点再次出发查询,增加了磁盘I/O操作次数
5 增删文件(节点)时,效率更高,因为B+树的叶子节点包含了所有关键字,并以有序的链表结构存储,这样可很好提高增删效率
一个页4k
2 B树的性质
一颗M阶B树T,满足以下条件:
1 每个节点至少拥有M颗子树
2 根节点至少有两棵子树
3 除了根节点外,其余每个分支节点至少有M/2颗子树
4 所有的叶节点都在同一层上
5 有k颗子树的分支节点则存在k-1个关键字,关键字按照递增顺序进行排序
6 关键字数量满足ceil(M/2)-1<=n<=M-1
3 B树的实现
3.1 数据结构
typedef int KEY_VALUE;
// 结点数据结构
typedef struct _btree_node {
KEY_VALUE *keys; // key数组
struct _btree_node **childrens; // 子节点指针数组
int num; // 拥有子节点
int leaf; // 是否叶子节点,1是,0不是
} btree_node;
// 树数据结构
typedef struct _btree {
btree_node *root; // 根节点
int t; // 2*t表示阶数
} btree;
3.2 创建/删除结点、创建树
- 创建结点,初始化结点的数据
btree_node *btree_create_node(int t, int leaf) {
btree_node *node = (btree_node*)calloc(1, sizeof(btree_node)); // 申请内存
if (node == NULL) assert(0); // 申请失败
node->leaf = leaf; // 是否叶子节点
node->keys = (KEY_VALUE*)calloc(1, (2*t-1)*sizeof(KEY_VALUE)); // 给key数组申请内存
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); // 创建结点作为根节点,根节点是叶子节点leaf=1
T->root = x;
}
3.3 插入结点
插入结点时几种情况:
1 找到插入的结点,并且未满(插入点都是叶子节点)
2 找到结点,已满:
2.1 找内节点已满,内节点分裂
2.2 找叶子结点已满,叶子节点分裂
分解出两个基本操作:分裂结点,在不满的节点里插入
- 分裂节点
root的分裂与其他节点的分裂有一点不同,但是这个基本操作是一样的
void btree_split_child(btree *T, btree_node *x, int i) {
// 参数:树、分裂节点的父节点、父节点在结点数组中的位置
int t = T->t;
btree_node *y = x->childrens[i];
btree_node *z = btree_create_node(t, y->leaf); // 用于拷贝分裂后的右半部分
z->num = t - 1; // 共2t-1个结点,分类后两边各t-1,中间的移到父节点上
// 1. 把y结点右边的拷贝给z
int j = 0;
for (j = 0;j < t-1;j ++) {
z->keys[j] = y->keys[j+t];
}
if (y->leaf == 0) { // 有子结点,也进行拷贝
for (j = 0;j < t;j ++) {
z->childrens[j] = y->childrens[j+t];
}
}
// 2. 把y的左边分割出来
y->num = t - 1;
// 3.把中间的结点移动到父节点x里
for (j = x->num;j >= i+1;j --) { // 结点后移
x->childrens[j+1] = x->childrens[j];
}
x->childrens[i+1] = z; // 中间结点放上去
// 更新x的key和num
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;
}
- 在不满的节点里插入
1 如果是叶子节点直接插入
2 如果是内节点,找到对应子节点,如果子节点满了先分裂在插入。
void btree_insert_nonfull(btree *T, btree_node *x, KEY_VALUE k) {
int i = x->num - 1;
// 叶子节点插入
if (x->leaf == 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 --;
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) {
//int t = T->t;
btree_node *r = T->root;
// 如果是root结点满了
if (r->num == 2 * T->t - 1) {
// 创建一个新的根节点,第一个children指向原来的根节点,就跟其他节点一样了
btree_node *node = btree_create_node(T->t, 0);
T->root = node;
node->childrens[0] = r;
btree_split_child(T, node, 0);
int i = 0;
if (node->keys[0] < key) i++; // 如果插入的key大,则在右边插入(下标1),否咋在左边插入(下标0)
btree_insert_nonfull(T, node->childrens[i], key);
} else {
// 如果根节点没满,按照非满插入,
btree_insert_nonfull(T, r, key);
}
}
3.4 删除数据
删除节点后可能不满足可能会不满足B树的性质,至少M/2颗子树。
判断子树key数量是不是M/2-1
如果相邻子树都是M/2-1,合并。
如果左边子树大于M/2-1,借一个结点。
如果右边子树大于M/2-1,借一个结点。
4 B+树应用场景
B+树主要用在磁盘文件组织、数据索引和数据库索引。