探索B-树系列

news2025/4/17 2:21:03

🌈前言🌈

        本文将讲解B树系列,包含 B-树,B+树,B*树,其中主要讲解B树底层原理,为什么用B树作为外查询的数据结构,以及B-树插入操作并用代码实现;介绍B+树、B*树。

📁常见的搜索结构

种类数据格式时间复杂度
顺序查找无要求O(N)
二分查找有序O(logN)
二叉搜索树无要求O(N)
二叉平衡树无要求O(logN)
哈希无要求O(1)

        以上数据结构适用于数据量不大的情况下,能够一次性存放在内存中,进行数据查找的场景。

        对内存进行访问是很快的,纳秒级别(10^-9)。但是同样时间内,访问磁盘的时间是很慢的,毫秒级别(10^-3)。

        从表格可以看出,高效的查找结构是二叉平衡树和哈希表,但是他们都有一定的缺点。

        对于二叉平衡树,时间复杂度还是很高,例如10亿的数据可能需要30次磁盘访问,这是难以接受的结果。        

        哈希表时间复杂度很低,但是哈希存在哈希冲突的问题,在某些极端场景下,某个位置冲突很多,导致访问次数剧增,也难以接受。

        因此,就有了B-树系列,通过压缩树的高度,来减少磁盘访问次数,提高对数据的访问速度。

📁 B树

 📂概念

        1970年,R.Bayer和E.mccreight提出了一种适合外查找的树,它是一种平衡的多叉树,称为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+1。n为结点中关键字的个数,满足ceil(m/2)-1≤n≤m-1。

 📂 插入

插入流程:

        1. 如果树为空,直接插入新节点,作为根节点。

        2. 树非空,找到待插入元素在树的插入位置(维护B树的性质,一定要在叶子节点中插入)

        3.  找到元素要插入的节点cur后,插入元素,注意元素一定是从小到大排序的。

        4. 检测该节点cur是否满足B树性质:即节点中元素个数是否等于M,如果小于则满足

        5. 如果不满足,就进行分裂:

                a. 申请新节点brother。

                b. 找到cur节点中间位置mid,mid之后的元素和孩子节点移到brother中。

                c. mid元素及brother节点插入到上层节点parent,重复上述操作,指导节点满足B树性质。

        一下代码是为了更好理解 B-树插入流程,并不是为了造更好的轮子,但是整体结构上大致是一样的。

template<class K, int M>
struct BTreeNode
{
        typedef BTreeNode<K, M> Node;

        K _keys[M];
        
        Node* _subs[M + 1];

        size_t _n = 0;

        Node* _parent = nullptr;


        BTreeNode()
        {
                for (int i = 0; i < M; ++i)
                {
                        _keys[i] = K();
                        _subs[i] = nullptr;
                }
                _subs[M] = nullptr;
        }
};

template<class K,int M = 3>
class BTree
{
        typedef BTreeNode<K, M> Node;
public:
        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[i])
                                {
                                        break;
                                }
                                else if (key > cur->_keys[i])
                                {
                                        ++i;
                                }
                                else
                                {
                                        return make_pair(cur, i);
                                }
                        }
                        parent = cur;
                        cur = cur->_subs[i];
                }

                return make_pair(parent, -1);
        }

        void insertKey(Node* node , const K& key , Node* child)
        {
                int end = node->_n - 1;
                while (end >= 0)
                {
                        if (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)
        {
                if (_root == nullptr)
                {
                        _root = new Node();
                        _root->_keys[0] = key;
                        _root->_n = 1;
                        return true;
                }

                //去重
                pair<Node*, int> it = Find(key);
                if (it.second != -1)
                {
                        return false;
                }

                Node* cur = it.first;
                insertKey(cur, key, nullptr);

                //节点未满,可以继续存放关键码 , 不需要分裂直接返回
                if (cur->_n != M)
                {
                        return true;
                }

                //节点满了,向左 向上分裂
                 //1. 找到节点数据域中间位置
                 //2. 给一个新节点,将中间位置以后数据移动到brother 
                 //3. 中间位置数据移动到父节点
                 //4. 节点连接
                while (cur->_n == M)
                {
                        Node* brother = new Node;
                        size_t mid = cur->_n >> 1;
                        int i = mid + 1, j = 0;
                        while (i < M)
                        {
                                //cur 把关键字及其左孩子给brother
                                brother->_keys[j] = cur->_keys[i];
                                brother->_subs[j++] = cur->_subs[i];

                                cur->_keys[i] = K();
                                cur->_subs[i] = nullptr;

                                ++i;
                        }

                        //最后处理一下最右位置的孩子
                        brother->_subs[j] = cur->_subs[i];
                        cur->_subs[i] = nullptr;

                        K midKey = cur->_keys[mid];
                        cur->_keys[mid] = K();

                        brother->_n = j;
                        cur->_n = cur->_n - j - 1;

                        Node* parent = cur->_parent;

                        if (parent == nullptr)
                        {
                                //分裂的是根节点
                                _root = new Node;
                                _root->_keys[0] = midKey;
                                _root->_subs[0] = cur;
                                _root->_subs[1] = brother;
                                _root->_n = 1;
                                cur->_parent = _root;
                                brother->_parent = _root;
                        }
                        else
                        {
                                //不是根节点 处理完当前层后,继续往上处理
                                insertKey(parent, midKey, brother);

                                cur = parent;
                                parent = cur->_parent;
                        }
                }
                

                return true;
        }

private:
        Node* _root = nullptr;
};

        B-树效率是很高的,大多数场景下,M设为1024,因此对于10亿的数据量,我们只需要3次磁盘访问即可,对比二叉平衡树的十几次的磁盘访问是很快的,大大减少了读取磁盘的次数。

📁 B+树

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

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

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

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

📁 B*树

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

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

📁 总结

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

        此外,B-树最常见的应用就是用来做索引。MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的数据结构,简单来说:索引就是数据结构。

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

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

相关文章

GRN前沿:DeepMCL:通过深度多视图对比学习从单细胞基因表达数据推断基因调控网络

1.论文原名&#xff1a;Inferring gene regulatory networks from single-cell gene expression data via deep multi-view contrastive learning 2.发表日期&#xff1a;2023 摘要&#xff1a; 基因调控网络&#xff08;GRNs&#xff09;的构建对于理解细胞内复杂的调控机制…

Linux 内核架构入门:从基础概念到面试指南*

1. 引言 Linux 内核是现代操作系统的核心&#xff0c;负责管理硬件资源、提供系统调用、处理进程调度等功能。对于初学者来说&#xff0c;理解 Linux 内核的架构是深入操作系统开发的第一步。本篇博文将详细介绍 Linux 内核的架构体系&#xff0c;结合硬件、子系统及软件支持的…

【竞技宝】PGL瓦拉几亚S4预选:Tidebound2-0轻取spiky

北京时间2月13日,DOTA2的PGL瓦拉几亚S4预选赛继续进行,昨日进行的中国区预选赛胜者组首轮Tidebound对阵的spiky比赛中,以下是本场比赛的详细战报。 第一局: 首局比赛,spiky在天辉方,Tidebound在夜魇方。阵容方面,spiky点出了幻刺、火枪、猛犸、小强、巫妖,Tidebound则是拿到飞…

EasyRTC智能硬件:小体积,大能量,开启音视频互动新体验

在万物互联的时代&#xff0c;智能硬件正以前所未有的速度融入我们的生活。然而&#xff0c;受限于硬件性能和网络环境&#xff0c;许多智能硬件在音视频互动体验上仍存在延迟高、卡顿、回声等问题&#xff0c;严重影响了用户的使用体验。 EasyRTC智能硬件&#xff0c;凭借其强…

【ESP32指向鼠标】——icm20948与esp32通信

【ESP32指向鼠标】——icm20948与esp32通信 ICM-20948介绍 ICM-20948 是一款由 InvenSense&#xff08;现为 TDK 的一部分&#xff09;生产的 9 轴传感器集成电路。它结合了 陀螺仪、加速度计和磁力计。 内置了 DMP&#xff08;Digital Motion Processor&#xff09;即负责执…

算法——结合实例了解深度优先搜索(DFS)

一&#xff0c;深度优先搜索&#xff08;DFS&#xff09;详解 DFS是什么&#xff1f; 深度优先搜索&#xff08;Depth-First Search&#xff0c;DFS&#xff09;是一种用于遍历或搜索树、图的算法。其核心思想是尽可能深地探索分支&#xff0c;直到无法继续时回溯到上一个节点…

SpringMVC学习使用

一、SpringMVC简单理解 1.1 Spring与Web环境集成 1.1.1 ApplicationContext应用上下文获取方式 应用上下文对象是通过new ClasspathXmlApplicationContext(spring配置文件) 方式获取的&#xff0c;但是每次从容器中获得Bean时都要编写new ClasspathXmlApplicationContext(sp…

运维-自动访问系统并截图

需求背景 因项目甲方要求需要对系统进行巡检&#xff0c;由于系统服务器较多&#xff0c;并且已经采用PrometheusGrafana对系统服务器进行管理&#xff0c;如果要完成该任务&#xff0c;需要安排一个人力对各个系统和服务器进行一一截图等操作&#xff0c;费时费力&#xff0c…

在CodeBlocks搭建SDL2工程虚拟TFT彩屏解码带压缩形式的Bitmap(BMP)图像显示

在CodeBlocks搭建SDL2工程虚拟TFT彩屏解码带压缩形式的Bitmap BMP图像显示 参考文章文章说明一、创建和退出SDL2二、 Bitmap(BMP)图片解码图三、Bitmap解码初始化四、测试代码五、主函数六、测试结果 参考文章 解码带压缩形式的Bitmap(BMP)图像并使用Python可视化解码后实际图…

mapbox进阶,添加绘图扩展插件,绘制任意方向矩形

👨‍⚕️ 主页: gis分享者 👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍⚕️ 收录于专栏:mapbox 从入门到精通 文章目录 一、🍀前言1.1 ☘️mapboxgl.Map 地图对象1.2 ☘️mapboxgl.Map style属性1.3 ☘️MapboxDraw 绘图控件二、🍀添加绘图扩…

初阶c语言(循环语句习题,完结)

前言&#xff1a; c语言为b站鹏哥&#xff0c;嗯对应视频37集 昨天做的c语言&#xff0c;今天在来做一遍&#xff0c;发现做错了 今天改了平均值的计算&#xff0c; 就是说最大值加上最小值&#xff0c;如果说这个数值非常大的话&#xff0c;两个值加上会超过int类型的最大…

提升编程效率,体验智能编程助手—豆包MarsCode一键Apply功能测评

提升编程效率&#xff0c;体验智能编程助手—豆包MarsCode一键Apply功能测评 &#x1f31f; 嗨&#xff0c;我是LucianaiB&#xff01; &#x1f30d; 总有人间一两风&#xff0c;填我十万八千梦。 &#x1f680; 路漫漫其修远兮&#xff0c;吾将上下而求索。 目录 引言豆包…

【deepseek-r1本地部署】

首先需要安装ollama,之前已经安装过了&#xff0c;这里不展示细节 在cmd中输入官网安装命令&#xff1a;ollama run deepseek-r1:32b&#xff0c;开始下载 出现success后&#xff0c;下载完成 接下来就可以使用了&#xff0c;不过是用cmd来运行使用 可以安装UI可视化界面&a…

多用户商城系统的客服管理体系建设

多用户商城系统的运营&#xff0c;客服管理体系建设至关重要。优质的客服服务不仅能提升用户购物体验&#xff0c;还能增强用户对商城的信任与忠诚度&#xff0c;进而促进商城业务的持续增长。以下从四个关键方面探讨如何建设完善的客服管理体系&#xff0c;信息化客服系统在其…

C++设计模式 - 模板模式

一&#xff1a;概述 模板方法&#xff08;Template Method&#xff09;是一种行为型设计模式。它定义了一个算法的基本框架&#xff0c;并且可能是《设计模式&#xff1a;可复用面向对象软件的基础》一书中最常用的设计模式之一。 模板方法的核心思想很容易理解。我们需要定义一…

CZML 格式详解,javascript加载导出CZML文件示例

示例地址&#xff1a;https://dajianshi.blog.csdn.net/article/details/145573994 CZML 格式详解 1. 什么是 CZML&#xff1f; CZML&#xff08;Cesium Zipped Markup Language&#xff09;是一种基于 JSON 的文件格式&#xff0c;用于描述地理空间数据和时间动态场景。它专…

OpenAI推出全新AI助手“Operator”:让人工智能帮你做事的新时代!

引言 随着人工智能技术的不断发展&#xff0c;OpenAI 再次推出令人兴奋的功能——Operator&#xff0c;一个全新的 AI 助手平台。这不仅仅是一个普通的助手&#xff0c;它代表了人工智能技术的又一次飞跃&#xff0c;将改变我们工作和生活的方式。 什么是“Operator”&#xff…

重看Spring聚焦BeanFactory分析

目录 一、理解BeanFactory &#xff08;一&#xff09;功能性理解 &#xff08;二&#xff09;BeanFactory和它的子接口 &#xff08;三&#xff09;BeanFactory的实现类 二、BeanFactory根接口 &#xff08;一&#xff09;源码展示和理解 &#xff08;二&#xff09;基…

将Docker容器打包成镜像提交

前言 Docker 是一个开源软件&#xff0c;也是一个开放平台&#xff0c;用于开发应用、交付&#xff08;shipping&#xff09;应用、运行应用。 Docker允许用户将基础设施&#xff08;Infrastructure&#xff09;中的应用单独分割出来&#xff0c;形成更小的颗粒&#xff08;容…

一文通俗理解为什么需要泛型以及泛型的使用

为什么需要泛型&#xff1f; public static void main(String[] args) {ArrayList list new ArrayList();// 由于集合没有做任何限定&#xff0c;任何类型都可以给其中存放list.add("abc");list.add("def");list.add(5);Iterator it list.iterator();wh…