面试常考数据结构:红黑树、B树、B+树各自适用的场景

news2025/2/26 18:50:43

1. 磁盘基础知识

  • 分页:

现代操作系统都使用虚拟内存来印射到物理内存,内存大小有限且价格昂贵,所以数据的持久化是在磁盘上。虚拟内存、物理内存、磁盘都使用页作为内存读取的最小单位。一般一页为4KB(8个扇区,每个扇区512B,8*512B=4KB)。

  • 局部性原理:

  1. 当一个数据被用到时,其附近的数据也通常会马上被使用。

  2. 程序运行期间所需要的数据通常比较集中。

  • 磁盘预读原理:

磁盘读取依靠的是机械运动,分为寻道时间、旋转延迟、传输时间三个部分,这三个部分耗时相加就是一次磁盘IO的时间,大概 9ms 左右。这个成本是访问内存的十万倍左右;

磁盘读取的速度远小于内存,所以尽量减少 I/O 次数是提高效率的关键。

根据局部性原理,且由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),所以即使只需要读取一个字节,磁盘也会读取一页的数据。即磁盘预读时通常会读取页的整倍数。

2. 树基础知识回顾

排序二叉树:左 < 跟 < 右 B 树:有序数组 + 多叉平衡树,节点存储关键字、数据、指针; B+ 树:有序数组链表 + 多叉平衡树,非叶子节点存储指针、关键字,不存储数据; 红黑树:红黑树是一种不大严格的平衡树(平衡树要求太高)

平衡树是为了防止二叉查找树退化为链表,而红黑树在维持平衡以确保 O(log2(n)) 的同时,不需要频繁着调整树的结构;

二叉树的存储结构

  1. 顺序存储(适用于完全二叉树)

index 之间的对应关系:

注意:二叉树的顺序存储只适合存储完全二叉树,否则 index 无法和节点对应起来,会有点恶心:

  1. 链式存储

这里要好好理解一下,不然会影响后面的理解。

相关视频推荐

4种红黑树的使用场景,从linux内核到应用开发(epoll、sk_buff、虚拟内存管理、nginx流量监控)

90分钟搞定红黑树应用

后端开发必学4种层式结构:B+/B-树、时间轮、跳表、LSM-Tree

免费学习地址:c/c++ linux服务器开发/后台架构师

需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

3. 为什么不能使用二叉树来存储数据库索引

先说结论:

  1. 平衡二叉树进行插入/删除时,大概率需要通过左旋/右旋来维持平衡;

  2. 旋转需要加载整个树,频繁旋转效率低;

  3. 二叉树的 I/O 次数近似为 O(log2(n));

  4. 范围查询时,二叉树的时间复杂度会退化成 O(n);

  5. 二叉树退化成链表时,时间复杂度也近似退化成了 O(n);

  6. 二叉树无法使用磁盘预读功能;

其实单论范围查询,在关系型数据库中就基本没有使用二叉树的可能了。但是为了加深对知识的了解,来看看其他的原因。

先剔除掉范围查询的情况,原因 1、2、6 可以通过红黑树来解决,那么其实就剩下 2 个原因:

  1. I/O 次数对比;

  2. 磁盘预读功能的利用;

4. 二叉树的 I/O 次数分析

先说 I/O 次数:

其实相比于二叉树,B 树、B+树, CPU 的运算次数并没有变化,甚至增多。但是 CPU 运算次数相比于 I/O 的消耗而言,可以忽略不计,所以 I/O 次数是评价一个数据库索引的效率高低的关键指标。

对于红黑树而言,其 I/O 次数近似为 log2(n),为什么是近似呢?

首先,索引是存储在磁盘上的,磁盘上的数据大部分情况下是连续的,但是随着增删改查的发生,有可能产生很多碎片,也就是说:

  • 索引在磁盘上的存储也不一定是连续的;

这里,严谨起见,我们来分两种情况:

  1. 索引节点,即树的节点在磁盘上存储是连续;

假设一个页能存储 5 个节点,假设二叉树如下:

注意,序号只代表在磁盘中存储的顺序,不代表对应节点的关键字的值;

二叉树可能是链式存储,也可能是顺序存储。但是这里假设节点在磁盘上的存储是连续的,所以这里可以近似理解成顺序存储。即使是链式存储,无非就是 pNext 指针指向下一个连续的内存地址而已。

现在假设搜索的结果是最左边的叶子节点 16,因为磁盘预读的特性,加上一个页能存储 5 个节点,第一次 I/O :

添加图片注释,不超过 140 字(可选)

如上,第一次 I/O 就读取了 5 个节点,不仅把根节点读取进内存了,还把节点 2 和 4 都读取进去了,看上去还节约了两次 I/O ?好厉害的样子……

此时,会根据二分法查找,对比 1 号节点然后去找节点 2,紧接着找节点 4,因为这两个节点都在内存中了,所以不需要进行 I/O

这里再说一次,序号不代表节点的关键字,而是单纯的表示节点在磁盘中的排列顺序;

紧接着,会需要 8 号节点,而 8 不再内存中,所以进行第二次 I/O 同样是读取一页,即 5 个节点:

这次虽然也是读取了 5 个节点,但是实际上只有 8 号节点有实际作用,其他节点并没什么卵用(这是二叉树无法使用预读功能的本质),但是现在还没体现出劣势,现在对比之后需要 16 号节点,继续第三次读取:

此时找到了 16,并将结果返回。

这是高度为 4 的情况,且只有 31 个数据。但是实际使用中,怎么可能就 31 个数据?假设要找的是 32 号节点,因为 16 号节点之后的 17-20 虽然被加载进内存了,但是完全没用。那么就需要再进行一次 I/O 来加载 32 号节点所在的页,同时也会将 33-36 加载进内存,但是这些节点并无卵用。

如果要找的是 1000 ,10000?

所以,随着层级的深入,会出现:

  1. 一个页中只有一个节点有用(二分法查找要的是子节点而不是兄弟节点);

  2. I/O 次数近似等于log2(n);

即:

  1. 第一次 I/O 可能的优势在层级加深之后就没有了;

  2. 就算是红黑树,也只能将时间复杂度维持在 log2(n);

上述讨论的是索引树在磁盘上的存储是连续的,如果不是连续的,那么按页读取到的脏数据会更多,上述的情况中,前几次 I/O 读取到有用的数据的概率会变低,所以 I/O 的次数只会增多而不会减少,即仍然是近似于 log2(n)。

5. B/B+树

B 树即:多路平衡查找树;

B 树的巧妙之处在于:

  1. 将一个节点的大小设置为一页的大小;

  2. 一个节点可以存放多个关键字(多叉树);

  3. 自平衡;

这 3 点结合起来就可以做到:

  1. 一个节点大小为一页,被加载进内存时,这些关键字在进行对比,找出需要 leftChild 还是 rightChild 时,都是有用的(如最右侧时需要对比所有节点);

  2. 一个节点可以存储多个关键字,有效降低了树的高度;

B+ 树的巧妙之处在于:

  1. 非叶子节点不存储数据,进一步增大了一页中存储关键字的数量;

  2. 叶子节点中存储数据且存在指向下一页的链表指针,可以使用顺序查询(支持范围查询);

6. B/B+树的索引数量

B 树的节点中存储:指针、关键字(主键)、数据 B+ 树的非叶子节点:指针、关键字 B+树的叶子节点:指针(链表)、关键字、数据

注意,这里不是绝对的,比如有的 B+ 树中叶子节点存储的不是数据,而是指向数据的指针。查询到指针之后再去对应地址取出数据,但是这样应该会增加一次 I/O 吧,应该也是在数据量和 I/O 次数之间做了取舍,具体先不讨论。

以 Sqlite3.12 之后为例,page_size = 16k,假设指针为 8 byte,假设关键字类型占 8 byte,假设数据占 1 KB;

B 树的一个节点:

一页能存储的数据量为:16kb / (1KB+8byte+8byte) ≈ 16;

高度为 3 的 B 树能存储 16 x16 x16 = 4096 条数据

相比于二叉树的 1 个而言,确实有效降低了树的层级。而且上述是假设数据为 1KB,如果数据没那么大,高度为 3 的 B 树能存储更多的数据,但是如果用在大型数据库索引上还是不够。

B+ 树:

如上图,B+树的核心在于非叶子节点不存储数据。

这样做可以减少非叶子结点占用的空间,增大一页所能存储的数据量,最大程度减少树的层级。

仍然是以上假设,假如树的高度为 3 ,那么就有两层存储关键字+指针,一层叶子节点来存储实际数据。

一页能存储的关键字为:16 * 1024 / (8 + 8) = 1024 一页能存储的数据量为:16KB / (1KB + 8byte + 8byte) = 16 (这里计算不完全准确,实际情况应该是1页数据中只有一个链表指针指向下一页) 能存储的关键字为:1024 * 1024 = 1048576;

因为端节点又有 1024 个指针,这些指针可以指向一个页,页中存储数据,也就是叶子节点,一页能存储 16 个叶子节点,所以总共能索引的数据量为 1048576 * 16 ≈ 1600万;如果高度为 4 ,则再乘以 1024 约为 17亿…..

上述推理中,理解终端节点的指针指向一个页,页中存储着关键字 + 数据 + 链表指针是关键。page 标记如下,有助理解:

虽然叶子节点很多,一个 page 对应一个叶子节点甚至是多个 page 才能存下一个叶子节点,但是这些是存在磁盘上的,找到对应的 page 之后才去加载对应的 page。索引超大数据量的同时,不会对 I/O 次数产生影响,这就是这个设计的牛逼之处。

但是这样也是有缺点的:

无论查询结果如何,都必须走到叶子节点才结束,也就是 I/O 次数固定为 O(h) 或者说是 log(n)(底数为节点子分支个数),这个 h 一般为 2-3,排除掉根节点常驻内存,高度为 3 的 B+ 树进行两次 I/O 就可以索引千万级别的数据,高度为 4 的 B+ 树,进行 3 次 I/O 就能索引十亿级别的数据量,这个效果还是很好的。

所以,这个缺点也可以说成是优点:稳定(稳如一条老狗🐶)

7. 实际应用

  • 红黑树优点

红黑树常用于存储内存中的有序数据,增删很快,内存存储不涉及 I/O 操作。

  • B/B+树的优点

更适合磁盘存储,减少了树的层级,进而减少 I/O 次数;

  • B 树和 B+ 树对比

都是 B 树,但是 B+树更适合范围查询,比如 Mysql,且查询次数很稳定,为 logn。而 B 树更适合键值对型的聚合数据库,比如 MongoDB,查询次数最优为 O(1);

红黑树更适合内存存储,B 树更适合键值对存储,B+ 树适合范围查询;

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

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

相关文章

eclipse 配置selenium环境

eclipse环境 安装selenium的步骤 配置谷歌浏览器驱动 Selenium安装-如何在Java中安装Selenium chrome驱动下载 eclipse 启动配置java_home&#xff1a; 在eclipse.ini文件中加上一行 1 配置java环境&#xff0c;网上有很多教程 2 下载eclipse&#xff0c;网上有很多教程 ps&…

掌握Python爬虫实现网站关键词扩展提升曝光率

在如今激烈竞争的网络世界中&#xff0c;如何提升网站的搜索曝光率成为了每个站长和营销人员都关注的重要问题。在这方面&#xff0c;Python爬虫可成为您的得力助手&#xff0c;通过扩展网站关键词&#xff0c;更好地满足用户搜索需求&#xff0c;提升网站在搜索引擎中的曝光率…

小视频APP源码定制化攻略:让你的短视频应用独树一帜

在当今流行的短视频应用市场中&#xff0c;为了突出个性和赢得用户青睐&#xff0c;许多人采用定制化小视频APP源码的方式&#xff0c;以创建独特的平台。本文将教你如何定制化小视频APP源码&#xff0c;让你的短视频应用在竞争激烈的市场中独树一帜。 1. 理解小视频APP源码的…

PyTorch 深度学习之多分类问题Softmax Classifier(八)

1. Revision: Diabetes dataset 2. Design 10 outputs using Sigmoid? 2.1 Output a Distribution of prediction with Softmax 2.2 Softmax Layer Example, 2.3 Loss Function-Cross Entropy Cross Entropy in Numpy Cross Entropy in PyTorch 注意交叉熵损失&#xff0c;最…

Openstack部署

搭建基础环境 #网络 #防火墙 #用户用 #解析 #同步时间 实验角色 OpenStack01OpenStack02OpenStack03192.168.1.101192.168.1.102192.168.1.103srv1srv2srv3 同步时间 [rootsrv1]# yum install chrony -y [rootsrv1]# vim /etc/chrony.conf # 修改第3行&#xff0c;将NT…

铅华洗尽,粉黛不施,人工智能AI基于ProPainter技术去除图片以及视频水印(Python3.10)

视频以及图片修复技术是一项具有挑战性的AI视觉任务&#xff0c;它涉及在视频或者图片序列中填补缺失或损坏的区域&#xff0c;同时保持空间和时间的连贯性。该技术在视频补全、对象移除、视频恢复等领域有广泛应用。近年来&#xff0c;两种突出的方案在视频修复中崭露头角&…

不同类型的球幕影院对观影体验有何影响?

随着各类投影技术在内容展示场所中的广泛应用&#xff0c;使大众看到了投影技术形式的魅力&#xff0c;其中在内容展示场所中基于球幕投影技术布置的多媒体展项&#xff0c;所表现出来的沉浸式观影体验&#xff0c;更是让人眼前一亮&#xff0c;不过随着各类主题场所对它的呈现…

Unity 表面凝结水珠效果(无需编码案例分享)

文章目录 前言正文1、准备工作2、瓶子的表面会先慢慢变白3、身周围的物体的反射会慢慢变得模糊4、周围的反射又会慢慢的变回清晰5、出现一个个不跪着的小水珠 结语 前言 今天跟大家分享一一个非常简单的使用Unity制作出在物体便面凝结小水珠的过程动画&#xff0c;老规矩先上图…

Java 中 Volatile 关键字

基本概念 补充一下 java 内存模型中的 可见性、原子性和有序性 可见性&#xff1a; 指的是线程之间的可见性&#xff0c;一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果&#xff0c;另一个线程马上可以看到。比如 &#xff1a;用 volatile 修饰的变量&am…

程序员自由创业周记#13:第一桶金

国庆假期 对于我而言是没有放假的概念的&#xff0c;可以说每天都是假期&#xff0c;但是孩子放假就不能做软件了&#xff0c;得陪着他&#xff0c;尤其他生了半个月的病&#xff0c;隔三差五就得去医院排两小时队看个医生&#xff0c;周记因此耽搁了两次。没有看到我的更新不…

【Java每日一题】——第二十九题:超市购物程序设计(2023.10.13)

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

二十一、动态内存管理

1 动态内存开辟 目前为止&#xff0c;我们已经掌握的内存开辟方式有&#xff1a; int val 20;//在栈空间上开辟四个字节 char arr[10] {0};//在栈空间上开辟10个字节的连续空间上述开辟空间的方式其实有两个缺点&#xff1a; 空间开辟的大小是固定的&#xff0c;一旦开辟就…

go语言基础之变量

目录 视频学习地址&#xff1a;Go零基础入门_在线视频教程-CSDN程序员研修院 一. 单变量声明和赋值 1、变量的声明 2、变量赋值 3、声明并赋值 二. 多变量声明和赋值 1、多变量声明 2、多变量赋值 三. 变量声明赋值的简易写法 1、单变量简易写法 2、多变量简易写法 …

14. SpringBoot项目之数据保存到数据库

SpringBoot项目之数据保存到数据库 1. 创建数据库&表 本栏目前面文章中已经讲过MySql连接SqlYog以及创建数据库&表 —>传送门 2. 导入ORM依赖&配置 在pom.xml中导入 要更新maven环境 <dependency><groupId>org.mybatis.spring.boot</groupI…

十个面试排序算法

一、 前言 最常考的是快速排序和归并排序&#xff0c;并且经常有面试官要求现场写出这两种排序的代码。对这两种排序的代码一定要信手拈来才行。还有插入排序、冒泡排序、堆排序、基数排序、桶排序等。面试官对于这些排序可能会要求比较各自的优劣、各种算法的思想及其使用场景…

Python爬虫(二十三)_selenium案例:动态模拟页面点击

本篇主要介绍使用selenium模拟点击下一页&#xff0c;更多内容请参考:Python学习指南 #-*- coding:utf-8 -*-import unittest from selenium import webdriver from selenium.webdriver.common.keys import Keys from bs4 import BeautifulSoup import timeclass douyuSelenium…

hive 之select 中文乱码

此处的中文乱码和mysql的库表 编码 latin utf 无关。 直接上案例。 有时候我们需要自定义一列&#xff0c;有时是汉字有时是字母&#xff0c;结果遇到这种情况了。 说实话看到这真是糟心。这谁受得了。 单独select 没有任何问题。 这是怎么回事呢&#xff1f; 经过一番检查&…

记录遇到的前端面试题,欢迎指正

css 1.隐藏元素的方式 转自将页面元素隐藏的10种方法 display:none display属性用于设置页面元素的显示方式&#xff0c;能够控制元素的显示或者隐藏&#xff0c;当他的值被设置为none时&#xff0c;就会隐藏对应的元素&#xff0c;使其不可见。 这种方法是最常用的隐藏元素的…

uboot启动流程-board_init_r函数执行过程

一. uboot启动流程 本文来了解 board_init_r 函数执行过程。_main函数会调用到 board_init_r 函数。 二. board_init_r函数执行过程 _main 函数会调用到 board_init_r 函数。 _main 函数在 uboot的 /arch/arm/lib/crt0.S 文件中。_main函数中&#xff0c;执行完 relocate_…

SLAM从入门到精通(ROS网络通信)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 之前我们学习ros的时候&#xff0c;大部分都是基于仿真来做的。但是最终&#xff0c;我们还是要把ros部署到小车上的。这就带来一个问题&#xff0…