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

文章目录

  • 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];}//没找到,返回parentreturn 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的返回值直接命名为parentNode* parent = ret.first;//参数中的key,const修饰不能修改,定义一个newkeyK 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.mzph.cn/news/673029.shtml

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

相关文章

使用WPS制作三线表

点击边框和底纹点击1、2、3、4并且应用于表格点击确定 再次选中表格点击右键表格属性选择边框和底纹 选中表格第一行右键点击表格属性选择边框和底纹 如果表格中存在虚线

【Linux开发工具】gcc/g++的使用

&#x1f4d9; 作者简介 &#xff1a;RO-BERRY &#x1f4d7; 学习方向&#xff1a;致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f4d2; 日后方向 : 偏向于CPP开发以及大数据方向&#xff0c;欢迎各位关注&#xff0c;谢谢各位的支持 目录 1.前言2.gcc/g使用方…

【自动化测试】---Selenium+Java

1.自动化测试分类 接口自动化测试UI自动化测试&#xff08;移动端自动化测试、Web端自动化测试&#xff09; 2.选择Selenium作为web自动化工具原因&#xff08;面试题&#xff09; 开源免费支持多个浏览器支持多个系统支持多语言Selenium包提供很多供测试使用的API 3.自动化是什…

C++内存模型的内存布局

C内存模型的内存布局 什么是内存模型内存布局及作用C程序的内存布局 本文章介绍了C程序的内存布局&#xff0c;并附有一段演示 数据区和 栈区存储不同类型变量的代码示例。 什么是内存模型 在计算机科学中&#xff0c;程序的内存模型是指程序在内存中的组织结构和存储方式的抽…

编译原理实验2——自上而下语法分析LL1(包含去消除左递归、消除回溯)

文章目录 实验目的实现流程代码运行结果测试1&#xff08;含公共因子&#xff09;测试2&#xff08;经典的ii*i文法&#xff0c;且含左递归&#xff09;测试3&#xff08;识别部分标识符&#xff09; 总结 实验目的 实现自上而下分析的LL1语法分析器&#xff0c;给出分析过程 …

[C#] 如何使用ScottPlot.WPF在WPF桌面程序中绘制图表

什么是ScottPlot.WPF&#xff1f; ScottPlot.WPF 是一个开源的数据可视化库&#xff0c;用于在 WPF 应用程序中创建高品质的绘图和图表。它是基于 ScottPlot 库的 WPF 版本&#xff0c;提供了简单易用的 API&#xff0c;使开发人员能够通过简单的代码创建各种类型的图表&#…

2、6作业

TCP和UDP的区别 TCP和UDP都是通信协议 TCP提供有连接的&#xff0c;稳定的&#xff0c;无误码无失真无乱序无丢失的通信 UDP提供无连接的&#xff0c;尽力的&#xff0c;可能误码可能乱序&#xff0c;可能丢失的通信 TCP每发一个数据包就需要对方回应一个应答包&#xff0c…

自定义npm包从vue2升级到vue3遇到的问题解决

1.执行npm run build时报错&#xff1a; (node:16724) UnhandledPromiseRejectionWarning: SyntaxError: Unexpected token ‘??’ at Loader.moduleStrategy (internal/modules/esm/translators.js:149:18 解决&#xff1a;更新node版本 查看了我使用的node版本是14.21.3&…

《合成孔径雷达成像算法与实现》Figure6.10

clc clear close all参数设置 距离向参数设置 R_eta_c 20e3; % 景中心斜距 Tr 2.5e-6; % 发射脉冲时宽 Kr 20e12; % 距离向调频率 alpha_os_r 1.2; % 距离过采样率 Nrg 320; % 距离线采样数 距离向…

UUID和雪花(Snowflake)算法该如何选择?

UUID和雪花(Snowflake)算法该如何选择&#xff1f; UUID 和 Snowflake 都可以生成唯一标识&#xff0c;在分布式系统中可以说是必备利器&#xff0c;那么我们该如何对不同的场景进行不同算法的选择呢&#xff0c;UUID 简单无序十分适合生成 requestID&#xff0c; Snowflake 里…

Flink实战六_直播礼物统计

接上文&#xff1a;Flink实战五_状态机制 1、需求背景 现在网络直播平台非常火爆&#xff0c;在斗鱼这样的网络直播间&#xff0c;经常可以看到这样的总榜排名&#xff0c;体现了主播的人气值。 人气值计算规则&#xff1a;用户发送1条弹幕互动&#xff0c;赠送1个荧光棒免费…

07-使用Package、Crates、Modules管理项目

上一篇&#xff1a;06-枚举和模式匹配 当你编写大型程序时&#xff0c;组织代码将变得越来越重要。通过对相关功能进行分组并将具有不同功能的代码分开&#xff0c;您可以明确在哪里可以找到实现特定功能的代码&#xff0c;以及在哪里可以改变功能的工作方式。 到目前为止&…

必收藏!第六版CCF推荐会议C类国际学术会议!(中国计算机学会)

中国计算机学会 中国计算机学会&#xff08;CCF&#xff09;是全国性、学术性、非营利的学术团体&#xff0c;由从事计算机及相关科学技术领域的个人和单位自愿组成。作为独立社团法人&#xff0c;CCF是中国科学技术协会的成员之一&#xff0c;是全国一级学会&#xff01; CCF的…

JavaScript基础第四天

JavaScript 基础第四天 今天我们学习js的函数&#xff0c;包括普通函数、匿名函数、箭头函数以及函数作用域。 1. 函数的初体验 1.1. 什么是函数 函数是 JavaScript 中的基本组件之一。一个函数是 JavaScript 过程一组执行任务或计算值的语句。要使用一个函数&#xff0c;你…

Linux下库函数、静态库与动态库

库函数 什么是库 库是二进制文件, 是源代码文件的另一种表现形式, 是加了密的源代码; 是一些功能相近或者是相似的函数的集合体. 使用库有什么好处 提高代码的可重用性, 而且还可以提高程序的健壮性;可以减少开发者的代码开发量, 缩短开发周期. 库制作完成后, 如何给用户…

大模型为什么会有 tokens 限制?

人是以字数来计算文本长度&#xff0c;大语言模型 &#xff08;LLM&#xff09;是以 token 数来计算长度的。LLM 使用 token 把一个句子分解成若干部分。 token 可以是一个单词、一个单词中的一个部分、甚至是一个字符&#xff0c;具体取决于它使用的标记化方法 (tokenization…

为电子表格嵌入数据库,Excel/WPS一键升级为管理系统

将Excel表格转化为管理系统&#xff0c;这款工具能够实现只需导入表格数据&#xff0c;即可自动生成相应的软件和APP。 表格办公的烦恼&#xff0c;有遇到吧&#xff1f; 对于具有一定规模的企业而言&#xff0c;各类表格如同繁星般众多&#xff0c;既有日常使用的常规表格&a…

泰克示波器——TBS2000系列界面整体介绍

目录 1.1 通道区域面板标识1.2 示波器测试输出&#xff08;检测探针与设置的好坏&#xff09;1.3 面板其他快捷按钮1.4 波器整体界面 1.1 通道区域面板标识 在通道面板的下方标识有示波器的通道属性以及参数值&#xff0c;如我使用的型号为“TBS2104X”的示波器&#xff0c;面…

格子表单GRID-FORM | 文档网站搭建(VitePress)与部署(Github Pages)

格子表单/GRID-FORM已在Github 开源&#xff0c;如能帮到您麻烦给个星&#x1f91d; GRID-FORM 系列文章 基于 VUE3 可视化低代码表单设计器嵌套表单与自定义脚本交互文档网站搭建&#xff08;VitePress&#xff09;与部署&#xff08;Github Pages&#xff09; 效果预览 格…

如何使用VMware分享出来的虚拟机系统(OVF文件)

前言 这几天看到很多小伙伴都在安装虚拟机&#xff0c;但成不成就不知道了。 所以小白准备把自己安装完成的系统打包分享给小伙伴。 如果你需要已经安装完成的虚拟系统&#xff0c;可以获取哦&#xff01;打开即用&#xff01; 虚拟机系统包括&#xff1a; Win10 专业版 Wi…