B树
B树是为磁盘或其他直接存储设备设计的一种平衡查找树。如下图所示。每一个结点箭头指向的我们称为入度,指出去的称为出度。树结构的结点入度都是1,不然就变成图了,所以我们一般说树的度就是指树结点的出度,也就是一个结点的子结点个数。有了度的概念我们就简单定义一下B树(假设一棵树的最小度数为M):
1.每个结点至少有M-1个关键码,至多有2M-1个关键码;
2.除根结点和叶子结点外,每个结点至少有M个子结点,至多有2M个子结点;
3.根结点至少有2个子结点,唯一例外是只有根结点的情况,此时没有子结点;
4.所有叶子结点在同一层。
我们看看它的结点的结构,如下图所示:
每个结点存放着关键字和指向子结点的指针,很容易看出指针比关键码多一个。
由B树的定义我们可以看出它的一些特点:
1.树高平衡,所有叶结点在同一层;
2.关键字没有重复,按升序排序,父结点的关键码是子结点的分界;
3.B树把值接近的相关记录放在同一磁盘页中,从而利用了访问局部性原理;
4.B树保证一定比例的结点是满的,能改进空间利用率。
B树结点的大小怎么确定呢?为了最小化磁盘操作,通常把结点大小设为一个磁盘页的大小。一般树的高度不会超过3层,也就是说,查找一个关键码只需要3次磁盘操作就可以了。
在实现的时候,我是参照了《算法导论》的内容,先假定:
1.B树的根结点始终在主存中,不需要读磁盘操作;但是,根结点改变后要进行一次写磁盘操作;
2.任何结点被当做参数传递的时候,要做一次读磁盘。
在实现的时候其实还做了简化,每个结点除了包含关键码和指针外,还应该有该关键码所对应记录所在文件的信息的,比如文件偏移量,要不然怎么找到这条记录呢。在实现的时候这个附加数据就没有放在结点里面了,下面是定义树的结构,文件名为btrees.h,内容如下:
01./* btrees.h */
02.
03.# define M 2
04./* B树的最小度数M>=2
05. * 每个非根结点必须至少有M-1个关键字。每个非根结点至少有M个子女
06. * 每个结点可包含至多2M-1个关键字。所以一个内结点至多可以有2M个子女
07. */
08.
09.typedef int bool;
10.
11.struct btnode{ /* B树结点 */
12. int keyNum; /* 节点中键的数目 */
13. int k[2*M-1]; /* 键 */
14. struct btnode * p[2*M]; /* 指向子树的指针 */
15. bool isleaf;
16.};
17.
18.struct searchResult{
19. struct btnode *ptr; /* 数据所在节点指针 */
20. int pos; /* 数据在节点中位置 */
21.};
下面是创建一颗空树的代码,文件名为btree.c:
01.# include <stdio.h>
02.# include <stdlib.h>
03.
04.# include "btrees.h"
05.
06./* 给一个结点分配空间 */
07.struct btnode * allocateNode(struct btnode *ptr){
08. int i,max;
09. ptr = (struct btnode *)malloc(sizeof(struct btnode));
10. if(!ptr){
11. printf("allocated error!/n");
12. exit(1);
13. }
14. max = 2*M;
15. for(i=0; i<max; i++)
16. ptr->p[i] = NULL; /* 初始化指针 */
17. memset(ptr->k, 0, (max-1)*sizeof(int)); /* 初始化键的值*/
18. return ptr;
19.}
20.
21./* 创建一个空的B树,就一个根结点 */
22.struct btnode * btreeCreate(struct btnode *root){
23. root = allocateNode(root);
24. root->keyNum = 0;
25. root->isleaf = 1;
26. return root;
27.}
B树的插入都是在叶子结点进行的,由于B树的结点中关键码的个数是有限制的,最小度数为M的B树的结点个数是从M-1到2M-1个。比如下图是最小度数为2的B树(又称为2-3树),如下图所示,它的结点的个数就是1-3个。
先定位到要插入的位置,如果叶子结点的关键码个数还没达到上限,比如插入32,就比较简单,直接插入就行;如果叶子结点的关键码数到达了上限,就要分裂成2个子结点,把中间的关键码往上放到父节点中。但有极端的情况,就是父结点也是满的,就需要再次分裂,可能最后要把根结点也分裂了。但是这种算法不太好实现。
在《算法导论》中实现用的是另外一种思想,就是先分裂,在查找插入位置的过程中,如果发现有满的结点,就先把它分裂了,这就保证了在最后叶结点上插入数据的时候,这个叶结点的父结点总是不满的。下面我们看一个例子:
我们用逐个结点插入的方法创建一棵B树,结点顺序分别是{18, 31, 12, 10, 15, 48, 45, 47, 50, 52, 23, 30, 20},我们看看具体过程:
1.创建一个空的B树;
2.插入18,这时候是非满的,如下所示:
4.插入10,这时候根结点是满的,就要分裂,由于根结点比较特殊,没有父结点,就要单独处理,先生成一个空结点做为新的根结点,再进行分裂,如下所示:
6.插入47,这次叶结点满了,就要先分裂,再插入,如下所示:
其他都是同样的道理,就不赘述了,下面是源码,加入到btree.c中,最后写了个main函数和一个广度优先显示树的方法,大家可以自己对比结果,代码的实现参照了《算法导论》和博客
http://hi.baidu.com/kurt023/blog/item/4c368d8b51c59ed3fc1f10cc.html
他博客里面已经实现了,只是在定义B树的时候指针数和关键码数成一样了,我于是自己重写了一下。
001.//函数目的:分裂存储数达到最大的节点
002.void btreeSplitChild(struct btnode *parent, int pos, struct btnode *child){
003. struct btnode *child2;
004. int i;
005. //为新分裂出的节点分配空间
006. child2 = allocateNode(child2);
007. //与被分裂点同级
008. child2->isleaf = child->isleaf;
009. //设置节点数
010. child2->keyNum = M-1;
011.
012. //复制数据
013. for(i=0; i<M-1; i++)
014. child2->k[i] = child->k[i+M];
015. //如果不是叶节点,复制指针
016. if(!child->isleaf)
017. for(i=0; i<M; i++)
018. child2->p[i] = child->p[i+M];
019. child->keyNum = M-1;
020.
021. //将中间数作为索引插入到双亲节点中
022. //插入点后面的关键字和指针都往后移动一个位置
023. for(i=parent->keyNum; i>pos; i--){
024. parent->k[i] = parent->k[i-1];
025. parent->p[i+1] = parent->p[i];
026. }
027. parent->k[pos] = child->k[M-1];
028. parent->keyNum++;
029. parent->p[pos+1] = child2;
030.}
031.
032./* 函数目的:向非满的节点中插入一个数据
033. * 注意:插入前保证key在原来的B树中不存在
034. */
035.void btreeInsertNoneFull(struct btnode *ptr, int data){
036. int i;
037. struct btnode *child; //要插入结点的子结点
038. i = ptr->keyNum;
039. //如果是叶节点,直接插入数据
040. if(ptr->isleaf){
041. while((i>0) && (data<ptr->k[i-1])){
042. ptr->k[i] = ptr->k[i-1];
043. i--;
044. }
045. //插入数据
046. ptr->k[i] = data;
047. ptr->keyNum++;
048. }
049. else{ //不是叶节点,找到数据应插入的子节点并插入
050. while((i>0) && (data<ptr->k[i-1]))
051. i--;
052. child = ptr->p[i];
053. if(child->keyNum == 2*M-1){
054. btreeSplitChild(ptr, i, child);
055. if(data > ptr->k[i])
056. i++;
057. }
058. child = ptr->p[i];
059. btreeInsertNoneFull(child, data); //在子树中递归
060. }
061.}
062.
063./* 插入一个结点 */
064.struct btnode * btreeInsert(struct btnode *root, int data){
065. struct btnode *new;
066. /* 检查是否根节点已满,如果已满,分裂并生成新的根节点 */
067. if(root->keyNum == 2*M-1){
068. new = allocateNode(new);
069. new->isleaf = 0;
070. new->keyNum = 0;
071. new->p[0] = root;
072. btreeSplitChild(new, 0, root);
073. btreeInsertNoneFull(new, data);
074. return new;
075. }
076. else{ //还没到最大数据数,直接插入
077. btreeInsertNoneFull(root, data);
078. return root;
079. }
080.}
081.
082.//函数目的:广度优先显示树
083.void btreeDisplay(struct btnode *root){
084. int i, queueNum=0;
085. int j;
086. struct btnode *queue[20];
087. struct btnode *current;
088.
089. //加入队列
090. queue[queueNum] = root;
091. queueNum++;
092.
093. while(queueNum>0){
094. //出队
095. current = queue[0];
096. queueNum--;
097. //移出第一个元素后后面的元素往前移动一个位置
098. for(i=0; i<queueNum; i++)
099. queue[i] = queue[i+1];
100. //显示节点
101. j = current->keyNum;
102. printf("[ ");
103. for(i=0; i<j; i++){
104. printf("%d ", current->k[i]);
105. }
106. printf("] ");
107.
108. //子节点入队
109. if(current!=NULL && current->isleaf!=1){
110. for(i=0; i<=(current->keyNum); i++){
111. queue[queueNum] = current->p[i];
112. queueNum++;
113. }
114. }
115. }
116. printf("/n");
117.}
118.
119.int main()
120.{
121. struct btnode *root;
122. int a[13] = {18, 31, 12, 10, 15, 48, 45, 47, 50, 52, 23, 30, 20};
123. int i;
124.
125. root = btreeCreate(root);
126. for(i=0; i<13; i++){
127. root = btreeInsert(root, a[i]);
128. btreeDisplay(root);
129. }
130.
131. return 0;
132.}
运行结果:
同样一批关键码用不同算法生成的B树可能是不同的,比如4个关键码的结点[1,2,3,4]分裂的时候,把2或3放上去都可以;同样的算法插入顺序不同也可能不同。
重点来了,我觉得删除是最复杂的过程,BTreeDelete在递归的同时确认所到的每个节点都是符合删除要求的,否则用MergeNode合并节点,DeleteData进行实质的删除,BTreeDelete写的太长了T_T。
//函数目的:删除树中的一个给定数据
//接收参数:*Root - 根节点指针
// data - 删除数据
//返回参数:删除结果,0为成功,1为数据不存在
int BTreeDelete(struct BTreeNode *Root, int data)
{
struct SearchResult Result;
struct BTreeNode *Pointer, *TempPointer;
int i,Temp;
Result = BTreeSearch(Root, data);
Pointer = Result.Pointer;
//数据不在树中,返回失败
if(Pointer == NULL)
return 1;
//数据在Root中
if(Pointer == Root)
{
//Root是叶节点
if(Pointer->IsLeaf == true)
{
DeleteData(Result.Pointer, Result.KeyPosition);
return 0;
}
//Root是中间结点
else
{
//前于数据的子节点中包含多于最小值个数据
if(Root->KeyPointer[Result.KeyPosition]->KeyNumber > MAX_KEY / 2 - 1)
{
//找到数据的前驱
TempPointer = Root;
Temp = Result.KeyPosition;
while(TempPointer->KeyPointer[Temp]->IsLeaf == false)
{
TempPointer = TempPointer->KeyPointer[Temp];
Temp = TempPointer->KeyNumber;
}
//记录前驱
TempPointer = TempPointer->KeyPointer[Temp];
Temp = TempPointer->KeyNumber;
Temp = TempPointer->Key[Temp - 1];
//删除前驱
BTreeDelete(TempPointer, Temp);
//用前驱替换数据
Root->Key[Result.KeyPosition] = Temp;
return 0;
}
//位于数据之后的子节点中包含多于最小值个数据
if(Root->KeyPointer[Result.KeyPosition + 1]->KeyNumber > MAX_KEY / 2 - 1)
{
//找到数据的后继
TempPointer = Root;
Temp = Result.KeyPosition;
while(TempPointer->KeyPointer[Temp + 1]->IsLeaf == false)
{
TempPointer = TempPointer->KeyPointer[Temp + 1];
Temp = 0;
}
//记录后继
TempPointer = TempPointer->KeyPointer[Temp + 1];
Temp = TempPointer->Key[0];
//删除后继
BTreeDelete(TempPointer, Temp);
//用后继替换数据
Root->Key[Result.KeyPosition] = Temp;
return 0;
}
//子节点都只含有最小值个数据
//合并两个子节点和数据data
i = Root->KeyPointer[Result.KeyPosition + 1]->KeyNumber;
//把右子节点中数据和指针移动到左子节点中
MergeNode(Root->KeyPointer[Result.KeyPosition], Root->Key[Result.KeyPosition],
Root->KeyPointer[Result.KeyPosition + 1]);
//释放右节点内存
free(Root->KeyPointer[Result.KeyPosition + 1]);
//删除数据和指向右子节点的指针
i = 0;
while(i < Root->KeyNumber)
{
Root->Key[i] = Root->Key[i + 1];
Root->KeyPointer[i + 1] = Root->KeyPointer[i + 2];
i++;
}
Root->KeyNumber--;
//删除数据
BTreeDelete(Root->KeyPointer[Result.KeyPosition], data);
return 0;
}
}
//数据不在Root中
else
{
//找到数据所在子节点的根
i = 0;
while((Root->Key[i] < data) && (i < Root->KeyNumber))
i++;
TempPointer = Root->KeyPointer[i];
//这个节点中只有最小数的数据
if(TempPointer->KeyNumber == MAX_KEY / 2 - 1)
{
//左兄弟节点中有多于最小数的数据
if((i > 0) && (Root->KeyPointer[i - 1]->KeyNumber > MAX_KEY / 2 - 1))
{
//把父节点中数据降至节点
BTreeInsertNonfull(TempPointer, Root->Key[i]);
//把左兄弟节点最右指针移动过来
TempPointer->KeyPointer[TempPointer->KeyNumber] =
Root->KeyPointer[i - 1]->KeyPointer[Root->KeyPointer[i - 1]->KeyNumber + 1];
//把左节点数据升至父节点
Root->Key[i] = Root->KeyPointer[i - 1]->Key[Root->KeyPointer[i - 1]->KeyNumber];
DeleteData(Root->KeyPointer[i - 1], Root->KeyPointer[i - 1]->KeyNumber);
}
//右兄弟节点中有多于最小数的数据
else if((i < Root->KeyNumber) &&
(Root->KeyPointer[i + 1]->KeyNumber > MAX_KEY / 2 - 1))
{
//把父节点中数据降至节点
BTreeInsertNonfull(TempPointer, Root->Key[i]);
//把右兄弟节点最左指针移动过来
TempPointer->KeyPointer[TempPointer->KeyNumber] =
Root->KeyPointer[i + 1]->KeyPointer[0];
//把右节点数据升至父节点
Root->Key[i] = Root->KeyPointer[i + 1]->Key[0];
DeleteData(Root->KeyPointer[i + 1], 0);
}
//兄弟节点都只有最小数据数的数据
else
{
//没有左兄弟,与右兄弟合并
if(i == 0)
{
MergeNode(TempPointer, Root->Key[i - 1], Root->KeyPointer[i + 1]);
//释放节点内存
free(TempPointer);
//删除数据和指向节点的指针
while(i < Root->KeyNumber)
{
Root->Key[i - 1] = Root->Key[i];
Root->KeyPointer[i] = Root->KeyPointer[i + 1];
i++;
}
Root->KeyNumber--;
Temp = 0;
}
//否则与左兄弟合并
else
{
MergeNode(Root->KeyPointer[i - 1], Root->Key[i - 1], TempPointer);
//释放节点内存
free(TempPointer);
Temp = i - 1;
//删除数据和指向节点的指针
while(i < Root->KeyNumber)
{
Root->Key[i - 1] = Root->Key[i];
Root->KeyPointer[i] = Root->KeyPointer[i + 1];
i++;
}
Root->KeyNumber--;
}
}
}
return BTreeDelete(Root->KeyPointer[Temp], data);
}
}
//函数目的:合并两个节点和父节点的一个数据
//接收参数:*Left - 左子节点指针
// *Right - 右子节点指针
// data - 父节点的一个数据
//返回参数:无
void MergeNode(struct BTreeNode *Left, int data, struct BTreeNode *Right)
{
int i = 0;
//把数据插入到左子节点中
BTreeInsertNonfull(Left, data);
//把右子节点的数据和指针移动到左子节点中
while(i < Right->KeyNumber)
{
Left->Key[i + Left->KeyNumber] = Right->Key[i];
Left->KeyPointer[i + Left->KeyNumber] = Right->KeyPointer[i];
i++;
}
//改变左节点的数据数
Left->KeyNumber = Left->KeyNumber + Right->KeyNumber;
}
//函数目的:删除节点中的数据
//接收参数:*Pointer - 节点指针
// Position - 删除数据在节点中的位置
//返回参数:无
void DeleteData(struct BTreeNode *Pointer, int Position)
{
int i;
//删除节点并移动数据和指针
for(i = Position; i < Pointer->KeyNumber - 1; i++)
{
Pointer->Key[i] = Pointer->Key[i + 1];
Pointer->KeyPointer[i] = Pointer->KeyPointer[i + 1];
}
//减少节点的数据数
Pointer->KeyNumber--;
}
本文深入解析B树的结构与特性,包括节点定义、树的创建、插入操作及代码实现细节。
2253

被折叠的 条评论
为什么被折叠?



