从红黑树的本质出发,彻底理解红黑树!
![从红黑树的本质出发,彻底理解红黑树!](https://static001.geekbang.org/infoq/da/da2f0ce5d6910dcd275d07222faddc70.png)
前言
原文链接:https://my.oschina.net/u/4108008/blog/4668932
上一节,我们一起从二叉树、二叉查找树、平衡树、AVL树、2-3树、2-3-4树、B树,一路讲到红黑树,最后得出红黑树的本质:红黑树就是2-3-4树,请看下图:
![](https://static001.geekbang.org/infoq/dd/dde3ffb4f23282417b58fee55af7c5c4.png)
我们知道2-3-4的插入、删除、查找元素的原理是相当简单的,那么,我们是不是可以利用2-3-4树来记忆红黑树呢?
答案是肯定的,本节,我们就来看看如何利用2-3-4树来快速掌握红黑树,再也不用死记硬背了~~
好了,让我们进入今天的学习吧。
再忆2-3-4树
我们给出一张图简单地回顾一下上一节关于2-3-4树插入元素N的过程:
![](https://static001.geekbang.org/infoq/6e/6e3b5bda986fa03908f022fc8aa1feda.png)
左倾红黑树、右倾红黑树、AA树
在正式讲解红黑树之前呢,彤哥先来给大家普及几个有意思的概念,分别是左倾红黑树、右倾红黑树、AA树。
图片太小?试试横屏!
![](https://static001.geekbang.org/infoq/92/92c68dcbf233085100d9c685cfdb8d9e.png)
请看上图,其实按照红黑树的概念,上面3颗树都是红黑树,而且元素也是一模一样,可以说是同一颗红黑树的不同变种。
> 细心的同学会发现①和②是同一颗2-3-4树演化而来,③是这颗2-3-4树缩小成2-3树的样子。
那么,到底什么是红黑树呢?
红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。
首先,红黑树是一颗二叉查找树,另外,它还必须满足以下五点要求:
节点是红色或黑色;
根节点是黑色;
所有叶子节点是黑色;(叶子节点是NULL节点)
每个红色节点的两个子节点都是黑色;(从根节点到每个叶子节点的路径上不能有两个连续的红节点)
从任何一个节点到每个叶子节点的所有路径都包含相同数目的黑色节点;
大家不用记这个概念哈,因为确实很难记得住哈,下面彤哥会教大家更简单的方法。
所以,你看上面三个图是不是都是红黑树呢?
并不是啊,因为叶子节点有的是红色的呀。
其实,它们都是红黑树,让我把叶子节点补齐:
![](https://static001.geekbang.org/infoq/01/01818955212e7871501bbc9f91b5ef9f.png)
你再仔细看看,是不是满足上面五条规则了?!
所以,你看,随便画一颗树,它都可能满足红黑树的定义,因此,为了方便记忆,我们将红黑树分成这么几种类型:左倾红黑树、右倾红黑树、AA树。
左倾红黑树(LLRB,Left-Learning Red-Black Tree),一个节点如果有红色子节点,那么,它的红色子节点是向左倾斜的。
怎么理解呢?
我们还是把上面的null节点干掉哈,叶子节点都是null节点,那是经典红黑树的讲法,到彤哥这里,完全不存在这种要求。
我们来看,一个节点要么有一个子节点,要么有两个子节点,对吧。
如果这个节点有红色的子节点呢,也是一个或者两个,如果只有一个红色子节点的话,那么,这个子节点只能在左边,如果是有两个红色子节点,那就不用管。
所以,整颗红黑树中,如果存在红色节点,那么只能是下面这两种形态。
![](https://static001.geekbang.org/infoq/5c/5c42a8e9a437e684a0aba952b07598d4.png)
同理,右倾红黑树(RLRB,Right-Learning Red-Black Tree),也是一样的道理,即红色子节点向右倾斜,它的红色子节点只能是下面这两种形态:
![](https://static001.geekbang.org/infoq/0f/0fae2e62842fba6053f84b4e63bdec27.png)
好了,左倾和右倾红黑树都还算比较正常的形态,还有一种变态的红黑树,叫作AA树(AA Tree)。
当然,这里的AA不是吃饭的时候大家各付各的哈,这里的AA是其作者的名字的缩写:Arne Andersson。
AA树,是指红黑树中所有的红色子节点必须只能是右节点,左子节点一律不允许是红色子节点,所以,在AA树中,红色子节点只能是下面这一种形态:
![](https://static001.geekbang.org/infoq/5f/5f83a40f01ed3ea4c3f8530e0db279d6.png)
也可以理解为严重右倾主义(我这么说会不会被约去喝茶^^)。
> 其实AA树可以看作2-3树的右倾演化而来,而不是2-3-4树,你可以画个图体验一下。
好了,上面就是左倾红黑树、右倾红黑树、AA树的概念,当然,也有可能存在一种红黑树,比如红色子节点只能是左子节点,是不是叫BB树,咱也不知道,还有一种可能是像下面这种红黑树:
![](https://static001.geekbang.org/infoq/a7/a7a61a962dddc5dc251223ba1a844355.png)
上面这颗树,它既有左边的红色子节点,也有右边的红色子节点,其实它也满足红黑树的定义,这种就只是普通(经典)的红黑树了。
既然红黑树有这么多完全不同的形态,我们要如何快速的记住它们呢?
很难,真的很难,所以,我们只需要记住一种形态就可以了,比如左倾红黑树,其它的形态都是一样的道理,完全不用强形记忆。
因此,下面的内容,我将全部以左倾红黑树来讲解,跟经典的红黑树讲法会有点出入,且跟你以前看到的所有文章都不一样,请不要纠结。
有趣的插入元素
首先,让我们约定一件事:插入的节点必须为红色,但如果是根节点,就把它涂成黑色。
有了这个约定之后,我们使用一步一图的方式来慢慢拆解红黑树(左倾,下同)插入元素的过程。
插入第一个元素F
![](https://static001.geekbang.org/infoq/9d/9d4df5f052e4bb295c525fc6864966e1.png)
插入第二个元素
![](https://static001.geekbang.org/infoq/13/13fe7e2881cff8ba96adac8826f7cab5.png)
![](https://static001.geekbang.org/infoq/aa/aadffde22a0ae0e35747bc215b055f8e.png)
![](https://static001.geekbang.org/infoq/0c/0cab7c4d52374891fdc8d9b0b8eaf084.png)
![](https://static001.geekbang.org/infoq/c2/c268f2840dff85fd0242645c41bad181.png)
![](https://static001.geekbang.org/infoq/bd/bd83ade9ff56eb43002972d48a139f6d.png)
插入第三个元素
![](https://static001.geekbang.org/infoq/db/db8c694f850708055c4824fd5898ab4e.png)
![](https://static001.geekbang.org/infoq/d4/d4b4b2029e29dc06e2e60892dc22d58e.png)
![](https://static001.geekbang.org/infoq/e4/e4da622560e57e42ca73b1801344e94d.png)
![](https://static001.geekbang.org/infoq/ca/ca507b31ac0145ea4de936817aa3d4aa.png)
![](https://static001.geekbang.org/infoq/b0/b0da320cb576524dcd725c80cc130aae.png)
![](https://static001.geekbang.org/infoq/48/483029673fa5d23e3157bebd7f1fc965.png)
![](https://static001.geekbang.org/infoq/dd/dd268c54893e1a7d3ff76c5203d1ddf4.png)
插入第四个元素
![](https://static001.geekbang.org/infoq/0c/0ceafce115e72680ed4c79165ee7fd4f.png)
![](https://static001.geekbang.org/infoq/0f/0fe17f3220161ab5ba4d408b4aab249a.png)
![](https://static001.geekbang.org/infoq/e6/e619523e2f506bf5f5afbdc244b7932d.png)
![](https://static001.geekbang.org/infoq/18/18cd84ec7f75809f27b35773d167d898.png)
插入第五个元素
删除元素
不管是2-3-4树还是左倾红黑树删除元素的过程都要比插入元素复杂得多,我们先来看2-3-4树删除元素的过程。
2-3-4树删除元素
为了方便讲解,我构造了一颗下图所示的2-3-4树:
![](https://static001.geekbang.org/infoq/02/02d7dbf590e494b634ed89a612adf0ce.png)
对于2-3-4树,删除3节点或4节点的叶子节点是最简单的,比如C D
和P Q R
这两个叶子节点,删除这两个节点中的任意一个元素直接删除即可,4节点删除一个元素后变成3节点,3节点删除一个元素之后变成2节点,并不影响原来树的平衡性,比如,删除C之后的结果如下:
![](https://static001.geekbang.org/infoq/78/7886c98f088218386b9096920e3d916f.png)
但是,删除2节点就不一样了,比如,上图删除A
、B
、F
、G
、H
、J
、L
、N
这几个节点,直接删除之后树就不平衡了,所以,需要想一些办法来保证删除L之后树依然是平衡的,怎么办呢?
答案是——偷!
没错,就是偷,从别的地方偷元素过来,把这个空缺补上,就像我们上班划水一样,总要找一些东西把工时补上对不对。
那么,怎么个偷法呢?
总体来说,分成两大类,子节点从父节点偷,父节点从子节点偷,偷着偷着可能还要合并或者迁移元素。
我们来分别看一下删除A
、B
、F
、G
、H
、J
、L
、N
这几个节点的过程是如何偷的,以下多图,请慎重!
(1)删除A
![](https://static001.geekbang.org/infoq/d7/d7d01f25d3fecde926fc7ebb17ca3d37.png)
删除A元素时,先从父节点偷个B过来,此时,B位置空缺了,原来B的位置再从其右子节点偷个C过来,搞定。
(2)删除B
![](https://static001.geekbang.org/infoq/dd/ddc33c58dc24091b65369c21489aa6f6.png)
删除B就很简单了,直接从右子节点偷个C过来就搞定了。
(3)删除F
![](https://static001.geekbang.org/infoq/9e/9ebd712b3cb907c0d8e122594e32236d.png)
删除F的过程就比较复杂了,总之,始终围绕着一个原则:子节点偷不到就偷父节点的,偷过来的元素之后记得可能会合并或者迁移元素。
合并的规则是要始终保证整颗树的有序性,比如,上面从父节点偷了个I过来,它本身就比H大,所以,H必须放在I的左子节点,而左子节点原来已经有G了,所以,只能把它们俩合并了。
同理,迁移J元素的过程也是一样的,J肯定是要放在K的左边,迁移到I的右子节点正好。
(4)删除G
其实跟删除F时从偷I开始是一样的,就不赘述了。
(5)删除H
与删除F的过程一模一样,不再赘述。
(6)删除J
![](https://static001.geekbang.org/infoq/8d/8d9fd34673cf37d6d3670e1fd53418d0.png)
删除J时,从父节点先偷个K过来,此时父节点变成了3节点,所以,直接把M左边的两个元素合并即可。
(7)删除L
![](https://static001.geekbang.org/infoq/9e/9ed6e04f4aa73eb9c2ab81f56e94eacf.png)
删除L的过程与删除J的过程有点像,也是从父节点偷K过来,然后再把M左边的两个元素合并。
(8)删除N
![](https://static001.geekbang.org/infoq/81/812b5eae699419b00deb3e4f48f9de84.png)
删除N时,从父节点偷个O过来,父节点再从其右子节点偷个P过来,偷个屁,偷个屁呀~~
好了,到此为止,2-3-4树删除元素的过程全解析完毕了,我这个示例中几乎包含了所有的场景,请多画图仔细体会,虽然画得想吐血了。
左倾红黑树删除元素
> 注:红黑树的删除稍微有点小复杂,如果强型跟2-3-4挂钩会变得更复杂,所以,下面的内容不完全跟2-3-4树挂钩。
首先,我想问一个问题:一颗二叉查找树删除元素之后如何还能保证它还是二叉查找树呢?
![](https://static001.geekbang.org/infoq/0b/0b9190b3b45a2d27519eb75bb652ea6b.png)
如果是叶子节点,删了也就删了,不影响,但如果是非叶子节点呢?比如,删除M这个元素。
其实,有两种方法:一种是找到M的前置节点并拿到M的位置,一种是找到M的后继节点并拿到M的位置。
什么是前置节点?什么是后继节点呢?好像二叉树里面只听说过父节点、子节点?
我们知道二叉查找树本质上是有序的,这个有序性指的是元素的自然顺序(还有一种有序性是插入顺序)。
所以,你把这颗二叉树中的所有元素排个序(或者中序遍历一下),在M前面的那个节点就是前置节点,在M后面的那个节点就是后继节点。
还有一种更形象的方法,M这个节点左子树中最大的元素就是M的前置节点,M节点右子树中最小的元素就是M的后继节点。
所以,删除M后,把L或者N移到M的位置就可以了,此时,就能保证二叉查找树依然是二叉查找树。
![](https://static001.geekbang.org/infoq/65/658a735f64a94ec5ccb5ade6260e6a58.png)
![](https://static001.geekbang.org/infoq/d1/d174b35de53e02aea184f980f4553a42.png)
不过,大家好像都喜欢移后继节点,即右子树中最小的节点你如果看源码的话,会看到一个单词叫作successor
,就是后继节点的意思。
好了,关于二叉查找树删除元素我们就讲这么多,还是回到红黑树删除元素的过程。
为了方便讲解,我构造了下面这么一颗红黑树:
![](https://static001.geekbang.org/infoq/97/97acc771d8b24e102d05d2823d9179ce.png)
我们先来看一种最简单的情况,如果删除的是红色的叶子节点,比如,上图中的C、P、R这三个元素,如果它的父节点只有它这么一个子节点,直接删之,啥也不用管,比如C,如果它的父节点有两个子节点,那么会分成两种情况,一种是删除的右子节点,则直接删,比如R,另一种是删除的左子节点,那就做一次简单的左旋即可,比如P。
> 我们这里讲的是左倾红黑树,如果是经典的红黑树,则删除红色叶子节点不需要旋转。
OK,我们再来看第二种情况,如果删除的是黑色的叶子节点呢?
我们知道,黑色节点删除之后,肯定不符合红黑树定义了,所以,肯定要进行再平衡的过程。
如果按照经典红黑树的说法,要看它的兄弟节点的颜色,有可能还要看它兄弟节点的子节点的颜色,情况大概有三四种,根本不可能记得住,我这里介绍一种更牛逼的方法,保证你看一遍就能记住。
我们以删除F节点为例,我先给出图示,下面再描述详细步骤:
![](https://static001.geekbang.org/infoq/c7/c7966f6ab982c33eaf8872c91707442f.png)
这种方法非常简单,F是黑色节点没错,那就想办法把它变成红色节点,怎么变呢?
那就得从它的上层节点动手,上层节点的红色其实是可以向下传递的,传递之后,整颗树其实还是红黑树,并不会打破原来红黑树的平衡,直到F变成红色的叶子节点,再一举把它删除,就很简单了。
这种方法相比于经典红黑树的方法,理解起来就容易得多了。
我们再举个删除L的例子,直接上图:
![](https://static001.geekbang.org/infoq/00/000e96035798f6332a76460089832844.png)
好了,上面说的都是删除叶子节点,那么,如果删除的是非叶子节点呢,比如删除E。
![](https://static001.geekbang.org/infoq/84/8482161ac247c134de7bfe9506ae5c16.png)
根据二叉查找树的特性,那么,我们会找到E的后继节点F,然后,把它移到E的位置,但是,此时,不符合红黑树的定义了,所以,你可以发现,其实,删除E相当于间接地删除F原来所在的节点位置,因此,又转化成了上面的删除叶子节点。
过程很简单,最后的结果与删除F的结果基本相同,只是原来E所在位置的元素变成了F,我就不画图了。
你可以想想删除M的过程~
总算讲完了,能看到这里的同学不容易,可能已经超出了一顿早餐的时间,我很抱歉!
后记
本节,我们从红黑树的本质,即2-3-4树出发,彻底掌握了一种不用死记硬背的方法来理解红黑树,你Get到了吗?欢迎留言评论。
有些同学看到这里,可能又说了:Talk is cheap, show me the code!
希望可以大家有帮助,喜欢的朋友可以关注LZ的公众号:小迁不秃头,每天不定时更新,全是干货!
评论