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

news2024/9/25 15:28:05

文章目录

  • 为什么需要引入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是目前非常流行的开源关系型数据库,不仅是免费的,可靠性高,速度也比较快,而且拥
有灵活的插件式存储引擎

在这里插入图片描述

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

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

相关文章

超结MOS/低压MOS在5G基站电源上的应用-REASUNOS瑞森半导体

一、前言 5G基站是5G网络的核心设备&#xff0c;实现有线通信网络与无线终端之间的无线信号传输&#xff0c;5G基站主要分为宏基站和小基站。5G基站由于通信设备功耗大&#xff0c;采用由电源插座、交直流配电、防雷器、整流模块和监控模块组成的电气柜。所以顾名思义&#xf…

谈思生物医疗直播|“靶向双硫死亡在肿瘤治疗中的应用”

细胞死亡是维持生物发育和内部环境稳态的生理过程。靶向细胞死亡相关通路杀死癌细胞是癌症治疗的一大方向。今年年初&#xff0c;有研究团队发现和鉴定了一种全新的细胞死亡类型——双硫死亡(Disulfidptosis)&#xff0c;为癌治疗开辟了新的可能性。 溶质载体家族成员 SLC7A11…

Linux网络编程(二):Socket 编程

参考引用 黑马程序员-Linux 网络编程 1. 套接字概念 Socket 本身有 “插座” 的意思&#xff0c;在 Linux 环境下&#xff0c;用于表示进程间网络通信的特殊文件类型 本质为内核借助缓冲区形成的伪文件 既然是文件&#xff0c;那么可以使用文件描述符引用套接字 与管道类似&am…

CGAL中流线的二维放置

本章介绍CGAL 2D流线放置包。定义一节给出了基本定义和概念。基本概念一节对整合过程进行了描述。最远点播种策略一节简要介绍了该算法。“实现”一节介绍了包的实现&#xff0c;“示例”一节详细介绍了两个示例放置。 该算法的核心思想是对域中最大空腔中心的流线进行积分&am…

HuggingFace下载模型

目录 方式一&#xff1a;网页下载 方式二&#xff1a;Git下载 方式一&#xff1a;网页下载 方式二&#xff1a;Git下载 有些模型的使用方法页面会写git clone的地址&#xff0c;有些没写&#xff0c;直接复制网页地址即可 网页地址&#xff1a; ​https://huggingface.co/…

12.19_黑马数据结构与算法笔记Java

目录 203 排序算法 选择排序 204 排序算法 堆排序 205 排序算法 插入排序 206 排序算法 希尔排序 207 排序算法 归并排序 自顶至下 208 排序算法 归并排序 自下至上 209 排序算法 归并加插入 210 排序算法 单边快排 211 排序算法 双边快排 212 排序算法 快排 随机基准…

技术博客:市面上加密混淆软件的比较和推荐

引言 市面上有许多加密混淆软件可供开发者使用&#xff0c;但哪些软件是最好用的&#xff1f;哪些软件受到开发者的喜爱&#xff1f;本文将根据一次在CSDN上的投票结果&#xff0c;为大家介绍几款在程序员中普及度较高的加密软件。以下是投票结果&#xff0c;希望能对大家的选…

【jvm从入门到实战】(十) 实战篇-内存调优

内存溢出和内存泄漏&#xff1a;在Java中如果不再使用一个对象&#xff0c;但是该对象依然在GC ROOT的引用链上&#xff0c;这个对象就不会被垃圾回收器回收&#xff0c;这种情况就称之为内存泄漏。内存泄漏绝大多数情况都是由堆内存泄漏引起的。少量的内存泄漏可以容忍&#x…

MySQL5.x与8.0

大致区别 1. 性能&#xff1a;MySQL 8.0 的速度要比 MySQL 5.7 快 2 倍 MySQL 8.0 在以下方面带来了更好的性能&#xff1a;读/写工作负载、IO 密集型工作负载、以及高竞争&#xff08;"hot spot"热点竞争问题&#xff09;工作负载2. NoSQL&#xff1a;MySQL 从 5.7 …

CPU算力分配 - 华为OD统一考试

OD统一考试 题解: Java / Python / C++ 题目描述 现有两组服务器A和B,每组有多个算力不同的CPU,其中 A 是A组第个CPU的运算能力,是 B组 第个CPU的运算能力。一组服务器的总算力是各CPU的算力之和。 为了让两组服务器的算力相等,允许从每组各选出一个CPU进行一次交换。 求…

基于PHP的蛋糕购物商城系统

有需要请加文章底部Q哦 可远程调试 基于PHP的蛋糕购物商城系统 一 介绍 此蛋糕购物商城基于原生PHP开发&#xff0c;数据库mysql&#xff0c;前端bootstrap。系统角色分为用户和管理员。 技术栈&#xff1a;phpmysqlbootstrapphpstudyvscode 二 功能 用户 1 注册/登录/注销…

做一个wiki页面是体验HTML语义的好方法

HTML语义&#xff1a;如何运用语义类标签来呈现Wiki网页 在上一篇文章中&#xff0c;我花了大量的篇幅和你解释了正确使用语义类标签的好处和一些场景。那么&#xff0c;哪些场景适合用到语义类标签呢&#xff0c;又如何运用语义类标签呢&#xff1f; 不知道你还记不记得在大…

爱芯派pro通过无线网卡rtl8188eu连接热点

爱芯派pro通过无线网卡rtl8188eu连接热点 爱芯派pro目前的底板的pcie的复位有问题&#xff0c;所以pcie接口无法挂载上去&#xff0c;所以自己购买的rtl8822网卡也用不了&#xff0c;然后想起来自己还有正点原子的rtl8188eu网卡&#xff0c;但是没有和工作人员进行摸索后才知道…

Swagger升级指南:Swagger2与Swagger3注解差异揭秘

在API开发的世界里&#xff0c;Swagger已经成为了一个不可或缺的工具&#xff0c;它让API的文档化和前后端的协作变得前所未有地简单。随着Swagger的进化&#xff0c;我们迎来了Swagger3&#xff0c;也被称为OpenAPI Specification 3.0。本篇博客将带大家深入了解Swagger2和Swa…

【Python 基础】-- 在 mac OS 中安装 多个 python 版本

目录 1、需求 2、实现 2.1 安装 pyenv 2.2 安装 pyenv-virtualenv 2.3 配置环境变量 2.4 创建 python 3.9.9 的环境 2.5 激活环境&#xff0c;在当前项目目录中使用&#xff0c;即执行 python 1、需求 由于项目所依赖的 python 版本有多个&#xff0c;需要在不同的 pyth…

在线客服系统中的全渠道服务:多渠道整合与无缝沟通体验

很多采购人员在了解在线客服系统的时候都会遇到一个名词——全渠道。很多人第一次接触可能并不理解它是什么意思&#xff0c;也不知道自己的企业是否需要这个”全渠道“。今天这篇文章就为大家解答一二。 一、全渠道是什么&#xff1f; 全渠道 (Omni-Channel)&#xff0c;就是…

DeepLabV3+实现sar影像海面溢油区识别

今天我们分享DeepLabV3的sai影像水体提取。 数据集 本次使用的数据集是Deep-SAR Oil Spill (SOS) dataset。该数据集由中国地质大学的朱祺琪团队制作并共享。该数据集包含墨西哥湾溢油区域和波斯湾溢油区域&#xff0c;分别获取于ALOS 和Sentinel-1A卫星。由ECHO研究组搜集制作…

说说 Spring Boot 实现接口幂等性有哪几种方案?

一、什么是幂等性 幂等是一个数学与计算机学概念&#xff0c;在数学中某一元运算为幂等时&#xff0c;其作用在任一元素两次后会和其作用一次的结果相同。 在计算机中编程中&#xff0c;一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数或幂等…

【go语言】error错误机制及自定义错误返回类型

简介 Go 语言通过内置的 error 接口来处理错误&#xff0c;该接口定义如下&#xff1a; type error interface {Error() string }这意味着任何实现了 Error() 方法的类型都可以作为错误类型。在 Go 中&#xff0c;通常使用 errors 包的 New 函数来创建简单的错误&#xff1a;…

三维尺寸中,您需要了解的设备及其特点

三维尺寸测量需要用到一些精密仪器&#xff0c;它们都有各自的特点。那么三维尺寸测量中常用的设备有哪些&#xff1f; 1、三坐标测量机 三坐标测量机即三坐标测量计算机数控系统&#xff0c;是一种高精度的测量设备。除了传统的点、线、面和圆柱体等基本轮廓外&#xff0c;还可…