B树的原理与CPP实现

B树是一种自平衡的多叉树数据结构,广泛应用于数据库系统和文件系统中。其设计初衷是为了在存储设备上实现高效的读写操作,特别是在磁盘存储或其他大规模存储场景下。B树的每个节点可以有多个子节点,这与二叉树不同,B树能有效减少树的高度,从而减少数据检索时磁盘的I/O操作次数。

本文将详细介绍B树的性质,以及它的查找、插入和删除操作。

一、B树的基本性质

节点的键值数量范围

每个节点的键值数量是有范围的,假设B树的阶(T),则每个节点中包含的键值数必须在T - 12 * T - 1 之间。

根节点特例

根节点至少包含1个键值(除非整个树为空),其他节点至少包含T - 1个键值。

子树数量限制

每个节点最多拥有 m 个子节点,且子节点的数量与节点的键值数量相关,假设某个节点有 k 个键值,则它会有 k+1 个子节点。

键值有序性

每个节点中的键值按从小到大的顺序排列。假设某节点中的键为 [k1, k2, ..., kn],则:

  • 所有左子树中的键值都小于k1
  • 所有右子树中的键值都大于kn
  • 中间子树中的键值介于相应的键值之间。
树的平衡性

B树是一棵严格平衡的树,所有叶子节点都位于同一层。这意味着树的高度始终保持较小,从而保证了良好的性能。

二、查找

查找过程描述
  • 从根节点出发,找到键值所在的节点。由于节点内的键值是有序的,可以使用二分查找来定位。

  • 递归或下朔

    • 如果目标键值存在于当前节点,则查找成功。
    • 如果目标键值不在当前节点,则根据当前节点的键值范围,选择对应的子节点进行递归查找,直到找到叶子节点。
  • 叶子节点查找

    • 如果在叶子节点中找到了目标键值,则查找成功。

    • 如果叶子节点中没有找到目标键值,则查找失败。

示例

B树可以看做是一个多叉平衡搜索树,那么其查找与二叉平衡搜索树也就是相似的,例如下面的B树:

image-20241016184605791

想要查找21这个值,那么在root节点,可以看到其比7大,所以到第二层第二个节点接着查找,可以看到比17大比24小,所以到第三层第四个节点接着查找,可以看到比18大,等于21,最终找到了这个值。

最终的比较路径就是

image-20241016185423370

三、插入

插入过程描述

在B树的插入操作过程中,必须遵循以下规则来保持B树的平衡性和性质:

  • 从根节点开始,沿着树的路径选择合适的子树,直到找到合适的叶子节点。

  • 元素必须插入到叶子节点中。如果叶子节点未满,直接插入并结束操作。

  • 如果插入时叶子节点已满(即包含m-1个键值,m为B树的阶数),则需要对该叶子节点进行分裂:

    • 将中间键上移到父节点。
    • 将该节点分裂为两个子节点,每个节点大约包含一半的键值。
  • 父节点满时的递归分裂

    • 如果父节点在插入中间键时也满了,继续将中间键上移并分裂父节点。

    • 这种操作可以递归进行,甚至导致根节点的分裂。若根节点分裂,则树的高度增加一层。

这些规则保证了B树始终保持平衡,从而维持其高效的搜索、插入和删除操作。

示例

B树的插入一定是插入到叶子结点上。假设现在有一个度为 T = 2 T=2 T=2 的B树,那么一个B树节点最多只能存在 2 ∗ T − 1 = 3 2*T-1=3 2T1=3 个键,假设我们先从根节点开始插入,依次插入

{1,5,7,4,16,35,24,42,21,17,18}

当节点满了就会产生上溢出,例如先插入1、5、7、4这四个元素值,这里根节点满了,最多只能存放3个键,这里却存放了4个,所以需要分裂

image-20241016183142556

分裂完就如下图所示

image-20241016183406788

此时,根节点和两个叶子结点都满足要求,接着插入16和35,全部会插入到右边的节点上,如下图

image-20241016183519921

又产生了上溢出,和上次的处理手段一样,拆分右边的叶子结点

image-20241016183610260

接着插入24和42

image-20241016183827754

又产生了上溢出,和之前的手段,拆分产生上溢出的节点

image-20241016184344770

接着插入21、17和18

image-20241016184317935

最底层第三个节点产生了上溢出,那么就处理这个节点

image-20241016184436379

但是此时根节点也溢出了,那么就处理根节点

image-20241016184605791

可以看到,通过这种方式,插入时只插入到叶子结点,就可以维持树一直是一个平衡树。

四、删除

B树的删除过程相较于插入要略显复杂一些,主要是如何维护B树的平衡与搜索树的性质。

删除过程描述
  • 从根节点找到要删除的键
    • 首先从根节点开始,沿着树的路径找到要删除的键
    • 如果删除的键在叶子节点中,那么直接删除该键
    • 如果删除的键不在叶子节点中,则需要特殊处理
  • 删除键
    • 删除叶子结点中的键
      • 如果删除后叶子节点的键数仍然不少于 T − 1 T-1 T1 ,那么直接删除即可
      • 如果删除后叶子结点的键数少于 T − 1 T-1 T1,那么需要借用或者合并
    • 删除内部节点中的键
      • 前驱法:用该键的左子树的最大键替代该键,然后删除该最大键(该最大键一定位于叶子节点中,因而转换为删除叶子节点中的键)。
      • 后继法:用该键的右子树的最小键替代该键,然后删除该最小键(同样,最小键在叶子节点中,最终也变成删除叶子节点中的键)。
  • 借用或合并
    • 如果删除后节点的键数少于 T − 1 T-1 T1 ,且该节点有兄弟节点(左兄弟或右兄弟)拥有多于 T − 1 T-1 T1 个键,那么可以从兄弟节点借一个键过来。从父节点中借用一个键下来到当前节点,兄弟节点中的一个键上移到父节点,以保持B树的平衡。
    • 如果兄弟节点没有多余的键可借用,则需要将当前节点与其兄弟节点合并:
      • 将当前节点、兄弟节点以及它们父节点中的分隔键合并成一个节点。
      • 这会导致父节点少一个键,如果父节点的键数少于 T − 1 T-1 T1 ,需要进一步递归进行借用或合并操作,甚至可能导致根节点被删除。
  • 根节点处理
    • 如果根节点被删除,并且它没有任何键值了(即根节点只有一个子节点),则直接删除根节点,树的高度减小一层,新的根节点成为唯一的子节点。
示例

假设我们有下面一个T=3的B树,即5阶B树

image-20241016192244821

我们要依次删除

{45,68,53,30,41,70}

首先删除45,查找45,可以发现是根节点,删除根节点的问题可以转换成删除叶子结点的问题,查找到45的后驱节点,是46,将46设置为根节点,删除45即可,此时叶子结点也满足最少为2的要求,不需要合并或者借用,此时为

image-20241016193054952

再删除68,删除完之后可以发现,这个叶子结点只有一个键了,这是不允许的

image-20241016193306178

但是他的兄弟节点,前面的和后面的都可以借,此时可以借用一个就完成了平衡,这里我们向前借用一个

image-20241016194631638

再删除53,发现又一个需要合并或者借用的

image-20241016194853442

但是此时左右的兄弟节点无法借用,只能合并,这里的合并我们与前驱节点合并

image-20241016195224214

再删除30,删除30的时候首先他不是叶子结点,需要转化为删除叶子节点的问题,我们找到他的后继节点40,替换30的位置再删除40。

image-20241016195310406

此时的41的叶子节点不满足要求,他的前面的兄弟节点和后面的兄弟节点都没法借用,所以可以执行合并操作了,将当前节点、兄弟节点以及它们父节点中的分隔键合并成一个节点,这里我们合并后一个兄弟节点。

image-20241016195441739

此时40的位置又不满足要求了,产生了下溢出,那么再借用一个即可

image-20241016200352627

最后删除70,向右边的兄弟节点借一个即可。

五、代码

#include <iostream>
#include <vector>
using namespace std;// B树节点
class Node {
public:vector<int> keys;    // 节点中的键vector<Node*> children; // 子节点指针bool isLeaf;         // 判断是否为叶节点int t;               // B树的最小度数Node(int _t, bool _isLeaf) {t = _t;isLeaf = _isLeaf;}// 在子树中查找键Node* search(int key) {int i = 0;while (i < keys.size() && key > keys[i])i++;if (i < keys.size() && keys[i] == key)return this;if (isLeaf)return nullptr;return children[i]->search(key);}// 分裂子节点void splitChild(int i, Node* y) {Node* z = new Node(y->t, y->isLeaf);z->keys.resize(t - 1);for (int j = 0; j < t - 1; j++)z->keys[j] = y->keys[j + t];if (!y->isLeaf) {z->children.resize(t);for (int j = 0; j < t; j++)z->children[j] = y->children[j + t];}y->keys.resize(t - 1);children.insert(children.begin() + i + 1, z);keys.insert(keys.begin() + i, y->keys[t - 1]);}// 插入非满节点void insertNonFull(int key) {int i = keys.size() - 1;if (isLeaf) {keys.insert(keys.begin() + (i + 1), key);while (i >= 0 && keys[i] > key) {keys[i + 1] = keys[i];i--;}keys[i + 1] = key;} else {while (i >= 0 && keys[i] > key)i--;if (children[i + 1]->keys.size() == 2 * t - 1) {splitChild(i + 1, children[i + 1]);if (keys[i + 1] < key)i++;}children[i + 1]->insertNonFull(key);}}// 从节点中删除键void remove(int key) {int idx = findKey(key);if (idx < keys.size() && keys[idx] == key) {if (isLeaf)removeFromLeaf(idx);elseremoveFromNonLeaf(idx);} else {if (isLeaf) {cout << "Key " << key << " is not in the tree.\n";return;}bool flag = (idx == keys.size());if (children[idx]->keys.size() < t)fill(idx);if (flag && idx > keys.size())children[idx - 1]->remove(key);elsechildren[idx]->remove(key);}}// 辅助函数int findKey(int key) {int idx = 0;while (idx < keys.size() && keys[idx] < key)++idx;return idx;}void removeFromLeaf(int idx) {keys.erase(keys.begin() + idx);}void removeFromNonLeaf(int idx) {int key = keys[idx];if (children[idx]->keys.size() >= t) {int pred = getPredecessor(idx);keys[idx] = pred;children[idx]->remove(pred);} else if (children[idx + 1]->keys.size() >= t) {int succ = getSuccessor(idx);keys[idx] = succ;children[idx + 1]->remove(succ);} else {merge(idx);children[idx]->remove(key);}}int getPredecessor(int idx) {Node* cur = children[idx];while (!cur->isLeaf)cur = cur->children[cur->keys.size()];return cur->keys[cur->keys.size() - 1];}int getSuccessor(int idx) {Node* cur = children[idx + 1];while (!cur->isLeaf)cur = cur->children[0];return cur->keys[0];}void fill(int idx) {if (idx != 0 && children[idx - 1]->keys.size() >= t)borrowFromPrev(idx);else if (idx != keys.size() && children[idx + 1]->keys.size() >= t)borrowFromNext(idx);else {if (idx != keys.size())merge(idx);elsemerge(idx - 1);}}void borrowFromPrev(int idx) {Node* child = children[idx];Node* sibling = children[idx - 1];child->keys.insert(child->keys.begin(), keys[idx - 1]);if (!child->isLeaf)child->children.insert(child->children.begin(), sibling->children[sibling->keys.size()]);keys[idx - 1] = sibling->keys[sibling->keys.size() - 1];sibling->keys.pop_back();if (!sibling->isLeaf)sibling->children.pop_back();}void borrowFromNext(int idx) {Node* child = children[idx];Node* sibling = children[idx + 1];child->keys.push_back(keys[idx]);if (!child->isLeaf)child->children.push_back(sibling->children[0]);keys[idx] = sibling->keys[0];sibling->keys.erase(sibling->keys.begin());if (!sibling->isLeaf)sibling->children.erase(sibling->children.begin());}void merge(int idx) {Node* child = children[idx];Node* sibling = children[idx + 1];child->keys.push_back(keys[idx]);for (int i = 0; i < sibling->keys.size(); ++i)child->keys.push_back(sibling->keys[i]);if (!child->isLeaf) {for (int i = 0; i <= sibling->keys.size(); ++i)child->children.push_back(sibling->children[i]);}keys.erase(keys.begin() + idx);children.erase(children.begin() + idx + 1);delete sibling;}
};// B树类
class BTree {
private:Node* root;int t;public:BTree(int _t) {root = nullptr;t = _t;}// 查找键Node* search(int key) {return root == nullptr ? nullptr : root->search(key);}// 插入键void insert(int key) {if (root == nullptr) {root = new Node(t, true);root->keys.push_back(key);} else {if (root->keys.size() == 2 * t - 1) {Node* s = new Node(t, false);s->children.push_back(root);s->splitChild(0, root);int i = (s->keys[0] < key) ? 1 : 0;s->children[i]->insertNonFull(key);root = s;} else {root->insertNonFull(key);}}}// 删除键void remove(int key) {if (!root) {cout << "The tree is empty\n";return;}root->remove(key);if (root->keys.size() == 0) {Node* tmp = root;if (root->isLeaf)root = nullptr;elseroot = root->children[0];delete tmp;}}
};// 测试 B 树
int main() {BTree t(3); // B树的度数为3t.insert(10);t.insert(20);t.insert(5);t.insert(6);t.insert(12);t.insert(30);t.insert(7);t.insert(17);cout << "Traversal of tree after insertions:\n";if (t.search(6) != nullptr) cout << "Found 6\n";else cout << "Not Found 6\n";t.remove(6);cout << "Traversal of tree after removing 6:\n";if (t.search(6) != nullptr) cout << "Found 6\n";else cout << "Not Found 6\n";return 0;
}

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

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

相关文章

深入学习二叉树(BinaryTree)(纯小白进)

目录&#xff1a; 一、 前言二、 正文2.1、 树的概念2.1.1、 树的结构2.1.2、 树的小知识 2.2、 认识二叉树2.2.1、 二叉树的概念2.2.2、 特殊的二叉树 2.3、 实现二叉树2.3.1、 结构2.3.2、 节点数2.3.3、 树深度2.3.4、 前、中、后序遍历 销毁2.3.4.1、 前序遍历2.3.4.2、 中…

《数据结构》课程综合设计(zzu校园导航)(迪杰斯特拉算法)

一、系统&#xff08;问题&#xff09;描述 目前根据郑州大学主校区面积区域的广大&#xff0c;以及南、北核心教学楼的教室分布密集且较多&#xff1b;另外&#xff0c;多数地图软件无法精细导航到一个具体的地点&#xff0c;容易造成原地转圈的烦恼。但是&#xff0c;我们转…

js中map,filter,find,foreach的用法介绍

js中map&#xff0c;filter&#xff0c;find&#xff0c;foreach的用法介绍 在 JavaScript 中&#xff0c;数组提供了一些常用的迭代方法&#xff0c;如 map、filter、find 和 forEach&#xff0c;这些方法允许你对数组中的每个元素进行操作&#xff0c;下面是它们的用法和区别…

Python爬虫实战:利用青果代理IP获取跨境电商数据

文章目录 一、跨境电商数据的作用1.1 市场趋势预测与洞察1.2 消费者行为分析1.3 库存管理优化1.4 定价策略制定 二、爬取目标三、环境准备四、代理IP获取4.1 为什么爬虫要用代理IP&#xff1f;4.2 为什么选择青果代理IP&#xff1f;4.3 青果代理IP领取4.4 利用代码获取IP 五、爬…

excel 表格中url转图片

待处理的单元格通过如下公式获取目标格式&#xff1a; "<table><img src"&A4&" height20></table>" 然后下拉后获取多列的单元格转换结果&#xff0c; 然后将这些转换后的结果拷贝到纯文本文档中&#xff0c; 然后再将纯文本…

新基建下的园区智慧化变革 | 科技驱动未来开放式智慧园区

在数智化浪潮席卷之下&#xff0c;千行百业的数字化转型步伐加快。智慧园区建设借助创新的数字化、智能化技术&#xff0c;对园区内的人、机、物、事进行建模和重构&#xff0c;克服传统园区数据割裂、分散管理、无集成、体验差的问题&#xff0c;构建更智慧的管理方式、服务体…

unity学习-雾的渲染

在Light面板下的Other Settings中勾选fog就会让场景中生成雾气 Coloer&#xff1a;颜色 Mode&#xff1a;预设 Density&#xff1a;密度 当Mode调整为Linear模式会多出两个选项 Start&#xff1a;往前从多少米开始 End&#xff1a;到多少米有雾气 Start&#xff1a;设置…

[单master节点k8s部署]41.部署springcloud项目

在之前的文章中我们配置了mysql和harbor&#xff0c;现在我们可以将一个springcloud部署在k8s集群中了。 项目概述 这个springcloud项目将采用maven进行打包部署。首先安装maven&#xff1a; yum install java-1.8.0-openjdk maven-3.0.5* -y 然后将该项目上传到k8s集群的m…

c#编写的各类应用程序

001 课程简介&#xff0c;C# 语言简介&#xff0c;开发环境准备 (yuque.com)https://www.yuque.com/yuejiangliu/dotnet/timothy-csharp-001 一个Solution里包含多个Project 一、见识 C# 编写的各类应用程序 二、类库的引用&#xff08;黑/白盒引用&#xff09; 1、黑盒引用&a…

C++从入门到起飞之——(multi)set与(multi)map的的使用 全方位剖析!

&#x1f308;个人主页&#xff1a;秋风起&#xff0c;再归来~&#x1f525;系列专栏&#xff1a;C从入门到起飞 &#x1f516;克心守己&#xff0c;律己则安 目录 1. 序列式容器和关联式容器 2. set系列的使⽤ 2.1 set和multiset参考⽂档 2.2 set类的介绍 2.3 se…

打印自然常数E

自然常数E 自然常数&#xff0c;符号e&#xff0c;为数学中一个常数&#xff0c;是一个无限不循环小数&#xff0c;且为超越数&#xff0c;其值约为2.718281828459045。它是自然对数函数的底数。 我们打印表达式(11/x)的x次方的值以及获取第一次大于2.718的正整数 新建C#控制…

Linux系统:Ubuntu上安装Chrome浏览器

Ubuntu系统版本&#xff1a;23.04 在Ubuntu系统上安装Google Chrome浏览器&#xff0c;可以通过以下步骤进行&#xff1a; 终端输入以下命令&#xff0c;先更新软件源&#xff1a; sudo apt update 或 sudo apt upgrade终端输入以下命令&#xff0c;下载最新的Google Chrome .…

HarmonyNext保存Base64文件到Download下

本文介绍如何保存Base64的文件到Download下 参考文档地址&#xff1a; 保存用户文件-Harmony Next 用到的是DOWNLOAD模式保存文件 用户在使用save接口时&#xff0c;可以将pickerMode配置为DOWNLOAD模式&#xff0c;该模式下会拉起授权接口&#xff0c;用户确认后会在公共路径…

net core 微信公众号发送模板消息完整实现

第一完整看一下微信官方的文档 链接&#xff1a;开发前必读 / 首页 (qq.com) 想要发送模板消息分为一下几步 第一步&#xff1a;想要发消息需要有这几个参数&#xff0c; openid&#xff0c;这是给谁发消息 access_token&#xff0c;调用接口必要的 appid、secret 这两个是生…

如何替换OCP节点(二):使用 antman脚本 | OceanBase应用实践

前言&#xff1a; OceanBase Cloud Platform&#xff08;简称OCP&#xff09;&#xff0c;是 OceanBase数据库的专属企业级数据库管理平台。 在实际生产环境中&#xff0c;OCP的安装通常是第一步&#xff0c;先搭建OCP平台&#xff0c;进而依赖OCP来创建、管理和监控我们的生…

开放式蓝牙耳机哪个品牌好用?开放式耳机排行榜测评!

开放式耳机&#xff0c;因其特殊的不入耳佩戴模式&#xff0c;让使用者在享受音乐或者进行通话的过程中&#xff0c;依然可以对外界声音保持敏感。在户外运动场景下&#xff0c;这种特性优势尽显&#xff0c;既保证了耳机佩戴的稳定和舒适&#xff0c;又提高了运动的安全性。为…

如何做好项目管理,实现高效协作?

项目管理难&#xff0c;难于上青天&#xff01; 小卫&#xff0c;某服装生产企业项目经理&#xff0c;说到项目难管理&#xff0c;他有话要说&#xff1a; 做项目&#xff0c;看似简单&#xff0c;不同部门各司其职&#xff0c;但也正因为涉及跨部门协作&#xff0c;管理难度也…

Qt:图片文字转base64程序

目录 一.Base64 1.编码原理 2.应用场景 3.优点 4.限制 5.变种 二.文字与Base64互转 1.ui设计 2.文字转Base64 3.Base64转文字 三.图片与Base64互转 1.ui设计 2.选择图片与图片路径 3.图片转Base64 4.Base64转图片 四.清空设置 五.效果 六.代码 base64conver…

基于SpringBoot+Vue+uniapp的时间管理小程序的详细设计和实现(源码+lw+部署文档+讲解等)

详细视频演示 请联系我获取更详细的演示视频 项目运行截图 技术框架 后端采用SpringBoot框架 Spring Boot 是一个用于快速开发基于 Spring 框架的应用程序的开源框架。它采用约定大于配置的理念&#xff0c;提供了一套默认的配置&#xff0c;让开发者可以更专注于业务逻辑而不…

大厂面试一上来就手撕 Transformer,心凉半截

在这两年&#xff0c;尤其是大模型问世之后&#xff0c;有关 Transformer 的面试题不仅数量众多&#xff0c;而且颇具新意。 今日&#xff0c;我将分享 18 道 Transformer 高频面试题&#xff08;如需获取更多专业面试题&#xff0c;扫描文末二维码即可&#xff09;&#xff0…