题目
2-3-4树是B树的特例,是度为2的B树。在B树这篇博客中,我们实现的B树是一个模板,因此要得到2-3-4树,即度为2的B树非常容易,只要如是声明就可以了——Btree<int,2>
bt,其中int是所存元素类型。
在本题中,要实现的是2-3-4树的链接与分裂。参看红黑树的连接操作我们不难得到2-3-4树的链接方法。现在,我们对于2-3-4树的链接也予以推广,即实现任意度数的B树的链接和分裂。
(a)
B树高度的变化只有两种方式:一是在插入关键字的时候,根节点满,那么其要分裂,树长高;二是在删除关键字的时候,关键字不在内节点,根节点只有一个关键字,且左右孩子均已达到最小关键字数,那么将会合并根唯一的关键字和左右孩子,树高将减1。
因此,在节点增加height域后。在insert中树长高时,将新树根的高设为旧根高再加1(root->height = p->height + 1;)代码请参看B树实现,下同;在erase_aux函数中,代码不用更改,因为旧根会被释放,直接指向孩子,该孩子将成为新根。
(b) 假设将树T‘链接到树T上,链接关键字为k,且满足条件任意key[T] < k < key[T‘],key[T] > k > key[T‘]类似。
为便于讨论,先抛出相关函数名称。
- nodeAtRightOfHeight(size_t h):在树T中找到高度为h的最右侧节点的父亲;
- nodeAtLeftOfHeight(size_t h):在树T中找到高度为h的最左侧节点的父亲;
- linkAtRight(k,T‘):在树T的右侧连接T‘和k;
- linkAtLeft(k,T‘):在树T的左侧链接T‘和k。
若树T较T‘高,
1、调用nodeAtRightOfHeight在树T右侧找到和树T‘一样的节点;
2、在查找过程中,对于已满节点,即关键字数目已达到2*degree - 1的节点,对其调用split函数予以分裂;
3、找到该节点的父亲,然后直接将k插入父节点,作为最末关键字,将树T‘整个作为k的右孩子,完成合并。由于只要是满节点则分裂,故此时父节点不可能满。
若树T和T‘高度一样,
1、构造新根,插入关键字k,将T,T‘分别作为该节点的左右孩子,更新相关域,合并完毕。
以上两种情况调用linkAtRight;
若树T较T‘矮,
1、交换两树;
2、调用linkAtLeft将交换后的树T‘和k从左侧链接到交换后的树T上,该过程和第一种情况对称。
时间复杂度分析:
显然,对于2-3-4树,树高是O(lgn),故每次下降查找的次数为O(lgn)。对于每一次,即使存在分裂的情况以调用split函数,由于关键字数为最多3,因此split函数中的for循环迭代次数不会超过1,相当于没有循环;而在插入时,若是右侧连接,则直接更新两个相关域,若是左侧链接,则最多移动两个关键字,不会对渐进时间造成影响;因而每一次时间均为O(1),所以总时间为O(lgn)。
以上过程均稍后贴出代码。
(c)在查找某一关键字k的过程中,查找路径(find path)会将整棵树分为两个集合以及一些关键字,我们通过以下图解来讨论,以寻找关键字K为例,每次在当前节点找到第一个不小于K的关键字,或者当该节点的关键字均比K小时得到最大一个关键字。
原2-3-4树结构:
第一次:
1、得到关键字D,索引为0;
2、在D处将树分裂,由于D比K小,则要继续向右查找,得到集合S‘的一棵树。结果图如下:
图意:
1、curr表示当前正在检查的节点;
2、蓝色表示分裂出来的集合S‘中的元素,是关键字均比K小的子2-3-4树,small_link_key表示分裂得到的关键字集合;
3、红色箭头表明下一次将要检查的方向(节点);
第二次:
1、得到关键字M,索引为1;
2、在M处将子树分裂,由于M比K大,则要向左找,得到集合S‘‘的一棵树。结果如下:
图意:
1、红色箭头所指还是这个节点,表明要继续检查该节点;
2、灰绿色表示分裂得到的集合S‘‘的树,是关键字均比K大的树,big_link_key表示因此得到的关键字集合;
第三次:
1、得到关键字F,索引为0;
2、在F处将子树分裂,由于F比K小,则要向右查找,得到一棵集合S‘的树,类似于第一次。结果图如下:
图意:
1、S‘中已经有两个元素了,关键字集合也已经有两个元素了;
第四次:
1、得到关键字K,索引为1;
2、将子树从K处分裂,找到所需关键字,且得到两棵树,分属于S‘和S‘‘。结果图如下:
图意:
1、到此分裂结束,集合S‘中有三个元素,关键字集合有两个元素;
2、集合S‘‘中有两个元素,关键字集合有一个元素。
注意:第二次的分裂结束后不需要更新curr,只有在向右搜寻时需要更新curr。这是我的理解,只有这样才能保证正确的分裂,不知道友们有没有其他方法。
根据上述分析不难得出(c)问答案:
1、height[Ti-1‘] >= height[Ti‘];
2、如上。对于任意y属于Ti-1‘‘和z属于Ti‘‘,有y > ki > z。
(d)
1、关于如何实现分裂不再赘述了,上面图解已经很清楚了,稍后给出代码;
2、在接下来实现中,对于不断进行的分裂,每生成集合S‘或者S‘‘的一棵树我们就将它与对应关键字合并,时刻保证各集合中只有一棵树,也就边分裂,边合并,该过程为splitTree,返回两棵树T‘和T‘‘。
实现代码如下,有以下约定:
1、全部贴出太多,只贴出相关代码,其他请参考B树。除了增加高度域height及其维护,B树实现还有下列三处微小修改;
2、B树实现中的split函数稍作修改,增加返回值,返回新建节点new_child的地址,这是修改的一个地方;
3、nodeAtRightOfHeight、nodeAtLeftOfHeight和inset都会处理根满情况,因而将这段代码包装成一个函数rootFull,B树实现修改的第二个地方;
4、B树增加了一个构造函数,私有的,只用于由node*构造子B树。
B树新增有关链接和分裂的函数声明:
private: node* nodeAtRightOfHeight(size_t); node* nodeAtLeftOfHeight(size_t); void linkAtRight(const T&, Btree&); void linkAtLeft(const T&, Btree&); void linkTwoTrees(Btree &lhs, const T &link_key, Btree &rhs) { if (lhs.empty()) { lhs.root = rhs.root; rhs.root = nullptr; } else lhs.link(link_key, rhs); } void rootFull() { node *p = root; root = new node;//树将会长高 root->child[0] = p; root->height = p->height + 1; root->leaf = false; split(root, 0);//树根分裂 } explicit Btree(node *r) :root(r), compare(r->compare){}//只在分裂函数中被调用,私有 public: void link(const T&, Btree&);//树连接,这是一个转发函数 void splitTree(const T&, Btree&, Btree&);//树分裂
下面是上述函数具体的实现:
template <typename T,int degree,class Compare = less<T>> node<T, degree, Compare>* Btree<T, degree, Compare>::nodeAtRightOfHeight(size_t h) {//找到给定高度的最右侧节点的父亲或者当树高为h时返回树根,给树右合并做准备,下降寻找时对于满的节点要予以分裂 if (root->num == node::max_num)//如果根节点满 rootFull(); node *curr = root; if (curr->height == h) return curr;//若根正是该节点 while (curr->child[curr->num]->height != h) {//一直往最右下找 if (curr->child[curr->num]->num == node::max_num)//若最右孩子满 curr = split(curr, curr->num);//则分裂,修改了一下split函数,使其返回新孩子地址 else curr = curr->child[curr->num]; } return curr; } template <typename T, int degree, class Compare = less<T>> node<T, degree, Compare>* Btree<T, degree, Compare>::nodeAtLeftOfHeight(size_t h) {//找到给定高度的最左侧节点的父亲,给树左合并做准备,下降寻找时对于满的节点要予以分裂 if (root->num == node::max_num)//如果根节点满 rootFull(); node *curr = root; while (curr->child[0]->height != h) {//一直往最右下找 if (curr->child[0]->num == node::max_num)//若最右孩子满 split(curr, 0);//则分裂 curr = curr->child[0]; } return curr; } template <typename T,int degree,class Compare = less<T>> void Btree<T, degree, Compare>::linkAtRight(const T &k, Btree &rhs) { node *curr = nodeAtRightOfHeight(rhs.root->height); if (curr == root && curr->height == rhs.root->height) {//若两棵树正好一样高 root = new node; root->insert(k); root->child[0] = curr; root->child[1] = rhs.root; root->height = curr->height + 1; root->leaf = false; } else {//否则,直接把关键k插入curr,然后将最右孩子指针指向被合并树。因为一路分裂下来,curr不可能满 curr->insert(k); curr->child[curr->num] = rhs.root; } } template <typename T, int degree, class Compare = less<T>> void Btree<T, degree, Compare>::linkAtLeft(const T &k, Btree &lhs) { node *curr = nodeAtLeftOfHeight(lhs.root->height); curr->insert(k); for (int i = curr->num - 1; i >= 0; --i) curr->child[i + 1] = curr->child[i]; curr->child[0] = lhs.root; } template <typename T,int degree,class Compare = less<T>> void Btree<T, degree, Compare>::link(const T &k, Btree &linkedTree) {//连接转发函数,按以下四种情况转发。前提是两树均不空 if (compare(this->root->key[0], k) && compare(k,linkedTree.root->key[0])) {//1、任意key[this] < k < key[linkedTree]。这里采用根的0号关键字只是区分一下是何种连接, //我们假设给定的关键字和树满足上述关系,下同 if (this->root->height >= linkedTree.root->height)//1.1 本树较高或者和被连接树一样高 linkAtRight(k, linkedTree);//则在本树右侧连接 else {//1.2 否则本树较矮 swap(root, linkedTree.root);//交换两树 linkAtLeft(k, linkedTree);//在新的本树左侧连接 } } else if (compare(linkedTree.root->key[0], k) && compare(k, this->root->key[0])) {//2、key[this] > k > key[linkedTree] if (linkedTree.root->height < this->root->height)//2.1 若本树高 linkAtLeft(k, linkedTree);//则在本树左侧连接 else {//2.2 否则本树较矮或者和被连接树一样高 swap(root, linkedTree.root);//则交换两树 linkAtRight(k, linkedTree);//在新的本树右侧连接 } } else { cout << "Error: bad input!" << endl; return; } linkedTree.root = nullptr; } template <typename T,int degree,class Compare = less<T>> node<T, degree, Compare>* Btree<T, degree, Compare>::underfillSplit(node *curr, int index) {//未满分裂,将curr节点从index处一分为二 node *new_child = new node; for (int i = index + 1; i < curr->num; ++i)//移动index之后的关键到新节点 new_child->key[i - index - 1] = curr->key[i]; if (!curr->leaf) {//若不是叶子 for (int i = index + 1; i <= curr->num; ++i)//则还要移动孩子指针 new_child->child[i - index - 1] = curr->child[i]; } new_child->num = curr->num - index - 1; new_child->leaf = curr->leaf; new_child->height = curr->height; curr->num = curr->num - new_child->num - 1; return new_child; } template <typename T, int degree, class Compare = less<T> > void Btree<T,degree,Compare>::splitTree(const T &k, Btree &smallTree,Btree &bigTree) {//以找寻关键字k的路径p分割树,将小于k的集合合并为smallTree,大的合并为bigTree。我们假设k存在 node *curr = root; root = nullptr; T small_link_key = T(), big_link_key = T(); while (true) {//index是curr中第一个不小于k的关键字索引,或者curr所有关键字比k都小时最后一个关键字索引 //但这并不影响分裂 int index = curr->search(k); T temp = curr->key[index]; node *new_node = underfillSplit(curr, index);//分裂该节点,返回新生成的节点地址 if (new_node->num == 0) {//若新节点没有关键字 node *r = new_node; new_node = new_node->child[0]; delete r; } if (curr->num == 0) {//若分裂后当前节点不再有关键字 node *r = curr; curr = curr->child[0]; delete r; } if (compare(k, temp)) {//若k小于index处关键字,则要往左走,这里我们不再更新curr,让其在该点继续循环 linkTwoTrees(bigTree, big_link_key, Btree(new_node)); big_link_key = temp;//记下这一次的分割关键字,以备下次再用 } else if (compare(temp, k)) {//否则若k大于index处关键字,则要往右走,这是特殊情况,当该节点关键字全部比k小时发生 linkTwoTrees(smallTree, small_link_key, Btree(curr)); small_link_key = temp; curr = new_node;//更新curr } else {//若相等,即已经分割完毕,那么合并左右两树,结束 if (curr != nullptr)//如果curr是叶子,且如果经过了上面if语句(满足num为0)的处理, //这时候curr应当为空,不需要合并 linkTwoTrees(smallTree, small_link_key, Btree(curr)); if (new_node != nullptr)//同上理 linkTwoTrees(bigTree, big_link_key, Btree(new_node)); break; } } }
下面是测试的例子:
int main() { Btree<char,2> bt,small,big; vector<char> cvec = { 'P', 'C', 'M', 'T', 'X', 'A', 'B', 'D', 'E', 'F', 'J', 'K', 'L', 'N'}; for (size_t i = 0; i != cvec.size(); ++i) bt.insert(cvec[i]); cout << "original B Tree------------" << endl; bt.sequentialPrint(); bt.splitTree('F', small, big); /*bt.splitTree('B', small,big); bt.splitTree('D', small, big); bt.splitTree('A', small, big);*/ cout << "small-----------------------" << endl; small.sequentialPrint(); cout << "big-------------------------" << endl; big.sequentialPrint(); getchar(); return 0; }
在上述第四个测试例子bt.splitTree(‘A‘, small, big);中,最终的small树是空的,这正确;但是big树却没有关键字B。这是因为B是集合S‘‘对应的关键字集合的最后一个元素,在A处分裂形成了两棵空树,因此最后一次不能合并,所以big树没有关键字B,这也是正确的。因为,合并操作不能合并一棵空树,否则该关键字将没有孩子,这不符合B树的性质。
其他测试也是正确的,对于其他的度数测试也正确,因此是个不折不扣的B树分裂和链接,不仅仅是2-3-4树。
最后给出上述第一个例子的运行截图: