B-树:解锁大数据存储和与快速存储的密码

在我们学习数据结构的过程中,我们会学习到二叉搜索树、二叉平衡树、红黑树。

这些无一例外,是以一个二叉树展开的,那么对于我们寻找其中存在树中的数据,这个也是一个不错的方法。

但是,如若是遇到了非常大的数据容量进行存储的时候,此时呢,无法一次性加载到内存当中,那么此时,这样的二叉结构的树就显得力不从心了。

这是因为,我们二叉树中,一个节点只保存了一个数据域,此时遇到非常大数据量的时候,导致树高变得很高,进行遍历查询树的时候,需要遍历树较深的地方,当然,查询的数据如若是在较浅树层,那么查询的时间还好,如若是非常深,那么此时查询的时间就会变得很耗时。

所以我们干脆在一个节点中存储多个数值域,以达到减少树高,从而获得高效率的查询效率。

而我们数据结构中,正好有一种符合此特征的,那就是B-树

B-树

那么又是B-树?

定义:一种适合外查找的树,它是一种平衡的多叉树

那么它具有什么性质呢?

一颗M阶(M > 2)的B树,是一棵平衡的M路平衡搜索树,可以是空树或者满足一下性质:

1.根节点至少有两个孩子

2.每个非根节点的关键字至少有M/2-1(向上取整),至多有M-1个关键字,并以升序排列

比如当M=4,至少有(4/2-1=1)个关键字,至多有(4-1=3)个关键字.

3.每个非根节点的孩子至少有(M/2)向上取整,至多有M个孩子

 比如当M=4,至少有(4/2=2)个孩子,至多有4个孩子

4. key[i]和key[i+1]之间的孩子节点的值介于key[i]、key[i+1]之间

5.所有叶子节点都在同一层

由此可得出,

孩子节点会比关键字多出一个。

值得注意的是,为了使其关键字和孩子访问速度较快,所以使用的是数组进行存储。

节点图例:

这里值得注意的是,上述节点图例,是一般情况下的,为了后续插入操作简易进行,所以节点内部会进行修改。后面的插入操作进行解释。

修改后的节点:

现在小编来分享下插入操作,以三叉树作为例子

插入

根据此修改后的插入节点图例,此节点定义代码如下:

 //这里约定以三叉树作为实现public static int BT=3;//定义节点static class Node{//keys关键字数组public int [] keys;//孩子数组public Node [] subs;//有效数据大小public int Usize;//父亲节点public Node parent;//提供一个构造方法public Node(){//多给一个节点,利于后续分裂this.keys=new int[BT];//孩子域会比父亲域多一个this.subs=new Node[BT+1];}}

插入操作其实是大致可说为,插入按照某个插入算法进行插入,满了就进行分裂。

假设我们有以下的插入数据

序列:53, 139, 75, 49, 145, 36, 101

那么初始插入,因为我们关键字数组是中是有序的序列,所以我们需要选用一个插入算法进行插入,这里选择的是直接插入排序

初始状态如下:

注意,我们是初始情况,即根节点是为空的

所以如何插入53的呢?

显然,先新建一个节点,然后直接放到keys[0],Usize++即可。

代码实例:

//定义一个头节点public Node root;public boolean insert(int key){//如若根节点为空if(root==null){root=new Node();root.keys[0]=key;root.Usize++;return true;}

那么此时如若不为空呢?
即我们插入75的数据,那么此时我们先要找到75是否是存在于树中。

那么接下来说说,如何实现查找的

查找

我们以一个实例说明(值得注意的是,数据有序是因为以直接插入排序进行的)

假设我们要寻找的是54这个节点

显然我们在根节点是寻找不到的,那么此时该去哪个数值的子树去寻找呢?

因为我们的49是比54小的,那么接着去下一个数值去比较,即75

那么54和75比较,那么此时的是大于54的,所以就结束我们的比较

所以我们得去75的孩子节点数组去找,

那么此时就到了53这棵树这里,此时显然,54也是不在这里的,那么此时我们按照逻辑,还是得去子树再找,那么这里呢,没有子树了,所以导致我们寻找节点变为null了

这里要保存其父亲节点,进行后续的操作。

那么就有一个问题了

如若我们找到了,那我们一般也是返回当前的节点,如若我们找不到了,也是返回此时的保存下来的父亲节点,那么怎么区分他们是找到还是没有找到呢?

这里提供一个方案:

新建一个泛型类,存储下当前的节点以及一个整数值,通过整数值去判断是否是找到了

泛型类代码:
 

public class Pair<k,v> {public k key;public v val;public Pair(k key,v val){this.key=key;this.val=val;}public void setKey(k key) {this.key = key;}public void setVal(v val) {this.val = val;}public v getVal() {return val;}public k getKey() {return key;}
}

接下来是寻找节点代码


寻找节点代码:

private Pair<Node,Integer> findNode(int key) {Node cur=root;Node parent=null;while (cur!=null){int i=0;while (i<cur.Usize){if(cur.keys[i]==key){return new Pair<>(cur,i);}else if(cur.keys[i]<key){i++;}else {break;}}parent=cur;cur=cur.subs[i];}return new Pair<>(parent,-1);}

此时,我们就可以判断返回值是不是负数即可。

  //当根节点不为空,那么此时找出这个key是否存在B树中Pair<Node,Integer> node=findNode(key);//根据返回值是否插入还是修改if(node.val!=-1){System.out.println("此key值已存在!");return false;}//没有找到,那么此时就要插入这个新节点,//此时的插入操作就是以直接插入方法进行Node parent=node.getKey();int i=parent.Usize-1;for (;i>=0;i--){if(parent.keys[i]>=key){parent.keys[i+1]=parent.keys[i];}else {break;}}//此时i变成负数,我们要把0下标的值放进去。parent.keys[i+1]=key;parent.Usize++;if(parent.Usize >= BT){//超出容量,此时进行分裂Split(parent);return true;}else {return true;}}

那么如若我们找不到的话,即这个数据是未曾插入的,所以我们要进行插入它。

那么插入的话选用了直接插入排序。

插入完成之后,我们还要判断下是否是大于了这个BT值,如若是大于了,此时我们就要进行分裂。

那么接下来就还有个分裂操作还没有分享。

分裂

这里的分裂分为根节点分裂,和孩子节点分裂

举例

根节点满时:

值得注意的是,我们把关键字移动的同时,也要把孩子数组对应到下标值,一一挪过去,挪过去的同时也要对subs[]数组中的值的父亲进行修改

然后呢,此时我们这里的举动是把同一层的孩子节点处理了,但是新出现的根节点

就比如现在的75,还没用进行处理,所以呢,我们要处理它,

比如keys[0]下标放的是75,subs[0]下标放的是53这个节点,sub[1]放的是139这个节点值

然后把53这个节点的父亲和139这个父亲,进行修改,然后还要修改对应的Usize。

  private void Split(Node cur) {Node parent=cur.parent;Node newNode=new Node();int mid=cur.Usize>>1;int j=0;int i=mid+1;for (;i<cur.Usize;i++){newNode.keys[j]=cur.keys[i];newNode.subs[j]=cur.subs[i];if(newNode.subs[i]!=null){//修改迁移过去的父亲节点newNode.subs[j].parent=newNode;}j++;}//由于分配多了一个节点,那么此时就要再次拷贝下孩子newNode.subs[j]=cur.subs[i];if(newNode.subs[i]!=null){newNode.subs[i].parent=newNode;}//此时进行修改Usize和新创建节点的父亲节点cur.Usize=cur.Usize-j-1;newNode.Usize=j;if(cur==root){root=new Node();root.keys[0]=cur.keys[mid];root.subs[0]=cur;root.subs[1]=newNode;root.Usize++;cur.parent=root;newNode.parent=root;return;}newNode.parent=parent;

这里是根节点的修改,还有孩子节点的分裂

孩子节点的分裂:

以下面例子举例:

显然,最左边的树,是满了,所以要进行分裂

孩子分裂出来的思路和刚刚根节点是差不多,

那么这里要说明的是下,对于孩子节点分裂完后,父亲节点的操作

即把49提到根节点后怎么操作。

插入49也是一个直接插入排序,所以不过多讲解。

如若我们定义一个endT=当前节点的.Usize-1,即是1-1=0,就是0下标。

那我们可以发现,没有分裂前,75这个节点subs[1]连接139这棵树。

那么如何把新增加53这棵树的节点插入到subs[1]呢?

显然,当我们把49和53排好序后,此时的endT=0

所以,subs[endT+2]的空间存储139这棵树节点即可。

等代码跳出循环后,再次把subs[1]的值赋值为53这棵树的节点值即可。

那么此时接着插入节点

可以注意到的是,101那边的这棵树也满了,继续分裂

再次注意到,出现了连续分裂,因为根节点这棵树也是满了,所以接着也要对根节点这棵树进行分裂,为了可以进行连续分裂,我们可以进行递归这样的方式

即判断下分裂完后,把139提到根节点后,当前的Usize值是否是满的,然后递归分裂

完整分裂代码:

 private void Split(Node cur) {Node parent=cur.parent;Node newNode=new Node();int mid=cur.Usize>>1;int j=0;int i=mid+1;for (;i<cur.Usize;i++){newNode.keys[j]=cur.keys[i];newNode.subs[j]=cur.subs[i];if(newNode.subs[i]!=null){//修改迁移过去的父亲节点newNode.subs[j].parent=newNode;}j++;}//由于分配多了一个节点,那么此时就要再次拷贝下孩子newNode.subs[j]=cur.subs[i];if(newNode.subs[i]!=null){newNode.subs[i].parent=newNode;}//此时进行修改Usize和新创建节点的父亲节点cur.Usize=cur.Usize-j-1;newNode.Usize=j;if(cur==root){root=new Node();root.keys[0]=cur.keys[mid];root.subs[0]=cur;root.subs[1]=newNode;root.Usize++;cur.parent=root;newNode.parent=root;return;}newNode.parent=parent;//此时开始对父亲节点开始操作int endT=parent.Usize-1;int midVal=cur.keys[mid];//采取的是直接插入for(;endT>=0;endT--){if (parent.keys[endT]>=midVal){parent.keys[endT+1]=parent.keys[endT];parent.subs[endT+2]=parent.subs[endT+1];}else {break;}}parent.keys[endT+1]=midVal;parent.subs[endT+2]=newNode;parent.Usize++;if(parent.Usize>=BT){Split(parent);}}

ok,B-树的整个插入操作就讲完啦

那么接下来的删除操作

删除

就讲下思路:

1.定位关键字

2.是否是叶子节点还算是非叶子节点

叶子节点,直接删除即可

非叶子节点

那么就要找到前驱节点(左树最大值节点)后继(右树最小值节点),进行替换删除

3.判断删除后的节点是否符合B树节点最少性质

如若不符合,那就要向兄弟节点借

如若兄弟节点不够,那么此时就要和兄弟节点进行合并。

关于更多详细操作讲解,可请看这篇操作

面试官问你 B树 和 B+ 树,就把这篇文章丢给他-腾讯云开发者社区-腾讯云

那么关于B-树相关操作小编就分享到这了。

这里额外简单分享下B+树

什么是B+树?

其实也是和B-树的定义差不多。

不同的是,就是叶子节点会形成一个链表,使其寻找效率会进一步提高。

举个例子:

那什么又是B*树呢?

B*树就是在B+树的基础上,再增加非叶子节点之间的联系。

从而再次进行优化。

完!

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

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

相关文章

【视频+图文详解】HTML基础4-html标签的基本使用

图文教程 html标签的基本使用 无序列表 作用&#xff1a;定义一个没有顺序的列表结构 由两个标签组成&#xff1a;<ul>以及<li>&#xff08;两个标签都属于容器级标签&#xff0c;其中ul只能嵌套li标签&#xff0c;但li标签能嵌套任何标签&#xff0c;甚至ul标…

网络工程师 (8)存储管理

一、页式存储基本原理 &#xff08;一&#xff09;内存划分 页式存储首先将内存物理空间划分成大小相等的存储块&#xff0c;这些块通常被称为“页帧”或“物理页”。每个页帧的大小是固定的&#xff0c;例如常见的页帧大小有4KB、8KB等&#xff0c;这个大小由操作系统决定。同…

LabVIEW无人机航线控制系统

介绍了一种无人机航线控制系统&#xff0c;该系统利用LabVIEW软件与MPU6050九轴传感器相结合&#xff0c;实现无人机飞行高度、速度、俯仰角和滚动角的实时监控。系统通过虚拟仪器技术&#xff0c;有效实现了数据的采集、处理及回放&#xff0c;极大提高了无人机航线的控制精度…

实现B-树

一、概述 1.历史 B树&#xff08;B-Tree&#xff09;结构是一种高效存储和查询数据的方法&#xff0c;它的历史可以追溯到1970年代早期。B树的发明人Rudolf Bayer和Edward M. McCreight分别发表了一篇论文介绍了B树。这篇论文是1972年发表于《ACM Transactions on Database S…

新一代搜索引擎,是 ES 的15倍?

Manticore Search介绍 Manticore Search 是一个使用 C 开发的高性能搜索引擎&#xff0c;创建于 2017 年&#xff0c;其前身是 Sphinx Search 。Manticore Search 充分利用了 Sphinx&#xff0c;显着改进了它的功能&#xff0c;修复了数百个错误&#xff0c;几乎完全重写了代码…

iperf 测 TCP 和 UDP 网络吞吐量

注&#xff1a;本文为 “iperf 测网络吞吐量” 相关文章合辑。 未整理去重。 使用 iperf3 监测网络吞吐量 Tom 王 2019-12-21 22:23:52 一 iperf3 介绍 (1.1) iperf3 是一个网络带宽测试工具&#xff0c;iperf3 可以擦拭 TCP 和 UDP 带宽质量。iperf3 可以测量最大 TCP 带宽…

神经网络的数据流动过程(张量的转换和输出)

文章目录 1、文本从输入到输出&#xff0c;经历了什么&#xff1f;2、数据流动过程是张量&#xff0c;如何知道张量表达的文本内容&#xff1f;3、词转为张量、张量转为词是唯一的吗&#xff1f;为什么&#xff1f;4、如何保证词张量的质量和合理性5、总结 &#x1f343;作者介…

MediaPipe与YOLO已训练模型实现可视化人脸和手势关键点检测

项目首页 - ZiTai_YOLOV11:基于前沿的 MediaPipe 技术与先进的 YOLOv11 预测试模型&#xff0c;精心打造一款强大的实时检测应用。该应用无缝连接摄像头&#xff0c;精准捕捉画面&#xff0c;能即时实现人脸检测、手势识别以及骨骼关键点检测&#xff0c;将检测结果实时、直观地…

JAVA篇12 —— 泛型的使用

​ 欢迎来到我的主页&#xff1a;【Echo-Nie】 本篇文章收录于专栏【JAVA学习】 如果这篇文章对你有帮助&#xff0c;希望点赞收藏加关注啦~ 1 泛型介绍 先对集合进行说明&#xff0c;不能对加入到集合中的元素类型进行约束&#xff08;不安全&#xff09;。遍历的时候需要…

JavaScript 数据类型

基本概念 什么是数据类型 JavaScript是一种 灵活的动态类型语言 &#xff0c;其数据类型构成了程序的基础构建块。它主要包括两类数据类型&#xff1a; 原始数据类型 &#xff1a;包括String、Number、Boolean、Undefined、Null和Symbol。 复杂数据类型 &#xff1a;以Object…

被裁与人生的意义--春节随想

还有两个月就要被迫离开工作了十多年的公司了&#xff0c;不过有幸安安稳稳的过了一个春节&#xff0c;很知足! 我是最后一批要离开的&#xff0c;一百多号同事都没“活到”蛇年。看着一批批仁人志士被“秋后斩首”&#xff0c;马上轮到我们十来个&#xff0c;个中滋味很难言清…

Redis代金卷(优惠卷)秒杀案例-多应用版

Redis代金卷(优惠卷)秒杀案例-单应用版-CSDN博客 上面这种方案,在多应用时候会出现问题,原因是你通过用户ID加锁 但是在多应用情况下,会出现两个应用的用户都有机会进去 让多个JVM使用同一把锁 这样就需要使用分布式锁 每个JVM都会有一个锁监视器,多个JVM就会有多个锁监视器…

绘制决策树尝试3

目录 代码解读AI 随机状态 种子 定义决策树回归模型 tree的decision regressor fit 还可用来预测 export 效果图 我的X只有一个特征 为何这么多分支 &#xff1f;&#xff1f;&#xff1f; 这是CART回归 CART回归 为什么说代码是CART回归&#xff1f; 不是所有的决…

为大模型提供webui界面的利器:Open WebUI 完全本地离线部署deepseek r1

为大模型提供webui界面的利器&#xff1a;Open WebUI Open WebUI的官网&#xff1a;&#x1f3e1; Home | Open WebUI 开源代码&#xff1a;WeTab 新标签页 Open WebUI是一个可扩展、功能丰富、用户友好的自托管AI平台&#xff0c;旨在完全离线运行。它支持各种LLM运行程序&am…

langchain 实现多智能体多轮对话

这里写目录标题 工具定义模型选择graph节点函数定义graph 运行 工具定义 import random from typing import Annotated, Literalfrom langchain_core.tools import tool from langchain_core.tools.base import InjectedToolCallId from langgraph.prebuilt import InjectedSt…

【Block总结】CPCA,通道优先卷积注意力|即插即用

论文信息 标题: Channel Prior Convolutional Attention for Medical Image Segmentation 论文链接: arxiv.org 代码链接: GitHub 创新点 本文提出了一种新的通道优先卷积注意力&#xff08;CPCA&#xff09;机制&#xff0c;旨在解决医学图像分割中存在的低对比度和显著…

Python从零构建macOS状态栏应用(仿ollama)并集成AI同款流式聊天 API 服务(含打包为独立应用)

在本教程中,我们将一步步构建一个 macOS 状态栏应用程序,并集成一个 Flask 服务器,提供流式响应的 API 服务。 如果你手中正好持有一台 MacBook Pro,又怀揣着搭建 AI 聊天服务的想法,却不知从何处迈出第一步,那么这篇文章绝对是你的及时雨。 最终,我们将实现以下功能: …

强化学习、深度学习、深度强化学习的区别是什么?

前言 深度强化学习就是 深度学习 和 强化学习 的结合体。它让计算机程序&#xff08;也就是智能体&#xff09;在特定环境中不断尝试&#xff0c;从错误中学习&#xff0c;最终找到最优的行动策略。 深度学习是AlphaGo从棋谱里学习&#xff0c;强化学些Alphazero 学习规则&am…

string类(详解)

为什么学习string类&#xff1f; 1.1 C语言中的字符串 C语言中&#xff0c;字符串是以\0结尾的一些字符的集合&#xff0c;为了操作方便&#xff0c;C标准库中提供了一些str系列的库函数&#xff0c;但是这些库函数与字符串是分离开的&#xff0c;不太符合OOP的思想&#xf…

【Redis】set 和 zset 类型的介绍和常用命令

1. set 1.1 介绍 set 类型和 list 不同的是&#xff0c;存储的元素是无序的&#xff0c;并且元素不允许重复&#xff0c;Redis 除了支持集合内的增删查改操作&#xff0c;还支持多个集合取交集&#xff0c;并集&#xff0c;差集 1.2 常用命令 命令 介绍 时间复杂度 sadd …