算法导论 第18章 思考题18-2 2-3-4树的链接与分裂,推广至B树

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zilingxiyue/article/details/44983919

题目

    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']类似。

   为便于讨论,先抛出相关函数名称。

  1.   nodeAtRightOfHeight(size_t h):在树T中找到高度为h的最右侧节点的父亲;
  2.   nodeAtLeftOfHeight(size_t h):在树T中找到高度为h的最左侧节点的父亲;
  3.   linkAtRight(k,T'):在树T的右侧连接T'和k;
  4.   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']或height[Ti'] + 1;

    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树。


最后给出上述第一个例子的运行截图:






阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页