开宗明义,B树是为磁盘或其他直接存取辅助设备而设计的一种平衡查找树。一般设计的简单数据结构都是面向主存而设计的,主存读取速度快但容量小;而磁盘读取速度慢而容量大,于是针对磁盘而设计的数据结构就不同于为主存而设计的。就树结构上来说,红黑树的二叉性质和高深度适合主存,而B树正是应磁盘特点而设计的高级树结构,其高度比红黑树小很多,但广度要大很多,其分支不只2个,分支因子越大,高度越小。
在考察算法性能时,需要考虑两方面:磁盘存取次数和CPU运行时间。主存存储量小,一旦要处理的数据量比较庞大,就需要以页在磁盘和主存之间交换,选择页复制到主存中操作再将修改过的页写回磁盘,这来回就是磁盘存取消耗的性能,因此交换页在操作系统磁盘模块是一个很重要的算法。
B树设计上,一个结点的大小相当于一个完整的磁盘页,其子女数就由磁盘页的大小来决定。这样一个节点的读取就交换一个页,减少磁盘IO次数。下面定义B树:
一棵B树T是具有如下性质的有根树(根为root[T]):
1)每个结点x有以下域构成:
a)n[x],当前存储在结点x中的关键字数;
b)n[x]个关键字本身,以非降序存放,key1[x]=<key2[x]=<…=<keyn[x][x]
c)leaf[x],布尔值,如果x是叶子该值为true,如果x是内结点则为false;
2)每个内结点x还包含n[x]+1个指向其子女的指针c1[x],c1[x],…,cn[x]+1[x],叶结点没有子女,故而ci域没有定义;
3)各关键字keyi[x]对储存在各子树中的关键字范围加以分割,如果ki为存储在以ci[x]为根的子树中的关键字,那么:
k1=<key1[x]=< k2=< key2[x]=<…=< kn[x]=<keyn[x]+1[x]
子节点的值是被父结点的值区隔开。
4)每个叶结点具有相同深度,即树的高度h;
5)每一个节点能包含的关键字数有一个上界和下界,界可用一个B树的最小度数的固定整数t>=2来表示;
a)每个非根的结点必须至少有t-1个关键字,每个非根的内结点至少有t个子女,如果树是非空的,则根节点至少包含一个关键字;
b)每个节点可包含至多2t-1个关键字,故一个内结点至多可有2t个子女,如果一个结点是满的,那就有2t-1个关键字。
如果t=2,就是每个非根结点的关键字数为2,其每个内结点有2个、3个或4个子女,是一颗2-3-4树。t其实就是限制了每个结点的关键字数,在t-1和2t-1范围内,取闭区间。
B树设计上一般考虑一个结点大小和一个磁盘页相当,因此磁盘存取次数和B树高度成正比。通过B树最坏情况的高度来衡量性能。如果n>=1,对任意一颗包含n个关键字、高度为h、最小度数t>=2的B树T,则:
定义了B树和分析其性能,下面对B树的基本操作进行描述。
B树的基本操作和二叉树类似,不同的是每个结点不只是二叉决定,而是根据B树的度(结点关键字数)做决定。B树的每个内结点x,都需要做n[x]+1路的分支决定。
1)B树的搜索操作
从根结点出发搜索关键字k,在算法上只需依次从顶层定位关键字值序的空间就可以。关注下,B树搜索操作的复杂度:搜索是从树根一直下降的过程,需存取的磁盘页面数为树的高度就是:O(h)=O(logtn),t是节点的度,n是关键字数,h是树高度;对于CPU运行时间消耗来说,每个节点的循环式为O(t),总共进行h次循环,那总的时间就是O(th)=O(tlogtn)。
2)B树的插入操作
B树插入操作比较复杂,必须对满结点进行分裂操作,以维持B树结点至多2t-1个关键字的性质。从根节点出发,寻找待插入关键字的序值位置,如果结点满,则分裂,将中间值插入到父节点,如果父节点也因此满,需要进一步分裂,递归如是。文中有一个思路很好,就是在寻找关键字要插入的结点过程中,在查找沿途过程中发现有满节点(包含叶结点本身)就分类,而不是等最后插入时发现了才分裂。看看满节点分裂算法的描述:
Fun_Btree_split_child(x,i,y){//x是父结点,y是子节点,i是x的分裂点
Allocat_node(z);//分配一个z结点
Leaf[z]=leaf[y];//把y中t个最大关键字(包含其t+1个子女)复制给z
n[z]=t-1;
for j=1 to t-1
do keyj[z]=keyj+t[y];
if not leaf[y] //y有子女
then for j=1 to t
do cj[z]=cj+t[y]
n[y]=t-1;
//到此已经将y节点,从中间分类成两个节点,y和z,其中z是较大关键字的那部分
//下面就是将y的中间关键字提升到父结点x中,左右分别指向y和z
for j=n[x]+1downto i+1
do cj+1[x]=cj[x];//x的子女向右移动一个位置
ci+1[x]=z;
for j=n[x] downto i
do keyj+1[x]=keyj[x];//x的关键字向右移动一个位置
keyi[x]=keyt[y]
n[x]=n[x+1]
Disk-Write(y);
Disk-Write(z);
Disk-Write(x);//回写磁盘,一个结点一页
}
有了分裂函数,插入算法就不描述了,在遇到满结点时调用分裂函数即可。对高度为h的B树,插入的磁盘存取次数是O(h),cpu运行时间是O(th)=O(tlogtn)。
3)B数的删除操作
B树删除操作和插入一样麻烦,需要确保删除后结点数不小于t-1个关键字的性质。删除的算法思路就是合并节点,复杂度和插入一样。这里不具体描述。
B树的应用是很广泛的,对于算法复杂度来说,关注CPU时间是不够的,因为多数情况,需要整体考虑设备性能,而磁盘IO能力是一个很关键但常常是制约性能的指标。