1.B-tree出现的意义
前面介绍了树的数据结构,但是用树对硬盘、磁带等二级存储设备上读写数据时,会有问题。我们都知道,I/O操作的基础单元是block,当需要从硬盘上读数据时,包含该数据的整个block都会读进内存。而二叉树的节点可能不在同一个block里,这样对树进行遍历时,就会进行多次的block更换。
如下图所示,不同的block用方框隔开。
B-Tree就能减少访问二级存储的时间,具体的原理是:B-Tree的一个节点可以有多个数据值,大小等于block,这样就可以充分利用每次读取的block,进行多个数值的比较。
2.A B-tree of Order m的定义
(1)根节点最少有两个子树(根节点是叶节点除外)
(2)既不是根节点又不是叶节点的内部节点有k-1个值和k个指向子树的引用,其中m/2<=k<=m(m/2是上限,即商+1)
(3)每个叶节点有k-1个值,其中m/2<=k<=m,没有指向子树的引用
(4)所有的叶节点在同一层
举例来说,A B-tree of order 7,则4<=k<=7,即每个内部节点有[3,6]个值,[4,7]个指向子树的引用。
3.B-tree的代码实现
3.1节点定义
从上述的定义中,可以看到多路搜索树的节点中存放了两个数组,一个数组存放节点中的值,另一个数组存放指向子树的引用
public class Node {
int m = 7;//即B-tree中order的值
boolean isLeaf = true;//默认为叶节点
int keyCount;//对节点中的key进行计数;
int[] keys = new int[m-1];//存放每个节点的数值,最多有m-1个值
Node[] refers = new Node[m];//存放指向子树的引用,最多有m个引用
public Node(int i ){
keys[0] = i;
keyCount =1;
for(int j = 0; j<m;j++){
refers[j] = null;
}
}
}
3.2查找
public Node search(int k,Node n){
if(n!=null){
int i = 0;
for(;i<n.keyCount&&k>n.keys[i];i++);//找到比k大的值,进而到子树中进行比较
if(k==n.keys[i])
return n;
else
search(k,n.refers[i]);
}
return null;
}
在该方法的实现中,用到了递归,即找出子树的正确位置后,再对子树进行相同的过程。上面的代码通过找到比k大的值,那么就进入k的左子树;如果该节点的所有值都比k小,则进入该节点的最后一个子树。
3.3插入
注:插入的操作只会发生在叶节点,因为非叶结点表示的是一个范围。主要考虑被插入的节点是否已满。
分为三种情况:
(1)在叶节点中插入,并且叶节点仍然有空间
(2)在叶节点中插入,但是叶节点已满,则找出该节点的中间值,然后将该节点分为两个子节点(除去了中间值),该中间值则放入父节点中,以此类推
(3)一直到了根节点,如果根节点也满,则按照(2)的方式将根节点分为两个子节点,并且中间值为新的根节点
为了简便起见,用伪码表示。
Insert(k)
Finda leaf node to insert k;
While(true)//跟递归的思想接近
if(the node is not full)
insertk and increment keycount;
else
findthe mid-key;
dividethe left keys into two nodes, node1 and node2;
if(thenode is the root)
root=newNode(mid-key)
return;
else
node=itsparent;//即再重复上述过程
4.B*-tree
从B-tree的定义可以知道,B-tree能够保证每个节点至少50%的full,那么另外的50%可能会浪费。有研究表明,经过一系列的插入删除操作后,B-tree大概69%的full,这之后再进行操作,这个概率的变化也会很小。
B*-tree是在B-tree基础上得到的,出了root,其他节点都要至少2/3full,好处是更加充分的利用空间。
5.B+-tree
B+-tree也是B-tree的一种变形,与B-tree不同的是:
(1)在B+-tree中,关键码只分布在叶节点中;所以会一直查到叶节点,进行的插入操作也是针对叶节点
(2)有两个头指针:一个指向根节点,可以进行自顶向下的随机搜索;一个指向关键码的最小的叶节点,进行顺序搜索。