一、了解什么是B树/B-/B+树
B树的定义:其中B-树就是B树,无差别意义。我们一般描述B树时是需要描述他的阶数M,阶数表示了一个结点最多有多少个分叉数量<M阶就是M个分叉数>,当M=2时也就是我们常见的二叉搜索树。所谓的B+树其实就是B树的叶子节点上加上了双向链表的操作。
B树/B+树的优势:
B树与B+树都是对磁盘友好的数据结构,能大幅降低磁盘访问次数。B树的优点在于数据存储在每个结点中,可以更快访问到,而不必须走到叶子结点,B树更多的用在文件系统中。B+树的每个非叶子结点都只充当索引,所以查询必须到叶子结点结束,但它十分适合“扫库”和区间查找,而且因为大多结点只用于索引,所以并不会存储真正的数据,在内存上会更紧凑,相同的内存就可以存放更多的索引数据了。比如字典的拼音和汉字是分离的,只需要几十页就能得到完整的拼音表,但是如果拼音和汉字掺杂在一起,要得到完整的索引(拼音)表就需要整个字典。B+树的这些特性使得它更适合用来做数据库的索引。其中mysql就是select就是使用了该B+树实现。
二、B树的性质
* 1、每个节点最多有M-1个关键字<所谓的关键字就是之里面内部的元素个数/元素数据数量>
* 2、根节点最少可以只有一个关键字
* 3、非根节点至少右ceil(M/2)-1个关键字
* 4、所有的叶子节点都是位于同一层也就是所谓的每个叶子结点的层高一致
* 5、每个结点的关键字都是存在顺序排列的<从小到大>,满足2叉树的性质,最左子树->最右子树 依次从小到大排列
* 6、关键字数量满足 ceil(M/2)-1<=k<=M-1
三、B树的插入和删除操作
只有插入的时候才会进行分裂;只有删除的时候才会进行合并。
插入操作
下图是插入F分裂的示意图:
叶子结点就可以直接插入
删除操作
在我们的key值数量等于 M/2-1时就需要归并了
删除有以下几种情况:
三、代码实现
一定要详细地去阅读注释,已经写得非常非常详细了... 使用A-Z的擦汗如即可测试.
/*
* B-Tree.c
*
* Created on: 2020年9月25日
* Author: Leonard
*
* B树的定义:其中B-树就是B树,无差别意义。我们一般描述B树时是需要描述他的阶数M,阶数表示了一个结点最多有多少个分叉数量<M阶就是M个分叉数>
* 当M=2时也就是我们常见的二叉搜索树
* B 树的属性:
* 1、每个节点最多有M-1个关键字<所谓的关键字就是之里面内部的元素个数/元素数据数量>
* 2、根节点最少可以只有一个关键字
* 3、非根节点至少右ceil(M/2)-1个关键字
* 4、所有的叶子节点都是位于同一层也就是所谓的每个叶子结点的层高一致
* 5、每个结点的关键字都是存在顺序排列的<从小到大>,满足2叉树的性质,最左子树->最右子树 依次从小到大排列
* 6、关键字数量满足 ceil(M/2)-1<=k<=M-1
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#define DEGREE 3 //这个是阶数
#define NONLEAFNODE 0
#define LEAFNODE 1
#define ROOTNODE 2
typedef int KEY_VALUE;
typedef struct _btree_node
{
int num; //keys的数量,也是关键字数量
int leaf; //1:是叶子节点 0:不是叶子节点 2:根节点
KEY_VALUE* keys; //这里是保存的关键字
struct _btree_node **childrens; //这里需要保存指针,也可以使用指针数组
void* reseave;
} *PBTreeNode,TBTreeNode;
typedef struct _btree
{
PBTreeNode root;
int degree; //这个是阶数
} *PBTREE,TBTREE;
//t是阶数,leaf表是否是叶子结点
PBTreeNode CreateBTreeNode(int t, int leaf,void* addData)
{
PBTreeNode node = (PBTreeNode)calloc(1,sizeof(TBTreeNode));
if(!node) assert(0); //直接断言了
node->keys = (KEY_VALUE*)calloc(1,(2*t-1)*sizeof(KEY_VALUE));
if(node->keys == NULL)
{
free(node);
return NULL;
}
node->childrens = (TBTreeNode **)calloc(1,(2*t)*sizeof(TBTreeNode));
if(node->childrens == NULL)
{
free(node->keys);
free(node);
return NULL;
}
node->leaf=leaf;
node->num = 0;
node->reseave = (void *)addData;
return node;
}
void DestroyBTreeNode(PBTreeNode node) {
assert(node);
if(node->childrens)
free(node->childrens);
if(node->keys)
free(node->keys);
if(node->reseave)
free(node->reseave);
free(node);
}
PBTREE CreateBTree(PBTREE btree, int degree,void *addData)
{
PBTREE _bTree = btree;
btree->root = CreateBTreeNode(degree,LEAFNODE,addData);
btree->degree = degree;
return _bTree;
}
/********************************************插入结点操作***********************************************/
/*
*插入主要考虑两种情况:
*1、如果是叶子结点的话就只需要直接插入到叶子结点即可
*2、如果是非叶子结点需要先找到对应的叶子结点,在此之前如果非叶子结点满足满容量关键字的情况就先进行分裂再插入
*/
static void SplitFullOfKeysNode(PBTREE btree,PBTreeNode node, int childIndex)
{
int t = btree->degree; //实际是2*degree所以是一半
int j = 0;
PBTreeNode y = node->childrens[childIndex];
PBTreeNode z = CreateBTreeNode(btree->degree,y->leaf,NULL);
y->num = z->num = t-1; //对半分了
//复制keys的值
for(j=0; j<t-1; j++){
z->keys[j] = y->keys[j+t];
}
//复制子树结点
if(y->leaf == NONLEAFNODE){
for(j=0;j<t;j++){
z->childrens[j] = y->childrens[j+t];
}
}
//拆分子树
for(j=node->num-1; j>childIndex+1; j--){
node->childrens[j+1] = node->childrens[j];
}
node->childrens[childIndex+1] = z;
//把分裂出来的结点复制到Node节点上去
for(j=node->num-1; j>=childIndex; j--){
node->keys[j+1] = node->keys[j];
}
node->keys[childIndex] = y->keys[t-1];
node->num += 1;
}
//递归的插入结点即可
static int InsertNodeInBTree(PBTREE btree, PBTreeNode node, KEY_VALUE key,void* addData)
{
int i = node->num - 1; // index
if(node->leaf == LEAFNODE) //叶子结点直接插入即可
{
//从后面往前面进行对比,所有这里是从后往前挪位置,
while (i >= 0 && node->keys[i] > key) {
node->keys[i+1] = node->keys[i];
i --;
}
node->keys[i+1] = key;
node->reseave = addData;
node->num += 1;
return 0;
}
else
{
while(i >= 0 && node->keys[i] > key) i--; //找到对应的索引值
//这里是子树的关键字数量 满了需要裂变
if (node->childrens[i+1]->num == (2*(btree->degree))-1) {
SplitFullOfKeysNode(btree,node, i+1);
if (key > node->keys[i+1]) i++; //这里是分裂之后需要判断插入的位置
}
InsertNodeInBTree(btree,node->childrens[i+1],key,addData);
}
}
//先进行分裂非叶子节点或者叶子结点分裂之后再插入或者叶子节点直接插入
int BTreeInsert(PBTREE btree,KEY_VALUE key,void* addData)
{
int i = 0;
PBTreeNode root = btree->root;
//这里表示根结点关键字数量达到饱和 需要分裂
if(root->num == 2*(btree->degree)-1){
PBTreeNode node = CreateBTreeNode(btree->degree,NONLEAFNODE,addData);
if(node == NULL) assert(0);
btree->root = node; //这里将根节点设为空结点,等待分裂结点将数据复制过来
node->childrens[0] = root; //指向之前的Btree的根节点
//step1: 先分裂
SplitFullOfKeysNode(btree,node,0);
//step2: 插入
if(node->keys[0] < key) i++; //这里是因为裂变了数据 需要进行一个简单的排序
InsertNodeInBTree(btree, node->childrens[i],key,addData);
}else{
InsertNodeInBTree(btree, root, key, addData);
}
return 0;
}
/******************************************************输出和搜索***************************************************/
void BTreeTraverse(PBTreeNode x) {
int i = 0;
for (i = 0;i < x->num;i ++) {
if (x->leaf == 0)
BTreeTraverse(x->childrens[i]);
printf("%C ", x->keys[i]);
}
if (x->leaf == 0) BTreeTraverse(x->childrens[i]);
}
void BTreePrint(PBTREE T, PBTreeNode node, int layer)
{
PBTreeNode p = node;
int i;
if(p){
printf("\nlayer = %d keynum = %d is_leaf = %d\n", layer, p->num, p->leaf);
for(i = 0; i < node->num; i++)
printf("%c ", p->keys[i]);
printf("\n");
#if 0
printf("%p\n", p);
for(i = 0; i <= 2 * T->t; i++)
printf("%p ", p->childrens[i]);
printf("\n");
#endif
layer++;
for(i = 0; i <= p->num; i++)
if(p->childrens[i])
BTreePrint(T, p->childrens[i], layer);
}
else
printf("the tree is empty\n");
}
int BTreeBinSearch(PBTreeNode node, int low, int high, KEY_VALUE key) {
int mid;
if (low > high || low < 0 || high < 0) {
return -1;
}
while (low <= high) {
mid = (low + high) / 2;
if (key > node->keys[mid]) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return low;
}
/*******************************************************************删除结点操作**************************************************************/
//所谓合并,就是将这个key的两个左右节点进行整合到一个结点上
static void BTreeMergeNode(PBTREE btree,PBTreeNode node,int index)
{
int i = 0;
PBTreeNode left = node->childrens[index];
PBTreeNode right = node->childrens[index+1];
//step 1: key merge
left->keys[left->num-1] = node->keys[index];
for(i = 0; i < btree->degree-1; i++)
left->keys[btree->degree+i] = right->keys[i];
if(left->leaf == NONLEAFNODE) //非叶子结点,需要复制childrens过去
{
for(i=0; i<btree->degree; i++)
left->childrens[btree->degree+i] = right->childrens[i];
}
left->num += btree->degree; //degree == M/2
//step 2: delete right node.
DestroyBTreeNode(right);
//step 3: node结点的处理,将key[index+1...]向前面挪位置
for(i=index+1; i<node->num; i++)
{
node->keys[i-1] = node->keys[i];
node->childrens[i] = node->childrens[i+1];
}
node->childrens[i+1] = NULL;
node->num -= 1;
if(node->num <= 0) //root 结点
{
btree->root = left;
DestroyBTreeNode(node);
}
}
/*
*1、删除key,不是叶子节点,
* a、前面的结点大于degree-1 <借1结点>
* b、后面的结点大于degree-1 <借1结点>
* <这里借1是指在左右之一正好等于degree-1时需要向父节点借一个key值,
* 如果父节点key值被借了而没有,则需要像多的那颗子树取比父节点key值大1的key补上即可>
* c、前后结点==degree-1 <进行合并>
*2、删除key,是叶子结点
* 是叶子结点就直接删除key即可
*
* 这里的degree==m/2
*
* 采用递归思想删除
*/
static int DeleteKeyInBTree(PBTREE btree, PBTreeNode node,KEY_VALUE key)
{
int i = 0;
int index = 0;
int R_flag = 0; //借位标记
if(!node) return 110;
//step 1: 查询找对应的结点索引或者子树
while(key > node->keys[index] && node->num > index) index ++;
//step 2: delete key
if(index < node->num && node->keys[index] == key) //删除的正好是当前节点里面的key值
{
if(node->leaf == LEAFNODE) //当前结点是叶子节点
{
//用后面的key值覆盖之前的,也就是0/NULL
for(i=index; i<node->num-1; i++)
node->keys[i] = node->keys[i+1];
node->keys[node->num - 1] = 0;
node->num--;
if(node->num == 0) //root 结点
{
DestroyBTreeNode(node);
btree->root = NULL;
}
return 0; //delete successful
}
else if(node->childrens[index]->num > btree->degree-1) //left
{
PBTreeNode left = node->childrens[index];
node->keys[index] = left->keys[left->num-1]; //借得的是左子树最大的那个key值
DeleteKeyInBTree(btree,left,left->keys[left->num-1]); //因为已经借1-key到父节点key上所以需要继续将该结点的key值删除,因为本身不属于叶子结点
}
else if(node->childrens[index+1]->num > btree->degree-1) //right
{
PBTreeNode right = node->childrens[index+1];
node->keys[index] = right->keys[0]; //借得的是右子树最小的那个key值
DeleteKeyInBTree(btree,right,right->keys[0]); //因为已经借1-key到父节点key上所以需要继续将该结点的key值删除,因为本身不属于叶子结点
}
else //left==degree-1 && right==degree-1
{
BTreeMergeNode(btree,node,index); //这里先合并再进行删除
DeleteKeyInBTree(btree, node->childrens[index],key);
}
}
else //找寻子树结点里面的key值
{
PBTreeNode child = node->childrens[index];
if (child == NULL) {
printf("the {key:%C} not exist...\n", key);
return 112;
}
/*
* 因为删除的时候有时需要借位或者整合key值及其结点是为了满足
* B树的属性{ceil(M/2)-1<keyCount<M-1},所以我们采取的策略是
* 删除的key所在key删除之后若是不满足属性那么就借位或者整合到可以删除
* 即可
*
*/
if(child->num == btree->degree-1){
PBTreeNode left = NULL;
PBTreeNode right = NULL;
//这里是为了方便借位及其删除所在的key[index]
if(index-1 >= 0) left=node->childrens[index-1];
if(index+1 <= node->num) right=node->childrens[index+1];
//开始整合或者借位
if((left && left->num >= btree->degree) || \
(right && right->num >= btree->degree) )
{
/*这里做的原因是需要将其借位来满足属性*/
if (right) R_flag = 1;
if (left && right) R_flag = (right->num > left->num) ? 1 : 0; //这里做的原因是尽量选择右方,可以直接覆盖过去
if(right && right->num >= btree->degree && R_flag) //向right结点借key/也就是借位 <最小的那个key>
{
//step 1: 左子节点借位 +1 数量也加1
child->keys[child->num] = node->keys[index];
child->childrens[child->num+1] = right->childrens[0];
node->keys[index] = right->keys[0];
child->num ++;
//step 2: 右子节点 覆盖前面借位的key[0]和childrens[0]
for(i=0; i<right->num-1; i++)
{
right->keys[i] = right->keys[i+1];
right->childrens[i] = right->childrens[i+1];
}
//step 3: 右子节点需要清除后面的尾部
right->keys[right->num-1] = 0;
right->childrens[right->num-1] = right->childrens[right->num];
right->childrens[right->num] = NULL;
right->num --;
}
else //向left借位
{
for (i = child->num; i > 0; i --) {
child->keys[i] = child->keys[i-1];
child->childrens[i+1] = child->childrens[i];
}
child->childrens[1] = child->childrens[0];
child->childrens[0] = left->childrens[left->num];
child->keys[0] = node->keys[index-1];
child->num ++;
node->keys[index-1] = left->keys[left->num-1];
left->keys[left->num-1] = 0;
left->childrens[left->num] = NULL;
left->num --;
}
}
else if((!left || (left->num == btree->degree - 1)) && \
(!right || (right->num == btree->degree - 1)) ) //左右子结点当且仅有M/2个key数量
{
if(left && left->num == btree->degree - 1){
BTreeMergeNode(btree,node,index-1);
child = left;
}
else if(right && right->num == btree->degree - 1){
BTreeMergeNode(btree,node,index);
}
}
}
//找下一个子树进行删除操作
DeleteKeyInBTree(btree, child, key);
}
return 0;
}
int BTreeDeleteKeyValue(PBTREE btree,KEY_VALUE key)
{
return (!btree->root) ? -1 : DeleteKeyInBTree(btree, btree->root, key);
}
int main(int argc,const char* argv[])
{
int i = 0;
char key[26] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
TBTREE BT = {0};
CreateBTree(&BT, DEGREE, NULL);
printf("\n==========================INSERT============================\n");
for (i = 0;i < 26;i ++) {
printf("%c ", key[i]);
BTreeInsert(&BT, key[i], NULL);
}
BTreePrint(&BT, BT.root, 0);
printf("\n==========================DELETE============================\n");
for (i = 0;i < 26;i ++) {
printf("\n---------------------------------\n");
BTreeDeleteKeyValue(&BT,key[25-i]);
//BTreeTraverse(BT.root);
BTreePrint(&BT, BT.root, 0);
}
return 0;
}