数据结构 | 第8章 - B-树(中)

122 阅读5分钟

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-树的查找过程,与二叉搜索树的查找过程基本类似:从根节点开始,通过关键码的比较不断深入至下一层,直到某一关键码命中(查找成功),或者到达某一外部节点(查找失败)。

    image-20220721211120128.png

    可见,只有在切换和更新当前节点时才会发生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 天,点击查看活动详情