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,内容如下:

1  
2    
3 # define M 2
4  
8    
9 typedef int bool ;
10    
11 struct btnode{     
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:

1 # include <stdio.h>
2 # include <stdlib.h>
3    
4 # include "btrees.h"
5    
6  
7 struct btnode * allocateNode( struct btnode *ptr){
8      int i,max;
9      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  
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,这时候是非满的,如下所示:


3.同理插入31和12,都比较简单,如下所示:


4.插入10,这时候根结点是满的,就要分裂,由于根结点比较特殊,没有父结点,就要单独处理,先生成一个空结点做为新的根结点,再进行分裂,如下所示:


5.再插入15,48,45,由于非满,直接插入,如下所示:


6.插入47,这次叶结点满了,就要先分裂,再插入,如下所示:

其他都是同样的道理,就不赘述了,下面是源码,加入到btree.c中,最后写了个main函数和一个广度优先显示树的方法,大家可以自己对比结果,代码的实现参照了《算法导论》和博客

http://hi.baidu.com/kurt023/blog/item/4c368d8b51c59ed3fc1f10cc.html

他博客里面已经实现了,只是在定义B树的时候指针数和关键码数成一样了,我于是自己重写了一下。

1 //函数目的:分裂存储数达到最大的节点
2 void btreeSplitChild( struct btnode *parent, int pos, struct btnode *child){
3      struct btnode *child2;
4      int i;
5      //为新分裂出的节点分配空间
6      child2 = allocateNode(child2);
7      //与被分裂点同级
8      child2->isleaf = child->isleaf;
9      //设置节点数
10      child2->keyNum = M-1;
11    
12      //复制数据
13      for (i=0; i<M-1; i++)
14          child2->k[i] = child->k[i+M];
15      //如果不是叶节点,复制指针
16      if (!child->isleaf)
17          for (i=0; i<M; i++)
18              child2->p[i] = child->p[i+M];
19      child->keyNum = M-1;
20    
21      //将中间数作为索引插入到双亲节点中
22      //插入点后面的关键字和指针都往后移动一个位置
23      for (i=parent->keyNum; i>pos; i--){
24          parent->k[i] = parent->k[i-1];
25          parent->p[i+1] = parent->p[i];
26      }
27      parent->k[pos] = child->k[M-1];
28      parent->keyNum++;
29      parent->p[pos+1] = child2;
30 }
31    
32  
35 void btreeInsertNoneFull( struct btnode *ptr, int data){
36      int i;
37      struct btnode *child;   //要插入结点的子结点
38      i = ptr->keyNum;
39      //如果是叶节点,直接插入数据
40      if (ptr->isleaf){
41          while ((i>0) && (data<ptr->k[i-1])){
42              ptr->k[i] = ptr->k[i-1];
43              i--;
44          }
45          //插入数据
46          ptr->k[i] = data;
47          ptr->keyNum++;
48      }
49      else {   //不是叶节点,找到数据应插入的子节点并插入
50          while ((i>0) && (data<ptr->k[i-1]))
51              i--;
52          child = ptr->p[i];
53          if (child->keyNum == 2*M-1){
54              btreeSplitChild(ptr, i, child);
55              if (data > ptr->k[i])
56                  i++;
57          }
58          child = ptr->p[i];
59          btreeInsertNoneFull(child, data);   //在子树中递归
60      }
61 }
62    
63  
64 struct btnode * btreeInsert( struct btnode *root, int data){
65      struct btnode * new ;
66     
67      if (root->keyNum == 2*M-1){
68          new = allocateNode( new );
69          new ->isleaf = 0;
70          new ->keyNum = 0;
71          new ->p[0] = root;
72          btreeSplitChild( new , 0, root);
73          btreeInsertNoneFull( new , data);
74          return new ;
75      }
76      else {    //还没到最大数据数,直接插入
77          btreeInsertNoneFull(root, data);
78          return root;
79      }
80 }
81    
82 //函数目的:广度优先显示树
83 void btreeDisplay( struct btnode *root){
84      int i, queueNum=0;
85      int j;
86      struct btnode *queue[20];
87      struct btnode *current;
88    
89      //加入队列
90      queue[queueNum] = root;
91      queueNum++;
92    
93      while (queueNum>0){
94          //出队
95          current = queue[0];
96          queueNum--;
97          //移出第一个元素后后面的元素往前移动一个位置
98          for (i=0; i<queueNum; i++)
99              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放上去都可以;同样的算法插入顺序不同也可能不同。
转载自:http://blog.sina.com.cn/s/blog_7530db6f0100q1xr.html
posted @ 2011-04-16 07:57  顽童书生  阅读(1034)  评论(1)    收藏  举报