一,B树的基本概念
那个啥,这篇文章的下面的代码风格,{} 这俩玩意我不知道为啥,它全部自动变成另一种风格了,可是我写的时候并不是这样写的,奇怪,离谱。我也懒得挨个改了,所以代码看起来会有点挫。
假设定义该B树为 m 阶(order)B树。
- 如果根节点不是叶子节点,则至少有两棵子树
- 每一个节点最多有 m-1个关键字, m个孩子。
- 所有的叶子节点都位于同一层
- 除根节点外,每个叶子节点至少有⌈m/2⌉(向下取整)个关键字。
二,B树的定义
typedef struct BTNode {
int keynum; //关键字的个数
int childnum; //孩子的个数
int order; //阶树
struct BTNode* parent; //双亲
ElemType* keys; //关键字数组
struct BTNode** children; //孩子数组,//结构体数组 · child数组里面每个数据是 BinNode* 所以是 **
} BTNode,*BTree;
1.初始化
初始化没搞懂,后面的基本白扯。
//初始化
void BTreeInit(BTree* T,int order) {
(*T) = (BTree)malloc(sizeof(BTNode));
if(*T == NULL) {
printf("开辟空间失败!\n");
exit(-1);
} else {
(*T) -> keynum = (*T) -> childnum = 0;
(*T) -> order = order;
(*T) -> parent = NULL;
(*T) -> keys = (ElemType*)malloc(sizeof(ElemType) * (order+1) ); //关键字数组得比孩子数组多一个
(*T) -> children = (BTNode**)malloc(sizeof(BTNode*) * order);
}
//初始化关键字和孩子数组
int i;
for (i = 0; i < order; i++) {
(*T) -> keys[i] = 0;
(*T) -> children[i] = NULL;
}
(*T) -> keys[i] = 0;
}
这段代码其中最重要的其实只有关键字数组和孩子数组的大小。
因为是5阶B树,所以开辟了6个大小的关键字数组,然后1号单元不放数据,孩子数组开辟了五个。然后一直插入数据。
当新的随着新的数据5的插入,关键字不是最多只能存放 m-1个吗?所以需要进行分裂。
而具体如何分裂的话下面会有,这里只是说为什么分配这样的大小。
三,B树的插入
在B树的插入之前,得先构造几个函数来辅助。
1.找当前节点合适的索引
啥意思呢,就是在你所传过来的节点中去,找到需要插入的位置。
//找到当前节点合适的索引
int FindSuitIndex(BTree cur, ElemType data) {
int i;
for (i = 1; i <= cur -> keynum; i++) {
if(cur -> keys[i] > data) {
return i;
}
}
return i;
}
2.找到合适的叶子节点
要插入一个数据,你得先找到合适的节点,再去合适的索引。
//从根节点开始找到和合适的叶子节点
BTree FindSuitLeafNode(BTree T,ElemType data) {
//如果根节点没有孩子,直接在根节点插入
if(T -> childnum == 0) {
return T;
} else {
//找到合适的索引之后
int index = FindSuitIndex(T,data);
//因为关键字的一号单元未存放数据
FindSuitLeafNode(T -> children[index - 1], data);
}
}
这里解释一下为FindSuitLeafNode(T -> children[index - 1], data); 这样的一个递归,观察下图,假设我现在需要插入一个 2.5 的数据,如果他没有孩子,那么直接在在3的前面插入就行,可是它有孩子,所以我的找到合适索引发现是 1 ,然后对应的下边 则是 1-1 = 0 的位置才可以,才能正好对应上这种从小到大的结构。
3.添加数据
(1)直接插入
//在当前的节点上去添加数据
void AddData(BTree* T,BTree cur,ElemType data) {
//在当前节点上找到合适的索引
int index = FindSuitIndex(cur,data);
int i;
for (i = cur -> keynum; i >= index; i--) {
cur -> keys[i+1] = cur -> keys[i];
}
cur -> keys[index] = data;
cur -> keynum++;
for 循环呢是为了把更小的数据插入到合适的位置,把大的数据向后移动。数组中最基本的操作了。
(2)分裂
当你发现关键字的个数等于了B树的阶数的时候,就该分裂了。
①分裂关键字
//关键字的个数最大是 阶数减一
//不满足条件则进行分裂
if(cur -> keynum == cur -> order) {
BTree lchild = (BTree)malloc(sizeof(BTNode));
BTree rchild = (BTree)malloc(sizeof(BTNode));
BTreeInit(&lchild,cur -> order);
BTreeInit(&rchild,cur -> order);
int mid = cur -> order / 2 + cur -> order % 2; //算中间下标的公式
int i;
//1. 分裂关键字, mid 位置所对应的关键字不进行处理
for (i = 1; i < mid; i++) {
//左边的关键去左孩子上
AddData(T,lchild,cur -> keys[i]);
}
for (i = mid+1; i <= cur -> keynum; i++) {
//右边的关键字去右边
AddData(T,rchild,cur -> keys[i]);
}
递归调用,因为加入新的数后,可能还会引起一系列的分裂。
②分裂当前节点所对应的孩子
//2. 分裂当前节点所对应的孩子们
for (i = 0; i < mid; i++) {
//把当前节点左边的孩子们给新的孩子带过去
lchild -> children[i] = cur -> children[i];
//如果真的有值
if(cur -> children[i] != NULL) {
//与父亲链接
cur -> children[i] -> parent = lchild;
lchild -> childnum++;
}
}
//右边的新孩子的下标得从 0 开始
int j = 0;
for ( i = mid; i < cur -> childnum; i++) {
rchild -> children[j] = cur -> children[i];
if(cur -> children[i] != NULL) {
//与父亲链接
cur -> children[i] -> parent = rchild;
rchild -> childnum++;
j++;
}
}
③生成新的父亲节点
如果没有父亲节点,那么将mid所对应的值直接提升为父亲节点即可。
//3.生成新的父亲节点
if(cur -> parent == NULL) {
BTree newparent= (BTree)malloc(sizeof(BTNode));
BTreeInit(&newparent,cur -> order);
//AddData(T,newparent,cur -> keys[mid]);
newparent -> children[0] = lchild;
newparent -> children[1] = rchild;
lchild -> parent = newparent;
rchild -> parent = newparent;
newparent -> childnum = 2;
AddData(T,newparent,cur -> keys[mid]);
*T = newparent;
} else {
//如果由父亲节点
//分裂出去的孩子的父亲还是我原来的父亲
lchild -> parent = rchild -> parent = cur -> parent;
//将我提上去
//先找到自己在父亲节点中合适的位置
index = FindSuitIndex(cur -> parent,cur -> keys[mid]);
//左孩子可以直接链接
cur -> parent -> children[index - 1] = lchild;
//右孩子则需要判断原来索引所对应的孩子的位置是否有值
if(cur -> parent -> children[index] != NULL) {
//开始向后挪动
for (i = cur -> parent -> childnum -1; i >= index ; i--) {
cur -> parent -> children[i+1] = cur -> parent -> children[i];
}
}
//挪完之后赋值
cur -> parent -> children[index] = rchild;
//由原来的自己本身的一个孩子 变成了 俩 所以还是加了一个
cur -> parent -> childnum++;
//最后将 mid 所对应的关键字插入到 父亲节点中去
AddData(T,cur -> parent, cur -> keys[mid]);
}
}
}
主要看第二种吧。
比如树的原型是这样的。
然后我连续插入几个数据之后发现得进行分裂。那么当前节点分裂出来的两个孩子的父亲,跟我原来的父亲是一样的,
index = 2
(index -1 ) 所对应的则是左孩子。 跟最开始所讲的FIndISuitLeafNode中的原理一样。
(index) 所对应的则是右孩子。
如果像图1一样没有 7,8.这个节点那么直接就可以链接起来,但是图2才是需要解决的,那么我们只需要将原来的往后挪动即可。
4. 构造插入函数
插入函数就这两句话,找到合适的节点,然后插入数据就好了。
//在B-树中插入数据
void BTreeInsert(BTree* T,ElemType data) {
//首先找到合适的节点
BTree cur = FindSuitLeafNode(*T,data);
//在合适的节点中去插入数据
AddData(T,cur,data);
}
1
四,结尾
最开始我也不会B树,头大,但是一点点的尝试,总归会实现的。
我也是在学习中,学习别人的代码,学习新的知识,这篇博客的代码学习来自于B站上的一位视频博主。
视频链接
源码链接