【高阶数据结构】B-树详解

news2024/11/15 23:22:25

文章目录

  • 1. 常见的搜索结构
  • 2. 问题提出
    • 使用平衡二叉树搜索树的缺陷
    • 使用哈希表的缺陷
  • 3. B-树的概念
  • 4. B-树的插入分析
    • 插入过程分析
    • 插入过程总结
  • 5. B-树的代码实现
    • 5.1 B-树的结点设计
    • 5.2 B-树的查找
    • 5.3 B-树的插入实现
      • InsertKey
      • 插入和分裂
      • 测试
  • 6. B-树的删除(思想)
  • 7. B-树的高度
    • 最小高度
    • 最大高度
  • 8. B-树的性能
  • 9. B-树的简单验证(中序遍历)

1970年,R.Bayer和E.mccreight提出了一种适合外查找的树,它是一种平衡的多叉树,称为B树。

那么在此之前,我们也已经学过很多的搜索结构了,我们来一起回顾一下:

1. 常见的搜索结构

在这里插入图片描述
以上结构适合用于数据量相对不是很大,能够一次性存放在内存中,进行数据查找的场景(内查找)。

2. 问题提出

如果数据量很大,比如有100G数据,无法一次放进内存中,那就只能放在磁盘上了,如果放在磁盘上,有时需要搜索某些数据,那么该如何处理呢?

我们可以考虑将关键字及其映射的数据的地址放到一个内存中的搜索树的节点中,当通过搜索树找到要访问数据的关键字时,取这个关键字对应的地址去磁盘访问数据。
在这里插入图片描述

但是呢,实际中我们去查找的这个key可能不都是整型:

可能是字符串比如身份证号码,那这时我们还把所有的key和对应数据的地址都存到内存,也可能是存不下的。

那这时候可以做一个改动:

我不再存储key了,只存储地址
在这里插入图片描述
那这样的话我如何判断找到了呢?
那就需要拿着当前的地址去访问磁盘进行判断。
比如现在要找key为77的这个数据,那从根结点开始,首先访问根结点中的地址对应磁盘的数据,是34,那77大于34,所以往右子树找,右子树0x77对应的是89(有一次访问磁盘),77比89小,再去左子树找,左子树地址0x56访问磁盘对应的是77找到了。

那这样做的问题是什么呢?

最坏的情况下我们要进行高度次的查找,那就意味着要进行高度次的磁盘IO。
如果我们使用红黑树或者AVL树的话,就是O( l o g 2 N log_2 N log2N)次。
那如果是在内存中的话,这个查找次数还是很快的,但是现在数据量比较大是在磁盘上存的,而磁盘的速度是很慢的。

所以:

使用平衡二叉树搜索树的缺陷

平衡二叉树搜索树的高度是logN,这个查找次数在内存中是很快的。但是当数据都在磁盘中时,访问磁盘速度很慢,在数据量很大时,logN次的磁盘访问,是一个难以接受的结果。

那如果用哈希表呢?

使用哈希表的缺陷

哈希表的效率很高是O(1),但是一些极端场景下某个位置哈希冲突很严重,导致访问次数剧增,也是难以接受的。

那如何加速对数据的访问呢?

1. 提高IO的速度(SSD相比传统机械硬盘快了不少,但是还是没有得到本质性的提升)
2. 降低树的高度——多叉树平衡树

那我们今天要学的B-树其实就是多叉平衡搜索树

3. B-树的概念

1970年,R.Bayer和E.mccreight提出了一种适合外查找的树,它是一种平衡的多叉树并且是绝对平衡,称为B树(后面有一个B树的改进版本B+树,然后有些地方的B树写的的是B-树,注意不要误读成"B减树")。

一棵m阶(m>2)的B树(B树中所有结点的孩子个数的最大值称为B树的阶),是一棵M路的平衡搜索树,可以是空树或者满足一下性质的树:

在这里插入图片描述
1. 树中每个结点至多有m棵子树,即至多含有m-1个关键字。
2. 若根结点不是终端结点,则至少有两棵子树。
3. 除根结点外的所有非叶子结点都包含k-1个关键字和k个孩子(终端结点孩子都是NULL),其中 ceil(m/2) ≤ k ≤ m (ceil是向上取整函数)
4. 所有的叶结点都出现在同一层次上,并且不带信息(可以视为外部结点或类似于折半查找判定树的查找失败结点,实际上这些结点不存在,指向这些结点的指针为空)
5. 每个节点中的关键字从小到大(也可以从大到小)排列,节点当中k-1个元素正好是k个孩子包含的元素的值域划分
6. 每个结点的结构为:(n,A0,K1,A1,K2,A2,… ,Kn,An)其中,Ki(1≤i≤n)为关键字,且Ki<Ki+1(1≤i≤n-1)。Ai(0≤i≤n)为指向子树根结点的指针。且Ai所指子树所有结点中的关键字均小于Ki+1,Ai+1所指子树所有结点中的关键字均大于Ki+1。(结点中关键字升序的情况下)
n为结点中关键字的个数,满足ceil(m/2)-1≤n≤m-1。

大家可以对照上面的图先来自行理解一下B树的这些性质,等后面我们熟悉了B树的结构之后大家可以再来反复理解这几条性质为什么是这样。
在这里插入图片描述

4. B-树的插入分析

那下面我们就来学习一下B-树的插入是怎样的。

那为了方便讲解,也方便大家理解,我们这里选取B-树的阶数取小一点,给一个3:

即三阶B-树(三叉平衡树),那每个结点最多存储两个关键字,两个关键字可以将区间分割成三个部分,因此节点应该有三个孩子(子树)
那每个结点的结构就应该是这样的在这里插入图片描述

但是呢,为了后续实现起来简单,节点的结构如下:

在这里插入图片描述
关键字和孩子我们都多给一个空间(后面大家就能体会到为什么要多给一个)

插入过程分析

那下面我们就来找一组数据分析一下插入的过程,用序列{53, 139, 75, 49, 145, 36, 101}构建B树的过程如下:

1. 插入53
在这里插入图片描述
满足B-树的性质,不用动
2. 插入139(关键字我们升序排列)
在这里插入图片描述
也不用做任何处理
3. 插入75
在这里插入图片描述
75插入之后是这样,但是因为我们多开了一个空间,3阶的话每个结点最多3-1=2个关键字。
所以现在这个结点关键字个数超了。
那此时怎么办呢?
要进行一个操作——分裂
怎么分裂呢?
在这里插入图片描述
1. 找到关键字序列的中间数,将关键字序列分成两半
2. 新建一个兄弟结点出来,将右半边的m/2个关键字分给兄弟结点
3. 将中间值提给父亲结点,新建结点成为其右孩子(没有父亲就创建新的根)
为什么中位数做父亲?——满足搜索树的大小关系(左<根<右)
4. 结点指针链接起来
在这里插入图片描述
那通过这里大家来体会一下上面的规则中为什么要求除根结点外的所有非叶子结点都包含k-1个关键字(ceil(m/2) ≤ k ≤ m,即k的最小值是ceil(m/2)),即最少包含ceil(m/2)-1个关键字
如果m是奇数比如9,那ceil(m/2)是5个,5-1是4,而9个的话分裂之后正好两边每个结点都是4个关键字,中间的一个提取给父亲。
如果是偶数比如10的话,ceil(m/2)是5,5-1是4,而10个分裂的话,肯定不平均,一边4个(最少的),一边5个,还有一个中间值要提取给父亲。
所以它们最少就是ceil(m/2)-1个关键字。

那我们再来插入几个看看:

还是我们上面给的那组数据,再往后插入49,145
在这里插入图片描述
在这里插入图片描述
接着再往后,36
在这里插入图片描述
那此时36插入的这个结点又满了,然后就要进行分裂。
大家现在体会,为什么我们要多开一个空间?这样的话我们就可以在插入之后关键字顺序已经调整好的情况下去分裂,就方便很多。
那然后我们来看这里的分裂怎么做?
在这里插入图片描述
新增一个兄弟结点之后,相当于它们的父亲结点就多了一个孩子,所以也需要增加一个关键字(关键值始终比孩子少一个),就把中间值提给父亲结点。
49上提插入到父亲,它比75小,所以45往后移(它的孩子也跟着往后移),然后49插入到前面。

再往下插入101:

在这里插入图片描述
那插入之后这个结点的关键字数量大于m-1了,进行分裂
分裂的步骤还是和上面一样
在这里插入图片描述
但是此时分裂之后我们发现父亲满了,所以需要继续向上分裂
在这里插入图片描述
那这就是一个完整的插入过程。
并且我们会发现B-树每一次插入之后他都是天然的完全平衡,不需要想红黑树AVL树那样,插入之后不满足平衡条件了,再去调整。
并且B-树的平衡是绝对平衡。每一棵树的左右子树高度之差都是0。
为什么他能保持天然的完全平衡呢?
通过上面的插入过程我们很容易发现B-树是向右和向上生成的,只会产生新的兄弟和父亲。

插入过程总结

  1. 如果树为空,直接插入新节点中,该节点为树的根节点
  2. 树非空,找待插入关键字在树中的插入位置(注意:找到的插入节点位置一定在终端节点中)
  3. 检测是否找到插入位置(假设树中的key唯一,即该元素已经存在时则不插入)
  4. 按照插入排序的思想将该关键字插入到找到的结点中
  5. 检测该节点关键字数量是否满足B-树的性质:即该节点中的元素个数是否等于M,如果小于则满足,插入结束
  6. 如果插入后节点不满足B树的性质,需要对该节点进行分裂:
    申请新的兄弟节点
    找到该节点的中间位置
    将该节点中间位置右侧的元素以及其孩子搬移到新节点中
    将中间位置元素(新建结点成为其右孩子)提取至父亲结点中插入,从步骤4重复上述操作

5. B-树的代码实现

那下面我们就来写写代码

5.1 B-树的结点设计

那首先我们来定义一下B-树的结点:

我们这里还是搞成模板,简单一点,我们就不实现成KV模型了,就搞个K,当然在搞个非类型模板参数M控制B树的阶
template<class K, size_t M>
然后结点的话,我们上面分析过一棵3阶的B-树
在这里插入图片描述
为了方便插入之后分裂,我们要多开一个空间:正常每个结点最多M-1个关键字,M个孩子;那增加一个就是M个关键字,M+1个孩子。
那我们如何定义这样一个结构呢?
在这里插入图片描述
那这就是两个数组嘛。
然后还需要一个父亲指针,指向父结点;还需要再给一个成员变量记录当前结点实际存储关键字的个数
然后我们也可以写一个默认构造
在这里插入图片描述
那结点我们就定义好了

5.2 B-树的查找

那我们先来实现一个find,因为后面插入也要先find嘛:

这里我们实现一个不允许键值冗余的版本,如果存在就不再插入了,如果不存在我们让find直接给我们返回找到的那个要插入位置的结点,便于我们在Insert函数中直接将值插入到该结点中。

画个图我们来分析一下:

在这里插入图片描述
就比如这个图,我们现在要查找53。
那首先和根结点的关键字进行比较,当前根结点只有一个值75,53小于75,所以去他的左子树查找。
那我们来分析一下一个关键字和它的左右孩子之间的关系:
在这里插入图片描述
其实很容易看出来,在b-树中
一个关键字的左孩子在_child数组中的下标和该关键字在_keys数组中的下标是一样的,而右孩子的话,比关键字的下标大1
在这里插入图片描述
所以就走到它的左子树49这个结点,也只有一个关键字,53大于49,所以再去关键字49的右子树(如果49后面还有关键字的话,就继续跟后面的比)
在这里插入图片描述
那此时就走到50,53这个结点。
首先跟第一个关键字50比,比50大,那就继续往后比,后面是53,相等,找到了
那如果查找52呢(不存在)?
前面是一样的,走到这个结点,比50大,往后比,比53小,所以去53的左子树,53的左子树为空,所以找不到了。
在这里插入图片描述

find就写好了:

pair<Node*, int> Find(const K& key)
{
	Node* parent = nullptr;
	//从根结点开始找
	Node* cur = _root;

	while (cur)
	{
		// 在一个结点中查找
		size_t i = 0;
		while (i < cur->_n)
		{
			if (key < cur->_keys[i])
			{
				break;
			}
			else if (key > cur->_keys[i])
			{
				++i;
			}
			else
			{
				return make_pair(cur, i);
			}
		}

		// 往孩子结点去跳
		parent = cur;
		cur = cur->_subs[i];
	}

	//没找到,返回parent
	return make_pair(parent, -1);
}

5.3 B-树的插入实现

接下来我们来写一下插入:
在这里插入图片描述

首先如果是第一次插入的话我们需要做一个单独处理:

判断root==nullptr,为空的话就是第一次插入

if (_root == nullptr)
{
	_root = new Node;
	_root->_keys[0] = key;
	_root->_n++;

	return true;
}

那再往下呢就是已经有结点的情况下插入:

那首先判断一下如果key已经存在,就不再插入
在这里插入图片描述

如果不存在,那就去插入(find顺便带回了要插入的那个目标位置的结点)
那我们接收一下find的返回值,在这个结点里面插入即可

InsertKey

那插入的时候需要保证结点里面关键字的顺序,可以用插入排序的思想把新的关键字插进去(如果是分裂之后向父亲插入的话,它可能还有孩子),那我们这里再单独封装一个InsertKey的函数:

在这里插入图片描述
代码就是这样的,插入排序如果大家遗忘了可以看之前的文章,还有不理解的地方建议大家看着我们上面分析插入的图走一遍代码

void InsertKey(Node* node, const K& key, Node* child)
{
	int end = node->_n - 1;
	while (end >= 0)
	{
		if (key < node->_keys[end])
		{
			// 挪动key和他的右孩子
			node->_keys[end + 1] = node->_keys[end];
			node->_subs[end + 2] = node->_subs[end + 1];
			--end;
		}
		else
		{
			break;
		}
	}

	node->_keys[end + 1] = key;
	node->_subs[end + 2] = child;
	if (child)
	{
		child->_parent = node;
	}

	node->_n++;
}

插入和分裂

然后我们就可以调用InsertKey接口去插入关键字,但是插入的话:

按理说是插入一次,但是如果插入之后存储关键字的数组满了(我们多开了一个空间,满的话就不满足B-树的性质——至多含有m-1个关键字了),就需要进行分裂
分裂的话,就又需要往parent去插入(插入中间值),当然分裂的兄弟结点也要成为parent的孩子(孩子和关键字都增加1,依然符合规则)
然后插入之后满了还需要再往上分裂,所以应该写成一个循环

最终完整的Insert函数:

在这里插入图片描述
这里还是比较麻烦的,不过注释写的比较清晰了,我就不过多解释了。
建议大家对照着我们上面画的图去理解。

bool Insert(const K& key)
{
	if (_root == nullptr)
	{
		_root = new Node;
		_root->_keys[0] = key;
		_root->_n++;

		return true;
	}

	// key已经存在,不再插入
	pair<Node*, int> ret = Find(key);
	if (ret.second != -1)
	{
		return false;
	}

	// 如果不存在,find顺便带回了要插入的那个目标位置的结点

	//因为后面可能需要分裂继续往父结点插入,
	//所以这里我们接收find的返回值直接命名为parent
	Node* parent = ret.first;

	//参数中的key,const修饰不能修改,定义一个newkey
	K newKey = key;

	//分裂的兄弟也要作为孩子插入到父结点,所以再定义一个child
	//当然第一次插入关键字的时候孩子是空
	Node* child = nullptr;

	//有可能多次分裂往上一直插入,所以需要写成循环
	while (1)
	{
		InsertKey(parent, newKey, child);
		// 没有满,插入就结束
		if (parent->_n < M)
		{
			return true;
		}
		else// 满了就要分裂
		{
			size_t mid = M / 2;
			// 分裂一半[mid+1, M-1]给兄弟
			Node* brother = new Node;
			size_t j = 0;
			size_t i = mid + 1;
			for (; i <= M - 1; ++i)
			{
				// 拷贝key和key的左孩子给兄弟结点
				brother->_keys[j] = parent->_keys[i];
				brother->_subs[j] = parent->_subs[i];
				//如果孩子不为空,链接父亲指针
				if (parent->_subs[i])
				{
					parent->_subs[i]->_parent = brother;
				}
				++j;

				// 拷走的key清除重置一下方便调式观察
				parent->_keys[i] = K();
				parent->_subs[i] = nullptr;
			}

			// 还有最后一个右孩子也拷过去
			brother->_subs[j] = parent->_subs[i];
			if (parent->_subs[i])
			{
				parent->_subs[i]->_parent = brother;
			}
			parent->_subs[i] = nullptr;

			//重新设置它们的有效关键字数量
			brother->_n = j;
			parent->_n -= (brother->_n + 1);//+这个1是因为还提走了中位数

			K midKey = parent->_keys[mid];
			//清除重置提走的mid中位数
			parent->_keys[mid] = K();


			if (parent->_parent == nullptr)// 说明分裂是根节点
			{
				//那要创建新的根
				_root = new Node;
				_root->_keys[0] = midKey;
				//上面分裂的结点及分裂出的兄弟成为新的根的孩子
				_root->_subs[0] = parent;
				_root->_subs[1] = brother;
				_root->_n = 1;

				//将原结点和分裂的兄弟链接到新的根上
				parent->_parent = _root;
				brother->_parent = _root;

				break;
			}
			else// 分裂的不是根,转换成往parent->parent 去插入parent->[mid] 和 brother
			{
				newKey = midKey;
				child = brother;
				parent = parent->_parent;
			}
		}
	}

	return true;
}

测试

那下面我们就来构建一棵树来测试一下:

就拿我们上面画图分析对应的那棵树:
在这里插入图片描述
我们通过监视窗口来观察一下
在这里插入图片描述
对比一下,是没什么问题的。

6. B-树的删除(思想)

学习B树的插入足够帮助我们理解B树的特性了,那至于删除呢我们这里可以给大家讲一讲思路,代码的话我们就不实现了,删除的代码也要比插入更加复杂,大家有兴趣的话呢可以参考《算法导论》-- 伪代码和《数据结构-殷人昆》–C++实现代码。

那下面我们来讲一下删除的思想:

同样也需要分情况讨论:

  1. 删除的关键字在非终端结点
    处理方法是:
    用其直接前驱或直接后继替代其位置,转化为对“终端结点”的删除
    直接前驱:当前关键字左边指针所指子树中“最右下”的元素
    直接后继:当前关键字右边指针所指子树中“最左下”的元素
    比如:
    在这里插入图片描述
    现在要删除75
    首先第一种方法可以用直接前驱55替代其位置
    在这里插入图片描述
    或者用直接后继101替代
    在这里插入图片描述
    所以对非终端结点关键字的删除操作,必然可以转化为对终端结点的删除

所以下面我们重点来讨论终端结点的删除

  1. 删除的关键字在终端结点且删除后结点关键字个数未低于下限
    若删除后结点关键字个数未低于下限ceil(m/2)-1,直接删除,无需做任何其它处理
    比如:
    在这里插入图片描述
    现在要删除36,所在的结点是终端结点,且删除之后,关键字的个数不少于ceil(3/2)-1=1,所以直接删除即可
    在这里插入图片描述

那如果删除之后关键字的个数低于下限ceil(m/2)-1呢?

  1. 若删除的关键字在终端结点且删除后结点关键字个数低于下限ceil(m/2)-1
    这时候的处理思路是这样的:
    删除之后关键字数量低于下限,那就去“借”结点,跟父亲借,父亲再去跟兄弟借
    如果不能借(即借完之后父亲或兄弟关键字个数也不满足了),那就按情况进行合并(可能要合并多次)

    最终使得树重新满足B-树的性质。
    比如:
    在这里插入图片描述
    现在要删40,那40删掉的话这个结点关键字个数就不满足性质了,那就去跟父亲借,49借下来,那这样父亲不满足了,父亲再向兄弟借(要删除的那个关键字所在结点的兄弟结点),53搞上去
    变成这样
    在这里插入图片描述
    此时就又符合是一棵B-树了
    那如果不能借的情况呢?
    比如:
    在这里插入图片描述
    现在要删除160
    160如果跟父亲借的话,150下来,那父亲不满足了,因为3个孩子,必须是2个关键字。而且此时兄弟145所在的这个结点也不能借了。因为此时它只有一个关键字,父亲借走一个的话,就不满足了。
    所以此时借结点就不行了,就需要合并了。
    如何合并呢?
    如果结点不够借,则需要将父结点内的关键字与兄弟进行合并。合并后导致父节点关键字数量-1,可能需要继续合并。
    那我们先来看这个
    在这里插入图片描述
    这个情况我们分析了不够借,所以要合并。大家看,160删掉的话,父亲就少了一个孩子,那关键字也应该减少一个,所以可以把父结点的150与145这个孩子合并
    在这里插入图片描述
    这样就可以了。
    当然还有些情况可能需要多次合并:
    比如:
    在这里插入图片描述
    现在要删145,怎么办呢?
    肯定是不够借的,所以要合并,确保合并之后依然满足B-树的规则就行了。
    大家看这个可以怎么合并:
    145干掉之后,左子树这里就不满足了,可以先将139跟102合并。
    在这里插入图片描述
    但是此时不平衡了(B-树是绝对平衡的)。
    那就要继续合并缩减高度:
    很容易看出来,我们可以将101和53合并作为根,这个正好两个关键字,3个孩子
    在这里插入图片描述
    就可以了

7. B-树的高度

问:含n个关键字的m阶B树,最小高度、最大高度是
多少?(注:大部分地方算B树的高度不包括叶子结点即查找失败结点)

最小高度

首先我们来分析一下最小高度:

n个关键字的m阶B树,关键字个数和B-树的阶数已经确定的话,那要让高度最小,我们是不是要让每个结点存的关键字是最满的啊。
那对于m阶的B树来说,每个结点最多m-1个关键字,m个孩子
第一层肯定只有一个根结点(最满的话是m-1个关键字,m个孩子),那第二层最多就有m个结点,每个结点最多m-1关键字,那第三层就是m*m个孩子嘛,以此类推…
那我们假设这个高度是h的话,关键字的总个数n就等于(关键字个数*结点个数):
(m-1)*(1+m+m^2+m^3+…+m^h-1)
即有:
n=(m-1)*(1+m+m^2+m^3+…+m^h-1)
解得最小高度h= l o g m ( n + 1 ) log_m(n+1) logm(n+1)

最大高度

那最大高度呢:

那要让树变得尽可能高的话,那就要让每个结点得关键字数量尽可能少(分支尽可能少)。
第一层只有一个根结点(关键字最少是1,孩子是2),根结点最少两个孩子/分支,所以第二层2个结点。
又因为除了根结点之外的结点最少有ceil(m/2)个孩子,所以第三层就最少有2*ceil(m/2)个结点,第四层就是2*ceil(m/2)^2,以此类推…
第h就是2*ceil(m/2)^h-2个结点。
那么叶子结点(查找失败结点)的个数就是2*ceil(m/2)^h-1
那这里我们不再像上面那样求出总的关键字个数去算,怎么算呢?
这里补充一个结论:n个关键字的B-树必然有n+1个叶子节点
所以我们得出:
n+1=2*ceil(m/2)^h-1
那么解得最大高度h=[ l o g c e i l ( m / 2 ) ( n + 1 ) / 2 log_{ceil(m/2)}(n+1)/2 logceil(m/2)(n+1)/2] +1

当然也可以算出关键字的总个数来求解:

上面我们已经知道每层的结点个数,然后我们知道根结点最少一个关键字,其它结点最少k-1个关键字,k最小是ceil(m/2)
那么第一层就是1个关键字,第二层往后就是该层的节点个数*每个结点的最小关键字个数(k-1)
在这里插入图片描述
那么因此就有n=1+2(kh-1-1)
同样解得最大高度:
h=[ l o g c e i l ( m / 2 ) ( n + 1 ) / 2 log_{ceil(m/2)}(n+1)/2 logceil(m/2)(n+1)/2] +1

8. B-树的性能

B-树的效率是很高的,对于N = 62*1000000000个节点,如果度M为1024。
查找的坏最坏就是高度次嘛,h=[ l o g c e i l ( M / 2 ) ( N + 1 ) / 2 log_{ceil(M/2)}(N+1)/2 logceil(M/2)(N+1)/2] +1≈ l o g m / 2 N log_{m/2}N logm/2N
l o g m / 2 N log_{m/2}N logm/2N <= 4,即在620亿个元素中,如果这棵树的度为1024,则需要小于4次即可定位到该节点,然后利用二分查找可以快速定位到该元素,大大减少了读取磁盘的次数。

9. B-树的简单验证(中序遍历)

那B-树呢也是搜索树,同样满足左子树<根<右子树,那我们可以对它进行一个验证,看中序遍历是否能得到一个有序序列。

那下面我们就来实现一下B-树的中序遍历:

我们还是来搞一个图对照着分析一下思路:
在这里插入图片描述
就拿这个来分析:
对于我们之前学的二叉树来说中序遍历的思想是:左子树、根、右子树
那B-树的话它可能是一个多叉的,那它的中序遍历应该怎么走呢?
首先肯定还是先访问左子树,搜索树中最左的结点一定是最小的
在这里插入图片描述
当然如果算上空结点的话最左的应该是空,左子树,然后依然是根,就是36,36就是最小的,没问题。
左子树、根,那然后呢?
是36的右子树吗?可以认为是36的右子树,但是我们要把它当作40的左孩子看。
36这个关键字访问完,就走到后面的40,对于40,同样是先左子树,再根
在这里插入图片描述
那这个第二个访问到的元素就是40,此时当前结点所有的关键字访问完了,最后再去访问最后一个关键字的右子树:
在这里插入图片描述
此时整个结点才被访问完。
那此时就相当于是49的左子树访问完了,然后访问根49,后面就是一样的处理…
在这里插入图片描述
在这里插入图片描述
🆗,大家看这样就可以了
所以B-树的中序遍历是怎么样的呢?
左子树、根;(下一个关键字的)左子树、根;(再下一个)左子树、根;…(一直往后直至走完最后一个关键字);右子树(最后一个关键字的右子树)
左 根 左 根 … 右

那理清了思路,我来实现一下代码:

在这里插入图片描述
注释写的比较清晰,就不多解释了

// 左 根  左 根  ...  右
void _InOrder(Node* root)
{
	if (root == nullptr)
		return;

	size_t i = 0;
	//依次访问当前结点中每个关键字的左子树和根
	for (; i < root->_n; i++)
	{
		_InOrder(root->_subs[i]);//先访问左子树
		cout << root->_keys[i] << " ";//再访问根
	}

	_InOrder(root->_subs[i]);//再访问最后一个关键字的右子树
}

void InOrder()
{
	_InOrder(_root);
}

那我们来验证一下呗,中序遍历一下我们上面插入之后的那个B-树:

在这里插入图片描述
没有问题!

那对于B-树的讲解我们就先到这里…
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1438557.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

机器人工具箱学习(一)

一、机器人工具箱介绍 机器人工具箱是由来自昆士兰科技大学的教授Peter Corke开发的&#xff0c;被广泛用于机器人进行仿真&#xff08;主要是串联机器人&#xff09;。该工具箱支持机器人一些基本算法的功能&#xff0c;例如三维坐标中的方向表示&#xff0c;运动学、动力学模…

软件定义网络 SDN 简介、OpenFlow

目录 软件定义网络 SDN 简介 1 SDN 与 协议 OpenFlow 1.1 SDN 1.2 OpenFlow 1.2.1 协议 OpenFlow 1.2.2 OpenFlow 数据层面 &#xff08;1&#xff09;匹配 动作 &#xff08;2&#xff09;流表 1.流表由远程控制器管理 2.流表结构 2 SDN 体系结构 3 SDN 控制器 软…

Matplotlib雷达图教程:学会绘制炫酷多彩的多维数据可视化【第53篇—python:Seaborn大全】

文章目录 Matplotlib雷达图绘制指南&#xff1a;炫酷雷达图参数解析与实战1. 普通雷达图2. 堆叠雷达图3. 多个雷达图4. 矩阵雷达图5. 极坐标雷达图6. 定制化雷达图外观7. 调整雷达图坐标轴范围8. 雷达图的子图布局9. 导出雷达图总结 Matplotlib雷达图绘制指南&#xff1a;炫酷雷…

D7 Elasticsearch-Mongodb(搜索记录)

我是南城余&#xff01;阿里云开发者平台专家博士证书获得者&#xff01; 欢迎关注我的博客&#xff01;一同成长&#xff01; 一名从事运维开发的worker&#xff0c;记录分享学习。 专注于AI&#xff0c;运维开发&#xff0c;windows Linux 系统领域的分享&#xff01; 知…

Vue3 之 Pinia

什么是Pinia Pinia是一个Vue的专属的最新状态管理库&#xff0c;是vuex状态管理工具的替代品 Pinia的优势 1.提供更加简单的API&#xff08;去掉了 mutation&#xff09; 2.提供符合组合式风格的API&#xff08;和vue3语法统一&#xff09; 3.去掉了modules的概念&#xff0…

推荐一款开源的跨平台划词翻译和OCR翻译软件:Pot

Pot简介 一款开源的跨平台划词翻译和OCR翻译软件 下载安装指南 根据你的机器型号下载对应版本&#xff0c;下载完成后双击安装即可。 使用教程 Pot具体功能如下&#xff1a; 划词翻译输入翻译外部调用鼠标选中需要翻译的文本&#xff0c;按下设置的划词翻译快捷键即可按下输…

已解决:tpm2_createpriimay: command not found

出现错误如下&#xff1a; ERROR: Could not change hierarchy for Owner. TPM Error:0x9a2 ERROR: Could not change hierarchy for Endorsement. TPM Error:0x9a2 ERROR: Could not change hierarchy for Lockout. TPM Error:0x98e ERROR: Unable to run tpm2_takeownership…

【经典项目】Java实现打地鼠小游戏(附源码)

一、游戏回顾 打地鼠游戏是一款简单而有趣的反应游戏。游戏中&#xff0c;你需要在地洞中出现的地鼠出现时迅速点击它们&#xff0c;以获得分数。以下是一般的打地鼠游戏玩法介绍&#xff1a; 准备阶段&#xff1a;游戏开始时&#xff0c;你会看到一个由多个地洞组成的游戏界面…

百面嵌入式专栏(面试题)C语言面试题22道

沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇我们将介绍C语言相关面试题 。 宏定义是在编译的哪个阶段被处理的?答案:宏定义是在编译预处理阶段被处理的。 解读:编译预处理:头文件包含、宏替换、条件编译、去除注释、添加行号。 写一个“标准”宏MIN,这个…

C#,栅栏油漆算法(Painting Fence Algorithm)的源代码

1 刷油漆问题 给定一个有n根柱子和k种颜色的围栏&#xff0c;找出油漆围栏的方法&#xff0c;使最多两个相邻的柱子具有相同的颜色。因为答案可以是大的&#xff0c;所以返回10^97的模。 计算结果&#xff1a; 2 栅栏油漆算法的源程序 using System; namespace Legalsoft.Tr…

机器学习 | 揭示EM算法和马尔可夫链的实际应用

目录 初识EM算法 马尔可夫链 HMM模型基础 HMM模型使用 初识EM算法 EM算法是一种求解含有隐变量的概率模型参数的迭代算法。该算法通过交替进行两个步骤&#xff1a;E步骤和M步骤&#xff0c;从而不断逼近模型的最优参数值。EM算法也称期望最大化算法&#xff0c;它是一个基…

C++进阶(十二)lambda可变参数包装器

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 文章目录 一、新的类功能1、默认成员函数2、类成员变量初始化3、 强制生成默认函数的关键字default:4、…

拿捏循环链表

目录&#xff1a; 一&#xff1a;单链表&#xff08;不带头单向不循环&#xff09;与循环链表&#xff08;带头双向循环&#xff09;区别 二&#xff1a;循环链表初始化 三&#xff1a;循环链表头插 四&#xff1a;循环链表尾插 五&#xff1a;循环链表头删 六&#xff1…

【leetcode】深搜、暴搜、回溯、剪枝(C++)1

深搜、暴搜、回溯、剪枝&#xff08;C&#xff09;1 一、全排列1、题目描述2、代码3、解析 二、子集1、题目描述2、代码3、解析 三、找出所有子集的异或总和再求和1、题目描述2、代码3、解析 四、全排列II1、题目解析2、代码3、解析 五、电话号码的字母组合1、题目描述2、代码3…

华为 Huawei 交换机 黑洞MAC地址的作用和配置示例

黑洞mac作用&#xff1a;某交换机上配置某个PC的mac地址为黑洞mac&#xff0c;那么这台PC发出来的包都会被交换机丢弃&#xff0c;不会被转发到网络中。 组网需求&#xff1a; 如 图 2-13 所示&#xff0c;交换机 Switch 收到一个非法用户的访问&#xff0c;非法用户的 MAC 地址…

Java实现民宿预定管理系统 JAVA+Vue+SpringBoot+MySQL

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 用例设计2.2 功能设计2.2.1 租客角色2.2.2 房主角色2.2.3 系统管理员角色 三、系统展示四、核心代码4.1 查询民宿4.2 新增民宿4.3 新增民宿评价4.4 查询留言4.5 新增民宿订单 五、免责说明 一、摘要 1.1 项目介绍 基于…

幻兽帕鲁官方服务器不稳定怎么办?5秒钟自建幻兽帕鲁服务器

幻兽帕鲁太火了&#xff0c;官方palworld服务器不稳定&#xff1f;不如自建服务器&#xff0c;基于腾讯云幻兽帕鲁服务器成本32元全自动部署幻兽帕鲁服务器&#xff0c;超简单有手就行&#xff0c;全程自动化一键部署10秒钟即可搞定&#xff0c;无需玩家手动部署幻兽帕鲁游戏程…

时光峰峦文物璀璨,预防性保护筑安全

在璀璨的历史长河中&#xff0c;珍贵文物如同时间的印记&#xff0c;承载着过往的辉煌。《人文山水时光峰峦——多彩贵州历史文化展》便是这样一场文化的盛宴&#xff0c;汇聚了众多首次露面的宝藏。然而&#xff0c;文物的保存对环境要求极为苛刻&#xff0c;温湿度波动都可能…

十六、Vben框架table内部合并行

在vben项目中合并内部的行是一个常规的操作,以前我们说过如果是一条数据内部只需要分割拿高撑开就可以实现,在第三章的时候我们已经讲过了,那么如果是不定的条数合并为一条数据呢,怎么能够实现呢,下面我们就来讲讲。 先看效果图 如图,能看到是三条数据,其实是…

用keytool 生成JWT的RSA非对称密钥

写在前面 JWT 令牌 可以由 X.509 证书或 256 位非对称密钥签名来充当&#xff0c;为了获得合法的JWT 令牌&#xff0c;我们可以使用JDK中的keytool.exe工具来生成。 本例的操作环境是Windows系统&#xff0c;操作的前置条件需要先安装好JDK&#xff0c;并配置好环境变量&…