1. B-Tree
1.1. B-Tree的节点结构
class BTreeNode {
//关键字数量
int keyNum;
//关键字数组
Integer[] key;
//指向父节点
BTreeNode parent;
//指向孩子节点的节点数组
BTreeNode[] son;
public BTreeNode(int keyNum, BTreeNode parent){
this.keyNum = keyNum;
this.key = new Integer[keyNum];
this.parent = parent;
this.son = new BTreeNode[keyNum+1];
}
// 其他成员方法省略
public void otherMethods(){}
}
1.2. B-Tree的特点:
B树的度M:非叶子节点最多有M个孩子,最少有M/2个孩子
B树关键字个数:M-1
- B-tree是一种多路搜索树(并不是二叉的),对于一棵M阶树:
- 定义任意非叶子结点最多只有M个孩子;且M>2;
- 根结点的孩子数为[2, M],除非根结点为叶子节点;
- 除根结点以外的非叶子结点的儿子数为[M/2, M];
- 非叶子结点的关键字个数=指向儿子的指针个数-1;
- 每个非叶子结点存放至少M/2-1(取上整)和至多M-1个关键字;
- 非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];
- 非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;
- 所有叶子结点位于同一层;
例:2-3树——每个非叶子节点存放 1-2个关键字,每个节点有3个指针。
1.3. B-Tree高度与复杂度
B树的高度是 ,而不是其他几种树的
其中T为度数(每个节点包含的元素个数),即阶数,n为总元素个数或总关键字个数。
B树查找的时间复杂度为O(log2N)。
1.4. B-Tree的基本操作
(1)查找操作
在B-树中查找给定关键字的方法类似于二叉排序树上的查找。不同的是在每个结点上确定向下查找的路径不一定是二路而是keynum+1路的。
对结点内的存放有序关键字序列的向量key[l..keynum] 用顺序查找或折半查找方法查找。若在某结点内找到待查的关键字K,则返回该结点的地址及K在key[1..keynum]中的位置;否则,确定K在某个key[i]和key[i+1]之间结点后,从磁盘中读son[i]所指的结点继续查找。直到在某结点中查找成功;或直至找到叶结点且叶结点中的查找仍不成功时,查找过程失败。
(2)查找操作的时间开销
B-树上的查找有两个基本步骤:
1.在B-树中查找结点,该查找涉及读盘DiskRead操作,属外查找;
2.在结点内查找,该查找属内查找。
查找操作的时间为:
1.外查找的读盘次数不超过树高h,故其时间是O(h);
2.内查找中,每个结点内的关键字数目keynum
1.实际上外查找时间可能远远大于内查找时间。
2.B-树作为数据库文件时,打开文件之后就必须将根结点读人内存,而直至文件关闭之前,此根一直驻留在内存中,故查找时可以不计读入根结点的时间。
(3)插入操作
插入一个元素时,首先在B树中是否存在,如果不存在,即在叶子结点处结束,然后在叶子结点中插入该新的元素,注意:如果叶子结点空间足够,这里需要向右移动该叶子结点中大于新插入关键字的元素,如果空间满了以致没有足够的空间去添加新的元素,则将该结点进行“分裂”,将一半数量的关键字元素分裂到新的其相邻右结点中,中间关键字元素上移到父结点中(当然,如果父结点空间满了,也同样需要“分裂”操作),而且当结点中关键元素向右移动了,相关的指针也需要向右移。如果在根结点插入新元素,空间满了,则进行分裂操作,这样原来的根结点中的中间关键字元素向上移动到新的根结点中,因此导致树的高度增加一层。
(4)删除操作
首先查找B树中需删除的元素,如果该元素在B树中存在,则将该元素在其结点中进行删除,如果删除该元素后,首先判断该元素是否有左右孩子结点,如果有,则上移孩子结点中的某相近元素到父节点中,然后是移动之后的情况;如果没有,直接删除后,移动之后的情况。
1.5. 2-3树
2-3树是最简单的B树结构,具有如下特点:
- 2-3树所有叶子节点都在同一层(只要是B树都满足);
- 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点;
- 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点;
- 2-3树是由二节点和三节点构成的树。
2. B+ Tree
B+ tree是应文件系统所需而产生的一种B-tree的变形树。
(1)B树和B+树的对比
一棵m阶的B+树和m阶的B树的异同点在于:
1.有n棵子树的结点中含有n-1 个关键字;
2.所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。 (而B 树的叶子节点并没有包括全部需要查找的信息,B+的数据只能在叶子节点命中)
3.所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 (而B 树的非终节点也包含需要查找的有效信息)
(2)为什么说B+-tree比B 树更适合实际应用中操作系统的文件索引和数据库索引?
- B+tree的磁盘读写代价更低
B+tree的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。
如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。
一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
举个例子,假设磁盘中的一个盘块容纳16bytes,而一个关键字2bytes,一个关键字具体信息指针2bytes。
一棵9阶B-tree(一个结点最多8个关键字)的内部结点需要2个盘快。而B+ 树内部结点只需要1个盘快。当需要把内部结点读入内存中的时候,B 树就比B+ 树多一次盘块查找时间(在磁盘中就是盘片旋转的时间)。
- B+tree的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
3. B* Tree
B*tree是B+tree的变体,在B+树的基础上(所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针),
- B*树中非根和非叶子结点再增加指向兄弟的指针;
- B树定义了非叶子结点关键字个数至少为**(2/3)M**,即块的最低使用率为2/3(代替B+树的1/2)。
4. B树、B+树、B*树的优缺点比较
4.1. B与B+比较:
- B-树是一种平衡的多路查找(又称排序)树,在文件系统中有所应用。主要用作文件的索引。其中的B就表示平衡(Balance)
- B+树有一个最大的好处,方便扫库,B树必须用中序遍历的方法按序扫库,而B+树直接从叶子结点挨个扫一遍就完了。
- B+树支持range-query(区间查询)非常方便,而B树不支持。这是数据库选用B+树的最主要原因。
- B树成功查询比B+树快:当要查询的值处于一个非叶子节点,B树找到该点成功并结束查询,B+树的非叶节点只是索引部分,需要到叶子节点中获取数据。
4.2. B+与B*比较:
- B*树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针;B树定义了非叶子结点关键字个数至少为(2/3)M,即块的最低使用率为2/3(代替B+树的1/2);
- B+树的分裂:当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针;
- B*树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;
- 所以,B*树分配新结点的概率比B+树要低,空间使用率更高;