【数据结构与算法】B_树

news2024/11/17 11:34:12

目录

前言:

一、B树

1、B树概念

2、B树查找

3、B树插入

4、B树前序遍历

5、B树性能

二、B+、B*树

1、B+树概念

2、B+树的插入

2、B*树概念

3、总结

三、B系列树的应用

总结


前言:

我们已经有很多索引的数据结构了

例如:

顺序查找 、二分查找 、二叉搜索树 、二叉平衡树(AVL树和红黑树) 、哈希
以上结构适合于数据量相对较小,能够一次性存放在内存中,进行数据查找
如果数据量很大,一次性无法放进内存中,那么只能存放到磁盘上,如果要进行搜索,只能将关键字映射的数据的地址存放到搜索树的节点中,要访问数据,就要先去磁盘中读取
磁盘的速度是远低于内存的,虽然平衡搜索树的时间复杂度是O(log H)(H是高度)
100亿个数据也就仅仅需要查找10次,但是这是在内存的情况,10次IO的速度和在内存中查找10次
的速度相差十分的大
哈希表虽然能够达到O(1)但是极端情况下哈希冲突非常严重,效率下降很多
所以为了解决大量数据的查询,在平衡搜索树的基础上提出了B树
B树主要进行了两点优化
1、压缩高度,二叉树变成多叉树
2、一个节点里面有多个关键字及映射的值

一、B树

1、B树概念

1970年,R.Bayer和E.mccreight提出了一种适用于外查找的树,它是一种平衡的多叉树,称为B树(或B-树、B_树)。

一棵m阶B树(balanced tree of order m)是一棵平衡的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
根至少有两个孩子的原因会在分裂时讲解
关键字的个数比孩子的数量少一个
例如M = 10
最少有 4个关键字, 5个孩子

最多有9个关键字,10个孩子

向上取整的原因会在插入时说明

同时每一层的关键字按特定的顺序排列(升序,降序,可以使用仿函数来控制)

 

2、B树查找

还是以上图为例,现在要找53

寻找的思路与二叉搜索树类型,不过这次是在一个数组中寻找,

它既要在纵向上搜索,也要在横向上搜索

先在横向搜索,从左向右遍历找到与它的key相等的节点,如果不相等比它小,就继续向右遍历

比它小就到下一层寻找,直到找到该节点或者将整棵树遍历完都没有找到

为了提高效率,因为对于每一个节点来说,它是有序的,可以使用二分查找提高查找效率

同时为了方便实现Insert,我们将Find的返回值类型定义为pair<Node*, int>

如果能够找到就返回该节点的地址,及它在该节点位置的下标

找不到就返回它的父节点及-1,可以根据pair的second来判断是否找到该节点

找到它的父节点就可以方便进行插入了

   // 顺序遍历
    std::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 std::make_pair(cur, i);
                }
            }
            parent = cur;
            cur = cur->_subs[i];
        }
        return std::make_pair(parent, -1);
    }

    // 二分查找
    std::pair<Node *, int> Find(const K &key)
    {
        Node *cur = _root;
        Node *parent = nullptr;
        while (cur)
        {
            int left = 0;
            int right = cur->_n - 1;
            while (left <= right)
            {
                int mid = (left + right) >> 1;
                if (key < cur->_keys[mid])
                {
                    right = mid - 1;
                }
                else if (key > cur->_keys[mid])
                {
                    left = mid + 1;
                }
                else
                {
                    return std::make_pair(cur, mid);
                }
            }
            parent = cur;
            cur = cur->_subs[left];
        }
        return std::make_pair(parent, -1);
    }

3、B树插入

B树的插入,如果不考虑分裂是比较简单的

bool Insert(const K &key, const V &val)
    {
        if (_root == nullptr)
        {
            _root = new Node;
            _root->_keys[0] = key;
            _root->_val[0] = val;
            _root->_n++;

            return true;
        }

 

        return true;
    }

接下来看具体的插入过程

 

 

 

 

 

 

 

 

 

 

 

   void InsertKey(Node *node, const K &key, const V &val, Node *child)
    {
        int end = node->_n - 1;
        while (end >= 0)
        {
            if (key < node->_keys[end])
            {
                node->_keys[end + 1] = node->_keys[end];
                node->_val[end + 1] = node->_val[end];

                // child 也要对应上
                node->_subs[end + 2] = node->_subs[end + 1];

                end--;
            }
            else
            {
                break;
            }
        }

        // 先插入key
        node->_keys[end + 1] = key;
        node->_val[end + 1] = val;

        // 最后插入child,关键字比节点数少一
        node->_subs[end + 2] = child;
        if (child)
        {
            child->_parent = node;
        }
        node->_n++;
    }

   bool Insert(const K &key, const V &val)
    {
        if (_root == nullptr)
        {
            _root = new Node;
            _root->_keys[0] = key;
            _root->_val[0] = val;
            _root->_n++;

            return true;
        }

        // 插入节点过程
        K newKey = key;
        V newVal = val;
        Node *child = nullptr;
        std::pair<Node *, int> ret = Find(newKey);
        //说明已经存在该节点了,不用插入
        if (ret.second >= 0)
        {
            return false;
        }

        Node *parent = ret.first;
        while (true)
        {
            InsertKey(parent, newKey, newVal, child);
            if (parent->_n < M)
            {
                return true;
            }

            // B树满了,开始分裂,创建新节点,并且将原节点的一半拷贝给brother
            size_t mid = M / 2;
            Node *brother = new Node;
            size_t j = 0;
            size_t i = mid + 1;
            for (; i < M; i++)
            {
                brother->_keys[j] = parent->_keys[i];
                brother->_val[j] = parent->_keys[i];
                brother->_subs[j] = parent->_subs[i];

                // parent->child->parent = brother
                if (parent->_subs[i])
                {
                    parent->_subs[i]->_parent = brother;
                }

                // 处理干净,指针必须处理,val可以不处理
                parent->_keys[i] = K();
                parent->_val[i] = V();
                parent->_subs[i] = nullptr;

                j++;
            }
            // child比key多一个,处理最后的右子树
            brother->_subs[j] = parent->_subs[i];
            if (parent->_subs[i])
            {
                parent->_subs[i]->_parent = brother;
            }
            parent->_subs[i] = nullptr;

            brother->_n = j;
            parent->_n -= j + 1; // 还有一个节点给了parent

            K midKey = parent->_keys[mid];
            K midVal = parent->_val[mid];

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

            //说明刚才分裂的是根节点
            if (parent->_parent == nullptr)
            {
                _root = new Node;
                _root->_keys[0] = midKey;
                _root->_val[0] = midVal;
                _root->_subs[0] = parent;
                _root->_subs[1] = brother;
                _root->_n = 1;

                parent->_parent = _root;
                brother->_parent = _root;
                break;
            }
            else
            {
                // 转化为parent->parent 中插入 newKey和brother
                newKey = midKey;
                newVal = midVal;

                child = brother;
                parent = parent->_parent;
            }
        }

        return true;
    }

4、B树前序遍历

它的前序遍历就是多叉树的前序遍历,同时不要忘记最后的右子树

   void _InOrder(Node *root)
    {
        if (root == nullptr)
        {
            return;
        }
        //左树,根,左树,根,左树,根……
        size_t i = 0;
        for (; i < root->_n; i++)
        {
            _InOrder(root->_subs[i]);
            std::cout << "Key  " << root->_keys[i] << " : "
                      << "val  " << root->_val[i] << std::endl;
        }

        //右数
        _InOrder(root->_subs[i]);
    }

    void InOrder()
    {
        _InOrder(_root);
    }

5、B树性能

对于一棵节点为N度为M的B-树,查找和插入需要log{M-1}N~log{M/2}N次比较,这个很好证明:
对于度为M的B-树,每一个节点的子节点个数为M/2 ~(M-1)之间,因此树的高度应该在要 log{M-1}Nlog{M/2}N之间,在定位到该节点后,再采用二分查找的方式可以很快的定位 到该元素
B-树的效率是很高的,对于N = 62*1000000000个节点,如果度M为1024,则log_{M/2}N <=
4,即在620亿个元素中,如果这棵树的度为1024,则需要小于4次即可定位到该节点,然后利用
二分查找可以快速定位到该元素,大大减少了读取磁盘的次数

二、B+、B*树

1、B+树概念

B+ 树是 B 树的变形,是在 B 树基础上优化的多路平衡搜索树, B+ 树的规则跟 B 树基本类似,但是又
B 树的基础上做了以下几点改进优化:
1、分支节点的子树指针与关键字个数相同
2、  分支节点的子树指针 p[i] 指向关键字值大小在 [k[i] k[i+1]) 区间之间
3、所有叶子节点增加一个链接指针链接在一起
4、  所有关键字及其映射数据都在叶子节点出现

总结:

分支节点跟叶子节点有重复的值,分支节点存的是叶子结点的索引

父亲中存的是孩子节点中的最小值做索引

B+树特性:

所有关键字都出现在叶子节点的链表中,且链表中的节点都是有序的

在分支节点无法获取value

分支节点相当于是叶子节点的索引,叶子节点才是存储数据的

 

2、B+树的插入

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

 

 

 

 

 

 

2、B*树概念

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

 

B*树主要是为了弥补B+树的空间利用率低的缺点

3、总结

通过以上介绍,大致将B树,B+树,B*树总结如下:
B树:有序数组+平衡多叉树;
B+树:有序数组链表+平衡多叉树;
B*树:一棵更丰满的,空间利用率更高的B+树

三、B系列树的应用

B-树最常见的应用就是用来做索引。索引通俗的说就是为了方便用户快速找到所寻之物,比如:
书籍目录可以让读者快速找到相关信息,hao123网页导航网站,为了让用户能够快速的找到有价
值的分类网站,本质上就是互联网页面中的索引结构。
MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的数据结构,简单来说:
索引就是数据结构
当数据量很大时,为了能够方便管理数据,提高数据查询的效率,一般都会选择将数据保存到数
据库,因此数据库不仅仅是帮助用户管理数据,而且数据库系统还维护着满足特定查找算法的数
据结构,这些数据结构以某种方式引用数据,这样就可以在这些数据结构上实现高级查找算法,
该数据结构就是索引。


总结


以上就是今天要讲的内容,本文仅仅简单介绍了B树的相关概念

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

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

相关文章

CAD外部参照文件的分解

最近遇到一个编图要求&#xff1a; “图纸文件的内容主要由模型空间和布局空间内的信息组成&#xff0c;尽量减少外部参照的使用。” 我们的综合图分幅主要依照外部参照来的&#xff0c;图件的本体只有1个&#xff0c;分幅图中只有布局试图有点线面等实体存在&#xff0c;模型…

阿里二面:用过GC日志可视化工具进行JVM调优吗?

上周有个小伙伴面了阿里&#xff0c;在二面中被问到GC日志分析&#xff0c;感觉回答的不是很好&#xff0c;过来找我复盘&#xff0c;大致听了他的回答&#xff0c;虽然回答出了部分&#xff0c;但是没抓到重点。 GC日志分析算是JVM调优中比较难的部分&#xff0c;今天这篇文章…

0123 双指针 Day12

剑指 Offer 25. 合并两个排序的链表 输入两个递增排序的链表&#xff0c;合并这两个链表并使新链表中的节点仍然是递增排序的。 示例1&#xff1a; 输入&#xff1a;1->2->4, 1->3->4 输出&#xff1a;1->1->2->3->4->4 /*** Definition for si…

安科瑞嵌入式多功能计量表AEM96 精度0.5S级 2-31次分次谐波

安科瑞 王晶淼/刘芳 一、产品概述 AEM系列三相嵌入式电能计量表是一款主要针对电力系统、工矿企业、公用设施的电能统计、管理需求而设计的智能电能表&#xff0c;集成三相电力参数测量及电能计量与考核管理&#xff0c;提供上24时&#xff0c;上31日以及上12月的电能数据统计…

DNS 区域传送漏洞(dns-zone-tranfer)学习

DNS 区域传送漏洞&#xff08;dns-zone-tranfer&#xff09;学习 ———— 相关知识理解 DNS&#xff08;域名系统&#xff09;就像一个互联网电话簿。它负责将人类可读的主机名解析为机器可读的 IP 地址。 DNS服务器分为主服务器&#xff0c;备份服务器&#xff0c;缓存服务…

【Docker】多个容器和宿主机之间如何进行数据同步和数据共享?容器数据卷从介绍到常用操作图文教程

专栏往期文章 《Docker是什么&#xff1f;Docker从介绍到Linux安装图文详细教程》《30条Docker常用命令图文举例总结》《Docker如何构建自己的镜像&#xff1f;从镜像构建到推送远程镜像仓库图文教程》 前言 你是否担心 Docker 容器被删除后&#xff0c;容器内的重要数据就丢…

VFIDILDKVENAIHNAAQVGIGFAKPFEKLINPK,果蝇抗菌肽

果蝇抗菌肽是一种含有Lys的抗菌多肽&#xff0c;序列中包含34个氨基酸&#xff0c;是一种含有α-折叠的抗菌多肽。 编号: 223981中文名称: 果蝇抗菌肽&#xff0c;Andropin英文名: Antimicrobial Peptide Andropin单字母: H2N-VFIDILDKVENAIHNAAQVGIGFAKPFEKLINPK-OH三字母: H2…

自然语言处理(NLP)数据集汇总 3(附下载链接)

&#x1f384;&#x1f384;【自然语言处理NLP】简介 &#x1f384;&#x1f384; 自然语言处理(Natural Language Processing, NLP)是计算机科学领域与人工智能领域中的一个重要方向。它研究能实现人与计算机之间用自然语言进行有效通信的各种理论和方法。自然语言处理是一门…

Allure:根据step动态设置description

背景 使用pytestAllure进行自动化的时候,为了报告展示更明确会使用 with allure.step(xxx)和 allure.step(xxx)测试结束后就可以看到 测试步骤 Allure还支持配置Description 之前直接在case中编写,例如 """ [用例描述]: 专家问诊 [前置步骤]:1. 打开h5页面…

ScheduledExecutorService的使用及守护线程

只运行一次 private static ScheduledExecutorService scheduler; public static void main(String[] args) throws Exception { scheduler Executors.newScheduledThreadPool(5); // 循环任务&#xff0c;按照上一次任务的发起时间计算下一次任务的开始时间 scheduler.schedu…

解决mysql存储emoji表情唯一索引报错问题

问题发现&#xff1a; 1、正常上班的一天&#xff0c;突然间有运营同事反馈&#xff0c;我们在添加数据的时候&#xff0c;发现添加了&#x1f438;之后&#xff0c;对应的&#x1f4a9;没有了&#xff0c;添加了&#x1f4a9;然后&#x1f438;就没有了&#xff0c;需要研发帮…

【Linux】四、Linux 进程概念(四)|进程地址空间

目录 十、进程地址空间 10.1 回顾C/C 地址空间 10.2 测试 10.3 感性理解虚拟地址空间 10.4 如何画大饼&#xff1f; 10.5 如何理解区域划分和区域调整 10.6 虚拟地址空间、页表和物理地址 10.7 为什么存在地址空间 10.7.1 保证物理内存的安全性 10.7.2 保证进程的独立…

铁蛋白-海藻酸钠纳米包埋ACE抑制肽|海藻酸钠修饰碳纳米管(SAL-MWNTs)

铁蛋白-海藻酸钠纳米包埋ACE抑制肽|海藻酸钠修饰碳纳米管(SAL-MWNTs) 铁蛋白-海藻酸钠纳米包埋ACE抑制肽产品描述&#xff1a;利用铁蛋白在较酸条件下可逆组装特性和海藻酸钠(sodium alginate,SA)的控释作用,以马脾脱铁铁蛋白(horse spleen apoferritin,HSF)和SA作为纳米载体,…

Rust 基础(八)—— 高级特性

十九、高级特性 到目前为止&#xff0c;您已经学习了Rust编程语言最常用的部分。在我们开始第20章的下一个项目之前&#xff0c;我们先来看一下你可能偶尔会碰到&#xff0c;但不是每天都在使用的语言的一些方面。当你遇到任何未知的情况时&#xff0c;你可以使用这一章作为参…

[毕业设计]2022-2023年最新最全计算机专业毕设选题推荐汇总

目录 ①javaweb信息管理系统或web应用选题(应用开发类) ②微信小程序开发方向 ③游戏动画、数字媒体方向 ④深度学习、机器学习方向 ⑤算法研究方向 ⑥物联网、嵌入式方向 ⑦信息安全、网络安全 ⑧大数据分析、大数据预测 ⑨Matlab 选题迷茫 选题的重要性 选题指导 对毕…

Springboot利用Security做OAuth2资源服务器

Springboot利用Security做OAuth2授权验证_LO嘉嘉VE的博客-CSDN博客_springbootsecurity oauth2 密码认证 验证服务器在上一篇文章中. 验证服务器是做权限验证&#xff0c;有没有登录&#xff0c;有没有权限访问某些内容等。资源服务器指提供业务功能的服务器&#xff0c;他们…

使用idea创建springboot项目

一、第一种创建方法 点击File——>New——>Project 接下来选择空项目&#xff0c;如下图所示&#xff1a; 接下来点击Next——>项目名称——>finish 点击finish后会弹出如下图&#xff0c;让你新建一个Moudles 如下图&#xff0c; 在项目结构里面点击Modules——…

Kubeadm 部署 k8s 集群

目录 1.初始化设置 2.所有节点安装docker 3.k8s节点安装kubeadm&#xff0c;kubelet和kubectl 4.部署K8S集群 5.部署Dashboard 6.部署harbor私有仓库 名称设置组件master192.168.116.70&#xff08;2C/4G&#xff0c;cpu核心数要求大于2&#xff09;docker、kubeadm、ku…

南开大学程明明-学术规范及论文写作指导

第一讲 学术规范与论文写作-写作规范 主要内容 why writing is important 导致剽窃的原因&#xff08;引用不当也会导致&#xff09;&#xff1a; 引用不是介绍别人的工作&#xff0c;而是更加清晰介绍自己的工作 第二讲 学术规范与论文写作-WrittingTips 各种工具的使用 Ove…

s5pv210 i2c 时序

1 低层时序 ①. 底层时序 ******** 低层时序&#xff1a; ①.空闲&#xff1a; scl clk都是高电平②.起始位&#xff1a; 一个时间段&#xff0c;这个段时间内&#xff0c;SCL高电平&#xff0c; SDA 出现下降沿 &#xff0c; 接收方收到以后&#xff0c;知道了&#xff0c;…