数据结构:B树、B+树详解和C语言实现
B树的定义
什么是B树
- B树是专门为磁盘或其他直接存取的辅助存储设备设计的一类平衡查找树,可以实现O(logN)时间复杂度的存取操作。
- 不同于红黑树,B树的每个节点可以存放多个数据。根据每个节点存放数据的多少,可以把B树分为不同的阶数。
为什么要用B树
- B树查找数据速度快,和红黑树等平衡二叉查找树相当,同时由于B树的每个节点可以包含多个数据关键字,相当于对数的底变大,查找的深度变小,减少了对磁盘的存取操作的次数。
B树的性质
我们按照如下性质,来定义一棵非空的m阶B树(m>2,2阶B树相当于二叉平衡查找树):
- 每个非叶子节点中存放若干关键字数据,并且有若干指向儿子节点的指针。指针数目=关键字数目+1
- 根节点有最少1个,最多m-1个关键字,最少2个,最多m个子节点。
- 非根节点最少有[m/2](向上取整),最多m-1个关键字
- 每个节点中的关键字从左到右以非降序排列
- 每个关键字均不小于其左子节点的关键字,不大于其右子节点的所有关键字
- 每个叶子节点都具有相同的深度
B+树
- B+树在B树的基础上进行了改进,在把所有的附加数据信息都存在叶子节点中,而非叶子节点只是存放其每颗子树中最大的关键字和指针,从而大大减小了内部节点的空间占用,使得一个磁盘块可以容纳更多的内部节点,减少了磁盘IO操作的次数,提高效率。
- 并且B+树的叶子结点本身依关键字的大小自小而大顺序链接,相当于一个顺序链表。需要范围查找的时候,b+树只需遍历叶子节点链表即可,b树却需要重复地中序遍历。
B+树与B树的区别
- 在B+树中,具有n个关键字的结点只含有n棵子树,即每个关键字对应一棵子树;而在B树中,具有n个关键字的结点含有(n+1)棵子树。
- 在B+树中,每个结点(非根结点)关键字个数n的范围是[m/2向上取整,m](根结点:1<=n<=m),在B树中,每个结点(非根结点)关键字个数n的范围是[m/2向上取整-1,m-1](根结点:1<=n<=m-1)。
- 在B+树中,叶结点包含信息,所有非叶结点仅起到索引作用,非叶结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
- 在B+树中,叶结点包含了全部关键字,即在非叶结点中出现的关键字也会出现在叶结点中;而在B树中,叶结点包含的关键字和其他结点包含的关键字是不重复的。
B+树的特点
通常在B+树中有两个头指针:一个指向根结点,另一个指向关键字最小的叶结点。因此,可以对B+树进行两种查找运算:一种是从最小关键字开始的顺序查找,另一种是从根结点开始,进行多路查找。
C语言实现
数据结构定义
#define ORDER 3 //B树的阶数
typedef int KeyType; //关键字数据类型
typedef struct BTNode //B树结点
{
int keynum; /// 结点中关键字的个数
KeyType key[ORDER-1]; /// 关键字数组,长度=阶数-1
struct BTNode* child[ORDER]; /// 孩子指针数组,长度=阶数
int isLeaf; /// 是否是叶子节点的标志
}BTNode;
typedef BTNode* BTree; ///定义BTree
创建B树
初始化一个空结点
BTree BTree_init()
{
BTree bt =(BTree)calloc(1,sizeof(BTNode));
bt->isLeaf = 1;
bt->keynum = 0;
bt->parent = NULL;
return bt;
}
根据给定的数据集合进行建树
void BTree_create(BTree *tree,const KeyType *data,int length)
{
//初始化一个B树节点
*tree = BTree_init();
//循环插入
for(int i = 0;i<length;i++)
{
BTree_insert(tree,data[i]);
}
}
结点插入
- 插入过程:
- 节点插从树根开始查找,遇到叶子节点后插入。
- 如果该叶子节点是满节点,则从中间分裂节点然后插入,相当于长高了一层。
- 为了避免叶子节点向上分裂的时候引起其父节点的分裂,所有在从树根向下查找的时候,就直接把遇到的所有满节点进行分裂操作,然后对分裂后的树执行插入操作
判断节点是否是满节点
int isfull(BTNode *node)
{
if(node->keynum < ORDER)
{
return 0;
}
else
{
return 1;
}
}
进行递归插入
void BTree_insert(BTree *tree,KeyType key)
{
BTNode *bnp = *tree;
//节点满,直接进行分裂
if(isfull(*tree))
{
split_tree(tree);
BTree_insert(tree,key);
return;
}
//是叶子节点且不满,直接插入
if(bnp->isLeaf && !isfull(bnp))
{
//比最大的关键字都大,直接插在末尾
if(key>=bnp->key[bnp->keynum -1])
{
bnp->keynum++;
bnp->key[bnp->keynum-1] = key;
}
else
{
for(int i = 0;i<ORDER-1;i++)
{
//找到一个比待插入关键字大的关键字,则直接插入
if(key< bnp->key[i] )
{
KeyType temp= bnp->key[i];
bnp->key[i] = key;
bnp->keynum++;
for(int j = i + 1;j< bnp->keynum;j++)
{
key = bnp->key[j];
bnp->key[j] = temp;
temp = key;
}
break;
}
}
}
return;
}
//不是叶子节点,查找对应的子树,递归插入
if(!bnp->isLeaf)
{
for(int i = 0;i < bnp ->keynum;i++)
{
if(key<bnp->key[i])
{
BTree_insert(&((*tree)->child[i]),key);
return;
}
}
BTree_insert(&((*tree)->child[bnp->keynum]),key);
}
}
子树的分裂
- 子树的分裂是比较麻烦的地方,需要多申请两个节点,进行数据的复制粘贴,注意下标的对应。
- 大致流程是如果父节点为空,申请一个新节点,把中间的关键字拿出来给它,然后让传入的二级指针解引用后指向它。如果不空,则把中间关键字插入到父节点中。
- 接着再申请一个节点,把中间节点的后半部分的关键字和子树指针复制过去,注意节点的性质(是否是叶子节点)也要复制。
- 然后修改原来节点的数据项,使其只保留前半部分的数据
- 最后把两个新得到的子树一左一右挂在新的根节点上。
void split_tree(BTree *tree)
{
BTree bnp1 = *tree;
BTree bnp2 = BTree_init();
BTree bp;
int num = bnp1->keynum;
int split = num/2;
if(bnp1->parent == NULL)
{
bp = BTree_init();
bp->parent = NULL;
bp->keynum = 1;
bp->isLeaf = 0;
bp->key[0] = bnp1->key[split];
bp->child[0] = bnp1;
bp->child[1] = bnp2;
}
else
{
bp = bnp1->parent;
bp->isLeaf = 0;
bp->keynum++;
KeyType temp1, temp2;
BTNode *tcp1, *tcp2;
for(int i = 0;i < bp->keynum;i++)
{
//新关键字插到末尾
if(i == bp->keynum-1)
{
bp->key[i] = bnp1->key[split];
bp->child[i] = bnp1;
bp->child[i+1] = bnp2;
break;
}
//新关键字插到中间
if(bp->key[i]>bnp1->key[split])
{
tcp2 = bnp2;
temp2 = bnp1->key[split];
for(int k = i;k<bp->keynum;k++)
{
//关键字后移
temp1 = bp->key[k];
bp->key[k] = temp2;
temp2 = temp1;
//子树指针后移
tcp1 = bp->child[k+1];
bp->child[k+1] = tcp2;
tcp2 = tcp1;
}
}
}
}
bnp1->keynum = split;
bnp2->keynum = num - split -1;
for(int i = 0, j = split+1;j<num;j++)
{
bnp2->key[i]=bnp1->key[j];
bnp2->child[i]=bnp1->child[j];
}
bnp2->child[bnp2->keynum] = NULL;
bnp2->isLeaf = bnp1->isLeaf;
bnp2->parent = bp;
bnp1->parent = bp;
(*tree) = bp;
}