红黑树详解(上)(红黑树的基本概念、查找操作、插入操作)

本文深入解析红黑树的原理与实现,涵盖基本性质、节点结构、查找与插入操作等内容,帮助读者掌握高效平衡查找树的设计。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

红黑树详解

前言

师傅渣哥在最后一节课上提到,如果能在简历上写“手写过红黑树”,给对方的感觉一定是不一样的。
正值寒假,笔者学习大量前人文章,又投入数个夜晚思考,逐个情况推导方案,编写代码,调试分析,终于完成了该数据结构的编写。
红黑树结构中涉及大量子问题,需要分类的情况繁多复杂。努力的结果是一棵高效率的查找树,既传承二叉平衡树的优点,又尽可能避免二叉平衡树在平衡过程中带来的过度性能开销。
透过红黑树,仿佛看到前人彻夜思考的身影,为哈希表等更高级的结构乃至操作系统的底层埋下奠基。
看前人智慧结晶如琉璃飘落,静静守候学习路上的人们,为他们指引,伴他们前行。

红黑树简介

红黑树简介

红黑树(Red Black Tree)是一种自平衡二叉查找树。
学习计算机科学的学生应该对平衡二叉树有所了解。平衡二叉树可以将查找的复杂度从线性(O(N))降低到对数级(O(logN)),实现在大型查找表中快速定位需要的信息。
然而,平衡二叉树的平衡要求太严格了,左右孩子高度差不能超过1,导致平衡过程经常涉及到整棵树的旋转,带来很大的性能开销。红黑树继承了平衡二叉树的快速查找特性,但是在平衡方面没有那么严格,因此大多数时候只需要在局部进行旋转即可完成自平衡。
红黑树结构的复杂之处在于操作需要对不同的情况分别做处理,而情况的数量特别多。
本文将带领你从认识红黑树,到研究清楚每种情况的处理方法。

知识储备要求

  • 不需要掌握任何特定的编程语言,但最好学习过至少一门编程语言。
  • 不需要知道什么是平衡二叉树,但如果知道的话,当然更好。
  • 需要知道什么是二叉树。如果你不知道,可以看这里:[枫铃树] 树和 Huffman Tree 哈夫曼树

注意

  • 本文仅对概念和思维进行讲解,不涉及任何代码。如果想看代码实现,请转至文章结尾查看红黑树的代码实现。
  • 本文描述红黑树节点时,只考虑节点的键(key),不考虑该节点存储的数据。因为数据本身不会影响节点顺序和关系等。实际代码实现中需要考虑到节点的数据。
  • 为简化描述,本文中节点的键都是非负整数。实际开发中是不能这么写的,但在学习中不应为自己引入过多的麻烦。
  • 后续描述节点时,可能会用节点的键(key)表示节点。比如,当提到节点A,说的实际是键为A的那个节点。如果你没看懂这条是在说啥,你就理解成节点上的那个数字就行了。
  • 本文中,如果出现诸如节点A在节点B的左侧,指的是在这棵树的中序遍历结果中,A在B的左侧。如果你不知道什么是中序遍历,但是能理解这句话的意思,那么你可以继续阅读本文。

红黑树的基本性质

红黑树简介

上图就是一颗红黑树。我们简单观察一下它。
首先,它是一棵二叉树,需要满足二叉树的性质(如,每个节点最多有两个孩子)。
然后,它是一棵查找树,需要满足查找树的基本性质:

  • 如果节点A节点B侧,那么一定有 A<B。比如,因为 3<5,所以上图中节点3节点5的左边。
  • 如果节点A节点B侧,那么一定有 A>B。比如,因为 13>8,所以上图中节点13节点8的右边。

另外,你一定发现了,每个节点是带有颜色的。这个颜色要么是红色,要么是黑色(据说这么设计是为了方便当时的打印机打印)。

在满足二叉查找树的基础上,红黑树具有以下几大性质:

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点是黑色的。
  3. 如果一个节点是红色的,那么它的父节点一定是黑色的(即:一条边的两端不能同时为红色)。
  4. 红色节点要么没有孩子,要么有两个黑色的孩子。
  5. 从根节点到每个叶节点的路径中,黑色节点的数量(计数时包含叶。但是否含根不影响结果)一定是一样的(比如上图中,从根节点7走到节点3,经过了3个黑色的节点(7, 4, 2),从7到14也经过了3个黑色的节点(7, 9, 14))。

(注意:上述描述可能与其他文章有所不同,但不影响对红黑树结构的理解。)

我们在对红黑树进行插入、删除等操作时,只需要保证其依旧满足这五条性质(及二叉查找树的基本性质)即可。
不难看出,由于第5条的存在,这棵树依旧是很平衡的,即便数据条件是极端的,它的查找效率仍然很高。而红色节点的引入,使得平衡过程不一定牵扯到整棵树的变化,改进了平衡过程的效率。

红黑树的节点

树是由节点构成的。那么红黑树的节点需要记录哪些信息呢?
作为二叉树,它应该记录左孩子右孩子的地址。为方便计算,我们再让它记录自己的爸爸(父节点)在哪里。当然,节点肯定是有键值的,不然鬼知道每个节点谁是谁呢。
此外,因为红黑树的节点是带有颜色的,那么红黑树的节点显然要带有颜色
节点除了键,应该还有数据,但在本文中不讨论。
总结一下,我们的红黑树节点记录以下四个信息:

  1. 左孩子地址
  2. 右孩子地址
  3. 父亲地址
  4. 自己的颜色

红黑树的基本操作

红黑树的操作,其实就是二叉查找树的基本操作:查找、插入、删除。

查找

查找操作较为简单。

  1. 走到根节点。
  2. 比较当前所在节点的键值和需要查找的键值
  3. 如果当前节点的键值和需要查找的键值相同,则所在节点即为目标。
  4. 如果需要找的键比当前节点的键小,则前往当前节点的左孩子,然后回到第2步。
  5. 如果需要找的键比当前节点的键大,则前往当前节点的右孩子,然后回到第2步。

注意:如果某刻,当前所在的节点为空,则查找失败,需要查找的键不在树里。

伪代码如下:

当前节点 = 根节点
while True:
    if 当前节点 == null:
        return 查找失败
    else if 目标键 == 当前节点的键:
        return 当前节点 # 查找成功
    else if 目标键 < 当前节点的键:
        当前节点 = 当前节点的左孩子
    else if 目标键 > 当前节点的键:
        当前节点 = 当前节点的右孩子

查找不会对树带来改变,自然不用做额外的调整操作。

插入

首先,不是所有的“插入”都是真正需要“插入”的。如果当前键已经在树里,只需要更新节点数据即可(本文不讨论节点数据问题,所以认为:当想要插入的键已经在树上时,不执行任何操作)。

在这里插入图片描述

如图(这张图跟上面的是有区别的!!),假如我们有一棵这样的红黑树,当我们需要插入节点18时,实际上是不需要执行任何操作的,因为节点18已经在树上了。
如果我们要插入节点7,那么我们需要在节点8的左边放入一个新节点。它是一个叶节点,键值是7.(如果不知道为什么,请复习一下平衡二叉树的知识)。
因此,插入过程其实也涉及了一个查找。只不过当我们找不到目标节点时,不是直接告诉上级“查找失败”,而是在最后那个位置创建新节点。
这个查找过程和平衡二叉树是一样的。相信聪明的你已经掌握。


查找部分的伪代码如下:

当前节点 = 根节点
当前节点的父亲 = null # 根节点没有父亲

while True:
    if 当前节点 == null:
        break
    else:
        当前节点的父亲 = 当前节点
        if 目标键 == 当前节点的键:
            return # 目标键已在树上,不做任何操作。
        if 目标键 < 当前节点的键:
            当前节点 = 当前节点的左孩子
        if 目标键 > 当前节点的键:
            当前节点 = 当前节点的右孩子

# 至此,已经锁定了新节点的父亲。
# 之后只需要创建这个节点,绑定到父亲即可。

问题来了:新的节点记录四个信息。如何填写?

因为新节点是叶子节点,那么它的左右孩子都是空。
它的父亲是谁,很显然。当然,如果树是空的,即新节点为根节点的时候,新节点没有父亲,父亲设为空。

颜色怎么办?

我们回忆一下红黑树的性质:

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点是黑色的。
  3. 如果一个节点是红色的,那么它的父节点一定是黑色的(即:一条边的两端不能同时为红色)。
  4. 红色节点要么没有孩子,要么有两个黑色的孩子。
  5. 从根节点到每个叶节点的路径中,黑色节点的数量(计数时包含叶。但是否含根不影响结果)一定是一样的。

如果新节点的颜色是黑色,那么它绝对会违背第5条性质。
如果新节点的颜色是红色,那么它有可能违背任何一条性质。比如上面那张图,在节点8的左边插入一个红色的节点7,不需要对树做任何调整,它依旧满足红黑树的性质。
因此,我们将新节点的颜色设为红色,然后只有当发现红黑树的性质被破坏后,再进行调整。


什么时候会违背红黑树的性质?
对于性质1,显然不会违背。
对于性质2,只有当插入的节点是根节点(即:插入前的树是空树)时,才会违背。这时,只需将新节点设为黑色,就没问题了。
性质4和性质5是不可能违背的。不信的话你可以自己试试。

但是,性质3是可能被违背的

例如,如果我们的树如下:

在这里插入图片描述

当我们想要插入节点9,会将树变成下图这样:

在这里插入图片描述

你看,红色的节点9,拥有一个红色的父亲,这显然违背了性质3. 那么我们只需要在插入的过程中,检查是否会违背性质3,并在违背性质3时进行修复即可。

相信读者很迫切地想知道如何修复上述问题。但在开始前,我们需要补充学习三个红黑树的内部操作:变色、左旋、右旋。

知识补充:红黑树的内部操作
变色

没啥可解释的。把一个红色的节点变成黑色,或者把一个黑色的节点变成红色,就是对这个节点的变色

左旋

与平衡二叉树的旋转操作类似。

如下图,如果我们的树有一个这样的局部:

在这里插入图片描述

(注意:这只是一个局部。节点5可以有父亲,节点1、4、7、9都可以有孩子。此外,图中除了节点5、8必须存在外,别的节点都可以是空的。如果不知道为什么,请复习平衡二叉树的知识)


针对上方的局部,对节点5做左旋,局部会变成下图这样:

在这里插入图片描述


右旋

与平衡二叉树的旋转操作类似。

如下图,如果我们的树有一个这样的局部:

在这里插入图片描述

(注意:这只是一个局部。节点5可以有父亲,节点1、4、7、9都可以有孩子。此外,图中除了节点5、2必须存在外,别的节点都可以是空的。如果不知道为什么,请复习平衡二叉树的知识)


针对上方的局部,对节点5做右旋,局部会变成下图这样:

在这里插入图片描述


可以看出,旋转操作不保证红黑树的性质不被破坏,但是能保证平衡二叉树的性质不被破坏。即:如果一个节点在另一个节点的左边,那么旋转后它依然在那个节点的左边。
如果你没有看懂旋转操作,一定不要稀里糊涂往后看。后文会涉及大量的旋转操作。这里没搞懂的话后面一定会晕。


当插入和删除操作破坏了红黑树的性质后,我们通过左旋、右旋和变色操作,即可恢复这棵树的性质。


好的,现在我们可以开始研究如何修复插入节点时破坏的性质了。
首先,只有当插入节点的父节点是红色时,才需要修复。如果父节点是黑色,就不用管。

如果父节点是红色,因为插入前的红黑树一定是满足那五条性质的,所以我们可以得到以下结论:

  • 父节点一定不是根(因为性质2)。
  • 兄弟节点一定不存在(否则会违背性质4)。
  • 爷爷节点一定是黑色(否则这棵树在插入前就已经违背性质3了)。
  • 叔叔节点要么不存在,要么是红色(否则会违背性质5)。

由此,我们可以分出两种情况,对这两种情况分别做处理:

  1. 叔叔是红色的
  2. 叔叔不存在

叔叔是红色时,局部如下:

在这里插入图片描述

我们将叔叔和爸爸都设成黑色,把爷爷设成红色。这样操作后,局部会变成下图这样:

在这里插入图片描述

这样做可以保证性质5依旧不被违背,同时可以让新插入的节点不再违背性质3.
这种修复操作与孩子所在的方向无关,所以我们不需要对下面四种情况做区分:

  1. 父亲是爷爷的孩子,新节点是父亲的孩子
  2. 父亲是爷爷的孩子,新节点是父亲的孩子
  3. 父亲是爷爷的孩子,新节点是父亲的孩子
  4. 父亲是爷爷的孩子,新节点是父亲的孩子

不论是上述四种情况的哪种,只需要将爷爷的两个孩子都进行反色,然后将爷爷自己反色,就可以了。
啊呀,但是,聪明的你一定发现了,这样做可能导致爷爷违背性质3.
之前,爷爷是黑色的。万一太爷爷是红色的,原本没有违背性质,现在违背了,怎么办?

怕啥!将爷爷视为新插入的节点,递归修复呗。只不过,我们需要对前面分情况时的第二种情况做出一点修改:
叔叔不存在的情况改成叔叔不存在或叔叔为黑色
因为递归修复的过程中,我们想要修复的那个红色节点不一定是叶子了,它下面可能是一棵含很多个黑色节点的树,因此叔叔也可以是黑色,并且可以有子树。至于要不要把叔叔不存在叔叔为黑色拆成两种情况,实际上是不需要的。后续研究修复方案的时候会发现,这两种情况的处理方式完全一致。


叔叔为黑色时,我们要将情况分成以下四种:

在这里插入图片描述

  1. 父亲是爷爷的孩子,新节点是父亲的孩子

在这里插入图片描述

  1. 父亲是爷爷的孩子,新节点是父亲的孩子

在这里插入图片描述

  1. 父亲是爷爷的孩子,新节点是父亲的孩子

在这里插入图片描述

  1. 父亲是爷爷的孩子,新节点是父亲的孩子

(注意:图中的叔叔节点可以是空的。叔叔节点存在时,是可以有子树的。图中的“修复目标”也可以有子树。因为看完“红色叔叔”的情况后,你会发现,我们的修复目标可能不仅是新插入的叶子,也可能是递归过程中需要修复的其他节点。当然,修复目标的父节点也是可以有孩子的)


不难发现,情况1和情况4是轴对称的。情况2和情况3是轴对称的。我们只需要把情况1和情况2的修复方式研究清楚,聪明的你一定能轻而易举地独立推出情况3和情况4的修复方式。
我们约定好,后面包括删除操作在内的分析,为精简篇幅,对于轴对称的局部,我们只研究其中的一个,另一个由聪明的你自行推导


情况1(图中蓝色表示我们不知道这个节点的颜色,甚至不一定知道它是否存在,但是这些不会干扰操作):

在这里插入图片描述

我们先对爷爷做右旋,得到下图:

在这里插入图片描述

之后,我们将原来的爷爷(节点17)设为红色,将原来的爸爸(节点9)设为黑色,得到下图:

在这里插入图片描述

凝视它,你会发现,在这个局部,从根到每个叶子所经过的黑色节点数量(含根和叶子)没有变,因此不会违背性质5. 修复过后,新插入的节点(节点4)不再违背性质3. 图中节点10,之前连接在红色节点的后面,因此在重新绑定到原来的爷爷(节点17)下面后,依旧不会违背性质3. 这样修复完,因为局部的根节点(节点9)是黑色的,它不会额外带来对性质3的违背。如此,红黑树的性质修复完毕。
观察这个过程,你可以看出,叔叔是否存在无关紧要。修复目标的兄弟(节点10)是否存在也无关紧要。叔叔的孩子是否存在、颜色如何亦无关紧要。

总结一下过程:

  1. 对爷爷做右旋
  2. 将原来的爷爷节点设为红色
  3. 将原来的父亲节点设为黑色

其中,你也可以先做反色,再做旋转。


既然你已经知道如何修复情况1了,那么你一定知道情况4应如何修复:

  1. 对爷爷做左旋
  2. 将原来的爷爷节点设为红色
  3. 将原来的父亲节点设为黑色

其实就是对称了一下,没什么困难的,是不是?


情况2,如下图:

在这里插入图片描述

先对父节点做左旋,得到的局部如下:

在这里插入图片描述

然后对爷爷节点做右旋,得到如下:

在这里插入图片描述

然后,我们将原来的爷爷设成红色,将修复目标设成黑色,得到如下:

在这里插入图片描述

你可以自己验证一下,它是不是修复了原来的问题,并且没有新违背性质。

几句话总结:

  1. 对父节点左旋
  2. 对爷爷做右旋
  3. 将修复目标设为黑色
  4. 将爷爷设为红色

情况3是情况2的对称情况:

  1. 对父节点右旋
  2. 对爷爷做左旋
  3. 将修复目标设为黑色
  4. 将爷爷设为红色

现在有个问题。在递归修复红色节点时,如果叔叔节点一直是红色的,什么时候结束?如果父节点不存在,怎么办?

先看第二个问题。什么时候父节点不存在?如果父节点不存在,那么修复目标一定是根节点!这时,只需要将它改成黑色的,就行了,是不是?
那么对于第一个问题,每次递归都是在往根靠近。如果中途遇到的叔叔一直是红色的,那么修复目标最终会是根。这时,不需要再递归,只是简单地将修复目标设为黑色,就够了。


至此,你已经知道了什么是红黑树,了解了红黑树的性质,并掌握了对红黑树的查找和插入操作。后续我们将对红黑树的删除操作展开研究。

后接:红黑树详解(下)(红黑树的删除操作)


如果你急于看代码,可以前往这里:
[枫铃树] C++ 实现红黑树结构

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值