B-树是一种平衡的多路查找树,它在文件系统中很有用。
一棵m阶的B-树,或为空树,或为满足下列特性的m叉树:
- 树中每个节点至多有m棵子树;
- 若根结点不是叶子结点,则至少有两棵子树;
- 除根之外的所有非终端结点至少有 ⌈m/2⌉ 棵子树;
- 所有的非终端结点中包含下列信息数据 (n,A0,K1,A1,K2,A2,…,Kn,An)
- 所有的叶子结点都出现在同一层次上,并且不带信息(看看作是外部结点或查找失败的结点,实际上这些结点不存在,指向这些结点的指针为空)。
其中: Ki(i=1,…,n) 为关键字,且 Ki<Ki+1(i=1,…,n−1) ; Ai(i=0,…,n) 为指向子树根结点的指针,且指针 Ai−1 所指子树中所有结点的关键字均小于 Ki(i=1,…,n) , An 所指子树中所有结点的关键字均大于 Kn , n(⌈m/2⌉)−1≤n≤m−1) 为关键字的个数。
在B-树上进行查找的过程是一个顺指针查找结点和在结点的关键字中进行查找交叉进行的过程。由于B-树主要用作文件的索引,因此它的查获早设计外存的存取,在次略去外存的读写,只做示意性的描述。
#include
#define m 4 /*B-树的阶数*/
typedef int DataType; /*B-树类型定义*/
typedef struct /*元素的定义*/
{
DataType key;
}KeyType;
typedef struct BTNode
{
int keynum; /*每个结点中的关键字个数*/
struct BTNode *parent; /*指向双亲结点*/
KeyType data[m+1]; /*结点中关键字信息*/
struct BTNode *ptr[m+1];/*指针向量*/
}BTNode,*BTree;
typedef struct /*返回结果类型定义*/
{
BTNode *pt; /*指向找到的结点*/
int pos; /*关键字在结点中的序号*/
int flag; /*查找成功与否的标志*/
}result;
B-树的查找其实是对二叉排序树查找的扩展, 与二叉排序树不同的地方是,B-树中每个节点有不止一棵子树。在B-树中查找某个结点时,需要先判断要查找的结点在哪棵子树上,然后在结点中逐个查找目标结点。
result BTreeSearch(BTree T,KeyType k)
{/*在m阶B-树T上查找关键字k,返回结果为r(pt,pos,flag)。如果查找成功,*/
/*则标志flag为1,pt指向关键字为k的结点,否则特征值tag=0*/
BTree p=T,q=NULL;
int i=0,found=0;
result r;
while(p&&!found)
{
i=Search(p,k); /*p-data[i].key<=kdata[i+1].key*/
if(i>0&&p->data[i].key==k.key)/*如果找到查找的关键字,标志found置为1*/
found=1;
else
{
q=p;
p=p->ptr[i];
}
}
if(found) /*查找成功,返回结点的地址和位置序号*/
{
r.pt=p;
r.flag=1;
r.pos=i;
}
else /*查找失败,返回k的插入位置信息*/
{
r.pt=q;
r.flag=0;
r.pos=i;
}
return r;
}
int Search(BTree T,KeyType k)
{/*在T指向的结点中查找关键字为k的序号*/
int i=1,n=T->keynum;
while(i<=n&&T->data[i].key<=k.key)
i++;
return i-1;
}
在B-树上插入结点与在二叉树上插入结点类似,都是插入前后保证B-树仍然是一棵排序树,即结点在左子树中每个节点的关键字小于根结点的关键字,右子树结点关键字大于根结点的关键字。但由于B-树结点中的关键字个数必须大于等于 ⌈m/2⌉−1 ,因此每次插入一个关键字不是在树中添加一个叶子结点,而是首先在最底层的某个非终端结点中添加一个关键字,若该结点的关键字个数不超过m-1,则插入完成,否则对该结点进行分裂。
例如下图是一棵3阶的B-树,在该B树中依次插入关键字35、25、78、43和32。
插入关键字35的过程:
插入关键字25的过程:
插入关键字78的过程:
插入关键字43的过程:
插入关键字32的过程:
void BTreeInsert(BTree *T,KeyType k,BTree p,int i)
{/*在m阶B-树T上结点*p插入关键字k。如果结点关键字个数>m-1,则进行结点分裂调整*/
BTree ap=NULL,newroot;
int finished=0;
int s;
KeyType rx;
if(*T=NULL) /*如果树*T为空,则生成的结点作为根结点*/
{
*T=(BTree)malloc(sizeof(BTNode));
(*T)->keynum=1;
(*T)->parent=NULL;
(*T)->data[1]=k;
(*T)->ptr[0]=NULL;
(*T)->ptr[1]=NULL;
}
else
{
rx=k;
while(p&&finished)
{
Insert(&p,i,rx,ap); /*将rx->key和ap分别插入p->key[i+1]和p->ptr[i+1]*/
if(p->keynum/*如果关键字个数小于m,则表示插入完成*/
{
finished=1;
}
else /*分裂结点*p*/
{
s=(m+1)/2;
split(&p,&ap); /*将p->key[s+1…m],p->ptr[s…m]和
p->recptr[s+1…m]移入新结点*ap*/
rx=p->data[s];
p=p->parent;
if(p)
i=Search(p,rx);/*在双亲结点*p中查找rx->key的插入位置*/
}
}
if(!finished) /*生成含信息(T,rx,ap)的新的根结点*T,原T和ap为子树指针*/
{
newroot=(BTree)malloc(sizeof(BTNode));
newroot->keynum=1;
newroot->parent=NULL;
newroot->data[1]=rx;
newroot->ptr[0]=*T;
newroot->ptr[1]=ap;
*T=newroot;
}
}
}
void Insert(BTree *p,int i,KeyType k,BTree ap)
{/*将r->key和ap分别插入p->key[i+1]和p->ptr[i+1]中*/
int j;
for(j=(*p)->keynum;j>i;j--) /*空出p->data[i+1]*/
{
(*p)->data[j+1]=(*p)->data[j];
(*p)->ptr[j+1]=(*p)->ptr[j];
}
(*p)->data[i+1].key=k.key;
(*p)->ptr[i+1]=ap;
(*p)->keynum++;
}
void split(BTree *p,BTree *ap)
{/*将结点p分裂成两个结点,前一半保留,后一半移入新生产的结点ap*/
int i,s=(m+1)/2;
*ap=(BTree)malloc(sizeof(BTNode)); /*生成新结点ap*/
(*ap)->ptr[0]=(*p)->ptr[s]; /*后一半移入ap*/
for(i=s+1;i<=m;i++)
{
(*ap)->data[i-s]=(*p)->data[i];
if((*ap)->ptr[i-s])
(*ap)->ptr[i-s]->parent=*ap;
}
(*ap)->keynum=m-s;
(*ap)->parent=(*p)->parent;
(*p)->keynum=s-1; /*p的前一半保留,修改keynum*/
}
若要在B-树中删除一个关键字,首先要利用B-树的查找算法找到关键字所在的结点,然后将该关键字从该结点删除。如果删除该关键字后该结点中的关键字个数仍然大于等于 ⌈m/2⌉−1 ,则删除完成;否则需要对结点进行合并。
B-树的删除操作可分为如下3种情况:
1.若删除的关键字所在的结点的关键字个数大于等于 ⌈m/2⌉ ,则仅需将关键字 Ki 和对应的指针 Pi 从该结点撒谎年初即可。删除该关键字后,该结点的关键字个数仍然满足大于等于 ⌈m/2⌉−1 。
2.被删除的关键字所在的结点的关键字个数等于 ⌈m/2⌉−1 ,而与该结点相邻的右兄弟(或左兄弟)结点中的关键字个数大于 ⌈m/2⌉−1 ,则删除关键字后,需要将其兄弟结点中的最小(或最大)的关键字上移至双亲结点中,将双亲结点中小于(或大于)且紧靠该上移关键字的关键字下移至被删关键字所在的结点中。
3.被删除的关键字所在结点和其相邻的兄弟结点的关键字个数均等于 ⌈m/2⌉−1 ,假设该结点有右兄弟,且其右兄弟结点地址由双亲结点中的指针 Pi 所指向,则在删除关键字之后,它所在的结点中剩余的关键字和指针加上双亲结点中的关键字 Ki 一起合并到 Pi 所指的兄弟结点中。若没有右兄弟结点,则合并至左兄弟结点中。
B+树是B-树的一种变形。它与B-树的主要区别为:
- 如果一个结点有n棵子树则必有n个关键字,即关键字个数与结点的子树个数相等。
- 所有叶子结点中包含了全部关键字的信息,及指向这些关键字记录的指针,且叶子结点本身依关键字的大小从小到大顺序链接。
- 所有非终端结点可以看成是索引部分,结点中仅含有其子树(根结点)中的最大(或最小)结点。
如下图所示,为一棵3阶B+树,通常B+树上有两个头指针,一个指向根结点,另一个指向关键字最小的叶子结点。因此可以对B+树进行两种查找运算,即从最小关键字起顺序查找和从根结点开始进行随机查找。
从根结点对B+树进行查找给定的关键字,需要从根结点开始经过非叶子结点到叶子结点进行查找。查找每一个结点,无论查找是否成功,都是走了一条从根结点到叶子结点的路径。在B+树上插入一个关键字和删除一个关键字都是叶子结点中进行,在插入关键字时,要保证每个结点中的关键字个数不能大于m,否则需要对该结点进行分裂。在删除关键字时,要每个结点中的关键字个数不能小于 ⌈m/2⌉ ,否则需要与兄弟结点合并。