数据结构:图解手撕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] 和 brothernewKey = 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.mzph.cn/news/232523.shtml

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

相关文章

单播、多播、广播、组播、泛播、冲突域、广播域、VLAN概念汇总

1 引言 “多播”可以理解为一个人向多个人&#xff08;但不是在场的所有人&#xff09;说话&#xff0c;这样能够提高通话的效率。如果你要通知特定的某些人同一件事情&#xff0c;但是又不想让其他人知道&#xff0c;使用电话一个一个地通知就非常麻烦&#xff0c;而使用日常…

Lua 中编写 C 函数的一些便捷技巧

零、前言 使用 Lua 时&#xff0c;在编写 C/C 函数经常需要对栈进行交互&#xff0c;而这中间更多的操作和数组、字符串相关。 一、数组操作的便捷方式 从之前分享的 “Lua 数据类型——表” 文章中知道 Lua 中的 “数组” 是以表的形式存在&#xff0c;只是他的 key 值是有…

Z-IETD-FMK;caspase-8 抑制剂 210344-98-2星戈瑞

Z-IETD-FMK是一种caspase-8抑制剂。它通过与caspase-8的活性位点结合&#xff0c;阻断其切割关键蛋白质&#xff0c;进而抑制细胞凋亡过程。该抑制剂具有高选择性、高活性、低毒性等优点。 Z-IETD-FMK通过与caspase-8的半胱氨酸残基形成共价键&#xff0c;从而抑制caspase-8的活…

谷歌发布Gemini 1.0,开启生成式AI模型新时代!

&#x1f3a5; 屿小夏 &#xff1a; 个人主页 &#x1f525;个人专栏 &#xff1a; IT杂谈 &#x1f304; 莫道桑榆晚&#xff0c;为霞尚满天&#xff01; 文章目录 &#x1f4d1;前言一. Gemini的发布前期1.1 Gemini的准备1.2 DeepMnid 二. Gemini的三大杀手锏2.1 多模态能力2…

AWS RDS慢日志文件另存到ES并且每天发送邮件统计慢日志

1.背景&#xff1a;需要对aws rds慢日志文件归档到es&#xff0c;让开发能够随时查看。 2.需求&#xff1a;并且每天把最新的慢日志&#xff0c;过滤最慢的5条sql 发送给各个产品线的开发负责人。 3.准备&#xff1a; aws ak/sk &#xff0c;如果rds 在不同区域需要认证不同的…

Apache RocketMQ 5.0 腾讯云落地实践

Apache RocketMQ 发展历程回顾 RocketMQ 最早诞生于淘宝的在线电商交易场景&#xff0c;经过了历年双十一大促流量洪峰的打磨&#xff0c;2016年捐献给 Apache 社区&#xff0c;成为 Apache 社区的顶级项目&#xff0c;并在国内外电商&#xff0c;金融&#xff0c;互联网等各行…

GBASE南大通用 ADO.NET 中的事务

GBASE南大通用 ADO.NET 中支持事务&#xff0c;可以使用GBASE南大通用Connection 对象的BeginTransaction 函数开始一个事务&#xff0c;并默认使用 ReadCommitted 模式初始化。 事务中可以对单个表执行多个操作&#xff0c;或者对多个表执行多个操作&#xff0c;在事务未提交…

用vue3封装自用的echarts组件

封装的组件 目录 封装的组件在项目中使用 BaseChart.vue <script setup>import {ref,onMounted,onBeforeUnmount,watch,markRaw} from vue;import {debounce} from "/utils"; //节流函数 import * as echarts from "echarts";const emit defineEmit…

TSINGSEE青犀边缘AI计算基于车辆结构化数据的车辆监控方案

随着人工智能技术的不断发展&#xff0c;边缘AI技术逐渐成为智能交通领域的研究热点。其中&#xff0c;基于边缘AI的车辆结构化数据技术与车辆监控系统是实现智能交通系统的重要手段之一。为了满足市场需求&#xff0c;TSINGSEE青犀边缘AI智能分析网关/视频智能分析平台推出了一…

windows安装库报错

报错信息 ERROR: Command errored out with exit status 1: ‘D:\test\venv\Scripts\python.exe’ -u -c ‘import io, os, sys, setuptools, tokenize; sys.argv[0] ‘"’"‘C:\Users\aaa\AppData\Local\Temp\pip-install-j oni55ju\xxx_350c8d1094f749eb97d8f049…

《代码随想录》--二叉树(一)

《代码随想录》--二叉树 第一部分 1、二叉树的递归遍历2、二叉树的迭代遍历3、统一风格的迭代遍历代码4、二叉树的层序遍历226.翻转二叉树 1、二叉树的递归遍历 前序遍历 中序遍历 后序遍历 代码 前序遍历 class Solution {public List<Integer> preorderTraversal(T…

阿里云国际版CDN查询实时带宽步骤

调用DescribeDomainRealTimeBpsData查询加速域名的带宽数据。 接口说明 单用户调用频率&#xff1a;100次/秒。如果您不指定StartTime和EndTime&#xff0c;该接口默认返回过去1小时的数据&#xff1b;指定StartTime和EndTime&#xff0c;该接口返回指定时间段的数据。 返回…

opencv 入门二(播放视频)

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

实时时钟(RTC)的选择与设计:内置晶体与外置晶体的优缺点对比

实时时钟(RTC)作为一种具备独立计时和事件记录功能的设备&#xff0c;现已广泛应用于许多电子产品中&#xff0c;并对时钟的精度要求越来越高。根据封装尺寸、接口方式、附加功能、时钟精度和待机功耗等因素进行分类&#xff0c;市场上有各种种类的RTC产品可供选择。 而在设计…

epi 外延炉 简介

因半导体制造工艺复杂&#xff0c;各个环节需要的设备也不同&#xff0c;从流程工序分类来看&#xff0c;半导体设备主要可分为晶圆制造设备&#xff08;前道工序&#xff09;、封装测试设备&#xff08;后道工序&#xff09;等。 本文介绍影响着晶体管性能和可靠性的外延炉。 …

C#调用阿里云接口实现动态域名解析,支持IPv6(Windows系统下载可用)

电信宽带一般能申请到公网IP&#xff0c;但是是动态的&#xff0c;基本上每天都要变&#xff0c;所以想到做一个定时任务&#xff0c;随系统启动&#xff0c;网上看了不少博文很多都支持IPv4&#xff0c;自己动手写了一个。 &#xff08;私信可全程指导&#xff09; 部署步骤…

衡量芯片运算能力的指标

MACCs MACCs&#xff08;Multiply-accumulate operations&#xff09;表示乘加运算&#xff1a;b乘c加a为一次MACC指令&#xff0c;两次OP。 乘加运算是模型运算里的基本单元&#xff0c;矩阵的运算基本都是乘加。 TOPS TOPS&#xff08;Tera Operation Per Second&#xf…

QT isEnable、isSelected、setEnabled 、 setClickable

isEnable&#xff1a;是否启用部件的键盘和鼠标事件 isSelected&#xff1a;判断某个元素是否被选中 setEnabled 和setClickable参考&#xff1a; qt -- setEnabled() 、 setClickable()_qt setenabled-CSDN博客 void SwitchButton::mousePressEvent(QMouseEvent *event) {…

Vue 使用 js-audio-recorder 实现录制、播放、下载音频

Vue 使用 js-audio-recorder 实现录制、播放、下载 PCM 数据 Vue 使用 js-audio-recorder 实现录制、播放、下载 PCM 数据js-audio-recorder 简介Vue 项目创建下载相关依赖主界面设计设置路由组件及页面设计项目启动源码下载 Vue 使用 js-audio-recorder 实现录制、播放、下载 …

FPGA时序分析与时序约束(二)——时钟约束

目录 一、时序约束的步骤 二、时序网表和路径 2.1 时序网表 2.2 时序路径 三、时序约束的方式 三、时钟约束 3.1 主时钟约束 3.2 虚拟时钟约束 3.3 衍生时钟约束 3.4 时钟组约束 3.5 时钟特性约束 3.6 时钟延时约束 一、时序约束的步骤 上一章了解了时序分析和约束…