目录
前言
对于大量的数据而言,链表的线性访问时间太慢,不宜使用。本章节将会介绍一种简单的数据结构:树(tree),其大部分操作的运行时间平均为O(logN)。在数据结构中树是非常有用的抽象概念,在本篇中我们将讨论一棵高阶搜索树——b树。前排提示:高阶搜索树的代码难度高出一个量级,建议先把逻辑理顺后再去看代码哦。
一、什么是B-tree
虽然迄今为止我们所看到的查找树都是二叉树,但是还有一种常用的查找树不是二叉结构的,这种树叫做B树(B-tree)。
那么问题来了,既然已经有了诸如AVL树的查找树,为什么还要存在B树呢?因为平衡树的数据放在内存当中,但在实际的项目中,数据量特别的大,要放在磁盘中,而面临大量数据时二叉树的高度大意味着与磁盘交互次数多,而每一次磁盘的I/O都是机械运动,读取速度相对于内存来说是很慢的,为了降低树的高度、减少磁盘I/O,B树就此诞生。
B树具有下列特性:
- 是一棵多路平衡树,一个节点有多个数据,通常要求空间大小和磁盘块的大小一致;
- 在逻辑上,所有的叶子都在同一层且不带信息;
- 对于节点规定了上界和下界,最常见的每个节点(根节点除外)包含【t-1,2t-1】个关键字(其中t是最小的度,t>=2),不是硬性规定,根据实际中的磁盘块来决定;
- 如果一个节点有x个关键字,就有x+1个孩子;
- 每一个节点的关键字按照升序排序,x和y之间孩子的范围就是(x, y)。
为了使大家在概念上更方便理解,正如先前在二叉排序树中做的那样,我们使实际的数据存储在叶子上,也可以存储在内部节点中。实际上B树有多种定义,这些定义在一些次要的细节上不同于我们定义的结构。如下所示就是一棵B树:
这里还要提一个概念,图中的B树是4阶B树的一个例子,它的更流行的称呼是2-3-4树,而3阶B树称作2-3树;我们将通过这棵2-3-4树来描述B树增删查改。
二、查找
B树的查找和二叉排序树的查找过程十分相似,唯一不同的是:二叉排序树的任意一个节点最多只能有两个孩子,而B树则最多可以有M+1个孩子(M为节点中关键字个数),但这并不影响我们用与二叉排序树相同的查找方法,只需要多一步在节点中的遍历(可以是顺序也可以是二分,方便就行),我在代码中对返回的结果做了封装,这样过程会更加规范,当然效果是一样的。
/*查找返回节点下标的函数 从1到keynum找key*/
int search(BTree p, KeyType key)
{
int i = 1;
while (i <= p->keynum && key > p->key[i])
{
i++;
}
return i;
}
/*返回封装的结果集的查找函数 不是真正进行查找步骤*/
void searchNode(BTree tree, KeyType key, Result& r)
{
int i;
int found = 0;//标记查找成功或者失败 成功是1 失败是0
//先定义指针指向根节点
BTree p = tree;
BTree q = NULL;//有时候需要指向双亲节点的指针
while (p != NULL)
{
i = search(p, key);
if (i <= p->keynum && key == p->key[i])
{
//不允许重复所以找到就是查找失败 说明这个数据已经存进去了
found = 1;
}
else
{
q = p;
p = p->ptr[i - 1];//指针下移
}
}
//如果查找到了
if (found == 1)
{
r.pt = p;
r.i = i;
r.tag = found;
}
//没有查找到
else
{
//没有找到就把父节点传回去 利于后续操作
r.pt = q;
r.i = i;
r.tag = found;
}
}
三、插入
B树的插入规则与二叉排序树的插入类似,即一个节点中的关键字总是有序的,并且总是要插入到实际的叶子节点;不同的是,为了满足B树的高度可控这一特性,需要时刻判断是否超过了规定的存储最大容量,如果达到容量上限就要进行分裂(split);
有如上一棵2-3树,当我们试图插入新关键字K时,发现K所属的节点已经满了,插入K将使得这个节点拥有四个关键字,这是不允许的,这时我们就要分裂,从中间对半分,得到两个新的节点;
但此时我们又发现,分裂出的新节点的父节点现在有了四个孩子,而它只有两个关键字,最多只能有三个孩子,这样又带来了一个新的问题,当然解决的办法也很简单,我们可以把中间值上移,是父节点的关键变成三个,这样父节点最大就能有四个孩子了。
分裂是维持B树性质的重要操作之一,一定要把逻辑理清楚!接下来我们对上文中的2-3-4树再过一遍完整的插入流程:
第一步:判断根节点是否初始化,如果没有,初始化root为根节点;
第二步:当我们插入节点i,与根节点p比较,小于就往左边插入;
第三步:发现p的左孩子节点已经满了,需要分裂;
第四步:继续比较,比L小,插L的左子树;
第五步:判断L的左子树是否满了,满了就分裂,没满就挨个比较找到正确的插入位置插入,结束。