相信很多人都知道红黑树,就是结点有红色和黑色两种。在Linux内存管理、java的Map里都大量使用了红黑树。那到底啥是红黑树,为什么会有红黑树呢?用AVL树不香吗?
关于什么是红黑树,有一个很出名的段子。某猿去一大厂面试,面试官就问什么是红黑树,该猿没有答上来。最后面试接近尾声,面试官象征性问是否还有其他问题的时候,该猿就说,请问红黑树是啥呢?结果面试官说:我也不记得了。
为啥会这样,因为在太多的材料里一提到红黑树就甩出5大条件,然后就是一堆的解释:
看到这5点, 估计没有人搞懂什么是红黑树,因为连看的欲望都没有,所以这样介绍红黑树是极不负责任的。我甚至都怀疑这么写的人是否真的能记住。
到底什么是红黑树呢?找啊找,在一个教程里提到说《算法》这本书的介绍最好,我就结合两者仔细看,感觉是真正看懂了。而被推荐的这本书的作者就是红黑树的发明者,而这位老爷子也是鼎鼎大名的《计算机程序设计的艺术》的作者高德纳的学生。有时候真感觉只有懂的人才能说出让人听懂的话。
这本书没有上来就给一堆红黑树的特性,而是先介绍了2-3树,最后介绍如何用红黑树来简化2-3,也就是说2-3树和红黑树是相通的,理解2-3树很容易 ,通过2-3理解红黑树也就容易很多了,所以我们也先从2-3树开始。
2-3树首先满足二分搜素树的基本性质。二分搜索树我们前面分析过,其基本性质是中序遍历正好是有序的。2-3树的不同之处是一个结点可以存一个元素也可以存两个,例如
对于右侧的结点,一个父节点就可有两个三个孩子,三个孩子的元素分布分别是小于b,b和c之间,以及大于c。
因为一个结点可以又两个也可以都三个孩子,所以就叫2-3树。
这样一个2-3树就可能由两种结点组成。如下图:
2-3树有一个非常重要的性质:一定是绝对平衡的树。也就是深度都是一样的,那是怎么做到的呢?请看下一节。
原则是:元素插入过程,数量超过2的时候,先融合再分裂,而不会将元素直接插入到空结点。
我们现在就通过一个构造的例子来看,假如序列是42 ,37,12 ,18,6,11,5,我们来构造一个2-3树。
首先插入元素为42和37的时候,因为2-3树一个节点可以放两个元素,因此此时结构为:
如果此时12进来了,则首先融合成一个具有3元素的结点:
然后再根据平衡树类似的原理分裂:
如果此时再来一个元素 18,会怎么样呢?首先根据平衡树的原则,18比37小,会插入到其左子树中,此时12位置只有一个元素,还能插入一个,所以就变成了这样子:
如果此时再来一个元素6,会怎么样呢?很明显此时结点元素数量超了,此时会先和12、18位置临时融合成如下结构:
之后再分裂,按照我们的正常理解,应该会变成这样子:
很明显,此时不是绝对平衡的,那该怎么办呢?此时12要和上面的元素融合,变成这样的结构;
此时又是绝对平衡了。
如果此时再增加11呢? 11小于12,要到左子树上去,而6位置只有一个元素,所以就是这样:
如果此时再来一个元素5呢?根据我们前面的分析,要先和6、11融合成这样:
但是此时又不是绝对平衡了,所以,6要和父节点融合成这样:
但是此时根元素也超了,需要进一步分裂 ,就变成这样
这样就重新变成绝对平衡了。这样貌似都是2树了,是否可以合并一下让某些节点可以放两个元素呢?上面的树依然保持2-3树的性质,没必要再合并了。
从上面我们可以看到,如果待插入的结点只有一个元素,可以直接插入。如果已有两个了,则先暂时融合,再分裂开。
如果分裂之后导致树不平衡了,则需要考虑向父节点融合。例如下面的过程插入4之后的变化过程: 如果说根节点元素也超了,此时会分裂,导致树增加了一层。例如下面这个结构插入4之后的样子
上面我们说2-3树就是一个节点可以有两个或者三个元素的绝对平衡树。
上面的结构有个坏处是每个节点可能有2个或者三个子树,这会给查找等带来麻烦,实现也比较复杂,那是否可以用二叉树表示上面这种结构呢?
其实bc节点连接3个子孩子可以这样表示:让前两个自孩子都连接到b上,如下:
然后再将b-c调整一下关系,此时就是下面这样子:
很明显b的地位变成c的子节点了,那此时怎么知道b和c本来是在一次的呢?很简单,我们将b的颜色给换一下:
这就是红黑树的由来。红和黑只是一个状态标记而已,本质上就是用一个二叉树来表示2-3树。
由于红色节点是从2-3树种左侧那个分下来的,所以所有的红色节点都是在左子树上(也称为向左倾斜)。
我们再来看一个更复杂点的对应图:
上面的红黑树种有三个红色节点,正好对应的就是原来2-3树中有两个元素的结点的数量。
看到了吧,红黑树就这么简单。如果感觉还是不太懂,我们可以先增加一个中间状态树:
这个就是红黑树的定义,但是这里我们要明白红色表示的其实是在2-3树中,有两个元素的结点靠左的那一个。
为什么根节点一定是黑色的呢?因为只有一个节点出现两个元素的时候左侧的向下并标记为红色,留下的是黑色的。所以根节点一定是黑色的。
[注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
因为每个叶子结点只有一个元素嘛,不可能为红色的。
不过我没明白这里的空是啥意思,叶子结点指向为空吗?
理解了上面的转换关系,这个其实也非常好理解了,因为红色节点下面连的结点不管是否发生了分裂融合,红色的两个指针,也就是图中1和2标记的位置一定是指向子树靠右的那一个元素,必然是黑色的。
前面4条都是废话,而这一条是红黑树最重要的性质,也是为什么要造一个邪乎的红黑树出来的原因。
这一条从红黑树的角度来看仍然不是那么好理解,但是从与之等价的2-3树就非常直观了:
从上面可以看到,访问叶子结点 12、18、37、48、88需要访问的黑色结点要么是42、33,要么是42、50。所以说访问的黑色结点数是一样的。
其实红黑树还有颜色反转和右旋的逻辑,后面再看要不要补充。
我觉得红黑树理解这些内容就差不多了。在面试过程中如果谈到HaspMap以及并发性时,可能会涉及。如果你真的懂 ,不妨引导一下让面试官问你红黑树。毕竟你自己引导他问你熟悉的问题,比他随便找你不熟悉的问题要好很多。
如果真的问你红黑树了,该怎么办呢?红黑树的问题不过是 什么是红黑树,为什么要用红黑树,红黑树的特征是什么,以及如何构造红黑树几个问题。
如果面试官问你红黑树了,你不要急着背那5条规则,背不下来,也没人愿意听。
你应该先说“要解释红黑树,首先应该明白为什么要有红黑树。”
面试官自然会问你:那为什么Map里要用红黑树。
你接着说:是因为使用二叉树存储时,不对其进行平衡操作,可能会导致树退化成线性的结构。而如果严格保持树要平衡,因为其调整的代价比较高,所以会影响执行的效果。而红黑树本质是一种半平衡的树。红黑树的特征是任何一个结点的左右子树的高度差小于两倍,这实现了查找效率和操作性能的平衡。
然后面试官会很自然的问:红黑树如何保持的。
你可以回答:要解释红黑树,应该先从2-3树开始。然后就是简要说一下上面的对应原理。最后就说:所以,颜色只是节点的一个标记字段,红色节点就是表示与父节点是同一级的。也正是因为如此,我们可以知道红黑树有5个特征。
如果期间面试官没有说话,说到这里,你一定要停下来,问一下:我要解释一下红黑树的5大特征吗?为什么要停下来?因为不见得面试官真正了解红黑树,更极少会从2-3一步步清晰理解红黑树,说道这里,需要给他时间消化一下。如果你不停地说,他会啥也记不住了。
然后你就可以从两者的关系简要说明5条规则,其中第五条是最重要的,但是用2-3解释的话,一句话就搞定了。如果说道这里,我估计面试官不会再说什么。
这样一轮下来,整个过程完全由你主导,而你的思维能力也得到了很好的体现,这才是比较好的面试。
红黑树的代码也不必写,因为太复杂。如果面试的时候让你写红黑树插入和删除的逻辑,那你就要考虑他是不是不想要你,想刁难你一下让你走。