数据结构:图解手撕B-树以及B树的优化和索引

文章目录

  • 为什么需要引入B-树?
  • B树是什么?
  • B树的插入分析
  • B+树和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;
};

B+树和B*树

B+树

B+树是B树的变形,是在B树基础上优化的多路平衡搜索树,B+树的规则跟B树基本类似,但是又在B树的基础上做了以下几点改进优化:

  1. 分支节点的子树指针与关键字个数相同
  2. 分支节点的子树指针p[i]指向关键字值大小在[k[i],k[i+1])区间之间
  3. 所有叶子节点增加一个链接指针链接在一起
  4. 所有关键字及其映射数据都在叶子节点出现

在这里插入图片描述
B+树的特性:

  1. 所有关键字都出现在叶子节点的链表中,且链表中的节点都是有序的
  2. 不可能在分支节点中命中
  3. 分支节点相当于是叶子节点的索引,叶子节点才是存储数据的数据层

B*树

B*树是B+树的变形,在B+树的非根和非叶子节点再增加指向兄弟节点的指针

在这里插入图片描述

分裂原理

B+树的分裂:
当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针

B*树的分裂:
当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针

所以,B*树分配新结点的概率比B+树要低,空间使用率更高

B树:有序数组+平衡多叉树
B+树:有序数组链表+平衡多叉树
B*树:一棵更丰满的,空间利用率更高的B+树

B树的应用

B树的最常见应用就是当做索引

MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的数据结构,简单来说:索引就是数据结构
当数据量很大时,为了能够方便管理数据,提高数据查询的效率,一般都会选择将数据保存到数据库,因此数据库不仅仅是帮助用户管理数据,而且数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用数据,这样就可以在这些数据结构上实现高级查找算法,该数据结构就是索引

mysql是目前非常流行的开源关系型数据库,不仅是免费的,可靠性高,速度也比较快,而且拥
有灵活的插件式存储引擎

在这里插入图片描述

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

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

相关文章

Maven仓库上传jar和mvn命令汇总

目录 导入远程仓库 命令结构 命令解释 项目pom 输入执行 本地仓库导入 命令格式 命令解释 Maven命令汇总 mvn 参数 mvn常用命令 web项目相关命令 导入远程仓库 命令结构 mvn deploy:deploy-file -Dfilejar包完整名称 -DgroupIdpom文件中引用的groupId名 -Dartifa…

使用 Node.js 删除文件 - 完整步骤教程

引言 在 Node.js 中处理文件尤其是移除文件&#xff0c;对于维护高效应用程序至关重要。储存和秩序当道的今天&#xff0c;删除不必要或冗余的文件能力显得尤为关键。本文深入探讨你会想要使用这个强大功能的时刻和原因&#xff0c;并通过各种案例展示了这个概念&#xff0c;同…

JavaScript基础(数组+正则表达+字符串)

目录 1.数组 1.1创建数组 1.2字面量创建数组 1.3length函数 1.4遍历数组1 1.5遍历数组2语法糖 1.6增删改查 1push 2pop 3unshift("x",x) 4shift() 5数组的截取 slice() splice() 6concat 7reverse 2.内置对象 2.1data 2.2Math对象 2.3字符串 1c…

2023年12月20日历史上的今天大事件早读

1722年12月20日 康熙皇帝驾崩 1803年12月20日 法国正式将新奥尔良移交给美国 1860年12月20日 南卡罗来纳州宣布脱离美国 1917年12月20日 全俄肃反委员会成立 1928年12月20日 国民党与英国签订中英关税条约 1939年12月20日 德“施佩伯爵”号遭英舰围困自沉 1945年12月20日…

Poi实现复杂Excel导出,理解POI操作Excel思路!!!

前言 对于简单excel报表导出&#xff0c;有很多简单的工具如easypoi&#xff0c;而且现在网上已经有很多工具类整合easypoi使用起来非常方便。但是简单的弊端往往无法适配一些负责场景&#xff0c;而我们实际生产中面临的都是客户自定以的一个负责报表导出&#xff0c;这是利用…

搭建 ElasticSearch 集群环境

安装基础环境 我们用虚拟机创建出3台机器&#xff0c;查看centos版本为7.9 [roots1 ~]# cat /etc/centos-release CentOS Linux release 7.9.2009 (AltArch)下载相关命令 yum -y install vim* yum -y install net-tools yum -y install lsof yum -y install wget yum -y ins…

对GPU进行压力测试

GPU压力测试工具安装指导&#xff08;RHEL8.2&#xff09; - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/443165016 1、下载gpu-burn工具 下载地址&#xff1a;https://github.com/wilicc/gpu-burn 2、上传到系统后安装 # unzip gpu-burn-master.zip # cd gpu-burn-mas…

关于MQ,你了解多少?(干货分享之二)

导语 本文梳理笔者 MQ 知识&#xff0c;从消息中间件的基础知识讲起&#xff0c;在有了基础知识后&#xff0c;对市面上各主流的消息中间件进行详细的解析&#xff0c;包括 RabbitMQ、RocketMQ、Kafka、Pulsar&#xff0c;最后再横向对比这几款主流的消息中间件。本篇是系列文…

Maven——如何快速生成bean的get、set方法,Lombok依赖引入和使用!!!

Lombok依赖引入和使用 1、项目pom.xml文件引入如下依赖&#xff1a;2、引入之后还要在idea中安装对应lombok插件:file–>plugins–>搜索框搜索lombok安装3、重启之后&#xff0c;便可以在实体类bean中使用提供注解快速生成对应的方法了 总结 本文介绍如何快速生成实体bea…

基于YOLOv5的姿态估计

一、基于YOLOV5的姿态估计与实现 相关论文&#xff1a; YOLO-Pose: Enhancing YOLO for Multi Person Pose Estimation Using Object Keypoint Similarity Loss 相关源码 edgeai-yolov5-yolo-pose 二、数据集 The dataset needs to be prepared in YOLO format so that the…

Sanic:Python中的高性能异步Web框架详解

概要 在众多Python Web框架中&#xff0c;Sanic以其高性能和易用性脱颖而出。它是一个异步Web框架&#xff0c;允许使用Python 3.6的新异步/等待语法编写代码&#xff0c;使得创建快速的HTTP响应成为可能。本文将深入探讨Sanic框架的核心特性、基本用法、路由管理、中间件处理…

【MATLAB第83期】基于MATLAB的LSTM代理模型的SOBOL全局敏感性运用

【MATLAB第83期】基于MATLAB的LSTM代理模型的SOBOL全局敏感性运用 引言 在前面几期&#xff0c;介绍了敏感性分析法&#xff0c;本期来介绍lstm作为代理模型的sobol全局敏感性分析模型。 【MATLAB第31期】基于MATLAB的降维/全局敏感性分析/特征排序/数据处理回归问题MATLAB代…

软件测试面试题及答案(史上最全)

以下是软件测试相关的面试题及答案&#xff0c;欢迎大家参考! 1、你的测试职业发展是什么? 测试经验越多&#xff0c;测试能力越高。所以我的职业发展是需要时间积累的&#xff0c;一步步向着高级测试工程师奔去。而且我也有初步的职业规划&#xff0c;前3年积累测试经验&…

界面控件DevExpress WPF Dock组件,轻松创建类Visual Studio窗口界面!

本文主要为大家介绍DevExpress WPF控件中的Dock组件&#xff0c;它能帮助用户轻松创还能受Microsoft Visual Studio启发的Dock窗口界面。 P.S&#xff1a;DevExpress WPF拥有120个控件和库&#xff0c;将帮助您交付满足甚至超出企业需求的高性能业务应用程序。通过DevExpress …

使用Python将OSS文件免费下载到本地:第三步 提供一个从ECS中下载和删除文件的接口

大家好&#xff0c;我是水滴~~ 本文将介绍了使用的知识点、以及利用 Flask 框架提供从 ECS 中下载和删除的文件接口代码、并对该代码进行详细解析、最后给出部署方案&#xff0c;希望能对你有所帮助&#xff01; 《Python入门核心技术》专栏总目录・点这里 文章目录 1. 本文知…

【Unity】运行时创建曲线(贝塞尔的运用)

[Unity]运行时创建线&#xff08;贝塞尔的运用&#xff09; 1. 实现的目标 在运行状态下创建一条可以使用贝塞尔方法实时编辑的网格曲线。 2. 原理介绍 2.1 曲线的创建 unity建立网格曲线可以参考Unity程序化网格体的实现方法。主要分为顶点&#xff0c;三角面&#xff0c…

msal auzer 强制刷新获取令牌

背景&#xff1a;msal auzer token 过期时间微软默认事60至90分钟&#xff0c;普遍取中间值&#xff0c;现渗透测试部分&#xff08;Qtester&#xff09;要求30分token 过期。且不可使用msal的安全机制。 解决方案&#xff1a;‘ 后端&#xff0c;解析token 获取发证时间 ia…

【C++入门到精通】 原子性操作库(atomic) C++11 [ C++入门 ]

阅读导航 引言一、原子性操作库简介二、原子变量1. 原子类型2. 原子类型函数3. 使用示例 三、总结温馨提示 引言 当谈及并发编程时&#xff0c;确保数据的安全性和一致性是至关重要的。在C11中引入的原子性操作库&#xff08;atomic&#xff09;为我们提供了一种有效且可靠的方…

我的创作纪念日——成为创作者第1024天

机缘 一、前言 早上收到CSDN的推送信息&#xff0c;今天是我成为创作者的第1024天&#xff0c;回想起自己已经好久没有写博客了&#xff0c;突然间很有感触&#xff0c;想水一篇文章&#xff0c;跟小伙伴们分享一下我的经历。 二、自我介绍 我出生在广东潮汕地区的一个小城…