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

题目

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‘];

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

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





时间: 2024-11-09 15:11:02

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

算法导论 第六章 思考题 6-1 用插入的方法建堆

BUILD-MAX-HEAP'(A) heap-size[A]<-1 for i <- 2 to length[A] do MAX-HEAP-INSERT(A, A[i]) 如上,题目给出一种使用插入的办法建堆的算法,而书中6.4节给出的建堆算法如下: BUILD-MAX-HEAP(A) heap-size[A] <-- length[A] for i <-- length[A] / 2 downto 1 do MAX-HEAPIFY[A, i] 可以发现元素调整的方向恰好反过来了

算法导论 第六章 思考题 6-3 d叉堆

d叉堆的实现相对于二叉堆变化不大,首先看它如何用数组表示. 考虑一个索引从1开始的数组,一个结点i最多可以有d个子结点,编号从id - (d - 2) 到 id + 1. 从而可以知道一个结点i的父结点计算方法为: (i + d - 2) / d. 第二个问题是 一个含有n个元素的d叉堆的高度,就是一个简单的等比数列的问题,可以知道的是一颗高度为h的满d叉树所含的结点数目为(d^(h +1) - 1) / (d - 1) 从而一颗含有 n个结点的d叉树满足的条件为: ,从而得到高度h为: 接下来

算法导论 第六章 思考题6-3 Young氏矩阵

这题利用二叉堆维持堆性质的办法来维持Young氏矩阵的性质,题目提示中写得很清楚,不过确实容易转不过弯来. a,b两问很简单.直接看c小问: 按照Young氏矩阵的性质,最小值肯定在左上角取得,问题在于取出最小值后如何保持矩阵的性质.可以参照max_heapify中的做法,先取出最小值,然后将矩阵左上角置为最大值,这样左上角处的元素必然导致Young氏矩阵的性质违背,于是考虑该元素右边的元素和该元素下边的元素,问题是该与右边元素交换还是与下边元素交换呢?可以发现,如果与T(右)和T(下)中较小的

算法导论 第13章 红黑树

二叉查找树的基本操作包括搜索.插入.删除.取最大和最小值等都能够在O(h)时间复杂度内实现,因此能在期望时间O(lgn)下实现,但是二叉查找树的平衡性在这些操作中并没有得到维护,因此其高度可能会变得很高,当其高度较高时,而二叉查找树的性能就未必比链表好了,所以二叉查找树的集合操作是期望时间O(lgn),最坏情况下为O(n). 红黑树也是一种二叉查找树,它拥有二叉查找树的性质,同时红黑树还有其它一些特殊性质,这使得红黑树的动态集合基本操作在最坏情况下也为O(lgn),红黑树通过给节点增加颜色和其它

算法导论 第6章 堆排序

堆数据结构实际上是一种数组对象,是以数组的形式存储的,但是它可以被视为一颗完全二叉树,因此又叫二叉堆.堆分为以下两种类型: 大顶堆:父结点的值不小于其子结点的值,堆顶元素最大 小顶堆:父结点的值不大于其子结点的值,堆顶元素最小 堆排序的时间复杂度跟合并排序一样,都是O(nlgn),但是合并排序不是原地排序(原地排序:在排序过程中,只有常数个元素是保存在数组以外的空间),合并排序的所有元素都被拷贝到另外的数组空间中去,而堆排序是一个原地排序算法. 1.在堆排序中,我们通常使用大顶堆来实现,由于堆在

算法导论 第6章 堆排序(简单选择排序、堆排序)

堆数据结构实际上是一种数组对象,是以数组的形式存储的,可是它能够被视为一颗全然二叉树,因此又叫二叉堆.堆分为下面两种类型: 大顶堆:父结点的值不小于其子结点的值,堆顶元素最大 小顶堆:父结点的值不大于其子结点的值,堆顶元素最小 堆排序的时间复杂度跟合并排序一样,都是O(nlgn),可是合并排序不是原地排序(原地排序:在排序过程中,仅仅有常数个元素是保存在数组以外的空间),合并排序的全部元素都被复制到另外的数组空间中去,而堆排序是一个原地排序算法. 1.在堆排序中,我们通常使用大顶堆来实现,因为堆

算法导论 第8章 线性时间排序

合并排序和堆排序的时间复杂度为O(nlgn),插入排序和冒泡排序的时间复杂度为O(n^2),快速排序的时间复杂度在平均情况下是O(nlgn),这些排序算法都是通过对元素进行相互比较从而确定顺序的,因此都叫比较排序. 比较排序可以看做是决策树(一个满二叉树),因为每一次比较都是一个分支.n个元素的序列,其排序的结果有 n! 种可能(n个元素的全排),所以这个决策树有 n! 个叶子结点,假设树的高度为h,则有:n! <= 2^h,所以h >= lg(n!) = Ω(nlgn).一次比较排序就是从决

算法导论 第7章 高速排序

高速排序在最坏情况下的时间复杂度为O(n^2),尽管在最坏情况下执行时间比較差,可是高速排序一般是用于排序的最佳选择.由于其平均性能相当好,期望的执行时间为O(nlgn),且在O(nlgn)的记号中隐含的常数因子非常小. 高速排序和合并排序有相似之处,都是须要划分序列,在合并排序中.划分的过程非常easy.直接选择元素序列的中间位划分位置,排序是在合并的过程中实现的,所以合并排序的合并过程非常重要.相比合并排序,高速排序就没有合并的过程.仅仅有划分,高速排序的划分过程非常重要,排序是在划分的过程

算法导论第7章___快速排序

快速排序本质上是插入排序,但是它在这个基础上增强了算法. 下面我们来分析一下快速排序: 有了前面的分析基础,我们在来看排序算法也就容易多了. public class Quick_Sort { private void quick_Sort(int []A,int left,int right){ if(left<right){ //划区比较,这个partition 第一次!得到的就是我们刚才说的2. int partition=partition(A, left, right); //实现第一

算法导论 第9章 中位数和顺序统计学

/* * 算法导论 第九章 中位数和顺序统计学 * 线性时间选择元素 */ #include <iostream> #include <ctime> using namespace std; int minimum(int *arr, int len); int randomizedSelect(int *arr, int p, int r, int i); int randomizedPartition(int *arr, int p, int r); void exchange