红黑树入门到精通

红黑树是一种数据结构中很重要的二叉树,在哈希表、Linux底层调度算法等地方均起到很大的作用。它也是面试中一个很经典也很有难度的知识点,我们了解他的结构有助于去理解一些底层的具体实现。只要用心去体会,理解他不是问题。本篇包括红黑树的概念、时间复杂度、应用场景、本质(提出过程、深层原理)、两种构建/删除的方法。

一、红黑树概念

红黑树是数据结构中非常典型的二叉查找树,在介绍他之前先回顾一下二叉树(B Tree)和平衡二叉树(AVL)的概念。

1、二叉树与平衡二叉树

我们知道二叉树是一种能够极大增大数据查找效率的数据结构。它的插入、删除、查找各操作时间复杂度与树的高度成正比。

从这张图我们会发现如下的规律:

(1)左子树上所有节点的值均小于或等于它的根结点的值。

(2)右子树上所有节点的值均大于或等于它的根结点的值。

如下图,树中有n个节点,通常树高度的数量级在logn,因此该二叉树时间复杂度为O(logn)。

在这里插入图片描述

但是有时候由于输入数据排列方式问题,可能会出现高度远高于logn的情况,比如下面的二叉树相当于退化成了链表,时间复杂度为O(n)。

在这里插入图片描述

 为了解决这一问题,需要设计一个能够保持左右子树平衡的二叉查找树,这样可以保证高度控制在logn量级。

 平衡二叉树严格遵循以下性质:1. 任意节点的左右子树高度相差不大于1    

                                                    2. 左右子树都是平衡二叉树

 虽然这种方式查找最快,但是为了保持平衡,在每次删除节点时都需要对树进行旋转等重组操作,而旋转的量级是O(logN),浪费很多时间。因此衍生出了一些类似算法,其中包括红黑树,这些要求没那么严格,并且时间复杂度也控制在logn数量级上。

 2、红黑树

        红黑树通过增加额外的一种颜色存储:黑与红实现树的自平衡。因为平衡要求没AVL严格,因此需要更少的旋转变换,如果增删很频繁,红黑树效率更高

    性质

  1. 每个节点只有两种颜色:红色和黑色。
  2. 根节点是黑色的。
  3. 每个叶子节点(NIL)是不存储数据的黑色空节点(为了将树补成全满状态)
  4. 任何相邻两个节点不能同时为红色。
  5. 从任何一个节点出发,到叶子节点,这条路径上都有相同数目的黑色节点。

       4. 假设红黑节点共n个,那么树的高度最多是2log(n+1),因空间复杂度之和n有关,因此为空间复杂度为O(logn)。接下来证明此结论。

证明思路:因为红黑树的第三个性质(任何节点到叶节点包含的黑色节点个数相同),所以其实去掉红色节点就会变成一个全黑的平衡树,树的高度主要是黑色高度在起作用,因此利用黑高来证明树高。

黑高bh(x):从某个节点x出发(不包含该节点)到达严格叶子节点的任意一条路径上,(包含叶子节点)黑色节点的个数。如下图各节点黑高值。

要想证明结论树的高度最多是2log(n+1),需证明以下两个子命题:

(1) 对于任意一个子树,根节点为x,至少有2^{bh(x)}-1 内部节点(数学归纳法)  后面补!!

(2)对任意x的树高h(x)有bh(x)\geq \frac{h(x)}{2}

二、红黑树增删查改操作(一)

第一种方法,先按照二叉树的方法进行删改,再优化保证红黑树的自身性质(即如果破坏了红黑树的规则就进行修改,不破坏规则则无需调整)。

比如下面是一颗典型的红黑树。

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 等等都能看到它的应用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值