数据结构:图解手撕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;
};

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

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

相关文章

竞赛保研 YOLOv7 目标检测网络解读

文章目录 0 前言1 yolov7的整体结构2 关键点 - backbone关键点 - head3 训练4 使用效果5 最后 0 前言 世界变化太快&#xff0c;YOLOv6还没用熟YOLOv7就来了&#xff0c;如果有同学的毕设项目想用上最新的技术&#xff0c;不妨看看学长的这篇文章&#xff0c;学长带大家简单的…

SpringBoot接入轻量级分布式日志框架GrayLog

1.前言 日志在我们日常开发定位错误&#xff0c;链路错误排查时必不可少&#xff0c;如果我们只有一个服务&#xff0c;我们可以只简单的通过打印的日志文件进行排查定位就可以&#xff0c;但是在分布式服务环境下&#xff0c;多个环境的日志统一收集、展示则成为一个问题。目…

掌握这个技巧,你也能成为学习高手!

在当今社会&#xff0c;思想政治学习具有重要的意义。随着社会的不断发展&#xff0c;个体与集体之间的关系、公共事务的处理、社会责任感等问题愈发凸显。 思政通学习小程序作为一种现代化的学习手段&#xff0c;为我们提供了更便捷、高效的途径&#xff0c;使我们更好地理解和…

实战 | API接口泄露=>未授权访问=>垂直越权=>信息泄露

文章目录 实战 | API接口泄露>未授权访问>信息泄露0x01 前言0x02 初探系统0x03 API接口泄露0x04 未授权访问系统日志接口0x05 垂直越权0x06 信息泄露 实战 | API接口泄露>未授权访问>信息泄露 0x01 前言 免责声明&#xff1a;请勿利用文章内的相关技术从事非法测…

TensorRT 深入介绍

前言 一、tensorRT 做了那些优化 1、算子融合 常见的算子融合有以下操作&#xff1a; conv bn reluconv reluconv relu max_poolconv add reluconv add 2、量化 因为我们在训练的时候&#xff0c;参数的每次更新的幅度都是十分微小的。所以为了保证在训练时梯度反…

rabbitmq界面主要参数分析

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

数据结构学习笔记(七)搜索结构

文章目录 1. 前言2. 概念3 静态搜索结构3.1 静态搜索表3.2 顺序搜索表3.2.1 基于有序顺序表和顺序搜索和折半搜索 4 二叉搜索树4.1 搜索二叉树的类定义4.2 搜索二叉树的搜索4.3 搜索二叉树的插入4.4 搜索二叉树的删除 5 AVL树5.1 平衡化旋转5.1.1 右旋&#xff1a;LL型状态5.1.…

OpenHarmony 启动流程优化

目前rk3568的开机时间有21s&#xff0c;统计的是关机后从按下 power 按键到显示锁屏的时间&#xff0c;当对openharmony的系统进行了裁剪子系统&#xff0c;系统app&#xff0c;禁用部分服务后发现开机时间仅仅提高到了20.94s 优化微乎其微。在对init进程的log进行分析并解决其…

win10系统计算机名称查看及重命名操作方法

win10系统计算机名称查看及重命名操作方法 一、在 Windows 10 操作系统中查看计算机的名称步骤二、重命名计算机名称 一、在 Windows 10 操作系统中查看计算机的名称步骤 点击桌面左下角的“开始”按钮&#xff0c;点击“设置”图标&#xff08;齿轮状图标&#xff09;&#x…

采购审批工作流程快速指南

对很多人来说&#xff0c;控制每一分钱的支出是一件棘手的事情&#xff0c;但在当下充满不确定性的环境里&#xff0c;这一点与收入增长同等重要。正如俗话说&#xff1a;要想赚钱&#xff0c;先学会花钱。 本文将分析支出控制的基石之一&#xff1a;采购审批工作流程。有了审…

安防视频云平台/可视化监控云平台EasyCVR获取设备录像失败,该如何解决?

视频云存储/安防监控EasyCVR视频汇聚平台基于云边端智能协同&#xff0c;支持海量视频的轻量化接入与汇聚、转码与处理、全网智能分发、视频集中存储等。GB28181音视频流媒体视频平台EasyCVR拓展性强&#xff0c;视频能力丰富&#xff0c;具体可实现视频监控直播、视频轮播、视…

计算机组成原理(存储器与CPU的连接)

题目&#xff1a; 设 CPU 共有 16 根地址线。8 根数据线&#xff0c;并用 作访存控制信号&#xff0c;R/作读/写命令信号。现有这些存储芯片:ROM (2K*8 位、4K*4 位、8K*8 位)&#xff0c;RAM(1K*4 位、2K*8 位、4K*8 位)及 74138 译码器和其他门电路(门电路自定)。试从上述规…