8.2.2 ADT接口及其实现
-
节点
0001 #include "vector/vector.h" 0002 template <typename T> struct BTNode; 0003 template <typename T> using BTNodePosi = BTNode<T>*; //B-树节点位置 0004 0005 template <typename T> struct BTNode { //B-树节点模板类 0006 // 成员(为简化描述起见统一开放,读者可根据需要进一步封装) 0007 BTNodePosi<T> parent; //父节点 0008 Vector<T> key; //关键码向量 0009 Vector<BTNodePosi<T>> child; //孩子向量(总比关键码多一个) 0010 // 构造函数 0011 BTNode() { parent = NULL; child.insert ( NULL ); } //无关键码节点 0012 BTNode ( T e, BTNodePosi<T> lc = NULL, BTNodePosi<T> rc = NULL ) { 0013 parent = NULL; key.insert ( e ); //作为根节点只有一个关键码,以及 0014 child.insert ( lc ); if ( lc ) lc->parent = this; //左孩子 0015 child.insert ( rc ); if ( rc ) rc->parent = this; //右孩子 0016 } 0017 };这里,同一节点的所有孩子组织为一个向量,各相邻孩子之间的关键码也组织为一个向量。当然,按照B-树的定义,孩子向量的实际长度总是比关键码向量多一。
-
B-树
0001 #include "BTNode.h" //引入B-树节点类 0002 0003 template <typename T> class BTree { //B-树模板类 0004 protected: 0005 int _size; //存放的关键码总数 0006 int _m; //B-树的阶次,至少为3——创建时指定,一般不能修改 0007 BTNodePosi<T> _root; //根节点 0008 BTNodePosi<T> _hot; //BTree::search()最后访问的非空(除非树空)的节点位置 0009 void solveOverflow ( BTNodePosi<T> ); //因插入而上溢之后的分裂处理 0010 void solveUnderflow ( BTNodePosi<T> ); //因删除而下溢之后的合并处理 0011 public: 0012 BTree ( int m = 3 ) : _m ( m ), _size ( 0 ) //构造函数:默认为最低的3阶 0013 { _root = new BTNode<T>(); } 0014 ~BTree() { if ( _root ) release ( _root ); } //析构函数:释放所有节点 0015 int const order() { return _m; } //阶次 0016 int const size() { return _size; } //规模 0017 BTNodePosi<T> & root() { return _root; } //树根 0018 bool empty() const { return !_root; } //判空 0019 BTNodePosi<T> search ( const T& e ); //查找 0020 bool insert ( const T& e ); //插入 0021 bool remove ( const T& e ); //删除 0022 }; //BTree后面将会看到,B-树的关键码插入和删除操作,可能会引发节点的上溢和下溢。因此,这里设有内部接口solveOverflow()和solveUnderflow(),分别用于修正此类问题。
8.2.3 关键码查找
-
算法
B-树结构非常适宜于在相对更小的内存中,实现对大规模数据的高效操作。
一般地如图8.13所示,可以将大数据集组织为B-树并存放于外存。对于活跃的B-树,其根节点会常驻于内存;此外,任何时刻通常只有另一节点(称作当前节点)留驻于内存。
B-树的查找过程,与二叉搜索树的查找过程基本类似:从根节点开始,通过关键码的比较不断深入至下一层,直到某一关键码命中(查找成功),或者到达某一外部节点(查找失败)。
可见,只有在切换和更新当前节点时才会发生I/O操作,而在同一节点内部的查找则完全在内存中进行。
因内存的访问速度远远高于外存,再考虑到各节点所含关键码数量N通常在128~512之间,故可直接使用顺序查找策略,而不必采用二分查找之类的复杂策略。
-
实现
0001 template <typename T> BTNodePosi<T> BTree<T>::search ( const T& e ) { //在B-树中查找关键码e 0002 BTNodePosi<T> v = _root; _hot = NULL; //从根节点出发 0003 while ( v ) { //逐层查找 0004 Rank r = v->key.search ( e ); //在当前节点中,找到不大于e的最大关键码 0005 if ( ( 0 <= r ) && ( e == v->key[r] ) ) return v; //成功:在当前节点中命中目标关键码 0006 _hot = v; v = v->child[r + 1]; //否则,转入对应子树(_hot指向其父)——需做I/O,最费时间 0007 } //这里在向量内是二分查找,但对通常的_m可直接顺序查找 0008 return NULL; //失败:最终抵达外部节点 0009 }
8.2.4 性能分析
B-树查找操作的效率主要取决于查找过程中的外存访问次数。那么,至多需要访问多少次外存呢?
由前节分析可见,与二叉搜索树类似,B-树的每一次查找过程中,在每一高度上至多访问一个节点。这就意味着,对于高度为h的B-树,外存访问不超过O(h - 1)次。
B-树节点的分支数并不固定,故其高度h并不完全取决于树中关键码的总数n。对于包含N个关键码的m阶B-树,高度h具体可在多大范围内变化?就渐进意义而言,h与m及N的关系如何?
-
树高
可以证明,若存有N个关键码的m阶B-树高度为h,则必有:h = Θ(logmN)。
-
复杂度
对于存有N个关键码的m阶B-树的每次查找操作,耗时不超过O(logmN)。
需再次强调的是,尽管没有渐进意义上的改进,但相对而言极其耗时的I/O操作的次数,却已大致缩减为原先的1/log2m。鉴于m通常取值在256至1024之间,较之此前大致降低一个数量级,故使用B-树后,实际的访问效率将有十分可观的提高。
“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 15 天,点击查看活动详情”