首先B树是搜索二叉树的一种拓展,B-Tree 是一种自平衡的树(所有的叶子节点拥有相同的高度)类型的数据结构。但是和其它树比如红黑树,AVL树只有两个孩子:左孩子和右孩子不同,B-Tree 的子节点多余或者等于2两个孩子。因此,有的时候M叉树,因为它可以有M个子节点(M>=2)。如图:
B树一开始是针对机械磁盘而设计的,因为机械键盘的磁头跳转消耗的时间比较多,为了减少跳转的次数,所以设计了B-Tree。B树的目标是在时间复杂度内,完成一些动态操作。红黑二叉树的时间复杂度也是
,所以B树的优势是比红黑树存储更多的节点。所以很多数据库的存储引擎是B树,或者B树的变种B+树,还有B*树,更多细节我们后面慢慢研究。
B树的定义
看了很多定义,觉得《算法导论》给的比较权威:
一棵B树T是具有以下性质的有根树(根为T.root):
1. 每个结点有下面属性:
- x.n表示当前存储在结点中的关键字个数。
- x.n个关键字本身
,以非降序存放,即
。
- x.leaf,一个boolean值,如果x是叶子节点则为true,否则为false。
2. 每个内部结点x还包含x.n+1个指向其孩子的指针
。叶结点没有孩子,所以它们的
属性没有定义。
3. 关键字
对存储在各子树中的关键字范围加以分割:如果为任意一个存储在以
为根的子树中的关键字,那么
4. 每个叶结点具有相同的深度,即树的高度h。
5. 每个结点所包含的关键字个数有上界和下界。用一个被称为B树的最小度数(minmum degree)的规定整数
表示这里的界。
- 除了根结点以外的每个结点必须至少有 t -1个关键字,因此,除了根结点以外的每个内部结点至少有t个孩子。如果树非空,根结点至少有一个关键字。
- 每个结点至多可包含2t - 1个关键字。因此,一个内部结点至多可有2个孩子。当一个结点恰好有2t-1个关键字时,称该结点是满的(full)。
首先我们先根据上面的特点建模。可能大家并不是很理解,可以做一个构建B-Tree的过程来帮大家理解:
t = 2时的B树是最简单的。每个内部结点有2个、3个或4个孩子,即一棵2-3-4树。然而在实际中,t的值越大,B树的高度就越小 。
1.
2.
3.
4.
5.
6.
7.
通过上面最简单的2-3-4树,大家应该可以大致明白B-Tree的作用。那么我们开始建模。
public class BTNode<K extends Comparable, V> {
//每个结点所包含的关键字个数有上界和下界。用一个被称为B树的最小度数(minmum degree)的规定整数 表示这里的界。
public final static int MIN_DEGREE = 2;
//除了根结点以外的每个结点必须至少有 t -1个关键字
public final static int LOWER_BOUND_KEYNUM = MIN_DEGREE - 1;
//每个结点至多可包含2t - 1个关键字。
public final static int UPPER_BOUND_KEYNUM = (MIN_DEGREE * 2) - 1;
//x.leaf,一个boolean值,如果x是叶子节点则为true,否则为false。
protected boolean mIsLeaf;
//x.n表示当前存储在结点中的关键字个数。
protected int mCurrentKeyNum;
//x.n个关键字本身,以非降序存放。
protected BTKeyValue<K, V> mKeys[];
//每个内部结点x还包含x.n+1个指向其孩子的指针。叶结点没有孩子,所以它们的属性没有定义。
protected BTNode mChildren[];
class BTKeyValue<K extends Comparable, V>
{
protected K mKey;
protected V mValue;
public BTKeyValue(K key, V value) {
mKey = key;
mValue = value;
}
}
...
//省略其它的构造方法
}
另外在这里可以得出一个定理:如果n 1, 那么对任意一颗包括个关键字、高度为h、最小度数为t
2的B树T,有
,这个定理的证明很简单,这里不给了(感兴趣的朋友可以看看《算法导论》),根据这个定理我们也可以知道为什么B-Tree的很多动态操作可以做到
复杂度了。
B-Tree的基本操作
搜索B树
在树众多动态中,搜索是最简单的,所以我们从搜索开始讲起。
在实际运用中,B-Tree面对的数据都是非常大量的数据,一般只会把root节点保存在内存中,其它的节点都是存储在磁盘之中。但是这些都不一定,这里实现B-Tree的时候,不会细究这些细节(两种情况:全部节点在内存中,部分节点在内存中)。
我会在后面一道题目中给出一个例子:
public V search(K key) {
BTNode<K, V> currentNode = mRoot;
BTKeyValue<K, V> currentKey;
int i, numberOfKeys;
while (currentNode != null) {
numberOfKeys = currentNode.mCurrentKeyNum;
i = 0;
currentKey = currentNode.mKeys[i];
//看看key在当前的节点的哪个区间
while ((i < numberOfKeys) && (key.compareTo(currentKey.mKey) > 0)) {
++i;
if (i < numberOfKeys) {
currentKey = currentNode.mKeys[i];
}
else {
--i;
break;
}
}
//到达该节点的区间,如果key相等,则返回这个值。如果已经是叶子节点,则返回null。
if ((i < numberOfKeys) && (key.compareTo(currentKey.mKey) == 0)) {
return currentKey.mValue;
} else if (currentNode.mIsLeaf) {
return null;
}
//获取对应的区间的子节点。
if (key.compareTo(currentKey.mKey) > 0) {
//这里也可以从磁盘中读取对应的数。
currentNode = BTNode.getRightChildAtIndex(currentNode, i);
} else {
//这里也可以从磁盘中读取对应的数。
currentNode = BTNode.getLeftChildAtIndex(currentNode, i);
}
}
return null;
}
向B-Tree中插入一个关键字
参考: