红黑树是一种数据结构中很重要的二叉树,在哈希表、Linux底层调度算法等地方均起到很大的作用。它也是面试中一个很经典也很有难度的知识点,我们了解他的结构有助于去理解一些底层的具体实现。只要用心去体会,理解他不是问题。本篇包括红黑树的概念、时间复杂度、应用场景、本质(提出过程、深层原理)、两种构建/删除的方法。
一、红黑树概念
红黑树是数据结构中非常典型的二叉查找树,在介绍他之前先回顾一下二叉树(B Tree)和平衡二叉树(AVL)的概念。
1、二叉树与平衡二叉树
我们知道二叉树是一种能够极大增大数据查找效率的数据结构。它的插入、删除、查找各操作时间复杂度与树的高度成正比。
从这张图我们会发现如下的规律:
(1)左子树上所有节点的值均小于或等于它的根结点的值。
(2)右子树上所有节点的值均大于或等于它的根结点的值。
如下图,树中有n个节点,通常树高度的数量级在logn,因此该二叉树时间复杂度为O(logn)。
但是有时候由于输入数据排列方式问题,可能会出现高度远高于logn的情况,比如下面的二叉树相当于退化成了链表,时间复杂度为O(n)。
为了解决这一问题,需要设计一个能够保持左右子树平衡的二叉查找树,这样可以保证高度控制在logn量级。
平衡二叉树严格遵循以下性质:1. 任意节点的左右子树高度相差不大于1
2. 左右子树都是平衡二叉树
虽然这种方式查找最快,但是为了保持平衡,在每次删除节点时都需要对树进行旋转等重组操作,而旋转的量级是O(logN),浪费很多时间。因此衍生出了一些类似算法,其中包括红黑树,这些要求没那么严格,并且时间复杂度也控制在logn数量级上。
2、红黑树
红黑树通过增加额外的一种颜色存储:黑与红实现树的自平衡。因为平衡要求没AVL严格,因此需要更少的旋转变换,如果增删很频繁,红黑树效率更高。
性质
- 每个节点只有两种颜色:红色和黑色。
- 根节点是黑色的。
- 每个叶子节点(NIL)是不存储数据的黑色空节点(为了将树补成全满状态)
- 任何相邻两个节点不能同时为红色。
- 从任何一个节点出发,到叶子节点,这条路径上都有相同数目的黑色节点。
4. 假设红黑节点共n个,那么树的高度最多是2log(n+1),因空间复杂度之和n有关,因此为空间复杂度为O(logn)。接下来证明此结论。
证明思路:因为红黑树的第三个性质(任何节点到叶节点包含的黑色节点个数相同),所以其实去掉红色节点就会变成一个全黑的平衡树,树的高度主要是黑色高度在起作用,因此利用黑高来证明树高。
黑高bh(x):从某个节点x出发(不包含该节点)到达严格叶子节点的任意一条路径上,(包含叶子节点)黑色节点的个数。如下图各节点黑高值。
要想证明结论树的高度最多是2log(n+1),需证明以下两个子命题:
(1) 对于任意一个子树,根节点为x,至少有 内部节点(数学归纳法) 后面补!!
(2)对任意x的树高h(x)有
二、红黑树增删查改操作(一)
第一种方法,先按照二叉树的方法进行删改,再优化保证红黑树的自身性质(即如果破坏了红黑树的规则就进行修改,不破坏规则则无需调整)。
比如下面是一颗典型的红黑树。
1、查询节点
查询节点是最简单的一个,他的查找过程和二叉查找树一样,查找元素比当前节点大,就从右子树继续查找比较,查找元素比当前节点小,就从左子树继续查找比较。查找过程就不再赘述了。
2、插入节点
插入节点分为三种情况。
第一种情况:新节点没有父节点
没有父节点只有一种情况,就是插入的节点是整棵树第一个节点,也就是根节点,为此我们只需要把插入节点涂成黑色就OK了。这也就保证了性质2:根节点是黑色的。
第二种情况:新节点的父节点是黑色
为此我们举一个例子,比如说上面的红黑树中,我们插入节点14。来看一下会发生什么情况?
由于父结点15是黑色结点,因此这种情况并不会破坏红黑树的规则,无需做任何调整。
第三种情况:新节点的父亲节点为红色
我们还是举个例子,比如我们在最开始的红黑树基础之上插入节点21,此时会发生什么情况呢?
此时还是老规矩,对照着红黑树的5个特征一个一个来看,只要是违反了一条就需要做出调整。我们来看一下:
(1)每个节点只有两种颜色:红色和黑色。这一条满足。
(2)根节点是黑色的。这一条也满足。
(3)每个叶子节点(NIL)都是黑色的空节点。这一条满足。
(4)从根节点到叶子节点,不会出现两个连续的红色节点。这一条发现不满足。
就是上面这一条规则没有满足,所以我们此时需要调整?问题来了如何调整呢?因为直接看父节点没办法实现,所以还需要观察另外的节点,也就是新节点的叔叔节点。根据叔叔节点的颜色来调整。调整的方式有两种:变色和旋转。
(1)叔叔节点是红色:
此时插入的节点是21,但是叔叔节点是27,刚好是红色。调整需要按照局部到整体的顺序,先满足局部规则,再向上级扩展。我们直接来看调整的步骤:
第一步:把新节点21的父节点22变成黑色。
此时重新看一下是否满足红黑树的五条特征了没,一条一条发现,第五条没有满足,也就是从任何一个节点出发,到叶子节点,这条路径上没有相同数目的黑色节点。比如从25出发。这时候怎么办呢?那就继续调整。
第二步:把22的父节点25变成红色
这时候还是老规矩,不要嫌弃麻烦,因为只有经历了一步又一步的麻烦之后,你才能牢记那5条规则特征。我们对照之后会发现节点25和节点27是两个连续的红色节点,这时候又破坏了规则4。怎么办呢?那就继续调整就OK了。
难道这时候还要继续往上调整吗?如果你这样做就错了,因为不断地往上调整最后就会把根节点变成了红色,会走进死胡同。我们往下走。
第三步:把节点27变成黑色
来吧,继续重新审查那5条规则特征。很明显节点17和节点25是两个连续的红色,又破坏了。但继续坚持下去,胜利就在眼前。
第四步:把节点17和节点18都变成黑色节点
现在你再对照一下那5条规则,是不是完全保证了。
看到这里,仅是红黑树插入的一种情况。这时,你感觉是不是还没有平衡二叉树好,调整很麻烦。但事实上,红黑树因有变色的功能,会少很多旋转,并且每次都可保证3次操作之内将红黑树调整好,开销更小。而二叉树为了保持平衡,却需要几乎每次的旋转操作,最坏的情况需要从根节点旋转到叶节点,复杂度为O(logn)。
写到这真的是太累了,和你读这篇文章的感觉一样一样的,不过这种情况也只是插入情况中的一种。继续往下看:
局面1:新结点(A)位于树根,没有父结点。
(空心三角形代表结点下面的子树)
这种局面,直接让新结点变色为黑色,规则2得到满足。同时,黑色的根结点使得每条路径上的黑色结点数目都增加了1,所以并没有打破规则5。
局面2:新结点(B)的父结点是黑色。
这种局面,新插入的红色结点B并没有打破红黑树的规则,所以不需要做任何调整。
局面3:新结点(D)的父结点和叔叔结点都是红色。
这种局面,两个红色结点B和D连续,违反了规则4。因此我们先让结点B变为黑色:
这样一来,结点B所在路径凭空多了一个黑色结点,打破了规则5。因此我们让结点A变为红色:
这时候,结点A和C又成为了连续的红色结点,我们再让结点C变为黑色:
经过上面的调整,这一局部重新符合了红黑树的规则。
局面4:新结点(D)的父结点是红色,叔叔结点是黑色或者没有叔叔,且新结点是父结点的右孩子,父结点(B)是祖父结点的左孩子。
我们以结点B为轴,做一次左旋转,使得新结点D成为父结点,原来的父结点B成为D的左孩子:
这样一来,进入了局面5。
局面5:新结点(D)的父结点是红色,叔叔结点是黑色或者没有叔叔,且新结点是父结点的左孩子,父结点(B)是祖父结点的左孩子。
我们以结点A为轴,做一次右旋转,使得结点B成为祖父结点,结点A成为结点B的右孩子:
接下来,我们让结点B变为黑色,结点A变为红色:
经过上面的调整,这一局部重新符合了红黑树的规则。
以上就是红黑树插入操作所涉及的5种局面。
或许有人会问,如果局面4和局面5当中的父结点B是祖父结点A的右孩子该怎么办呢?
很简单,如果局面4中的父结点B是右孩子,则成为了局面5的镜像,原本的右旋操作改为左旋;如果局面5中的父结点B是右孩子,则成为了局面4的镜像,原本的左旋操作改为右旋。
2、删除节点
这里有两种情况:
(1)如果被删节点为叶子节点,则可以直接删除叶子节点,随后再根据上面5种局面进行调平
(2)如果被删节点有子节点,需要与相邻数值的叶子节点(前驱/后继元素)进行替换,然后再执行(1)操作
看到这里,小伙伴们可能已经发懵了,这些太复杂了,需要全部记下来吗?其实通过实战演练几遍,自己就会调整了。并且在下面我会介绍另外一种简便的不需记颜色的方法,而在讲他之前,需要理解红黑树的本质——23、234树。
三、 红黑树的本质
23树即2-3阶B树,234树即2-3-4阶B树。
问:2-3阶,2-3-4阶是什么意思呢?
答:通俗来讲就是这个节点向下的分支的个数就是他的阶。
2-3阶的B树长这样
2-3-4阶相比于2-3阶树多了4阶,4节点就是这样的
问:2-3阶树和2-3-4阶树和红黑树有什么关系呢?
答:就像Java中类和对象的关系一样,红黑树是2-3-4阶树的一种实现方式。
2-3-4阶树中的节点和红黑树的节点的对应规则:
那么按照这个规则我们按照3节点都为左倾红黑树将一个2-3阶树转换为红黑树
如此一来,理解红黑树就不再那么抽象了,至于红黑树的其他操作,增删节点,在2-3阶树的增删基础上去理解就容易很多了。
以下内容操作设2-3树均为左倾的红色节点表示,即一定是左儿子。这种限定能够很大的减少红黑树调整过程中的复杂性,我们将在接下来的内容中体会到这一点。
四、红黑树的增删查改操作(二)
我们在了解红黑树的插入删除操作之前,需要先了解2-3树的插入删除操作,这样才能理解红黑树中染色和旋转背后的意义。
让我们来看一下对于2-3树的插入。我们的插入操作需要遵循一个原则:先将这个元素尝试性地放在已经存在的节点中,如果要存放的节点是2节点,那么插入后会变成3节点,如果要存放的节点是3节点,那么插入后会变成4节点(临时)。然后,我们对可能生成的临时4节点进行分裂处理,使得临时4节点消失。
事实上,这正对应了红黑树在插入的时候一定会把待插入节点涂成红色,因为红色节点的意义是与父节点进行关联,形成概念模型2-3树中的3节点或者临时4节点。
而红黑树之所以需要在插入后进行调整,正是因为可能存在着概念模型中的临时4节点(反应在红黑树中是双红的情况)。
试想在2-3树中如果待插入节点是个2节点,那么反应在红黑树中,不正好对应着黑色父节点吗,在黑色父节点下面增加一个红色儿子,确实不会违背红黑树的任何规则,这也对应着我们向2-3树中的2节点插入一个元素,只需要简单的把2节点变成3节点。
接下来让我们来看一下对于2-3树的删除。对于2-3树的删除我们主要要考虑待删除元素在2节点这种情况,因为如果待删除元素在3节点,那么可以直接将这个元素删除,而不会破坏2-3树的任何性质(删除这个元素不会引起高度的变化)。
当待删除元素在2节点的时候,由于删除这个元素会导致2节点失去自己唯一的元素,引发2节点自身的删除,会使得树中某条路径的高度发生变化,树变得不平衡。
因此我们有两种方案去解决这个问题:
- 第一种方案,先删除这个2节点,然后对树进行平衡调整。
- 第二种方案,我们想办法让这个被删除的元素不可能出现在2节点中。
第一种方法前面已经讲过,这里采用第二种。我们在搜索到这个节点的路径中,不断地判断当前节点是否为2节点,如果是,就从它的兄弟节点或者它的父节点借一个元素,使得当前节点由2节点成为一个3节点或者一个临时4节点。如果父节点、兄弟节点均为2节点,则将三节点合并为临时4节点,再进行删除。思路就是将被删元素变为非2节点,再进行删除。
来看它的五条定义:
1.节点颜色有红色和黑色
【2-3树到红黑树的转化已经解释过】
2.根节点必为黑色
【2-3树中如果根节点为2节点,那么它本来就对应红黑树中黑节点;如果根节点为3节点,也可以用黑色节点表示较大的那个元素,然后较小的元素作为左倾红节点存在于红黑树中】
3.所有叶子节点都是黑色
4.任意节点到叶子节点经过的黑色节点数目相同
【红黑树中的红节点是和黑色父节点绑定的,在2-3树中本来就是同一层的,只有黑色节点才会在2-3树中真正贡献高度,由于2-3树的任一节点到空链接距离相同,因此反应在红黑树中就是黑色完美平衡】
5.不会有连续的红色节点
【2-3树中本来就规定没有4节点,2-3-4树中虽然有4节点,但是要求在红黑树中体现为一黑色节点带两红色儿子,分布左右,所以也不会有连续红节点】
1. 红黑树插入案例
给定一组关键字{20,30,50,52,60,68,70},创建一个红黑树,此方法为先创建2-3阶B树,再转换为红黑树。
根据B树要求,三阶m=3,除了根节点外,非叶子节点至少有[3/2]-1=1个关键字,最多有3-1=2个关键字。所以依次插入20,30关键字
2.删除案例
1. 删除叶子节点。
1.1 删除节点为3节点,直接删
1.2 删除节点为2节点,兄弟为3节点
1.3 删除节点为2节点,兄弟为2节点,将父节点其中一个值与兄弟节点合并,再删除自己。
2. 删除非叶子节点
找出相邻关键字,替换,再执行上述步骤。
是第三种情况,兄弟节点不够借,用兄弟和双亲合并,再删除自己。
全部结束。
五、应用场景
红黑树保证了最坏情形下在 O(logn) 时间复杂度内完成查找、插入及删除操作;因此红黑树可用于很多场景,比如在 Java 的集合框架 (HashMap、TreeMap、TreeSet)、Nginx 的 Timer 管理、Linux 虚拟内存管理以及 C++ 的 STL 等等都能看到它的应用。