在如今,随着数据库存储量越来越大,实践证明,分级存储才是行之有效的方法。在由内存与外存(磁盘)组成的二级存储系统中, 数据全集往往存放于外存中,计算过程中则可将内存作为外存的高速缓存,存放最常用数据项的 复本。借助高效的调度算法,如此便可将内存的“高速度”与外存的“大容量”结合起来。 两个相邻存储级别之间的数据传输,统称I/O操作。各级存储器的访问速度相差悬殊,故应 尽可能地减少I/O操作。仍以内存与磁盘为例,其单次访问延迟大致分别在纳秒(ns)和毫秒(ms) 级别,相差5至6个数量级。因此,我们应该关注外存的访问次数。
当数据规模大到内存已不足以容纳时,常规平衡二叉搜索树的效率将大打折扣。其原因在于, 查找过程对外存的访问次数过多。例如,若将10^9个记录在外存中组织为AVL树,则每次查找大 致需做30次外存访问。那么,如何才能有效减少外存操作呢? 为此,需要充分利用磁盘之类外部存储器的另一特性:就时间成本而言,读取物理地址连续 的一千个字节,与读取单个字节几乎没有区别。既然外部存储器更适宜于批量式访问,不妨通过 时间成本相对极低的多次内存操作,来替代时间成本相对极高的单次外存操作。相应地,需要将 通常的二叉搜索树,改造为多路搜索树。
多路平衡搜索树:
所谓m阶B-树(B-tree),即m路平衡搜索树:(m>=2)
- 内部节点不超过m-1个关键码
- 内部节点的分支在sup(m/2)至m之间
- 所有外部节点(叶子节点)均深度相等
B-树的外部节点(external node),它们实际上未必意味着查找失败, 而可能表示目标关键码存在于更低层次的某一外部存储系统中,顺着该节点的指示,即可深入至 下一级存储系统并继续查找
内部节点的模板类
#define BTNodePosi(T) BTNode<T>* //B-树节点位置
template <typename T> //B-树节点模板类
struct BTNode
{
BTNodePosi(T) parent; //父节点
vector<T> key; //关键码向量
vector<BTNodePosi(T)> child;
BTNode() { parent = NULL; child.insert(0, NULL); }
};
查找过程:
与二叉搜索树类似,B-树的每一次查找过程中,在每一高度上至多访问 一个节点。这就意味着,对于高度为h的B-树,外存访问不超过O(h - 1)次。
树高:
从图中的证明可以看出:树高不超过,从而查找的时间复杂度也不超过
B+树:
是一种基于B-树的变形树,对于文件系统和数据库更加友好;
与B-树不同的是
- 其节点的关键字和分支数是相同的
- 内部节点不保存数据,只用来索引,所有数据都保存在叶节点
- 将每个叶节点连接起来,形成链表,以供查找
你可以看到各层之间有重复元素,且叶节点被链接了起来;
每一个父节点都出现子元素中,且是子节点的最大或者最小元素;
B+树把所有信息都放在叶节点,而内部节点只用来索引,是为了更好的查询性能:
B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
举个例子,假设磁盘中的一个盘块容纳16bytes,而一个关键字2bytes,一个关键字具体信息指针2bytes。一棵9阶B-tree(一个结点最多8个关键字)的内部结点需要2个盘快。而B+ 树内部结点只需要1个盘快。当需要把内部结点读入内存中的时候,B 树就比B+ 树多一次盘块查找时间(在磁盘中就是盘片旋转的时间)。
并且对于查找某个区间的数据,那么就可以通过叶节点的链表性质,实现更加方便的查找,而B-树却做不到;