55

图解“红黑树”原理,一看就明白!

 4 years ago
source link: http://developer.51cto.com/art/201908/601688.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

图解“红黑树”原理,一看就明白!

学过数据结构都知道二叉树的概念,而又有多种比较常见的二叉树类型,比如完全二叉树、满二叉树、二叉搜索树、均衡二叉树、完美二叉树等。

【51CTO.com原创稿件】 学过数据结构都知道二叉树的概念,而又有多种比较常见的二叉树类型,比如完全二叉树、满二叉树、二叉搜索树、均衡二叉树、完美二叉树等。

1e0a715cd4564a1bf6e5269ab8dbee0a.jpg-wh_651x-s_308429222.jpg

图片来自 Pexels

今天我们要说的红黑树就是就是一棵非严格均衡的二叉树,均衡二叉树又是在二叉搜索树的基础上增加了自动维持平衡的性质,插入、搜索、删除的效率都比较高。红黑树也是实现 TreeMap 存储结构的基石。

二叉搜索树

二叉搜索树又叫二叉查找树、二叉排序树,我们先看一下典型的二叉搜索树,这样的二叉树有何规则特点呢?

二叉搜索树有如下几个特点:

  • 节点的左子树小于节点本身
  • 节点的右子树大于节点本身
  • 左右子树同样为二叉搜索树

下图就是一棵典型的二叉搜索树:

95f4c570308b8623cf5926e9231ef01f.jpg-wh_600x-s_301486625.jpg

二叉搜索树是均衡二叉树的基础,我们看一下它的搜索步骤如何。我们要从二叉树中找到值为 58 的节点。

第一步:首先查找到根节点,值为 60 的节点。

08a0ffb74acd07aff1834d5f473b5cf7.jpg-wh_600x-s_2594039218.jpg

第二步:比较我们要找的值 58 与该节点的大小。

如果等于,那么恭喜,已经找到;如果小于,继续找左子树;如果大于,那么找右子树。

很明显 58<60,因此我们找到左子树的节点 56,此时我们已经定位到了节点 56。

67a42bddfbd9dee72377046d9cd93a1b.jpg-wh_600x-s_328044261.jpg

第三步:按照第二步的规则继续找。

58>56 我们需要继续找右子树,定位到了右子树节点 58,恭喜,此时我们已经找到了。

04d5fe6b480a7c59df51dc7d79b0012b.jpg-wh_600x-s_3181818456.jpg

我们经过三步就已经找到了,其实就是我们平时所说的二分查找,这种二叉搜索树好像查找效率很高,但同样它也有缺陷,如下面这样的二叉搜索树。

7bcc1d3862cbb5e87c2cd879c902a375.jpg

看到这样的二叉搜索树是否很别扭,典型的大长腿瘸子,但它也是二叉搜索树,如果我们要找值为 50 的节点,基本上和单链表查询没多大区别了,性能将大打折扣。

这个时候我们的均衡二叉树就粉墨登场了,均衡二叉树就是在二叉搜索树的基础上添加了自动维持平衡的性质。

上面的大长腿瘸子二叉搜索树经过自动平衡后,可能就成为了下面这样的二叉树。

c6c8cd144231838292dfb6c4e73af006.jpg

经过了自动平衡,再去找值为 50 的节点,查找性能将提升很多。红黑树就是非严格均衡的二叉搜索树。

红黑树规则特点

红黑树具体有哪些规则特点呢?具体如下:

  • 节点分为红色或者黑色。
  • 根节点必为黑色。
  • 叶子节点都为黑色,且为 null。
  • 连接红色节点的两个子节点都为黑色(红黑树不会出现相邻的红色节点)。
  • 从任意节点出发,到其每个叶子节点的路径中包含相同数量的黑色节点。
  • 新加入到红黑树的节点为红色节点。

规则看着好像挺多,没错,因为红黑树也是均衡二叉树,需要具备自动维持平衡的性质,上面的 6 条就是红黑树给出的自动维持平衡所需要具备的规则。

我们看一看一个典型的红黑树到底是什么样儿?

a71e90bc85de14591c8763ca1447bcf0.jpg-wh_600x-s_2358636483.jpg

首先解读一下规则,除了字面上看到的意思,还隐藏了哪些意思呢?

①从根节点到叶子节点的最长路径不大于最短路径的 2 倍

怎么样的路径算最短路径?从规则 5 中,我们知道从根节点到每个叶子节点的黑色节点数量是一样的,那么纯由黑色节点组成的路径就是最短路径。

什么样的路径算是最长路径?根据规则 4 和规则 3,若有红色节点,则必然有一个连接的黑色节点,当红色节点和黑色节点数量相同时,就是最长路径,也就是黑色节点(或红色节点)*2。

②为什么说新加入到红黑树中的节点为红色节点

从规则 4 中知道,当前红黑树中从根节点到每个叶子节点的黑色节点数量是一样的,此时假如新的是黑色节点的话,必然破坏规则。

但加入红色节点却不一定,除非其父节点就是红色节点,因此加入红色节点,破坏规则的可能性小一些,下面我们也会举例来说明。

什么情况下,红黑树的结构会被破坏呢?破坏后又怎么维持平衡,维持平衡主要通过两种方式【变色】和【旋转】,【旋转】又分【左旋】和【右旋】,两种方式可相互结合。

下面我们从插入和删除两种场景来举例说明。

红黑树节点插入

当我们插入值为 66 的节点时,红黑树变成了这样:

3752e698fabf710c9222a01895130afe.jpg-wh_600x-s_3413603327.jpg

很明显,这个时候结构依然遵循着上述 6 大规则,无需启动自动平衡机制调整节点平衡状态。

如果再向里面插入值为 51 的节点,这个时候红黑树变成了这样:

d766f0cdda906d2fb7252556c25d264a.jpg-wh_600x-s_825100703.jpg

很明显现在的结构不遵循规则 4 了,这个时候就需要启动自动平衡机制调整节点平衡状态。

变色

我们可以通过变色的方式,使结构满足红黑树的规则:

  • 首先解决结构不遵循规则 4 这一点(红色节点相连,节点 49-51),需将节点 49 改为黑色。
  • 此时我们发现又违反了规则 5(56-49-51-XX 路径中黑色节点超过了其他路径),那么我们将节点 45 改为红色节点。
  • 哈哈,妹的,又违反了规则 4(红色节点相连,节点 56-45-43),那么我们将节点 56 和节点 43 改为黑色节点。
  • 但是我们发现此时又违反了规则 5(60-56-XX 路径的黑色节点比 60-68-XX 的黑色节点多),因此我们需要调整节点 68 为黑色。

19b3282be41719a6b048bcb660396e2b.jpg-wh_600x-s_2426541467.jpg

最终调整完成后的树为:

40adce9e385dfe64ab3e7a1394fb53b7.jpg-wh_600x-s_2438170775.jpg

但并不是什么时候都那么幸运,可以直接通过变色就达成目的,大多数时候还需要通过旋转来解决。

如在下面这棵树的基础上,加入节点 65:

f27e26604d447d2072a13489a5af15bd.jpg-wh_600x-s_1946926027.jpg

插入节点 65 后进行以下步骤:

1188ba59e871c8b1cebed4e81286af5c.jpg-wh_600x-s_2630235063.jpg

这个时候,你会发现对于节点 64 无论是红色节点还是黑色节点,都会违反规则 5,路径中的黑色节点始终无法达成一致,这个时候仅通过【变色】已经无法达成目的。

我们需要通过旋转操作,当然【旋转】操作一般还需要搭配【变色】操作。旋转包括【左旋】和【右旋】。

左旋:逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点。

左旋操作步骤如下:首先断开节点 PL 与右子节点 G 的关系,同时将其右子节点的引用指向节点 C2;然后断开节点 G 与左子节点 C2 的关系,同时将 G 的左子节点的应用指向节点 PL。

ef7f0e2178c7cb0885b846f0d221e1c1.jpg-wh_600x-s_3359031553.jpg

右旋:顺时针旋转两个节点,让一个节点被其左子节点取代,而该节点成为左子节点的右子节点。

右旋操作步骤如下:首先断开节点 G 与左子节点 PL 的关系,同时将其左子节点的引用指向节点 C2;然后断开节点 PL 与右子节点 C2 的关系,同时将 PL 的右子节点的应用指向节点 G。

2c1256da588138f0fcaf57ed6bb55067.jpg-wh_600x-s_3498772572.jpg

无法通过变色而进行旋转的场景分为以下四种:

左左节点旋转

这种情况下,父节点和插入的节点都是左节点,如下图(旋转原始图1)这种情况下,我们要插入节点 65。

规则如下:以祖父节点【右旋】,搭配【变色】。

cc7ed7fdcab94bcc9ce9e39b1890a20d.jpg

按照规则,步骤如下:

0f4c32eb0acd2b38bb3b4e43ed49d657.jpg-wh_600x-s_102656732.jpg

左右节点旋转

这种情况下,父节点是左节点,插入的节点是右节点,在旋转原始图 1 中,我们要插入节点 67。

规则如下:先父节点【左旋】,然后祖父节点【右旋】,搭配【变色】。

按照规则,步骤如下:

524b1486219533c3ddbd341611abb3bd.jpg-wh_600x-s_1460777435.jpg

右左节点旋转

这种情况下,父节点是右节点,插入的节点是左节点,如下图(旋转原始图 2)这种情况,我们要插入节点 68。

规则如下:先父节点【右旋】,然后祖父节点【左旋】,搭配【变色】。

319db6e9a293a8beb206b9dfbe625a87.png

按照规则,步骤如下:

9b7e6a3f062b860bc3a81dc70137a6c6.jpg-wh_600x-s_674736321.jpg

右右节点旋转

这种情况下,父节点和插入的节点都是右节点,在旋转原始图 2 中,我们要插入节点 70。

规则如下:以祖父节点【左旋】,搭配【变色】。

按照规则,步骤如下:

5de480e31859441c0cced8ad3b840aa1.jpg-wh_600x-s_2716796514.jpg

红黑树插入总结

红黑树插入总结如下图:

9a804cb1eb9e09191dae5432c6cb0ea7.jpg-wh_600x-s_1010387625.jpg

红黑树节点删除

相比较于红黑树的节点插入,删除节点更为复杂,我们从子节点是否为 null 和红色为思考维度来讨论。

子节点至少有一个为 null

当待删除的节点的子节点至少有一个为 null 节点时,删除了该节点后,将其有值的节点取代当前节点即可。

若都为 null,则将当前节点设置为 null,当然如果违反规则了,则按需调整,如【变色】以及【旋转】。

499c251d28c55ecded40700f5ba8a2b5.jpg-wh_600x-s_3083656115.jpg

子节点都是非 null 节点

这种情况下,第一步:找到该节点的前驱或者后继。

前驱:左子树中值最大的节点(可得出其最多只有一个非 null 子节点,可能都为 null)。

后继:右子树中值最小的节点(可得出其最多只有一个非 null 子节点,可能都为 null)。

前驱和后继都是值最接近该节点值的节点,类似于该节点.prev=前驱,该节点.next=后继。

第二步:将前驱或者后继的值复制到该节点中,然后删掉前驱或者后继。

如果删除的是左节点,则将前驱的值复制到该节点中,然后删除前驱;如果删除的是右节点,则将后继的值复制到该节点中,然后删除后继。

这相当于是一种“取巧”的方法,我们删除节点的目的是使该节点的值在红黑树上不存在。

因此专注于该目的,我们并不关注删除节点时是否真是我们想删除的那个节点,同时我们也不需考虑树结构的变化,因为树的结构本身就会因为自动平衡机制而经常进行调整。

前面我们已经说了,我们要删除的实际上是前驱或者后继,因此我们就以前驱为主线来讲解。

后继的学习可参考前驱,包括下面几种情况:

①前驱为黑色节点,并且有一个非 null 子节点

3ece5876c07c1d7cbecde90001b99e20.jpg-wh_600x-s_3935459426.jpg

分析:因为要删除的是左节点 64,找到该节点的前驱 63;然后用前驱的值 63替换待删除节点的值 64,此时两个节点(待删除节点和前驱)的值都为 63;

删除前驱 63,此时成为上图过程中间环节,但我们发现其不符合红黑树规则 4,因此需要进行自动平衡调整。这里直接通过【变色】即可完成。

②前驱为黑色节点,同时子节点都为 null

4ae61581044ffbadc0e995dca870dcd0.jpg-wh_600x-s_2748297792.jpg

分析:因为要删除的是左节点 64,找到该节点的前驱 63;然后用前驱的值 63 替换待删除节点的值 64,此时两个节点(待删除节点和前驱)的值都为 63。

删除前驱 63,此时成为上图过程中间环节,但我们发现其不符合红黑树规则 5,因此需要进行自动平衡调整。这里直接通过【变色】即可完成。

③前驱为红色节点,同时子节点都为 null

dc5c264c13968d27b3c30ca45f984a72.jpg-wh_600x-s_1254695137.jpg

分析:因为要删除的是左节点 64,找到该节点的前驱 63;然后用前驱的值 63替换待删除节点的值 64,此时两个节点(待删除节点和前驱)的值都为 63;删除前驱 63,树的结构并没有打破规则。

红黑树删除总结

红黑树删除的情况比较多,但也就存在以下情况:

  • 删除的是根节点,则直接将根节点置为 null。
  • 待删除节点的左右子节点都为 null,删除时将该节点置为 null。
  • 待删除节点的左右子节点有一个有值,则用有值的节点替换该节点即可。
  • 待删除节点的左右子节点都不为 null,则找前驱或者后继,将前驱或者后继的值复制到该节点中,然后删除前驱或者后继。
  • 节点删除后可能会造成红黑树的不平衡,这时我们需通过【变色】+【旋转】的方式来调整,使之平衡,上面也给出了例子,建议大家多多练习,而不必背下来。

总结

本文主要介绍了红黑树的相关原理,首先红黑树的基础二叉搜索树,我们先简单说了一下二叉搜索树,并且讲了一下搜索的流程。

然后就针对红黑树的六大规则特点,红黑树的插入操作,删除操作,都使用了大量的图形来加以说明。

技术都是练出来的,有时候很多似是而非的地方,当动笔去写的时候,其实很好理解。

红黑树的使用非常广泛,如 TreeMap 和 TreeSet 都是基于红黑树实现的,而 JDK8 中 HashMap 当链表长度大于 8 时也会转化为红黑树。

红黑树比较复杂,本人也是还在学习过程中,如果有不对的地方请批评指正,望共同进步谢谢。

作者:梁洪

简介:网名工匠初心,热爱技术,喜欢钻研与分享,6 年 Java 开发经验,专注于 Java 以及 Spring 生态圈,同时也喜欢研究物联网、大数据、AI 等前沿技术,带过 15 人以下的小团队,做过项目管理,现在是一家软件公司的部门经理。

06b2a54c97a0c09401603bab7aea68d4.gif-wh_600x-s_2946624692.gif

【51CTO原创稿件,合作站点转载请注明原文作者和出处为51CTO.com】

【编辑推荐】

【责任编辑:武晓燕 TEL:(010)68476606】

点赞 5


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK