B-Tree

B-树(是B-树不是B减树)的使用,是为了平衡的容量硬盘与较小的内存、以及不同存储器层次之间I/O操作速度的巨大差异。分级存储是行之有效的方法,借助高效的调度方法,可以将内存的“高速度”和外存的“大容量”结合起来。内存的访问速度,与硬盘的访问速度相差几个数量级,因此最好能把一部分经常用到的数据放在内存中。具体可以参照《CSAPP》以及操作系统等,其中有关页的部分以及缺页的处理。

B-树的结构,可以从搜索树来演变。例如,把二叉搜索树,以两层为间隔,各节点与其左右孩子合并为一个大节点,原节点和孩子共三个关键码保留,孩子原有的四个分支也保留并按照中序遍历的次序排列,如下图所示。这样每个大节点最多有四个分支,称为四路搜索树。

可以看出,此时的树与二叉搜索树一样,不过关键码的访问,是从外存读取一组关键码,并且关键码在物理内存上连续。

定义:m阶B-树,即m路平衡搜索树,所有外部节点均深度相等,每个内部节点不超过m-1个关键码,按升序排列;同时,没个关键码有m个分支。同时,每个内部节点的关键码和分支数也不能太少,最少有[m/2]-1(向上取整)个关键码,各节点的分支树为[[m/2],m],故也称为([m/2],m)-树。

外部节点查找失败,在实际意义上可以理解为,可能表示目标关键码存在于更低一级的存储结构中,例如缺页。此时,需要到下一级存储结构中进行查找。

B-树的一个重要性质,就是m很大的时候,宽度可能远远大于高度。定义B-树的节点:

 1 #define BTNodePosi(T) BTNode<T>*
 2 template<typename T> struct BTNode
 3 {
 4     BTNodePosi(T) parent;
 5     vector<T> key;
 6     vector<BTNodePosi(T)> child;//孩子节点向量,数量比key多1
 7     BTNode() { parent = NULL; child.insert(0, NULL); }
 8     BTNode(T e, BTNodePosi(T) lc = NULL, BTNodePosi(T) rc = NULL)
 9     {
10         parent = NULL;
11         key.insert(0, e);
12         child.insert(0, lc); child.insert(1, rc);//两个孩子
13         if (lc)  lc->parent = this;
14         if (rc) rc->parent = this;
15     }
16 };

节点包括,关键码数组、孩子指针数组以及父亲指针。

 1 template<typename T> class BTree
 2 {
 3 protected:
 4     int _size;
 5     int _order;
 6     BTNodePosi(T) _root;
 7     BTNodePosi(T) _hot;
 8     void solveOverflow(BTNodePosi(T) v);//处理插入导致的上溢(分裂处理)
 9     void solveUnderflow(BTNodePosi(T) v);//处理删除导致的下溢(合并处理)
10 public:
11     BTree(int order = 3) :_order(order), _size(0) { _root = new BTNode<T>(); }
12     ~BTree() { if (_root) delete _root; }
13     int order() const { return _order; }
14     int size() const { return _size; }
15     BTNodePosi(T)& root() { return _root; }
16     bool empty() const { return !_root; }
17     BTNodePosi(T) search(const T& e);
18     bool insert(const T& e);
19     bool remove(const T& e);
20 };

B-树的实现有点复杂,代码部分我贴了邓俊辉大大的书0 0...

查找

查找操作的思路很简单,从根节点开始,首先在key数组中查找该数,未命中则根据查找停止的位置,转入相应的child子树进行继续查询,直到命中或者外部节点未命中。

 1 template<typename T> BTNodePosi(T) BTree<T>::search(const T& e)
 2 {
 3     BTNodePosi(T) v = _root; _hot = NULL;
 4     while (v)
 5     {
 6         int r = v->key.search(e);//search返回不大于e的位置
 7         if ((r >= 0) && (v->key[r] == e)) return v;
 8         _hot = v;v = v->child[r + 1];//在查找返回的位置的后面子树继续查找
 9     }
10     return NULL;
11 }

查找操作的复杂度,不超过O(logmN),也即该树的高度。

插入

大体思路:调用查找,找到合适的插入点,进行key数组的插入操作,同时也新插入一个child。不过,需要注意的一个问题是上溢,即该节点在插入后可能会导致key的数量超过了m-1。这时的策略,是把这个节点进行分裂,即把中心的关键码升入父亲节点,把两侧的关键码分裂为两个新的节点,并将相应的子树也进行分配,如果溢出节点已经是根节点,那么全树高度上升一层。如下图所示:

 1 template<typename T> bool BTree<T>::insert(const T& e)
 2 {
 3     BTNodePosi(T) v = search(e); if (v) return false;
 4     Rank r = _hot->key.search(e);//肯定找不到,但是会返回一个插入位置
 5     _hot->key.insert(r + 1, e);
 6     _hot->child.insert(r + 2, NULL);//创建一个空子树指针
 7     _size++;
 8     solveOverflow(_hot);
 9     return true;
10 }
11 template<typename T> void BTree<T>::solveOverflow(BTNodePosi(T) v)
12 {
13     if (_order >= v->child.size()) return;//递归基
14     Rank s = _order / 2;//轴点(_order=key.size()=child.size()-1)
15     BTNodePosi(T) u = new BTNode<T>();
16     for (Rank j = 0; j < _order - s - 1; j++)//v右侧_order-s-1个孩子及关键码分裂为右侧节点u
17     {
18         u->child.insert(j, v->child.remove(s + 1));//从中间逐渐向后侧删除,效率不高
19         u->key.insert(j, v->key.remove(s + 1));
20     }
21     u->child[_order - s - 1] = v->child.remove(s + 1);//移动最靠右的孩子
22     if (u->child[0])
23         for (Rank j = 0; j < _order - s; j++)//u的孩子非空,令他们的父亲节点为u
24             u->child[j]->parent = u;
25     BTNodePosi(T) p = v->parent;.
26     if (!p) //v已经是根节点的情况
27     {
28         _root = p = new BTNode<T>(); p->child[0] = v; v->parent = p;//创建一个新的根节点
29     }
30     Rank r = 1 + p->key.search(v->key[0]);//p指向u的指针的秩
31     p->key.insert(r, v->key.remove(s));//轴点关键码上升
32     p->child.insert(r + 1, u); u->parent = p;//新节点u与父节点p互联
33     solveOverflow(p);//递归,向上一层
34 }

 

删除

同样,删除操作也需要查找,不同之处在于,元素对应了一棵子树,因此需要寻找后续,并且与后继交换位置,再执行删除后继的操作。同样,删除操作可能会导致上溢,即节点中关键码的数量少于[m/2]-1。此时,存在两种可能:

(1)左兄弟或者右兄弟的关键码数量不少于[m/2],这种情况下,可以从父亲“借”一个关键码,父亲节点从其兄弟借一个关键码,这种操作不会影响其他节点,如下图所示:

(2)左右兄弟都太“瘦”,即都无法借出一个关键码。此时,可以进行合并操作,即将父亲与兄弟、当前节点合并为一个节点。因为节点总数为[m/2]-2+[m/2]-1+1小于等于m-1,因此新合并的节点不可能再次溢出,但是可能会导致父亲所在的节点下溢,因此需要递归向上。

 1 template<typename T> bool BTree<T>::remove(const T& e)
 2 {
 3     BTNodePosi(T) v = search(e); if (!v) return false;
 4     Rank r = v->key.search(e);
 5     if (v->child[0])//如果不是叶节点,那么定位到后继
 6     {
 7         BTNodePosi(T) u = v->child[r + 1];//右子树中寻找后继
 8         while (u->child[0]) u = u->child[0];
 9         v->key[r] = u->key[0]; v = u; r = 0;//替换,并且统一操作v
10     }
11     v->key.remove(r); v->child.remove(r + 1); _size--;//删除e
12     solveUnderflow(v);//处理下溢
13     return true;
14 }
15 template<typename T> void BTree<T>::solveUnderflow(BTNodePosi(T) v)
16 {
17     if ((_order + 1) / 2 <= v->child.size()) return;//递归基,最少m/2-1个关键码,m/2个孩子(向上取整)
18     BTNodePosi(T) p = v->parent;
19     if (!p)//递归基,若已经是根节点,没有孩子的下限
20     {
21         if (!v->key.size() && v->child[0])//树根没有关键码但是有孩子
22         {
23             _root = v->child[0]; _root->parent = NULL;//把孩子直接当树根
24             v->child[0] = NULL; release(v);
25         }
26         return;
27     }
28     Rank r = 0; while (p->child[r] != v) r++;//确定v是p的第r个孩子,v可能不含有关键码,所以不能通过关键码查找
29     //情况1:向左兄弟借关键码
30     if (r > 0)//若v不是p的第一个孩子
31     {
32         BTNodePosi(T) ls = p->child[r - 1];//左兄弟必然存在
33         if ((_order + 1) / 2 < ls->child.size())//该兄弟够胖
34         {
35             v->key.insert(0, p->key[r - 1]);//父亲借一个关键码给v,作为最小关键码
36             p->key[r - 1] = ls->key.remove(ls->key.size() - 1);//左兄弟最大的给p的借出位置
37             v->child.insert(0, ls->child.remove(ls->child.size() - 1));//左兄弟最后侧孩子给v,放在最左侧
38             if (v->child[0]) v->child[0]->parent = v;
39             return;//已经完成,这种情况不会下溢传递
40         }
41     }
42     //情况2:向右兄弟借
43     if (p->child.size() - 1 > r)//v不是最后一个孩子
44     {
45         BTNodePosi(T) rs = p->child[r + 1];//右兄弟必然存在
46         if ((_order + 1) / 2 < rs->child.size())//该兄弟够胖
47         {
48             v->key.insert(v->key.size(), p->key[r]);//父亲借一个关键码给v,作为最大关键码
49             p->key[r] = rs->key.remove(0);//右兄弟最小的给p的借出位置
50             v->child.insert(v->child.size(), rs->child.remove(0));//右兄弟最左侧孩子给v,放在最右侧
51             if (v->child[v->child.size() - 1]) v->child[v->child.size()]->parent = v;
52             return;//已经完成,这种情况不会下溢传递
53         }
54     }
55     //情况3:左右兄弟要么为空(不同时),要么都太瘦----合并
56     if (r > 0)//与左兄弟合并
57     {
58         BTNodePosi(T) ls = p->child[r - 1];
59         ls->key.insert(ls->key.size(), p->key.remove(r - 1)); p->child.remove(r);//p的第r-1个转入ls,v不再是p的第r个孩子
60         ls->child.insert(ls->child.size(), v->child.remove(0));//v最左侧孩子过继给ls为最右侧孩子
61         if (ls->child[ls->child.size() - 1])
62             ls->child[ls->child.size() - 1]->parent = ls;
63         while (!v->key.empty())//v中剩余的关键码和孩子依次转入ls
64         {
65             ls->key.insert(ls->key.size(), v->key.remove(0));
66             ls->child.insert(ls->child.size(), v->child.remove(0));
67             if (ls->child[ls->child.size() - 1]) ls->child[ls->child.size() - 1]->parent = ls;
68         }
69         release(v);
70     }
71     else//与右兄弟合并
72     {
73         BTNodePosi(T) rs = p->child[r + 1];
74         rs->key.insert(0, p->key.remove(r)); p->child.remove(r);//p的第r个转入rs,v不再是p的第r个孩子
75         rs->child.insert(0, v->child.remove(v->child.size() - 1));//v最右侧孩子过继给rs为最左侧孩子
76         if (rs->child[0])
77             rs->child[0]->parent = rs;
78         while (!v->key.empty())//v中剩余的关键码和孩子依次转入rs
79         {
80             rs->key.insert(0, v->key.remove(v->key.size() - 1));
81             rs->child.insert(0, v->child.remove(v->child.size() - 1));
82             if (rs->child[0]) rs->child[0]->parent = rs;
83         }
84         release(v);
85     }
86     solveUnderflow(p);//上升一层,如果有必要继续合并
87 }

 

B-tree的插入删除操作,大部分情况下可以在O(logmN)时间内完成,最坏情况下可能会导致从底到树根的连续递归。

 

posted @ 2017-08-02 09:07  luStar  阅读(390)  评论(1编辑  收藏  举报