数据结构与算法笔记:基础篇 - 红黑树(上):为什么工程中都用红黑树这种二叉树?

news2024/12/23 18:24:23

概述

上两篇文章,我们依次讲解了树、二叉树、二叉查找树。二叉查找树是最常用的一种二叉树,它支持快速插入、删除、查找操作,各个操作的时间复杂度跟树的高度成正比,理想情况下,时间复杂度是 O ( l o g n ) O(logn) O(logn)

不过,二叉查找树在频繁的动态更新过程中,可能会出现树的高度远大于 l o g 2 n log_2n log2n 的情况,从而导致哥哥操作的效率下降。极端情况下,二叉树会退化为链表,时间复杂度会退化到 O ( n ) O(n) O(n)。上篇文章说了,要解决这个复杂度退化的问题,我们需要设计一种平衡二叉树,这也是本章要将的这种数据结构。

很多书籍,但凡讲到平衡二叉查找树,就会拿红黑树作为例子。不仅如此,如果你有一定的开发经验,你会发现,在工程中,很多用到平衡二叉查找树的地方都会用红黑树。你有没有想过,为什么工程中都喜欢用红黑树,而不是其他平衡二叉查找树呢?


什么是 “平衡二叉查找树”?

平衡二叉树的严格定义是这样的:二叉树中任意一个结点的左右子树的高度相差不能大于 1。从这个定义来看,上篇文章讲的完全二叉树、满二叉树其实都是平衡二叉树,但是非并完全二叉树也有可能是平衡二叉树。

在这里插入图片描述

平衡二叉查找树不仅满足上面平衡二叉树的定义,还满足二叉查找树的特点。最先被发明的平衡二叉查找树是 AVL 树,它严格符合刚刚讲到的平衡二叉查找树的定义,即任何子节点的左右子树的高度相差不超过 1,是一种高度平衡的二叉查找树。

但是很多平衡二叉查找树其实并没有严格符合上面的定义,所以,我感觉没必要去死抠定义。对于平衡二叉查找树这个概念,我觉得我们要从这个数据结构的由来,去 “理解” 平衡的意思。

发明平衡二叉查找树这类数据结构的初衷是,解决普通二叉查找树在频繁的插入、删除等动态更新的情况下出现时间复杂度退化的问题。

所以,平衡二叉查找树中 “平衡” 的意思,其实就是让整棵树左右看起来都比较 “对称”、比较 “平衡”,不要出现左子树很高、右子树很矮的情况。这样就能让整棵树的高度相对来说低一些,相应的插入、删除、查找等操作的效率高一些。

所以,如果我们现在设计一个新的平衡二叉查找树,只要树的高度不比 l o g 2 n log_2n log2n 大很多(比如书的高度仍然是对数量级的),尽管它不符合我们前面的严格的平衡二叉查找树的定义,但我们仍可以说,这是一个合格的平衡二叉查找树。

如何定义一棵 “红黑树”?

平衡二叉查找树其实有很多,比如 Splay Tree(伸展树)、Treap(树堆)等,但是,我们提到平衡二叉查找树,听到的基本都是红黑树。它的出镜率甚至要高于 “平衡二叉查找树” 这几个字,有时候,我们甚至默认平衡二叉查找树就是红黑树,那我们现在就来看看这个 “明星树”。

红黑树的英文是 “Red-Black Tree”,简称 R-B Tree。它是一种不严格的平衡二叉查找树,前面说了,它的定义是不严格符合平衡二叉查找树的定义的。那红黑树的究竟是怎么定义的呢?

顾名思义,红黑树中的节点,一类被标记为红色,一类被标记为红色。此外,一棵红黑树还要满足这样几个要求:

  • 根节点是黑色的
  • 每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不能存储数据
  • 任何相邻的节点不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
  • 每个节点,从该节点到达其可达的叶子节点的所有路径,都包含相同数目的黑色节点;

这里的第二点要求 “每个叶子节点都是黑色的空节点” ,稍微有些奇怪,它主要是为了简化红黑树的代码实现而设置的,下篇文章我们讲解红黑树的实现时会降到。本章暂时不考虑这一点,所以,在画图和讲解的时候,将黑色的、空的叶子节点都省略掉了。

下面画了两个红黑树的图例,你可以对照看下。

在这里插入图片描述

为什么说红黑树都是 “近似平衡” 的?

前面也讲到,平衡二叉查找树的初衷,是为了解决二叉查找树因为动态更新导致的性能退化问题。所以,“平衡” 的意思可以等价为性能不退化。 “近似平衡” 就等价为性能不会退化得太严重。

上篇文章讲过,二叉查找树很多操作的性能都跟树的高度成正比。一棵极其平衡的二叉树(满二叉树或者完全二叉树)的高度大约是 l o g 2 n log_2n log2n,所以如果要证明红黑树是近似平衡的,我们只需要分析,红黑树的高度是否比较稳定地趋近 l o g 2 n log_2n log2n 就好了。

红黑树的高度不是很好分析,我带你一步步来推导。

首先,我们来看,如果我们将红色节点从红黑树中去掉,那单纯包含黑色节点的红黑树的高度是多少呢?

红色节点删除之后,有些节点就没有父节点了,我们会直接拿这些节点的祖父节点(父节点的父节点)作为父节点。所以,之前的二叉树就变成了四叉树。

在这里插入图片描述

前面红黑树的定义里有这么一条:从任意节点到可达节点的叶子节点的每个路径包含相同数目的黑色节点。我们从四叉树中取出某些节点,放到叶子节点位置,四叉树就变成了完全二叉树。所以,仅包含黑色节点的四叉树的高度,比包含相同节点个数的完全二叉树的高度还要小。

上篇文章我们说,完全二叉树的高度近似 l o g 2 n log_2n log2n,这里的四叉 “黑树” 的高度要低于完全二叉树,所以去掉红色节点的 “黑树” 的高度也不会超过 l o g 2 n log_2n log2n

现在我们知道只包含黑色节点的 “黑树” 的高度,那我们现在把红色节点加回去,高度会变成多少呢?

从上面画的红黑树的例子和定义看,在红黑树中,红色节点不能相邻,也就是说,有一个红色节点就要至少一个黑色节点,将它跟其他红色节点隔开。红黑树中包含最多黑色节点的路径不会超过 l o g 2 n log_2n log2n,所以加入红色节点之后,最长路径不会超过 2 l o g 2 n 2log_2n 2log2n,也就是说,红黑树的高度近似 2 l o g 2 n 2log_2n 2log2n

所以,红黑树的高度之比高度平衡的 AVL 树( l o g 2 n log_2n log2n)仅仅大了一倍,在性能上,下降得并不多。这样推导出来的结果不够精确,实际上红黑树的性能更好。

为什么工程中都用红黑树这种二叉树?

刚刚提到了很多平衡二叉查找树,现在我们来看下,为什么工程中都用红黑树这种二叉树?

前面提到 Treap、Splay Tree,绝大部分情况下,它们的操作效率都很高,但是也无法避免极端情况下时间复杂度的退化。尽管这种情况出现的概率不大,但是对于单词操作时间非常敏感的场景来说,它们并不适用。

AVL 是一种高度平衡的二叉树,所以查找的效率非常高,但是,有利就有弊,AVL 树为了维持这种高度的平衡,就要付出更多的代价。每次插入、删除都要做调整,就比较复杂、耗时。所以,对于频繁插入、删除操作的集合,使用 AVL 树的代价就有点高了。

红黑树只是做到了近似平衡,并不是严格的平衡,所以在维护平衡的成本上,要比 AVL 树要低。

所以,红黑树的插入、删除、查找各种操作性能都比较稳定。对于工程应用来说,要面对各种异常情况,为了支撑这种工业级的应用,我们更倾向于性能稳定的平衡二叉查找树。

小结

很多同学觉得红黑树很难,的确,它算式最难掌握的一种数据结构。其实红黑树最难的地方是它的实现,在下一章节我们会专门讲解。

不过,我认为,我们其实不应该把学些的侧重点,放到它的实现上。

我们学习数据结构和算法,要学习它的由来、特性、适用场景以及它能解决的问题。对于红黑树,也不例外。你如果能搞懂这几个问题,其实就已经足够了。

红黑树是一种平衡二叉查找树。它是为了解决普通二叉查找树在数据更新的过程中,复杂度退化的问题而产生的。红黑树的高度近似 l o g 2 n log_2n log2n,所以它是近似平衡,插入、删除、查找操作的时间复杂度都是 O ( l o g n ) O(logn) O(logn)

因为红黑树是一种性能非常稳定的二叉查找树,所以,在工程中,但凡是用到动态插入、删除、查找数据的场景,都可以用到它。不过,它实现起来比较复杂,如果自己写代码实现,难度会有些高,这个时候,我们其实更倾向于用跳表来替代它。

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

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

相关文章

【PL理论】(16) 形式化语义:语义树 | <Φ, S> ⇒ M | 形式化语义 | 为什么需要形式化语义 | 事实:部分编程语言的设计者并不会形式化语义

💭 写在前面:本章我们将继续探讨形式化语义,讲解语义树,然后我们将讨论“为什么需要形式化语义”,以及讲述一个比较有趣的事实(大部分编程语言设计者其实并不会形式化语义的定义)。 目录 0x00…

第十四周 6.4 内部类部分知识点

一、理解 1.定义在一个类内部的类称为内部类 2.语法: class 类名{ class 类名{} } 3.内部类编译之后生成独立的.class文件,文件命名为:外部类类名$内部类的类名.class 4.内部类分类:成员内部类、静…

插卡式仪器模块:波形发生模块(插入式)

• 16 位分辨率 • 125 MHz 刷新率 • 支持生成 FSK/ASK 信号 • 生成任意标准波形或用户自定义波形 • 在特殊协议通信中模拟某个波形 • 无线充电(信号调制) 通道11输出阻抗Low-ZLow-Z输出范围 5 V 5 V耦合DCDC带宽4 MHz10 MHzADC 分辨率16 Bits1…

字符串常量池字符串常量的几种创建方式及其位置

从JDK7开始,字符串常量池被移到了堆区中,因此Java程序中的字符串常量对象要么在堆区的字符串常量池之中,要么在堆区的字符串常量池之外。为了做区分,下文将堆区的字符串常量池区域称为字符串常量池,将堆区字符串常量池…

汇编语言作业(六)

目录 一、实验目的 二、实验内容 三、实验步骤以及结果 四、实验结果与分析 五、实验总结 一、实验目的 掌握加减法运算指令对各状态标志位的影响及测试方法掌握汇编语言长整数的加法的操作方法 二、实验内容 对于以下几组数, 087H和034H 0C2H和5FH 0F3H和0F3H&am…

6.7 作业

搭建一个货币的场景,创建一个名为 RMB 的类,该类具有整型私有成员变量 yuan(元)、jiao(角)和 fen(分),并且具有以下功能: (1)重载算术运算符 和 -&#xff…

kettle从入门到精通 第六十七课 ETL之kettle 再谈kettle阻塞,阻塞多个分支的多个步骤

想真正学习或者提升自己的ETL领域知识的朋友欢迎进群,一起学习,共同进步。由于群内人员较多无法直接扫描进入,公众号后台加我微信入群,备注kettle。 场景:ETL沟通交流群内有小伙伴反馈,如何多个分支处理完…

C# 解决 Excel 自动适应列宽的问题

目录 问题现象 原因分析 范例运行环境 解决问题 生成测试文本 实现自适应 小结 问题现象 通过 COM 操作 Excel 自动适应列宽的方法是 AutoFit 方法,该方法适于自动适应列宽或行高。 最近在我们的一款应用里发现效果并没有符合预期,我们提供了一…

vAttention:用于在没有Paged Attention的情况下Serving LLM

文章目录 0x0. 前言(太长不看版)0x1. 摘要0x2. 介绍&背景0x3. 使用PagedAttention模型的问题0x3.1 需要重写注意力kernel0x3.2 在服务框架中增加冗余0x3.3 性能开销0x3.3.1 GPU上的运行时开销0x3.3.2 CPU上的运行时开销 0x4. 对LLM服务系统的洞察0x5…

VUE2.7项目配置webpack打包-详细操作步骤

一、Webpack简介 Webpack是一个打包工具,可以把JS、CSS、Node Module、Coffeescrip、SCSS/LESS、图片等都打包在一起,因此,现在几乎所有的SPA项目、JS项目都会用到Webpack。 官网:https://webpack.js.org GitHub为https://git…

全球溃败,苹果可能要全球大降价了,试图摆脱中国制造的后果

苹果一季度在中国市场的出货量暴跌,导致它不得不在中国市场大降价,从3月份就在中国市场大幅度降价,然而目前它在美国和欧洲两大市场也出现大幅衰退,降价可能将成为苹果在全球的举措。 市调机构Canalys公布的一季度数据显示&#x…

MySQL高性能(SQL性能分析)

MySQL性能系列 SQL性能分析 前言1.SQL执行评率2. 慢查询日志3. profile详情4. Explain执行计划4.1. Explain — id4.2. Explain — type4.3. Explain — table4.4. Explain — key 前言 本篇文章采用的MySQL版本是8代,同时自己使用的是Linux mysql8,本篇…

SOA的设计模式_3.微服务模式

SOA的架构中,复杂的ESB企业服务总线依然处于非常重要的位置,整个系统的架构并没有实现完全的组件化以及面向服务,它的学习和使用门槛依然偏高。而微服务不再强调传统SOA架构里面比较重的ESB企业服务总线,同时SOA的思想进入到单个业…

多态的应用——数组多态

介绍 ai查询 在Java中,动态数组通常通过ArrayList类来实现,它是Java集合框架(Java Collections Framework)的一部分。ArrayList是一个可调整大小的数组实现,提供了比标准数组更多的灵活性和功能。 以下是使用ArrayLis…

安装后或升级启智环境到飞桨2.6版本(develop)

启智社区的启智大脑调试环境非常好用,但是里面的飞桨环境版本比较低,为了能够运行大模型(llm),需要升级飞桨到2.6版本或者开发版本。 首先创建启智大脑调试环境,注意选cuda12.1的环境。 进入调试环境后&am…

Discuz! X3.4免备案无执照接入支付宝微信支付插件

下载地址:Discuz! X3.4免备案无执照接入支付宝微信支付插件 [充值会员]支付宝当面付版 微信支付

MySQL普通表转换为分区表实战指南

码到三十五 : 个人主页 引言 本文将详细指导新手开发者如何将MySQL中的普通表转换为分区表。分区表在处理庞大数据集时展现出显著的性能优势,不仅能大幅提升查询速度,还能有效简化数据维护工作。通过掌握这一技巧能够更好地应对数据密集型应…

简单聊一下Oracle,MySQL,postgresql三种锁表的机制,行锁和表锁

MySQL: MySQL使用行级锁定和表级锁定。行级锁定允许多个会话同时写入表,适用于多用户、高并发和OLTP应用。表级锁定只允许一个会话一次更新表,适用于只读、主要读取或单用户应用。 比如mysql开启一个窗口执行 begin; update xc_county_a…

【C++初阶学习】第十三弹——优先级队列及容器适配器

C语言栈:数据结构——栈(C语言版)-CSDN博客 C语言队列:数据结构——队列(C语言版)-CSDN博客 C栈与队列:【C初阶学习】第十二弹——stack和queue的介绍和使用-CSDN博客 前言: 在前面,我们已经…

SpringMVC[从零开始]

SpringMVC SpringMVC简介 1.1什么是MVC MVC是一种软件架构的思想,将软件按照模型、视图、控制器来划分 M:Model,模型层,指工程中的JavaBean,作用是处理数据 JavaBean分为两类: 一类称为实体类Bean:专…