B_树主要用于文件系统中,在B_树中,每个结点的大小为一个磁盘页,结点中所包含的关键字及其孩子的数目取决于页的大小。一棵度为m的B_树称为m阶B_树,其定义是:
一棵m阶B_树,或者是空树,或者是满足以下性质的m叉树:
⑴ 根结点或者是叶子,或者至少有两棵子树,至多有m棵子树;
⑵ 除根结点外,所有非终端结点至少有m/2棵子树,至多有m棵子树;
⑶ 所有叶子结点都在树的同一层上;
⑷ 每个结点应包含如下信息:
(n,A0,K1,A1,K2,A2,… ,Kn,An)
其中Ki(1≤i≤n)是关键字,且Ki<Ki+1 (1≤i≤n-1);Ai(i=0,1,… ,n)为指向孩子结点的指针,且Ai-1所指向的子树中所有结点的关键字都小于Ki ,Ai所指向的子树中所有结点的关键字都大于Ki ;n是结点中关键字的个数,且m/2-1≤n≤m-1,n+1为子树的棵数。
当然,在实际应用中每个结点中还应包含n个指向每个关键字的记录指针.
根据m阶B_树的定义,结点的类型定义如下:
#define M 5 /* 根据实际需要定义B_树的阶数 */
typedef struct BTNode
{
int keynum ; /* 结点中关键字的个数 */
struct BTNode *parent ; /* 指向父结点的指针 */
KeyType key[M+1] ; /* 关键字向量,key[0]未用 */
struct BTNode *ptr[M+1] ; /* 子树指针向量 */
RecType *recptr[M+1] ;
/* 记录指针向量,recptr[0]未用 */
}BTNode ;
B_树的查找
⑴ 算法思想
① 从树的根结点T开始,在T所指向的结点的关键字向量key[1…keynum]中查找给定值K(用折半查找) :
若key[i]=K(1≤i≤keynum),则查找成功,返回结点及关键字位置;否则,转⑵;
② 将K与向量key[1…keynum]中的各个分量的值进行比较,以选定查找的子树:
◆ 若K<key[1]:T=T->ptr[0];
◆ 若key[i]<K<key[i+1](i=1, 2, …keynum-1):T=T->ptr[i];
◆ 若K>key[keynum]:T=T->ptr[keynum];
转①,直到T是叶子结点且未找到相等的关键字,则查找失败。
⑵ 算法实现
int BT_search(BTNode *T, KeyType K, BTNode *p)
/* 在B_树中查找关键字K, 查找成功返回在结点中的位置 */
/* 及结点指针p; 否则返回0及最后一个结点指针 */
{
BTNode *q ;
int n ;
p=q=T ;
while (q!=NULL)
{
p=q ;
q->key[0]=K ; /* 设置查找哨兵 */
for (n=q->keynum ; K<q->key[n] ; n--)
if (n>0&&EQ(q->key[n], K) ) return n ;
q=q->ptr[n] ;
}
return 0 ;
}
⑶ 算法分析
在B_树上的查找有两中基本操作:
◆ 在B_树上查找结点(查找算法中没有体现);
◆ 在结点中查找关键字:在磁盘上找到指针ptr所指向的结点后,将结点信息读入内存后再查找。因此,磁盘上的查找次数(待查找的记录关键字在B_树上的层次数)是决定B_树查找效率的首要因素。
根据m阶B_树的定义,第一层上至少有1个结点,第二层上至少有2个结点;除根结点外,所有非终端结点至少有m/2棵子树,…,第h层上至少有m/2h-2个结点。在这些结点中:根结点至少包含1个关键字,其它结点至少包含m/2-1个关键字,设s=m/2,则总的关键字数目n满足:
因此有:
即在含有n个关键字的B_树上进行查找时,从根结点到待查找记录关键字的结点的路径上所涉及的结点数不超过1+ ㏒ém/2ù((n+1)/2) 。
B_树的插入
B_树的生成也是从空树起,逐个插入关键字。插入时不是每插入一个关键字就添加一个叶子结点,而是首先在最低层的某个叶子结点中添加一个关键字,然后有可能“分裂”。
⑴ 插入思想
① 在B_树的中查找关键字K,若找到,表明关键字已存在,返回;否则,K的查找操作失败于某个叶子结点,转 ②;
② 将K插入到该叶子结点中,插入时,若:
◆ 叶子结点的关键字数<m-1:直接插入;
◆ 叶子结点的关键字数=m-1:将结点“分裂” 。
⑵ 结点“分裂”方法
设待“分裂”结点包含信息为:
(m,A0,K1,A1,K2,A2,… ,Km,Am),从其中间位置分为两个结点:
(ém/2ù-1,A0,K1,A1,… ,Kém/2ù-1 ,Aém/2ù-1 )
(m-ém/2ù,Aém/2ù,Kém/2ù+1,Aém/2ù+1 ,… ,Km,Am )
并将中间关键字Kém/2ù插入到p的父结点中,以分裂后的两个结点作为中间关键字Kém/2ù的两个子结点。
当将中间关键字Kém/2ù插入到p的父结点后,父结点也可能不满足m阶B_树的要求(分枝数大于m),则必须对父结点进行“分裂”,一直进行下去,直到没有父结点或分裂后的父结点满足m阶B_树的要求。
当根结点分裂时,因没有父结点,则建立一个新的根,B_树增高一层。
⑶ 算法实现
要实现插入,首先必须考虑结点的分裂。设待分裂的结点是p,分裂时先开辟一个新结点,依此将结点p中后半部分的关键字和指针移到新开辟的结点中。分裂之后,而需要插入到父结点中的关键字在p的关键字向量的p->keynum+1位置上。
BTNode *split(BTNode *p)
/* 结点p中包含m个关键字,从中分裂出一个新的结点 */
{
BTNode *q ;
int k, mid, j ;
q=(BTNode *)malloc(sizeof( BTNode)) ;
mid=(m+1)/2 ;
q->ptr[0]=p->ptr[mid] ;
for (j=1,k=mid+1; k<=m; k++)
{
q->key[j]=p->key[k] ;
q->ptr[j++]=p->ptr[k] ;
} /* 将p的后半部分移到新结点q中 */
q->keynum=m-mid ;
p->keynum=mid-1 ;
return(q) ;
}
void insert_BTree(BTNode *T, KeyType K)
/* 在B_树T中插入关键字K,*/
{
BTNode *q, *s1=NULL, *s2=NULL ;
int n ;
if (!BT_search(T, K, p)) /* 树中不存在关键字K */
{
while (p!=NULL)
{
\ p->key[0]=K ; /* 设置哨兵 */
for (n=p->keynum ; K<p->key[n] ; n--)
{
p->key[n+1]=p->key[n] ;
p->ptr[n+1]=p->ptr[n] ;
} /* 后移关键字和指针 */
p->key[n]=K ;
p->ptr[n-1]=s1 ;
p->ptr[n+1]=s2 ; /* 置关键字K的左右指针 */
if (++(p->keynum ))<m break ;
else
{
s2=split(p) ;
s1=p ; /* 分裂结点p */
K=p->key[p->keynum+1] ;
p=p->parent ; /* 取出父结点*/
}
if (p==NULL) /* 需要产生新的根结点 */
{
p=(BTNode *)malloc(sizeof( BTNode)) ;
p->keynum=1 ;
p->key[1]=K ;
p->ptr[0]=s1 ;
p->ptr[1] =s2 ;
}
}
利用m阶B_树的插入操作,可从空树起,将一组关键字依次插入到m阶B_树中,从而生成一个m阶B_树。
B_树的删除
在B_树上删除一个关键字K ,首先找到关键字所在的结点N,然后在N中进行关键字K的删除操作。
若N不是叶子结点,设K是N中的第i个关键字,则将指针Ai-1所指子树中的最大关键字(或最小关键字)K’放在(K)的位置,然后删除K’,而K’一定在叶子结点上。如图9-15(b),删除关键字h,用关键字g代替h的位置,然后再从叶子结点中删除关键字g。
从叶子结点中删除一个关键字的情况是:
⑴ 若结点N中的关键字个数>ém/2ù-1:在结点中直接删除关键字K。
⑵ 若结点N中的关键字个数=ém/2ù-1:若结点N的左(右)兄弟结点中的关键字个数>ém/2ù-1,则将结点N的左(或右)兄弟结点中的最大(或最小)关键字上移到其父结点中,而父结点中大于(或小于)且紧靠上移关键字的关键字下移到结点N,。
⑶ 若结点N和其兄弟结点中的关键字数=ém/2ù-1:删除结点N中的关键字,再将结点N中的关键字、指针与其兄弟结点以及分割二者的父结点中的某个关键字Ki,合并为一个结点,若因此使父结点中的关键字个数<ém/2ù-1 ,则依此类推。
算法实现
在B_树上删除一个关键字的操作,针对上述的⑵和⑶的情况,相应的算法如下:
int BTNode MoveKey(BTNode *p)
/* 将p的左(或右)兄弟结点中的最大(或最小)关键字上移 */
/* 到其父结点中,父结点中的关键字下移到p中 */
{
BTNode *b , *f=p->parent ; /* f指向p的父结点 */
int k, j ;
for (j=0; f->ptr[j]!=p; j++) /* 在f中找p的位置 */
if (j>0) /* 若p有左邻兄弟结点 */
{
b=f->ptr[j-1] ; /* b指向p的左邻兄弟 */
if (b->keynum>(m-1)/2)
/* 左邻兄弟有多余关键字 */
{
for (k=p->keynum; k>=0; k--)
{
p->key[k+1]=p->key[k];
p->ptr[k+1]=p->ptr[k];
} /* 将p中关键字和指针后移 */
p->key[1]=f->key[j];
f->key[j]=b->key[keynum] ;
/* f中关键字下移到p, b中最大关键字上移到f */
p->ptr[0]= b->ptr[keynum] ;
p->keynum++ ;
b->keynum-- ;
return(1) ;
}
if (j<f->keynum) /* 若p有右邻兄弟结点 */
{
b=f->ptr[j+1] ; /* b指向p的右邻兄弟 */
if (b->keynum>(m-1)/2)
/* 右邻兄弟有多余关键字 */
{
p->key[p->keynum]=f->key[j+1] ;
f->key[j+1]=b->key[1];
p->ptr[p->keynum]=b->ptr[0];
/* f中关键字下移到p, b中最小关键字上移到f */
for (k=0; k<b->keynum; k++)
{
b->key[k]=b->key[k+1];
b->ptr[k]=b->ptr[k+1];
} /* 将b中关键字和指针前移 */
p->keynum++ ;
b->keynum-- ;
return(1) ;
}
}
return(0);
} /* 左右兄弟中无多余关键字,移动失败 */
}
BTNode *MergeNode(BTNode *p)
/* 将p与其左(右)邻兄弟合并,返回合并后的结点指针 */
{
BTNode *b, f=p->parent ;
int j, k ;
for (j=0; f->ptr[j]!=p; j++) /* 在f中找出p的位置 */
if (j>0) b=f->ptr[j-1]; /* b指向p的左邻兄弟 */
else
{
b=p;
p=p->ptr[j+1];
} /* p指向p的右邻 */
b->key[++b->keynum]=f->key[j] ;
b->ptr[p->keynum]=p->ptr[0] ;
for (k=1; k<=b->keynum ; k++)
{
b->key[++b->keynum]=p->key[k] ;
b->ptr[b->keynum]=p->ptr[k] ;
} /* 将p中关键字和指针移到b中 */
free(p);
for (k=j+1; k<=f->keynum ; k++)
{
f->key[k-1]=f->key[k] ;
f->ptr[k-1]=f->ptr[k] ;
} /* 将f中第j个关键字和指针前移 */
f->keynum-- ;
return(b) ;
}
void DeleteBTNode(BTNode *T, KeyType K)
{
BTNode *p, *S ;
int j,n ;
m=BT_search(T, K, p) ; /* 在T中查找K的结点 */
if (j==0) return(T) ;
if (p->ptr[j-1])
{
S=p->ptr[j-1] ;
while (S->ptr[S->keynum])
S=S->ptr[S->keynum] ;
/* 在子树中找包含最大关键字的结点 */
p->key[j]=S->key[S ->keynum] ;
p=S ; j=S->keynum ;
}
for (n=j+1; n<p->keynum; n++)
p->key[n-1]=p->key[n] ;
/* 从p中删除第m个关键字 */
p->keynum-- ;
while (p->keynum<(m-1)/2&&p->parent)
{
if (!MoveKey(p) ) p=MergeNode(p);
p=p->parent ;
} /* 若p中关键字树目不够,按⑵处理 */
if (p==T&&T->keynum==0)
{
T=T->ptr[0] ;
free(p) ;
}
}