数据结构 —— B树

news2024/11/14 15:11:46

数据结构 —— B树

  • B树
  • B树的插入操作
    • 分裂
      • 孩子分裂
      • 父亲分裂

我们之前学过了各种各样的树,二叉树,搜索二叉树,平衡二叉树,红黑树等等等等,其中平衡二叉树和红黑树都是控制树的高度来控制查找次数。

但是,这都基于内存能放的下

平衡二叉树和红黑树等数据结构确实是为了在内存中高效处理动态数据集而设计的。它们的主要目标是在各种操作(如查找、插入和删除)中保持对数时间复杂度O(log n),其中n是树中节点的数量。这意味着随着数据集的增长,操作时间不会线性增长,而是以较慢的速度增长,保持较高的性能。
在设计上,平衡二叉树和红黑树假设整个数据集能够被加载到内存中,这是因为这些树的算法和操作需要频繁地遍历树的不同部分。磁盘I/O操作比内存访问要慢得多,因此如果数据集太大而不能完全装入内存,使用这些数据结构将大大降低效率。

但是,直接在磁盘上实现平衡二叉树或红黑树通常不是最有效的方法,因为磁盘访问的成本高,而这类树的许多操作涉及多次磁盘读写。因此,对于大规模数据集,更常见的做法是使用专门针对磁盘访问优化的数据结构,如B树或B+树,它们在设计上考虑了磁盘I/O的特性,可以更有效地处理大量数据

B树

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

说白了,我们以前的树中,一个结点包含的数据量只能有一个
在这里插入图片描述B树的基本思想就是一个结点可以携带多个数据

在这里插入图片描述
但是每个结点也有自己的要求:一个结点要包括数据和孩子

假设我们是一棵3阶的B树,规定每个结点的孩子最多有3个,而数据个数比孩子个数小1

在这里插入图片描述但是我们数据个数为偶数的话,后面的分裂操作不好操作,所以我们会多开一个数据空间,但是不会装入任何数据,同时会增加一个孩子:
在这里插入图片描述

B树的插入操作

我们这里以3阶的B树为例:

int a[] = {53, 139, 75, 49, 145, 36, 101} //构建b树

在这里插入图片描述
下面我们要插入75:
在这里插入图片描述这个时候数据个数已经超过了两个,需要进行分裂

分裂

这个时候75被提出来:
在这里插入图片描述在这里插入图片描述
接下来我们插入49,145:
在这里插入图片描述

孩子分裂

接着我们插入36
在这里插入图片描述

在这里插入图片描述

父亲分裂

我们这样一直插入,插入到139:

在这里插入图片描述
在这里插入图片描述
B树的插入过程是反人类的,是先有孩子,再有父亲,其中孩子会分裂,父亲也会分裂

我们来看看代码,大家能看懂多少看多少:

#pragma once
#include <iostream>
using namespace std;

// B-树节点模板定义
template<class K, size_t M>
struct BTreeNode
{
    // 键数组,存储节点的关键字
    K _keys[M];

    // 子节点数组,存储指向子节点的指针
    BTreeNode<K, M>* _Children[M + 1];

    // 指向父节点的指针
    BTreeNode<K, M>* _Parent;

    // 节点中的实际关键字数量
    size_t _n;

    // 构造函数,初始化节点
    BTreeNode()
    {
        // 初始化所有键为空
        for (int i = 0; i < M; i++)
        {
            _keys[i] = K();
            _Children[i] = nullptr;
        }

        // 初始化最后一个子节点指针为空
        _Children[M] = nullptr;
        _Parent = nullptr;
        _n = 0; // 节点中实际关键字数量为0
    }
};

// B-树模板类定义
template<class K, size_t M>
class BTree
{
    typedef BTreeNode<K, M> _Node;

public:
    // 查找键的函数
    pair<_Node*, int> Find(const K& key)
    {
        _Node* cur = _root; // 从根节点开始查找
        _Node* parent = nullptr;

        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->_Children[i];
        }

        // 没有找到键,返回父节点和-1
        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])
            {
                node->_keys[end + 1] = node->_keys[end];
                node->_Children[end + 2] = node->_Children[end + 1];
                end--;
            }
            else
            {
                break;
            }
        }

        // 插入新的键和子节点
        node->_keys[end + 1] = key;
        node->_Children[end + 2] = child;
        node->_n++;
    }

    // 插入键的主函数
    bool Insert(const K& key)
    {
        if (_root == nullptr)
        {
            // 如果树为空,创建根节点并插入键
            _root = new _Node();
            _root->_keys[0] = key;
            _root->_n++;
            return true;
        }

        pair<_Node*, int> ret = Find(key);

        if (ret.second >= 0)
        {
            // 键已存在,返回false
            return false;
        }

        _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;

                // 创建一个新节点,并将父节点的一部分键和子节点移动到新节点中
                _Node* brother = new _Node;
                size_t j = 0;
                size_t i = mid + 1;
                for (; i <= M - 1; i++)
                {
                    brother->_keys[j] = parent->_keys[i];
                    brother->_Children[j] = parent->_Children[i];

                    if (parent->_Children[i])
                    {
                        parent->_Children[i]->_Parent = brother; //父节点分裂,分裂出了父亲的brother
                    }
                    j++;

                    parent->_keys[i] = K();
                }

                // 处理新节点的最后一个右孩子节点
                brother->_Children[j] = parent->_Children[i];
                if (parent->_Children[i])
                {
                    parent->_Children[i]->_Parent = brother;
                }

                brother->_n = j;
                parent->_n -= j + 1;

                // 将父节点的中间键提升到新节点
                newkey = parent->_keys[mid];
                child = brother;

                if (parent->_Parent == nullptr)
                {
                    // 如果父节点是根节点,则创建新的根节点
                    _root = new _Node();
                    _root->_keys[0] = parent->_keys[mid];
                    _root->_Children[0] = parent;
                    _root->_Children[1] = brother;
                    _root->_n = 1;

                    parent->_keys[mid] = K();

                    parent->_Parent = _root;
                    brother->_Parent = _root;
                    break;
                }
                else
                {
                    // 继续向父节点的父节点插入分裂后的节点
                    newkey = parent->_keys[mid];
                    parent->_keys[mid] = K();
                    child = brother;
                    parent = parent->_Parent;
                }
            }
        }

        return true;
    }

    // 中序遍历树的函数
    void InOrder()
    {
        _InOrder(_root);
    }

    // 递归实现的中序遍历
    void _InOrder(_Node* pRoot)
    {
        if (nullptr == pRoot)
            return;

        for (size_t i = 0; i < pRoot->_n; ++i)
        {
            _InOrder(pRoot->_Children[i]);
            cout << pRoot->_keys[i] << " ";
        }
        _InOrder(pRoot->_Children[pRoot->_n]);
    }

private:
    // 树的根节点
    _Node* _root = nullptr;
};

// 测试函数
void Test()
{
    int a[] = { 53, 139, 75, 49, 145, 36, 101, 34, 1, 9, 41 };
    BTree<int, 3> t;

    for (auto e : a)
    {
        t.Insert(e);
    }

    t.InOrder();
}

我们可以测试,如果打印的顺序是升序,说明我们代码的逻辑没问题:
在这里插入图片描述
写代码要注意几点:

  1. 插入新结点,有可能改变父子关系(是第一个数据的孩子,插入后有可能会变成第二个数据的孩子)。
  2. 分裂结点也有可能改变父子关系(变成父亲兄弟的孩子)

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

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

相关文章

李彦宏论AI:技术革新与产业价值的双重驱动

文章目录 每日一句正能量前言AI技术应用场景探索1. **医疗健康**2. **自动驾驶**3. **工业制造**4. **金融服务**5. **教育**6. **农业**7. **环境监测**8. **安全监控**9. **零售业**10. **艺术与娱乐** 避免超级应用陷阱的策略1. **明确应用目标**2. **优化用户体验**3. **注…

NFT革命:数字资产的确权、营销与元宇宙的未来

目录 1、NFT&#xff1a;数字社会的数据确权制度 2、基于低成本及永久产权的文化发现 3、PFP&#xff1a;从“小图片”到“身份表达”&#xff0c;再到社区筛选 4、透明表达&#xff1a;NFT 在数字化营销中的商业价值 5、可编程性&#xff1a;赋予 NFT 无限可能的应用 5.…

/秋招突击——7/21——复习{堆——数组中的第K大元素}——新作{回溯——全排列、子集、电话号码的字母组合、组合总和、括号生成}

文章目录 引言复习数组中的第K大的最大元素复习实现参考实现 新作回溯模板46 全排列个人实现参考实现 子集个人实现参考实现 电话号码的字母组合复习实现 组合总和个人实现参考实现 括号生成复习实现 总结 引言 昨天的科大讯飞笔试做的稀烂&#xff0c;今天回来好好练习一下&a…

git实操之线上分支合并

线上分支合并 【 1 】本地dev分支合并到本地master上 # 本地dev分支合并到本地master上# 远程(线上)分支合并# 本地dev分支合并到本地master上# 远程(线上)分支合并#####本地和线上分支同步################ #### 远程创建分支&#xff0c;拉取到本地####-远程创建分支&#…

服务攻防-应用协议cve

Cve-2015-3306 背景&#xff1a; ProFTPD 1.3.5中的mod_copy模块允许远程攻击者通过站点cpfr和site cpto命令读取和写入任意文件。 任何未经身份验证的客户端都可以利用这些命令将文件从文件系统的任何部分复制到选定的目标。 复制命令使用ProFTPD服务的权限执行&#xff0c;…

《2024 年 7 月 17 日最新开发者服务 API 推荐》

在当今的数字货币领域&#xff0c;对代币持有信息的精准洞察至关重要。而 Bitquery 代币持有信息查询 API 接口的出现&#xff0c;为开发者和投资者提供了强大的工具。无论是想要揭示代币趋势&#xff0c;检测虚假交易&#xff0c;发现热门代币&#xff0c;还是评估代币财富差距…

查找算法③-斐波那契查找算法/黄金分割查找算法

一、算法原理 斐波那契查找算法又称黄金分割查找算法&#xff0c;它是在二分查找基础上根据斐波那契数列进行分割的一种衍生算法&#xff0c;简单来说&#xff0c;二分查找是一分为二进行查找&#xff0c;斐波那契查找是使用斐波那契数列进行分割查找。而斐波那契数列就是我们通…

【Dison夏令营 Day 26】PyGame 中的赛车游戏

在本文中&#xff0c;我们将了解如何使用 Pygame 在 Python 中创建一个赛车游戏。在这个游戏中&#xff0c;我们将拥有驾驶、障碍物碰撞、通过关卡时速度增加、暂停、倒计时、记分牌和说明书屏幕等功能。 所需模块&#xff1a; 在继续之前&#xff0c;请在命令提示符下运行以下…

百科词条可以删除吗?删除百科词条的方法

大多时候大家都是想创建百度词条&#xff0c;然而有时候也会需要删除某些词条&#xff0c;因为其内容有错误、不实或者涉及某些敏感信息。但是百科词条删除需要非常明确的理由&#xff0c;不然也是很难通过的&#xff0c;这里小马识途百科顾问先初步分享下删除百科词条的流程。…

一套C#语言开发的医学影像归档与通讯系统PACS源码,三甲以下医院都能满足

医学影像归档与通讯系统&#xff08;PACS&#xff09;系统&#xff0c;是一套适用于从单一影像设备到放射科室、到全院级别等各种应用规模的医学影像归档与通讯系统。PACS集患者登记、图像采集、存档与调阅、报告与打印、查询、统计、刻录等功能为一体&#xff0c;有效地实现了…

Logstash docker发布

一 下载Logstash 不废话了&#xff0c;我下载的7.17.6 二 新增配置文件 在logstash/pipeline中&#xff0c;添加logstash.conf input {jdbc { # 连接jdbc_connection_string > "jdbc:mysql://192.168.1.1:3306/kintech-cloud-bo&#xff1f;characterEncodingUTF-8&…

【Linux网络】套接字编程

本篇博客整理了 socket 套接字编程的相关内容&#xff0c;包括 socket 网络通信原理、socket 相关的系统调用接口等&#xff0c;分别演示了基于UDP协议、TCP协议的 socket 网络编程&#xff0c;旨在让读者更加深入理解网络通信原理和设计&#xff0c;对网络编程有初步的认识和掌…

ECCV2024中有哪些值得关注的扩散模型相关的工作?

Diffusion Models专栏文章汇总:入门与实战 The Fabrication of Reality and Fantasy: Scene Generation with LLM-Assisted Prompt Interpretation 本文探讨了如何利用扩散模型生成需要艺术创造力或专业知识的复杂和富有想象力的图像提示。提出了一个新颖的评估框架RealisticF…

VulnHub:insomnia

靶机下载地址 信息收集 主机发现和端口扫描 攻击机网段192.168.31.0/24。 # 主机发现 nmap 192.168.31.0/24 -Pn -T4 # 靶机ip:192.168.31.207 端口扫描 nmap 192.168.31.207 -A -p- -T4 经过nmap扫描发现目标主机有http服务&#xff0c;端口是8080。 目录扫描 访问http…

Android RSA 加解密

文章目录 一、RSA简介二、RSA 原理介绍三、RSA 秘钥对生成1. 密钥对生成2. 获取公钥3. 获取私钥 四、PublicKey 和PrivateKey 的保存1. 获取公钥十六进制字符串1. 获取私钥十六进制字符串 五、PublicKey 和 PrivateKey 加载1. 加载公钥2. 加载私钥 六、 RSA加解密1. RSA 支持三…

YOLOv2小白精讲

YOLOv2是一个集成了分类和检测任务的神经网络&#xff0c;它将目标检测和分类任务统一在一个单一的网络中进行处理。 本文在yolov1的基础上&#xff0c;对yolov2的网络结构和改进部分进行讲解。yolov1的知识点可以看我另外一篇博客&#xff08;yolov1基础精讲-CSDN博客&#xf…

MySQL的索引、事务

MySQL的索引 索引的概念 索引是一个排序的列表&#xff0c;在列表当中存储索引的值以及索引值对应数据所在的物理行。 索引值和数据是一一映射的关系。 索引的作用 使用索引之后就不需要扫描全表来定位某行的数据 加快数据库查询的速度。 索引可以是表中的一列也可以是多…

Dify中接入GPT-4o mini模型

GPT-4o mini模型自己承认是基于GPT-3.5架构的模型&#xff0c;有图有真相&#xff1a; 一.GPT-4o mini官网简介 GPT-4o mini&#xff08;“o"代表"omni”&#xff09;是小型型号类别中最先进的型号&#xff0c;也是OpenAI迄今为止最便宜的型号。它是多模态的&#x…

C++11: auto 关键字

目录 **前言****1. 推导规则****2. 不能使用 auto 的场景****3. 常见的使用场景** 前言 在 C11 以前&#xff0c;auto 关键字的语义继承自 C 语言&#xff0c;表示 进入块后&#xff0c;自动分配内存&#xff0c;即分配堆栈内存。也就是说 auto 只能用于函数内&#xff0c;然而…

昇思25天学习打卡营第14天|基于MindNLP+MusicGen生成自己的个性化音乐

MusicGen是由Meta AI的团队开发出的一种音乐生成模型&#xff0c;它用一个语言模型来根据文本描述或音频提示制作音乐。这个模型分三步工作&#xff1a;首先&#xff0c;把用户给的文本转换成一系列状态&#xff1b;然后&#xff0c;用这些状态来预测音乐的音频token&#xff1…