B树是为磁盘或其他直接存储辅助存储设备而设计的一种平衡二叉查找树(通常说的B树是B-树,在1972年由R.Bayer和E.M.McCreight提出,B+树是B树的一种变形),B树与红黑树类似,但在降低磁盘I/O操作次数方面要更好一些,数据库就是通常用B树来进行存储信息。
B树的结点可以有许多子女,从几个到几千个不等,一个B树结点可以拥有的子女数是由磁盘页的大小所决定,这是因为一个结点的大小通常相当于一个完整的磁盘页。磁盘存取次数是按需要从盘中读出或向盘中写入的信息的页数来度量的,所以,存取磁盘的总时间可以近似为读或写的页数。因此,B树一般都选择大的分支因子,这样可以大大降低树的高度,以及寻找任意关键字所需的磁盘存取次数。一棵分支因子为1001, 高度为2的B树,可以储存超过10亿个关键字,同时因为根节点可以持久地保留在内存中,故在这棵树中,寻找一个关键字至多只需要两次磁盘存取。
一:B树的特性
B树的性质:
1) 每个节点x的域:
a)x.n,x中的关键字数,若x是B树中的内节点,则x有x.n + 1个子女。
b)x.n个关键字本身,以非降序排列,key1[x] <= key2[x] <= … <= keyn[x]
c)x.leaf,布尔值,如果x是叶节点,则为TRUE,若为内节点,则为FALSE
2) 每个内节点x还包含x.n + 1个指向其子女的指针x.c1, x.c2, …, x.c(n+1)。内部结点跟叶节点结构相同,但是结点中的指针属性没有定义!
3) 如果ki为存储在以ci[x]为根的子树中的关键字,则k1 <= key1[x] <= k2 <= key2[x] <= … <= keyn[x][x] <= keyn[x] + 1(有二叉查找树的特性)
4) 每个叶节点具有相同的深度
5) 每个结点包含关键字个数有上下界。B树的最小度数t>=2满足:
a) 每个非根的节点必须至少有t – 1个关键字
b) 每个节点可包含至多2t – 1个关键字。
当t=2时,内部结点有1-3个关键字,因此有2-4个孩子,称为2-3-4树。
B树的高度:树根的深度为1来算的话,h<=logt((n+1)/2)+1
分裂时树长高的唯一途径。
二:B树上的基本操作
1:搜索B树
搜索过程类似二叉搜索树,只是每个结点不是两路选择,而是多路分支选择(x.n+1路)。
代码:
struct tree_node *tree_search(tree t, struct tree_node *x, void *key,int *index)
{
int i = 0;
while (i < x->num && key>x->key[i]){ //找到x的对应位置
++i;
}
if (i < x->num && key == x->key[i]) { //找到关键字key返回
*index = i;
return x;
}
if (x->leaf) { //如果没找到,判断是否是叶节点,不是叶节点继续查找
return NULL;
} else {
DISK_READ(x->child[i]);
return tree_search(t, x->child[i], key, index);
}
}
2:创建空树
伪代码:
//这个函数只是创建一个根节点,树是由插入过程来成长的
B-TREE-CREATE(T)
{
x = ALLOCATE-NODE(); //为一个新结点分配一个磁盘页
x.leaf = TRUE; //结点标记设置为TRUE
x.n = 0; //初始化参数
DISK-WRITE(x)
T.root = x; //调整根节点指向
}
3:插入关键字
B树插入是指插入到一个已知的叶节点上(插入的结点必须为叶结点),因为不能把关键字插入到一个满的叶结点上,故引入一个操作,将一个满的结点y(有2t – 1个关键字)按其中间关键字key[y]分裂成两个各含t – 1个关键字的节点,中间关键字提升到y的双亲结点,如果y的双亲也是满的,则自底向上传播分裂。
另外一种方法:如同二叉查找树,插入时,需要从根部沿着树下降到叶子,当沿着树往下查找新关键字所属位置时,就分裂遇到的每一个满结点,这样就能保证,要分裂一个满结点y时,就能确保它的双亲不是满的。(提前预防的方法,防止了回溯,下面以这种方法讲解)。
插入的时间复杂度为O(th)=O(tlogtn)。
(1)分裂结点B-TREE-SPLIT-CHILD()
分裂示意图
伪代码:
//分裂结点。x结点中ci指针指向的结点已满,需要分裂,这涉及到父节点x,因此要传入x和i
B-TREE-SPLIT-CHILD(x,i)
{
z = ALLOCATE-NODE(); //创建一个新的结点,含有n个关键字域和n+1个指针,x.n以及x.leaf
y = x.c[i];
z.leaf = y.leaf; //给新结点标记赋值,因为叶节点都在同一层,所以z和y标记相同
z.n = t - 1;
for(j = 1;j <= t-1;j++) //将y中后t-1个关键字复制到z当中
z.key[j] = y.key[j+t];
if(y.leaf == false){ //不是叶节点的话,就将后半区的指针也复制到z中去
for(j=1;j<=t;j++)
z.c[j] = y.c[j+t];
}
y.n = t-1; //y的个数缩减到t-1
for(j = x.n+1;j>=i+1;j--) //将y的第t个数据升到x中去,因此要将x中的指针后移
x.c[j+1] = x.c[j];
x.c[j+1] = z; //z指针放入父节点
for(j=x.n;j>=i;j--) //移动x中的数据
x.key[j+1]=x.key[j];
x.key[i] = y.key[t]; //将y中升入的结点放入x合适位置
x.n++; //x的个数增加
DISK-WRITE(y);
DISK-WRITE(z);
DISK-WRITE(x);
}
注:a)代码中的下标是从1-n,不符合c中的规范,自己写代码时要调整。b)这段代码的前提是父节点非满,因此,从上往下遍历时,一定要分裂途中遇到的满结点。
(2)以沿树单程下行方式插入关键字B-TREE-INSERT(T,K)
分裂示意图
伪代码
//这个函数只处理根节点已满的情况,非根节点调用B-TREE-INSERT-NONFULL(s,k);递归处理
B-TREE-INSERT(T,K)
{
r = T.root;
if(r.n == 2t - 1){ //判断根节点是否已满
s = ALLOCATE-NODE(); //创建结点,并初始化一些参数
T.root = s; //生成新节点,要调整根指针
s.leaf = FALSE;
s.n = 0;
s.c1 = r;
B-TREE-SPLIT-CHILD(s,1); //根节点暂时还没有元素,只有一个指针,指针从1开始计数,用c实现的时候要用(s,0)参数
B-TREE-INSERT-NON