数据结构:图解手撕B-树

文章目录

  • 为什么需要引入B-树?
  • B树是什么?
  • B树的插入分析

本篇总结的内容是B-树

为什么需要引入B-树?

回忆一下前面的搜索结构,有哈希,红黑树,二分…等很多的搜索结构,而实际上这样的结构对于数据量不是很大的情况是比较适用的,但是假设有一组很大的数据,大到已经不能在内存中存储,此时应该如何处理呢?可以考虑将关键字及其映射的数据的地址放到一个内存中的搜索树的节点,优先考虑去这个地址处访问数据

在这里插入图片描述
在这里插入图片描述
从上面的文段中可以看出,问题出现在文件的IO是有损耗的,因此在使用哈希或是其他的数据结构,在搜索的过程中会不断地进行文件的IO,这样带来的降低效率是不建议出现的,因此解决方案之一就是可以降低树的高度,因此就引出了B-树:多叉平衡树

B树是什么?

1970年,R.BayerE.mccreight提出了一种适合外查找的树,它是一种平衡的多叉树,称为B树(后面有一个B的改进版本B+树,然后有些地方的B树写的的是B-树,一棵m阶(m>2)的B树,是一棵平衡的M路平衡搜索树,可以是空树或者满足一下性质:

  1. 根节点至少有两个孩子
  2. 每个分支节点都包含k-1个关键字和k个孩子,其中 ceil(m/2) ≤ k ≤ m ceil是向上取整函数
  3. 每个叶子节点都包含k-1个关键字,其中 ceil(m/2) ≤ k ≤ m
  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+1n为结点中关键字的个数,满足ceil(m/2)-1≤n≤m-1

上面的规则很复杂,那么下面用图片来解释B树的插入原理,并进行一次模拟

B树的插入分析

假设此时的M = 3,也就是这是一个三叉树,那么从上面的规则来说,每个节点要存储两个数据,还有三个孩子节点,而实际上的过程中会分别多创建一个节点,也就是说会创建三个数据域和四个孩子节点,这样的好处后续在实现代码的过程中会体现出来

假设现在给定了这样的一组数据:53, 139, 75, 49, 145, 36, 101

图解如下:

在这里插入图片描述

那么超过了可以容纳的数据个数,该如何处理呢?看B树的规则是如何处理的:

B树的分裂规则:

当超过了最多能容纳的数据个数后,会进行分裂:

  1. 找到节点数据域的中间位置
  2. 创建一个兄弟节点,把后一半的数据挪动到兄弟节点中
  3. 把中位数的数据移动到父节点中
  4. 对这些节点建立链接

在这里插入图片描述

此时就完成了B树的一次分裂,从中就能看出B树的基本原理,既然是多叉搜索树,那么也要满足搜索树的规则,因此采取这样的分裂规则是可以满足搜索树的要求的

继续进行插入数据,按照上述的原理即可

在这里插入图片描述
当继续插入的时候,就会产生新的分裂问题:

由此得出了最终的生成图

在这里插入图片描述

从这个图中也能看出,确实是符合搜索树的预期的,那么下一步就是要把上面写的这一系列的过程转换成代码来进行实现

#include <iostream>
using namespace std;

// 定义B树节点的信息
template <class K, size_t M>
struct BTreeNode
{
	// 节点内部包含一个数组用来存储数据域信息,和一个指针数组用来保存孩子节点信息,以及双亲的信息
	// 还要包括节点中已经存储的数据的数量
	// 多开辟一个空间,方便于判断是否需要进行分裂
	K _keys[M];
	BTreeNode<K, M>* _subs[M + 1];
	BTreeNode<K, M>* _parent;
	size_t _n;

	BTreeNode()
		:_parent(nullptr)
		, _n(0)
	{
		for (int i = 0; i < M; i++)
		{
			_subs[i] = nullptr;
			_keys[i] = nullptr;
		}
		_subs[M] = nullptr;
	}
};

// 存储B树的信息
template <class K, size_t M>
class BTree
{
	typedef BTreeNode<K, M> Node;
public:
	// 查找函数,找某个确定的Key值,如果找到了返回它所在的节点和所处节点的位置
	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)
				{
					// 说明此时要查找的节点信息不在这一层,一定是要到下面一层去找,因此跳出这一层的循环
					break;
				}
				else if (key > cur->_keys)
				{
					// 说明此时要查找的信息可能在当前查找的节点的后面,还要去后面找找看
					i++;
				}
				else
				{
					// 说明此时找到节点的信息了,直接把节点的信息进行返回即可
					return make_pair(cur, i);
				}
			}
			// 说明此时这一层已经找完了,现在该去下一层进行寻找了
			parent = cur;
			// 由B树的性质得出,subs[i]中存储的信息是比当前信息要小的树,因此去这里寻找目标Key值
			cur = cur->_subs[i];
		}

		// 运行到这里就是这个值在B树中不存在,返回查找最后的层数的节点和错误值即可
		return make_pair(parent, -1);
	}

	// 把元素插入到表中,本质上是一种插入排序
	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++;
	}

	bool Insert(const K& key)
	{
		// 如果_root是一个空树,那么直接创建节点再把值放进去即可
		if (_root == nullptr)
		{
			_root = new Node;
			_root->_keys[0] = key;
			_root->_n++;
			return true;
		}

		// 运行到这里,说明这个树不是一个空树,那么首先要寻找插入的节点在现有的B树中是否存在
		pair<Node*, int> ret = Find(key);
		if (ret.second != -1)
		{
			// 键值对的第二个信息不是-1,说明在B树中找到了要插入的信息,这是不被允许的,因此返回
			return false;
		}

		// 运行到这里,说明要开始向B树中插入新节点了
		// 并且前面的ret还找到了parent,也就是实际应该是插入的位置信息
		Node* parent = ret.first;
		K newKey = key;
		// 创建孩子指针的意义是,后续可能会重复的进行分裂情况,并且插入元素的节点中可能原先有值
		// 直接插入后,会把原来孩子节点的双亲节点发生替换,因此要改变孩子节点的指向
		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;

					// 被拷贝走的元素所在的位置进行重置,表明已经被拷贝走了
					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);

				K midKey = parent->_keys[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 去插入parent->[mid] 和 brother
					newKey = midKey;
					child = brother;
					parent = parent->_parent;
				}
			}
		}
		return true;
	}
private:
	Node* _root = nullptr;
};

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

(C++)vector--迭代器失效问题

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 前言 迭代器的作用就是能够让算法不用关心底层数据结构&#xff0c;其底层实际上就是一个指针&#xff0c;或者对指针做了封装&#xff0c;比如vector和string的迭代器就是原生态指针T*&#xff0c;因此迭代器失效&#xff…

Python爬虫-解决使用requests,Pyppeteer,Selenium遇到网站显示“您的连接不是私密连接”的问题|疑难杂症解决(2)

前言 本文是该专栏的第13篇,后面会持续分享python爬虫案例干货,记得关注。 相信很多同学在处理爬虫项目的时候,会遇到一些网站出现如下图所示的情况: 就是当你不论是使用requests进行协议请求,还是使用自动化框架pyppeteer或者selenium都会出现上图中的情况。这相信会或多…

element组件库的日期选择器如何限制?

本次项目中涉及到根据日期查找出来的数据进行调整,所以修改的数据必须是查找范围内的数据.需要对调整数据的日期进行限制,效果如下: 首先我们使用了element 组件库的日期选择器,其中灌完介绍, picker-options中函数disabledDate可以设置禁用状态,代码如下: <el-date-pickerv…

K8s内容器拓扑图工具

1.背景&#xff1a;随着线上容器越来越多&#xff0c;需要一个可视化的方式展示各个容器之间的拓扑图。 2.需求&#xff1a;轻量级&#xff0c;部署方便。 3.部署 helm repo add groundcover https://helm.groundcover.com/ helm repo update helm install caretta --namespa…

初步认识spring,一问掌握spring应用知识文集。

&#x1f3c6;作者简介&#xff0c;普修罗双战士&#xff0c;一直追求不断学习和成长&#xff0c;在技术的道路上持续探索和实践。 &#x1f3c6;多年互联网行业从业经验&#xff0c;历任核心研发工程师&#xff0c;项目技术负责人。 &#x1f389;欢迎 &#x1f44d;点赞✍评论…

关于“Python”的核心知识点整理大全26

目录 10.3.9 决定报告哪些错误 10.4 存储数据 10.4.1 使用 json.dump()和 json.load() number_writer.py number_reader.py 10.4.2 保存和读取用户生成的数据 对于用户生成的数据&#xff0c;使用json保存它们大有裨益&#xff0c;因为如果不以某种方式进行存储&#xf…

opencv 入门二(播放视频)

环境配置如下&#xff1a; opencv 入门一&#xff08;显示一张图片&#xff09;-CSDN博客 用OpenCV播放视频就像显示图像一样简单。唯一不同的是&#xff0c;我们需要某种循环来读取视频序列中的每一帧。 源码如下&#xff1a; #include <iostream> #include <str…

主馆位置即将售罄“2024北京国际信息通信展会”众多知名企聚京城

2024北京国际信息通信展&#xff0c;将于2024年9月份在北京国家会议中心盛大召开。作为全球信息通信技术领域的重要盛会&#xff0c;此次展会将汇集业内顶尖企业&#xff0c;展示最新的技术成果和产品。 目前&#xff0c;主馆位置即将售罄&#xff0c;华为、浪潮、中国移动、通…

PIC单片机项目(5)——基于PIC16F877A的多功能防盗门

1.功能设计 本次设计的功能如下&#xff1a;如果红外对管检测到有人经过&#xff0c;LCD1602可以显示&#xff0c;我设计的是显示字符串“someone”。 如果有人强行破门&#xff0c;FSR402压力传感器会检测到压力过大&#xff0c;然后触发蜂鸣器报警&#xff0c;LCD1602也显示“…

EMC测试与整改实践?|深圳比创达电子

电磁兼容(EMC)测试和整改是当今社会对电磁兼容(EMC)意识日益深入的表现&#xff0c;EMC测试与整改随着社会对电磁环境要求的不断提高&#xff0c;越来越受到重视&#xff0c;下面就EMC测试与整改实践进行一下详细介绍。 一、什么是EMC测试&#xff1f; EMC测试是指在一定的电…

rabbitmq界面主要参数分析

本篇主要分析rabbitmq broker界面参数 rabbitmq界面主要参数分析 1、connections User Name: user - 连接所使用的用户名。 State: running - 连接当前的状态&#xff0c;这里表明连接是活动的。 SSL/TLS: ○ - 表示这个连接没有使用SSL/TLS加密。 内部或受信任的网络中可能…

C# pictureBox显示一张图片,我想先释放这个图片以免占用无法修改,(旋转)改完再显示这张图片

效果 public static bool RotateFlip(MyDel Log, string fileName){try{string tempPath Path.GetTempFileName();using (Bitmap bmp new Bitmap(fileName)){float resolution 600; //x,y必须为这个数 误差小于-1bmp.RotateFlip(RotateFlipType.Rotate90FlipNone);bmp.Save(…

SQL Server 查询处理过程

查询处理--由 SQL Server 中的关系引擎执行&#xff0c;它获取编写的 T-SQL 语句并将其转换为可以向存储引擎发出请求并检索所需结果的过程。 SQL Server 需要四个步骤来处理查询&#xff1a;分析、代化、优化和执行。 前三个步骤都由关系引擎执行&#xff1b;第三步输出的是…

前端开发新趋势:Web3、区块链和虚拟现实

目录 前言 Web3&#xff1a;下一代互联网 区块链技术 去中心化应用程序&#xff08;DApps&#xff09; 区块链&#xff1a;重塑数字世界 数字钱包 NFT&#xff08;非同质化代币&#xff09; 虚拟现实&#xff1a;沉浸式体验 WebVR和WebXR 三维图形 新挑战与机会 性…

众和策略:短线交易看什么?短线交易看什么指标?

短线交易看什么&#xff1f; 1、k线 当k线出现黄昏十字星、黑乌鸦、乌云盖顶等卖出形状图时&#xff0c;是一种卖出信号&#xff0c;当k线出现早晨十字星、红三兵、等买入形状图时&#xff0c;是一种买入信号。 2、均线 当均线出现死叉、空头摆放时是一种卖出信号&#xff…

中国邮政旋转图像证码识别方案

最近研究了一下中国邮政旋转验证码的识别&#xff0c;居然正确率高达99%。所以可以说基本上的完美解决了这个问题&#xff0c;可以实现自动化验证。最后也给大家准备了识别代码。 1、下载图片 这里的图片一定要下载足够多&#xff0c;品种足够丰富&#xff0c;数据量越大&…

Kafka 安装与部署

目录 Kafka 下载 &#xff08;1&#xff09;将 kafka_2.11-2.4.1.tgz 上传至 /opt/software/ &#xff08;2&#xff09;解压安装包至 /opt/module/ [huweihadoop101 ~]$ cd /opt/software/ [huweihadoop101 software]$ tar -zxvf kafka_2.11-2.4.1.tgz -C ../module/&#…

小型洗衣机哪个牌子好十大排名?口碑好的小型洗衣机分享

现在很多小伙伴每天的工作压力已经非常大了&#xff0c;每天下班就希望可以躺平&#xff0c;但我们的贴身衣物还要每天来手动清洗&#xff0c;这对于上班人来说是一件很痛苦的事情&#xff0c;而现在市面上的内衣洗衣机真的给我们提供太多帮助&#xff0c;今天咱们来聊聊内衣洗…

IDEA中自定义注解支持SEL代码提示, 自定义参数, 函数参数, 返回值

背景 首先 IDEA 默认是不支持 SpEL 的代码提示的 根据网上教程, 我们只能使用java-annotations库, 并添加Language("SpEL")注解 但这样仅仅是能够支持SpEL表达式, 并不支持自定义变量, 也不支持提示方法参数和返回值. 尤其是对写框架和第三方库的人来说, 特别不友…

鸿蒙原生应用/元服务开发-Stage模型能力接口(七)

ohos.app.ability.EnvironmentCallback (EnvironmentCallback)一、说明 EnvironmentCallback模块提供应用上下文ApplicationContext对系统环境变化监听回调的能力&#xff0c;包括onConfigurationUpdated方法。本模块首批接口从API version 9 开始支持。后续版本的新增接口&…