树及树的算法(5) —— B树(上)

1970年,鲁道夫·贝尔(R.Bayer)的先于红黑树提出了B树。这种适用于外查找的树,是一种平衡的多叉树,又称B-树。

 

B树与红黑树最大的不同在于,B树的结点可以有许多子女,从几个到几千个。那为什么又说B树与红黑树很相似呢?因为与红黑树一样,一棵含n个结点的B树的高度也为Olog2n),但可能比一棵红黑树的高度小许多,应为它的分支因子比较大。所以,B树可以在Olog2n)时间内,实现各种如插入(insert),删除(delete)等动态集合操作。

 

如下图所示,即是一棵B树,一棵关键字为英语中辅音字母的B树,现在要从树中查找字母R(包含n[x]个关键字的内结点xxn[x]+1]个子女(也就是说,一个内结点x若含有n[x]个关键字,那么x将含有n[x]+1个子女)。所有的叶结点都处于相同的深度,带阴影的结点为查找字母R时要检查的结点):

 

 

相信,从上图你能轻易的看到,一个内结点x若含有n个关键字,那么x将含有n+1个子女。如含有2个关键字D H的内结点有3个子女,而含有3个关键字Q T X的内结点有4个子女。

 

一、B(B-)的概念

 

B 树又叫平衡多路查找树。一棵m阶的B (m叉树)的特性如下:

 

1.  树中每个结点最多含有m个孩子(m>=2);

2.  除根结点和叶子结点外,其它每个结点至少[ceil(m / 2)]个孩子(其中ceil(x)是一个向上去整的函数);

3.  如果根结点不是叶子结点,则至少有2个孩子(特殊情况:没有孩子的根结点,即根结点为叶子结点,整棵树只有一个根节点);

4.  所有叶子结点都出现在同一层,叶子结点不包含任何关键字信息(可以看做是外部接点或查询失败的接点,实际上这些结点不存在,指向这些结点的指针都为null)

5.  每个非终端结点中包含有n个关键字信息: (nP0K1P1K2P2......KnPn)。其中:

a)         Ki (i=1...n)为关键字,且关键字按顺序升序排序K(i-1)< Ki

b)        Pi为指向子树根的节点,且指针P(i-1)指向子树种所有结点的关键字均小于Ki,但都大于K(i-1)

c)        关键字的个数n必须满足: [ceil(m / 2)-1]<= n <= m-1

 

针对上面第5点,再阐述下:B树中每一个结点能包含的关键字数有一个上界和下界。这两个界可以用一个称作B树的最小度数tt>=2)表示。

 

每个非根的结点必须至少含有t-1个关键字。每个非根的内结点至少有t个子女。如果树是非空的,则根结点至少包含一个关键字;

 

每个结点可包含之多2t-1个关键字。所以一个内结点至多可有2t个子女。如果一个结点恰好有2t-1个关键字,我们就说这个结点是满的(而稍后介绍的B*树作为B树的一种常用变形,B*树中要求每个内结点至少为2/3满,而不是像这里的B树所要求的至少半满);

 

当关键字数t=2t=2的意思是,tmin=2t可以>=2)时的B树是最简单的(有很多人会因此误认为B树就是二叉查找树,但二叉查找树就是二叉查找树,B树就是B树,B树的真正最准确的定义为:一棵含有tt>=2)个关键字的平衡多路查找树)。每个内结点可能因此而含有2个、3个或4个子女,亦即一棵2-3-4树,然而在实际中,通常采用大得多的t值。

 

B树中的每个结点根据实际情况可以包含大量的关键字信息和分支(当然是不能超过磁盘块的大小,根据磁盘驱动(disk drives)的不同,一般块的大小在512B~4K左右);这样树的深度降低了,这就意味着查找一个元素只要很少结点从外存磁盘中读入内存,很快访问到要查找的数据。

 

B树的概念介绍完了,对于这么复杂和抽象的算法,要想把它吃透,只有拿出我们看家本领了,具体化:

为了简单,这里用少量数据构造一棵3叉树的形式,实际应用中的B树结点中关键字很多的。上面的图中比如根结点,其中17比表示一个磁盘文件的文件名;小红方块表示这个17文件内容在硬盘中的存储位置;p1表示指向17左子树的指针。

其结构可以简单定义为:

typedef struct {
    /*文件数*/
    int  file_num;
    /*文件名(key)*/
    char * file_name[max_file_num];
    /*指向子节点的指针*/
     BTNode * BTptr[max_file_num+1];
     /*文件在硬盘中的存储位置*/
     FILE_HARD_ADDR offset[max_file_num];
}BTNode;


 

假如每个盘块可以正好存放一个B树的结点(正好存放2个文件名)。那么一个BTNODE结点就代表一个盘块,而子树指针就是存放另外一个盘块的地址。

下面,咱们来模拟下查找文件29的过程:

1.  根据根结点指针找到文件目录的根磁盘块1,将其中的信息导入内存。【磁盘IO操作 1次】

2.  此时内存中有两个文件名1735和三个存储其他磁盘页面地址的数据。根据算法我们发现17<29<35,因此我们找到指针p2

3.  根据p2指针,我们定位到磁盘块3,并将其中的信息导入内存。【磁盘IO操作 2次】

4.  此时内存中有两个文件名2630和三个存储其他磁盘页面地址的数据。根据算法我们发,26<29<30,因此我们找到指针p2

5.  根据p2指针,我们定位到磁盘块8,并将其中的信息导入内存。【磁盘IO操作 3次】

6.  此时内存中有两个文件名2829。根据算法我们查找到文,29,并定位了该文件内存的磁盘地址。

 

分析上面的过程,发现需要3次磁盘IO操作和3次内存查找操作。关于内存中的文件名查找,由于是一个有序表结构,可以利用折半查找提高效率。至于IO操作时影响整个B树查找效率的决定因素。

 

当然,如果我们使用平衡二叉树的磁盘存储结构来进行查找,磁盘4次,最多5次,而且文件越多,B树比平衡二叉树所用的磁盘IO操作次数将越少,效率也越高。

 

二、B+树的概念

 

B+树是应文件系统所需而产生的一种B-tree的变形树。一棵m阶的B+树和m阶的B树的差异在于:

1.  n棵子树的结点中含有n个关键字; (B 树是n棵子树有n-1个关键字)

2.  所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。 (B 树的叶子节点并没有包括全部需要查找的信息)

3.  所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 (B 树的非终节点也包含需要查找的有效信息)

为什么说B+-treeB 树更适合实际应用中操作系统的文件索引和数据库索引?

 

1B+-tree的磁盘读写代价更低

 

B+-tree的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。

 

举个例子,假设磁盘中的一个盘块容纳16bytes,而一个关键字2bytes,一个关键字具体信息指针2bytes。一棵9B-tree(一个结点最多8个关键字)的内部结点需要2个盘快。而B+ 树内部结点只需要1个盘快。当需要把内部结点读入内存中的时候,B 树就比B+ 树多一次盘块查找时间(在磁盘中就是盘片旋转的时间)

 

2B+-tree的查询效率更加稳定

 

由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

 

B+-tree的应用:VSAM(虚拟存储存取法)文件(来源论文 the ubiquitous Btree 作者:D COMER - 1979 )

三、B*树的概念

 

B*树是B+树的变体,在B+ 树非根和非叶子结点再增加指向兄弟的指针;B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为2/3(代替B+树的1/2)。给出了一个简单实例,如下图所示:

B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针。

 

B*树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针。

 

所以,B*树分配新结点的概率比B+树要低,空间使用率更高。

你可能感兴趣的:(算法分析)