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)时间内完成,最坏情况下可能会导致从底到树根的连续递归。