原文作者:oliver-l
转自链接:https://learnku.com/articles/43763
目录
什么是 “平衡⼆叉查找树”?
如何定义⼀棵 “红⿊树”?
为什么说红⿊树是 “近似平衡” 的?
实现红⿊树的基本思想
插⼊操作的平衡调整
删除操作的平衡调整
1. 针对删除节点初步调整
2. 针对关注节点进⾏⼆次调整
平衡⼆叉树的严格定义是这样的:⼆叉树中任意⼀个节点的左右⼦树的⾼度相差不能⼤于 1。从这个定义来看,完全⼆叉树、满⼆叉树其实都是平衡⼆叉树,但是⾮完全⼆叉树也有可能是平衡⼆叉树。
平衡⼆叉查找树不仅满⾜上⾯平衡⼆叉树的定义,还满⾜⼆叉查找树的特点。最先被发明的平衡⼆叉查找树是 AVL 树,它严格符合我刚讲到的平衡⼆叉查找树的定义,即任何节点的左右⼦树⾼度相差不超过 1,是⼀种⾼度平衡的⼆叉查找树。
但是很多平衡⼆叉查找树其实并没有严格符合上⾯的定义(树中任意⼀个节点的左右⼦树的⾼度相差不能⼤于 1),⽐如红⿊树,它从根节点到各个叶⼦节点的最⻓路径,有可能会⽐最短路径⼤⼀倍。
平衡⼆叉查找树中 “平衡” 的意思,其实就是让整棵树左右看起来⽐较 “对称”、⽐较 “平衡”,不要出现左⼦树很⾼、右⼦树很矮的情况。这样就能让整棵树的⾼度相对来说低⼀些,相应的插⼊、删除、查找等操作的效率⾼⼀些。
平衡⼆叉查找树其实有很多,⽐如,Splay Tree(伸展树)、Treap(树堆)等,但是我们提到平衡⼆叉查找树,听到的基本都是红⿊树。它的出镜率甚⾄要⾼于 “平衡⼆叉查找树” 这⼏个字,有时候,我们甚⾄默认平衡⼆叉查找树就是红⿊树,那我们现在就来看看这个 “明星树”。
顾名思义,红⿊树中的节点,⼀类被标记为⿊⾊,⼀类被标记为红⾊。除此之外,⼀棵红⿊树还需要满⾜这样⼏个要求:
- 根节点是⿊⾊的;
- 每个叶⼦节点都是⿊⾊的空节点(NIL),也就是说,叶⼦节点不存储数据;
- 任何相邻的节点都不能同时为红⾊,也就是说,红⾊节点是被⿊⾊节点隔开的;
- 每个节点,从该节点到达其可达叶⼦节点的所有路径,都包含相同数⽬的⿊⾊节点;
平衡⼆叉查找树的初衷,是为了解决⼆叉查找树因为动态更新导致的性能退化问题。所以,“平衡” 的意思可以等价为性能不退化。“近似平衡” 就等价为性能不会退化的太严重。
⼆叉查找树很多操作的性能都跟树的⾼度成正⽐。⼀棵极其平衡的⼆叉树(满⼆叉树或完全⼆叉树)的⾼度⼤约是 log2n,所以如果要证明红⿊树是近似平衡的,我们只需要分析,红⿊树的⾼度是否⽐较稳定地趋近 log2n 就好了。
⾸先,我们来看,如果我们将红⾊节点从红⿊树中去掉,那单纯包含⿊⾊节点的红⿊树的⾼度是多少呢?
红⾊节点删除之后,有些节点就没有⽗节点了,它们会直接拿这些节点的祖⽗节点(⽗节点的⽗节点)作为⽗节点。所以,之前的⼆叉树就变成了四叉树。
前⾯红⿊树的定义⾥有这么⼀条:从任意节点到可达的叶⼦节点的每个路径包含相同数⽬的⿊⾊节点。我们从四叉树中取出某些节点,放到叶节点位置,四叉树就变成了完全⼆叉树。所以,仅包含⿊⾊节点的四叉树的⾼度,⽐包含相同节点个数的完全⼆叉树的⾼度还要⼩。
完全⼆叉树的⾼度近似 log2n,这⾥的四叉 “⿊树” 的⾼度要低于完全⼆叉树,所以去掉红⾊节点的 “⿊树” 的⾼度也不会超过 log2n。
我们现在知道只包含⿊⾊节点的 “⿊树” 的⾼度,那我们现在把红⾊节点加回去,⾼度会变成多少呢?
从上⾯我画的红⿊树的例⼦和定义看,在红⿊树中,红⾊节点不能相邻,也就是说,有⼀个红⾊节点就要⾄少有⼀个⿊⾊节点,将它跟其他红⾊节点隔开。红⿊树中包含最多⿊⾊节点的路径不会超过 log2n,所以加⼊红⾊节点之后,最⻓路径不会超过 2log2n,也就是说,红⿊树的⾼度近似 2log2n。
所以,红⿊树的⾼度只⽐⾼度平衡的 AVL 树的⾼度(log2n)仅仅⼤了⼀倍,在性能上,下降得并不多。这样推导出来的结果不够精确,实际上红⿊树的性能更好。
不知道你有没有玩过魔⽅?其实魔⽅的复原解法是有固定算法的:遇到哪⼏⾯是什么样⼦,对应就怎么转⼏下。你只要跟着这个复原步骤,就肯定能将魔⽅复原。
实际上,红⿊树的平衡过程跟魔⽅复原⾮常神似,⼤致过程就是:遇到什么样的节点排布,我们就对应怎么去调整。只要按照这些固定的调整规则来操作,就能将⼀个⾮平衡的红⿊树调整成平衡的。
前面定义了红黑树满足的四点要求,在插⼊、删除节点的过程中,第三、第四点要求可能会被破坏,⽽我们今天要讲的 “平衡调整”,实际上就是要把被破坏的第三、第四点恢复过来。
在正式开始之前,我先介绍两个⾮常重要的操作,左旋(rotate left)、右旋(rotate right)。左旋全称其实是叫围绕某个节点的左旋,那右旋的全称估计你已经猜到了,就叫围绕某个节点的右旋。
自己思考:左旋右旋有啥意义呢?交换颜色,变换位置(eg: 右旋中 x.right 原本在 右边,后来变到左边)
左旋:
右旋:
红黑树插入的4种情形
(1)新节点位于根节点,其没有父节点时, 处理思路:将该插入的红色节点直接设为黑色即可; -------处理简单
(2)新节点的父节点已然是黑色时, 处理思路:因为插入的节点是红色,而父节点是黑色,所以不用动,这已然是一颗红黑树 ;------处理简单
(3)父节点和叔节点都是红色时, 处理思路:a.将父节点和叔节点设为黑色; b.将祖父节点设为红色; c.将祖父节点设为当前节点,并继续对新当前节点进行操作
(4)父节点是红色,叔节点是黑色时,又分如下四种情况:
- 当前节点是父亲的左孩子,父亲是祖父的左孩子(Left-Left), 处理思路:a.将祖父节点右旋;b.交换父节点和祖父节点的颜色
- 当前节点是父亲的右孩子,父亲是祖父的左孩子(Right-Left), 处理思路:a.将父节点左旋,并将父节点作为当前节点; b.然后再使用Left Left情形
- 当前节点是父亲的右孩子,父亲是祖父的右孩子(Right-Right), 处理思路:a.将祖父节点左旋;b.交换父节点和祖父节点的颜色
- 当前节点是父亲的左孩子,父亲是祖父的右孩子(Left-Right), 处理思路:a.将父节点右旋,并将父节点作为当前节点; b.然后再使用Right Right情形
![]()
![]()
![]()
插入规则
红⿊树规定,插⼊的节点必须是红⾊的。
红⿊树的平衡调整过程是⼀个【迭代】的过程。
我们把正在处理的节点叫作 【关注节点/当前节点】 。【关注节点/当前节点】会随着不停地迭代处理,⽽不断发⽣变化。最开始的关注节点就是新插⼊的节点。
红⿊树插⼊操作的平衡调整还不是很难,但是它的删除操作的平衡调整相对就要难多了。不过原理都是类似的,我们依旧只需要根据关注节点与周围节点的排布特点,按照⼀定的规则去调整就⾏了。
一、从树中删除节点X(以寻找后继节点的方式进行删除)
情况①:如果X没有孩子,且如果X是红色,直接删除X;如果X是黑色,则以X为当前节点进行旋转调色,最后删掉X
情况②:如果X只有一个孩子C,交换X和C的数值,再对新X进行删除。根据红黑树特性,此时X不可能为红色,因为红色节点要么没有孩子,要么有两个黑孩子。此时以新X为当前节点进行情况①的判断
情况③:如果X有两个孩子,则从后继中找到最小节点D,交换X和D的数值,再对新X进行删除。此时以新X为当前节点进行情况①或②的判断
二、旋转调色(N=旋转调色的当前节点[等于情况①中的X],P=N的父亲,W=N的兄弟,Nf=N的远侄子,Nn=N的近侄子)
情况1:N是根或者N是红色,则:直接将N设为黑色
情况2:N不是根且N是黑色,且W为红色,则:将W设为黑色,P设为红色,对P进行旋转(N为P的左子时进行左旋,N为P的右子时进行右旋),将情况转化为情况1、2、3、4、5
情况3:N不是根且N是黑色,且W为黑色,且W的左右子均为黑色,则:将W设为红色,将P设为当前节点进行旋转调色,将情况转化为情况1、2、3、4、5
情况4:N不是根且N是黑色,且W为黑色,且Nf为黑色,Nn为红色,则:交换W与Nn的颜色,并对W进行旋转(N为P的左子进行右旋,N为P的右子进行左旋),旋转后N的新兄弟W有一个红色WR,则转换为情况5
情况5:N不是根且N是黑色,且W为黑色,且Nf为红色,Nn为黑色,则:将W设为P的颜色,P和Nf设为黑色,并对P进行旋转(N为P的左子进行左旋,N为P的右子进行右旋),N设为根