B树

本文深入解析B树的结构与特性,包括节点定义、树的创建、插入操作及代码实现细节。

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

view source

print?

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

view source

print?

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树的结点中关键码的个数是有限制的,最小度数为MB树的结点个数是从M-12M-1个。比如下图是最小度数为2B(又称为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.
同理插入3112,都比较简单,如下所示:


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


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


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

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

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

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

view source

print?

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个关键码的结点[1234]分裂的时候,把23放上去都可以;同样的算法插入顺序不同也可能不同。

重点来了,我觉得删除是最复杂的过程,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--;
}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值