B树B+树B*树原理及应用

来源:互联网 发布:python mysql模块安装 编辑:程序博客网 时间:2024/06/06 05:30

二叉查找树和平衡二叉树都是典型的二叉查找树结构,查找的时间复杂度O(log2N)与树的深度相关,因此降低树的高度自然对查找效率有所帮助,为了降低树的高度,可令每个节点存储多个元素,并将平衡二叉查找树拓展为平衡多叉查找树,这时神奇的B树就从石头里蹦出来了,B树,B+树与红黑树很大的不同点在于B树的结点有多个子女。

1 .B树定义

定义:一棵m 阶的B树,或者为空树,或为满足下列特性的m 叉树:
⑴树中每个结点至多有m 棵子树;
⑵若根结点不是叶子结点,则至少有两棵子树;

⑶除根结点之外的所有非终端结点至少有[m/2] 棵子树;
⑷所有的非终端结点中包含以下信息数据:(n,A0,K1,A1,K2,…,Kn,An)

其中:Ki(i=1,2,…,n)为关键码,且Ki<Ki+1,Ai为指向子树根结点的指针(i=0,1,…,n),且指针Ai-1 所指子树中所有结点的关键码均小于Ki 且大于Ki-1.

关键字个数需要满足 
⑸所有的叶子结点都出现在同一层次上,并且不带信息(可以看作是外部结点或查找失败的结点,实际上这些结点不存在,指向这些结点的指针为空)。

例图如下:

查找算法如下:

typedef int KeyType ;  #define m 5                 /*B 树的阶,暂设为5*/  typedef struct Node{      int keynum;             /* 结点中关键码的个数,即结点的大小*/      struct Node *parent;    /*指向双亲结点*/       KeyType key[m+1];       /*关键码向量,0 号单元未用*/       struct Node *ptr[m+1];  /*子树指针向量*/       Record *recptr[m+1];    /*记录指针向量*/  }NodeType;                  /*B 树结点类型*/    typedef struct{      NodeType *pt;           /*指向找到的结点*/      int i;                  /*在结点中的关键码序号,结点序号区间[1…m]*/      int tag;                /* 1:查找成功,0:查找失败*/  }Result;                    /*B 树的查找结果类型*/    Result SearchBTree(NodeType *t,KeyType kx)  {       /*在m 阶B 树t 上查找关键码kx,反回(pt,i,tag)。若查找成功,则特征值tag=1,*/      /*指针pt 所指结点中第i 个关键码等于kx;否则,特征值tag=0,等于kx 的关键码记录*/      /*应插入在指针pt 所指结点中第i 个和第i+1 个关键码之间*/      p=t;q=NULL;found=FALSE;i=0; /*初始化,p 指向待查结点,q 指向p 的双亲*/      while(p&&!found)      {   n=p->keynum;i=Search(p,kx);          /*在p-->key[1…keynum]中查找*/          if(i>0&&p->key[i]= =kx) found=TRUE; /*找到*/          else {q=p;p=p->ptr[i];}      }      if(found) return (p,i,1);               /*查找成功*/      else return (q,i,0);                    /*查找不成功,反回kx 的插入位置信息*/  }  int Search(BTree p,int k){//查找该关键字应在的位置int i = 1;while (i <= p->keynum && k > p->key[i]){ i++; }return i++;}
B树的查找过程是根据给定值查找结点和在结点的关键字中进行查找交叉进行。从根节点开始,重复以下过程:

若给定关键字等于结点中某个关键字Ki,则查找成功;若给定关键字比结点中的K1小,则进入指针A0指向的下一层结点继续查找,若在两个关键字Ki和Ki+1之间,则进入他们之间的指针Ai指向的下一层结点继续查找;若查找到叶子结点,则说明给定值对应的数据记录不存在,则查找失败。

  

   插入操作如下:通过查找算法找到关键字k的插入位置,若找到相同关键字则不需插入,否则在该插入点插入,若其关键字总数n未达到m,算法结束;否则,需分裂结点。分裂操作:生成一新节点,从中间位置把结点分成两部分。前半部分留在旧结点中,后半部分复制到新结点中,中间位置的关键字连同新节点的存储位置插入到父节点中。如果插入后父节点的关键字个数也超过m-1,则要再分裂,向上类推。

   删除操作如下:通过查找算法找出该节点位置,如果是最下层非终端结点,则进行以下判断:

1.该节点关键字个数 >= m/2向上取整,则直接删除即可。


2.如果关键字个数等于m/2-1,说明删去该关键字后该节点不满足B树的定义,需要调整。调整过程为:如果其左右兄弟结点中关键字个数 >= m/2向上取整,则可将右(左)兄弟结点中最小(大)关键字上移至双亲结点。而将双亲结点中小(大)与该上移关键字的关键字下移至被删关键字所在结点处。


3.如果双亲结点中没有多余的关键字,这时比较复杂,需要把删除关键字的结点与其左(或右)兄弟结点以及双亲结点中分割二者的关键字合并成一个结点,即在删除关键字后,该节点中剩余的关键字加指针,加上双亲结点中的关键字Ki一起,合并到Ai(即双亲结点指向该删除关键字结点的左(右)兄弟结点的指针)所指的兄弟结点中去。如果因此使双亲结点中关键字个数小于m/2向上取整-1,则对此双亲结点做同样处理,以致可能直到对根节点做这样的处理而使整个树减少一层。


若该结点不是最下层非终端结点,且被删关键字为该节点中第i个 关键字key[i],则可从指针ptr[i]所指的子树中找出位于最下层非终端结点的最小关键字Y,替代key[i],然后在最底层非终端结点中移除Y,因此,把在非终端结点删除关键字k的问题就编程了删除最下层非终端结点中的关键字问题了。

删除操作代码如下:

void DeleteBTree(BTree p,int i){

if(p->ptr[i-1] != null){ //若不是最下层非终端结点 

Successor(p,i); //由后继最下层非终端结点的最小关键字代替它

DeleteBTree(p,1); //变成删除最下层非终端结点中的最小关键字

}else{

Remove(p,i); //从结点p中删除key[i]

if(p->keynum < (m-1)/2) //删除后关键字个数如果小于(m-1)/2

Restore(p,i); //调整B树

}

}

上述算法中的Successor、DeleteBTree、Restore具体算法实现可由读者自行完成。

B树的应用场景:多用在内存放不下,需要放在外村的情况,应为B树层数相对较少,能保证磁盘读取的次数相对较少,主要应用在文件系统上。

B+树:B+树是应文件系统所需要而提出的一种B树的变形。一棵m阶B+树和m阶B树的差别在于:

1.有n棵子树的结点中含有n个关键字,每个关键字不保存数据,只保存索引。

2.所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接

3.所有的非终端结点可以看成是索引部分,结点中仅含其子树(根结点)中最大(或最小)关键字。

B+树通常有两个头指针,一个指向根结点,一个指向关键字最小的叶子结点。因此可以对B+树进行两种方式的查找:一种是从最小关键字起顺序查找,另一种是从根结点开始。

B+树每个结点的关键字个数最多为m,而B树则最多为m-1


在B+树上查找、插入、和删除的过程基本上就与B树类似。只是在查找时,若非终端结点上的关键码等于给定值,并不终止,而是继续向下走到叶子结点,因此,不管找到与否,最后都会走到叶子结点

解释一下插入时的情况,根据插入值的大小,逐步向下直到对应的叶子节点。如果叶子节点关键字个数小于2t,则直接插入值或者更新卫星数据;如果插入之前叶子节点已经满了,则分裂该叶子节点成两半,并把中间值提上到父节点的关键字中,如果这导致父节点满了的话,则把该父节点分裂,如此递归向上。所以树高是一层层的增加的,叶子节点永远都在同一深度。

B+树的删除也仅在叶子结点进行,当叶子结点中的最大关键字被删除时,其在非终端结点中的值可以作为一个“分界关键字”存在。若因删除而使结点中关键字的个数少于m/2 (m/2结果取上界,如5/2结果为3)时,其和兄弟结点的合并过程亦和B-树类似。

相信来看这篇文章的小伙伴都应该了解B+树是数据库索引的重要一员,下面就来分析一下该项内容。

1. 索引在数据库中的作用 
        在数据库系统的使用过程当中,数据的查询是使用最频繁的一种数据操作。
        最基本的查询算法当然是顺序查找(linear search),遍历表然后逐行匹配行值是否等于待查找的关键字,其时间复杂度为O(n)。但时间复杂度为O(n)的算法规模小的表,负载轻的数据库,也能有好的性能。  但是数据增大的时候,时间复杂度为O(n)的算法显然是糟糕的,性能就很快下降了。这时就需要一种结构来解决这个问题,提高查找效率,这就是索引。
       索引是对数据库表 中一个或多个列的值进行排序的结构。与在表 中搜索所有的行相比,索引用指针 指向存储在表中指定列的数据值,然后根据指定的次序排列这些指针,有助于更快地获取信息。通常情 况下 ,只有当经常查询索引列中的数据时 ,才需要在表上创建索引。索引将占用磁盘空间,并且影响数 据更新的速度。但是在多数情况下 ,索引所带来的数据检索速度优势大大超过它的不足之处。
2.为什么说B+-tree比B 树更适合实际应用中操作系统的文件索引和数据库索引?
1) B+-tree的磁盘读写代价更低
B+-tree的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
    举个例子,假设磁盘中的一个盘块容纳4K字节,而一个关键字4字节,一个关键字具体信息指针4字节。而B+ 树关键字只需要4字节。B+树比B树可以在块中存放多一倍的关键字,当需要把内部结点读入内存中的时候,B 树就比B+ 树多一次盘块查找时间(在磁盘中就是盘片旋转的时间),B+比B快了一倍。

2) B+-tree的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
3)还有一个重要原因就是B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。正是为了解决这个问题,B+树应运而生。B+树只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作(或者说效率太低)。

还有一种B*树:

B*树在B+树的基础上之上在非根结点和非叶子结点的结点增加了指向兄弟的指针(能跟左右兄弟联系是优点,缺点是消耗了相应资源),B*树定义了非叶子结点至少有(2/3)*m个关键字(B树是m/2)


B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针;
B*树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;
所以,B*树分配新结点的概率比B+树要低,空间使用率更高;

该文章主要讲了这三种树的原理及应用,后面的文章会接着分析各树在索引中的具体原理及不同存储引擎中所用索引的不同,以及红黑树的知识。欢迎一起讨论