CategoryResourceRepost/极客时间专栏/数据结构与算法之美/基础篇/25 | 红黑树(上):为什么工程中都用红黑树这种二叉树?.md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

109 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="25 | 红黑树(上):为什么工程中都用红黑树这种二叉树?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5b/58/5b8ee5da6bc0b1416c7798798751de58.mp3"></audio>
上两节我们依次讲了树、二叉树、二叉查找树。二叉查找树是最常用的一种二叉树它支持快速插入、删除、查找操作各个操作的时间复杂度跟树的高度成正比理想情况下时间复杂度是O(logn)。
不过二叉查找树在频繁的动态更新过程中可能会出现树的高度远大于log<sub>2</sub>n的情况从而导致各个操作的效率下降。极端情况下二叉树会退化为链表时间复杂度会退化到O(n)。我上一节说了,要解决这个复杂度退化的问题,我们需要设计一种平衡二叉查找树,也就是今天要讲的这种数据结构。
很多书籍里,但凡讲到平衡二叉查找树,就会拿红黑树作为例子。不仅如此,如果你有一定的开发经验,你会发现,在工程中,很多用到平衡二叉查找树的地方都会用红黑树。你有没有想过,**为什么工程中都喜欢用红黑树,而不是其他平衡二叉查找树呢?**
带着这个问题,让我们一起来学习今天的内容吧!
## 什么是“平衡二叉查找树”?
平衡二叉树的严格定义是这样的二叉树中任意一个节点的左右子树的高度相差不能大于1。从这个定义来看上一节我们讲的完全二叉树、满二叉树其实都是平衡二叉树但是非完全二叉树也有可能是平衡二叉树。
<img src="https://static001.geekbang.org/resource/image/dd/9b/dd9f5a4525f5029a8339c89ad1c8159b.jpg" alt="">
平衡二叉查找树不仅满足上面平衡二叉树的定义,还满足二叉查找树的特点。最先被发明的平衡二叉查找树是[AVL树](https://zh.wikipedia.org/wiki/AVL%E6%A0%91)它严格符合我刚讲到的平衡二叉查找树的定义即任何节点的左右子树高度相差不超过1是一种高度平衡的二叉查找树。
但是很多平衡二叉查找树其实并没有严格符合上面的定义树中任意一个节点的左右子树的高度相差不能大于1比如我们下面要讲的红黑树它从根节点到各个叶子节点的最长路径有可能会比最短路径大一倍。
我们学习数据结构和算法是为了应用到实际的开发中的,所以,我觉得没必去死抠定义。对于平衡二叉查找树这个概念,我觉得我们要从这个数据结构的由来,去理解“平衡”的意思。
发明平衡二叉查找树这类数据结构的初衷是,解决普通二叉查找树在频繁的插入、删除等动态更新的情况下,出现时间复杂度退化的问题。
所以,**平衡二叉查找树中“平衡”的意思,其实就是让整棵树左右看起来比较“对称”、比较“平衡”,不要出现左子树很高、右子树很矮的情况。这样就能让整棵树的高度相对来说低一些,相应的插入、删除、查找等操作的效率高一些。**
所以如果我们现在设计一个新的平衡二叉查找树只要树的高度不比log<sub>2</sub>n大很多比如树的高度仍然是对数量级的尽管它不符合我们前面讲的严格的平衡二叉查找树的定义但我们仍然可以说这是一个合格的平衡二叉查找树。
## 如何定义一棵“红黑树”?
平衡二叉查找树其实有很多比如Splay Tree伸展树、Treap树堆但是我们提到平衡二叉查找树听到的基本都是红黑树。它的出镜率甚至要高于“平衡二叉查找树”这几个字有时候我们甚至默认平衡二叉查找树就是红黑树那我们现在就来看看这个“明星树”。
红黑树的英文是“Red-Black Tree”简称R-B Tree。它是一种不严格的平衡二叉查找树我前面说了它的定义是不严格符合平衡二叉查找树的定义的。那红黑树究竟是怎么定义的呢
顾名思义,红黑树中的节点,一类被标记为黑色,一类被标记为红色。除此之外,一棵红黑树还需要满足这样几个要求:
<li>
根节点是黑色的;
</li>
<li>
每个叶子节点都是黑色的空节点NIL也就是说叶子节点不存储数据
</li>
<li>
任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
</li>
<li>
每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点;
</li>
这里的第二点要求“叶子节点都是黑色的空节点”,稍微有些奇怪,它主要是为了简化红黑树的代码实现而设置的,下一节我们讲红黑树的实现的时候会讲到。**这节我们暂时不考虑这一点,所以,在画图和讲解的时候,我将黑色的、空的叶子节点都省略掉了。**
为了让你更好地理解上面的定义,我画了两个红黑树的图例,你可以对照着看下。
<img src="https://static001.geekbang.org/resource/image/90/9a/903ee0dcb62bce2f5b47819541f9069a.jpg" alt="">
## 为什么说红黑树是“近似平衡”的?
我们前面也讲到,平衡二叉查找树的初衷,是为了解决二叉查找树因为动态更新导致的性能退化问题。所以,**“平衡”的意思可以等价为性能不退化。“近似平衡”就等价为性能不会退化得太严重**。
我们在上一节讲过二叉查找树很多操作的性能都跟树的高度成正比。一棵极其平衡的二叉树满二叉树或完全二叉树的高度大约是log<sub>2</sub>n所以如果要证明红黑树是近似平衡的我们只需要分析红黑树的高度是否比较稳定地趋近log<sub>2</sub>n就好了。
红黑树的高度不是很好分析,我带你一步一步来推导。
**首先,我们来看,如果我们将红色节点从红黑树中去掉,那单纯包含黑色节点的红黑树的高度是多少呢?**
红色节点删除之后,有些节点就没有父节点了,它们会直接拿这些节点的祖父节点(父节点的父节点)作为父节点。所以,之前的二叉树就变成了四叉树。
<img src="https://static001.geekbang.org/resource/image/7e/ed/7e6ecc308fe44120f30de809822215ed.jpg" alt="">
前面红黑树的定义里有这么一条:从任意节点到可达的叶子节点的每个路径包含相同数目的黑色节点。我们从四叉树中取出某些节点,放到叶节点位置,四叉树就变成了完全二叉树。所以,仅包含黑色节点的四叉树的高度,比包含相同节点个数的完全二叉树的高度还要小。
上一节我们说完全二叉树的高度近似log<sub>2</sub>n这里的四叉“黑树”的高度要低于完全二叉树所以去掉红色节点的“黑树”的高度也不会超过log<sub>2</sub>n。
**我们现在知道只包含黑色节点的“黑树”的高度,那我们现在把红色节点加回去,高度会变成多少呢?**
从上面我画的红黑树的例子和定义看在红黑树中红色节点不能相邻也就是说有一个红色节点就要至少有一个黑色节点将它跟其他红色节点隔开。红黑树中包含最多黑色节点的路径不会超过log<sub>2</sub>n所以加入红色节点之后最长路径不会超过2log<sub>2</sub>n也就是说红黑树的高度近似2log<sub>2</sub>n。
所以红黑树的高度只比高度平衡的AVL树的高度log<sub>2</sub>n仅仅大了一倍在性能上下降得并不多。这样推导出来的结果不够精确实际上红黑树的性能更好。
## 解答开篇
我们刚刚提到了很多平衡二叉查找树,现在我们就来看下,为什么在工程中大家都喜欢用红黑树这种平衡二叉查找树?
我们前面提到Treap、Splay Tree绝大部分情况下它们操作的效率都很高但是也无法避免极端情况下时间复杂度的退化。尽管这种情况出现的概率不大但是对于单次操作时间非常敏感的场景来说它们并不适用。
AVL树是一种高度平衡的二叉树所以查找的效率非常高但是有利就有弊AVL树为了维持这种高度的平衡就要付出更多的代价。每次插入、删除都要做调整就比较复杂、耗时。所以对于有频繁的插入、删除操作的数据集合使用AVL树的代价就有点高了。
红黑树只是做到了近似平衡并不是严格的平衡所以在维护平衡的成本上要比AVL树要低。
所以,红黑树的插入、删除、查找各种操作性能都比较稳定。对于工程应用来说,要面对各种异常情况,为了支撑这种工业级的应用,我们更倾向于这种性能稳定的平衡二叉查找树。
## 内容小结
很多同学都觉得红黑树很难,的确,它算是最难掌握的一种数据结构。其实红黑树最难的地方是它的实现,我们今天还没有涉及,下一节我会专门来讲。
不过呢,我认为,我们其实不应该把学习的侧重点,放到它的实现上。那你可能要问了,关于红黑树,我们究竟需要掌握哪些东西呢?
还记得我多次说过的观点吗?**我们学习数据结构和算法,要学习它的由来、特性、适用的场景以及它能解决的问题。对于红黑树,也不例外。你如果能搞懂这几个问题,其实就已经足够了。**
红黑树是一种平衡二叉查找树。它是为了解决普通二叉查找树在数据更新的过程中复杂度退化的问题而产生的。红黑树的高度近似log<sub>2</sub>n所以它是近似平衡插入、删除、查找操作的时间复杂度都是O(logn)。
因为红黑树是一种性能非常稳定的二叉查找树,所以,在工程中,但凡是用到动态插入、删除、查找数据的场景,都可以用到它。不过,它实现起来比较复杂,如果自己写代码实现,难度会有些高,这个时候,我们其实更倾向用跳表来替代它。
## 课后思考
动态数据结构支持动态的数据插入、删除、查找操作,除了红黑树,我们前面还学习过哪些呢?能对比一下各自的优势、劣势,以及应用场景吗?
欢迎留言和我分享,我会第一时间给你反馈。