红黑树

红黑树(不必细抠实现,理解思路应该够了)

什么是平衡二叉查找树

平衡二叉树的严格定义是这样的:二叉树中任意一个节点左右子树高度相差不能大于1

完全二叉树、满二叉树其实都是平衡二叉树,但是非完全二叉树也有可能是平衡二叉树。
 
但是很多平衡二叉查找树其实并没有严格符合上面的定义(树中任意一个节点的左右子树的高度相差不能大于 1),比如我们下面要讲的红黑树,它从根节点到各个叶子节点的最长路径有可能比最短路径大一倍
 

 发明平衡二叉查找树这类数据结构的初衷是,解决普通二叉查找树在频繁的插入、删除等动态更新的情况下,出现时间复杂度退化的问题。

如何定义一棵红黑树

平衡二叉查找树其实有很多,比如, Splay Tree (伸展树)、 Treap(树堆)等,但是我们提到平衡二叉查找树,听到的基本都是红黑树。它的出镜率甚至要高于 平衡二叉查找树 这几个字,有时候,我们甚至默认平衡二叉查找树就是红黑树,那我们现在就来看看这个 明星树
 
红黑树的英文是 “Red-Black Tree” ,简称 R-B Tree 。它是一种不严格的平衡二叉查找树.
顾名思义,红黑树中的节点,一类被标记为黑色,一类被标记为红色。除此之外,一棵红黑树还需要满足这样几个要求:
  • 根节点黑色的;
  • 每个叶子节点都是黑色空节点NIL),也就是说,叶子节点不存储数据
  • 任何相邻的节点不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
  • 每个节点,从该节点到达其可达叶子节点所有路径,都包含相同数目黑色节点
这里的第二点要求 叶子节点都是黑色的空节点 ,主要是为了简化红黑树的代码实现而设置的.

为什么说红黑树是近似平衡的?

平衡二叉查找树的初衷,是为了解决二叉查找树因为动态更新导致的性能退化问题。所以, 平衡 的意思可以等价为性能不退化。 近似平衡 就等价为性能不会退化的太严重。
二叉查找树很多操作的性能都跟树的高度成正比。一棵极其平衡的二叉树(满二叉树或完全二叉树)的高度大约是 log 2 n,所以如果要证明红黑树是近似平衡的,我们只需要分析,红黑树的高度是否比较稳定地趋近 log 2 n 就好了。
我们来看,如果我们将红色节点从红黑树中去掉,那单纯包含黑色节点的红黑树的高度是多少呢?
红色节点删除之后,有些节点就没有父节点了,它们会直接拿这些节点的祖父节点(父节点的父节点)作为父节点。所以,之前的二叉树就变成了四叉树。
前面红黑树的定义里有这么一条:从任意节点到可达的叶子节点的每个路径包含相同数目的黑色节点。我们从四叉树中取出某些节点,放到叶节点位置,四叉树就变成了完全二叉树。所以,仅包含黑色节点的四叉树的高度,比包含相同节点个数完全二叉树高度还要小
完全二叉树的高度近似 log 2 n ,这里的四叉 黑树 的高度要低于完全二叉树,所以去掉红色节点的 黑树 的高度也不会超过 log 2 n
我们现在知道只包含黑色节点的 黑树 的高度,那我们现在把红色节点加回去,高度会变成多少呢?
从上面我画的红黑树的例子和定义看,在红黑树中,红色节点不能相邻,也就是说,有一个红色节点就要至少有一个黑色节点,将它跟其他红色节点隔开。红黑树中包含最多黑色节点的路径不会超过 log 2 n ,所以加入红色节点之后,最长路径不会超过 2log2n ,也就是说,红黑树的高度近似 2log 2 n
所以,红黑树的高度只比高度平衡的 AVL 树的高度( log 2 n)仅仅大了一倍,在性能上,下降得并不多。这样推导出来的结果不够精确,实际上红黑树的性能更好。

为什么在工程中大家都喜欢用红黑树这种平衡二叉查找树?

树堆( Treap伸展树( Splay Tree,绝大部分情况下,它们操作的效率都很高,但是也无法避免极端情况下时间复杂度的退化。尽管这种情况出现的概率不大,但是对于单次操作时间非常敏感的场景来说,它们并不适用。
AVL 树是一种高度平衡的二叉树,所以查找的效率非常高,但是,有利就有弊, AVL树为了维持这种高度的平衡,就要付出更多的代价。每次插入、删除都要做调整,就比较复杂、耗时。所以,对于有频繁的插入、删除操作的数据集合,使用 AVL 树的代价就有点高了。
红黑树只是做到了近似平衡,并不是严格的平衡,所以在维护平衡的成本上,要比 AVL 树要低。
所以,红黑树的插入、删除、查找各种操作性能都比较稳定。对于工程应用来说,要面对各种异常情况,为了支撑这种工业级的应用,我们更倾向于这种性能稳定的平衡二叉查找树。

实现红黑树的基本思想

  • 根节点是黑色的;
  • 每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存储数据;
  • 任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
  • 每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点。
在插入、删除节点的过程中,第三、第四点要求可能会被破坏,而 平衡调整 ,实际上就是要把被破坏的第三、第四点恢复过来。
两个非常重要的操作,左旋( rotate left )、右旋( rotate right)。左旋全称其实是叫围绕某个节点的左旋,那右旋则是围绕某个节点的右旋。

插入操作的平衡调整

红黑树规定,插入的节点必须是红色的。而且,二叉查找树中新插入的节点都是放在叶子节点上。所以,关于插入操作的平衡调整,有这样两种特殊情况:
  • 如果插入节点的父节点是黑色的,那我们什么都不用做,它仍然满足红黑树的定义。
  • 如果插入的节点是根节点,那我们直接改变它的颜色,把它变成黑色就可以了。

除此之外,其他情况都会违背红黑树的定义,于是我们就需要进行调整,调整的过程包含两种基础的操作:左右旋转和改变颜色。

红黑树的平衡调整过程是一个迭代的过程。我们把正在处理的节点叫作关注节点。关注节点会随着不停地迭代处理,而不断发生变化。最开始的关注节点就是新插入的节点
新节点插入之后,如果红黑树的平衡被打破,那一般会有下面三种情况。我们只需要根据每种情况的特点,不停地调整,就可以让红黑树继续符合定义,也就是继续保持平衡。
我们下面依次来看每种情况的调整过程。为了简化描述,我把父节点的兄弟节点叫作叔叔节点,父节点的父节点叫作祖父节点。
CASE 1 :如果关注节点是 a ,它的叔叔节点 d 是红色,我们就依次执行下面的操作:
  • 将关注节点a父节点b叔叔节点d的颜色都设置成黑色
  • 将关注节点a祖父节点c的颜色设置成红色
  • 关注节点变成a祖父节点c
  • 跳到CASE 2或者CASE 3

CASE 2 :如果关注节点是 a ,它的叔叔节点 d 是黑色,关注节点 a 是其父节点 b 的右子节点,我们就依次执行下面的操作:
  • 关注节点变成节点a的父节点b
  • 围绕新的关注节点b左旋;
  • 跳到CASE 3

CASE 3 :如果关注节点是 a ,它的叔叔节点 d 是黑色,关注节点 a 是其父节点 b 的左子节点,我们就依次执行下面的操作:
  • 围绕关注节点a的祖父节点c右旋;
  • 将关注节点a的父节点b、兄弟节点c的颜色互换。
  • 调整结束。

删除操作的平衡调整

红黑树插入操作的平衡调整还不是很难,但是它的删除操作的平衡调整相对就要难多了。不过原理都是类似的,我们依旧只需要根据关注节点与周围节点的排布特点,按照一定的规则去调整就行了。
删除操作的平衡调整分为两步:
第一步是针对删除节点初步调整。初步调整只是保证整棵红黑树在一个节点删除之后,仍然满足最后一条定义的要求,也就是说,每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点;
第二步是针对关注节点进行二次调整,让它满足红黑树的第三条定义,即不存在相邻的两个红色节点。
1.针对删除节点初步调整
这里需要注意一下,红黑树的定义中 只包含红色节点和黑色节点 ”,经过初步调整之后,为了保证满足红黑树定义的最后一条要求,有些节点会被标记成两种颜色, - 或者 - 。如果一个节点被标记为了 - ,那在计算黑色节点个数的时候,要算成两个黑色节点。
在下面的讲解中,如果一个节点既可以是红色也可以是黑色,在画图的时候,我会用一半红色一半黑色来表示。如果一个节点是 - 或者 - ”,我会用左上角的一个小黑点来表示额外的黑色。
CASE 1 :如果要删除的节点是 a ,它只有一个子节点 b ,那我们就依次进行下面的操作:
  • 删除节点a,并且把节点b替换到节点a的位置,这一部分操作跟普通的二叉查找树的删除操作一样;
  • 节点a只能是黑色,节点b也只能是红色,其他情况均不符合红黑树的定义。这种情况下,我们把节点b改为黑色;
  • 调整结束,不需要进行二次调整。
CASE 2 :如果要删除的节点 a 有两个非空子节点,并且它的后继节点就是节点 a 的右子节点 c 。我们就依次进行下面的操作:
  • 如果节点a的后继节点就是右子节点c,那右子节点c肯定没有左子树。我们把节点a删除,并且将节点c替换到节点a的位置。这一部分操作跟普通的二叉查找树的删除操作无异;
  • 然后把节点c的颜色设置为跟节点a相同的颜色;
  • 如果节点c是黑色,为了不违反红黑树的最后一条定义,我们给节点c的右子节点d多加一个黑色,这个时候节点d就成了-或者-
  • 这个时候,关注节点变成了节点d,第二步的调整操作就会针对关注节点来做。

CASE 3 :如果要删除的是节点 a ,它有两个非空子节点,并且节点 a 的后继节点不是右子节点,我们就依次进行下面的操作:
  • 找到后继节点d,并将它删除,删除后继节点d的过程参照CASE 1
  • 将节点a替换成后继节点d
  • 把节点d的颜色设置为跟节点a相同的颜色;
  • 如果节点d是黑色,为了不违反红黑树的最后一条定义,我们给节点d的右子节点c多加一个黑色,这个时候节点c就成了-或者-
  • 这个时候,关注节点变成了节点c,第二步的调整操作就会针对关注节点来做。

2.针对关注节点进行二次调整

CASE 1 :如果关注节点是 a ,它的兄弟节点 c 是红色的,我们就依次进行下面的操作:
  • 围绕关注节点a的父节点b左旋;
  • 关注节点a的父节点b和祖父节点c交换颜色;
  • 关注节点不变;
  • 继续从四种情况中选择适合的规则来调整。

CASE 2 :如果关注节点是 a ,它的兄弟节点 c 是黑色的,并且节点 c 的左右子节点 d e 都是黑色的,我们就依次进行下面的操作:
  • 将关注节点a的兄弟节点c的颜色变成红色;
  • 从关注节点a中去掉一个黑色,这个时候节点a就是单纯的红色或者黑色;
  • 给关注节点a的父节点b添加一个黑色,这个时候节点b就变成了-或者-
  • 关注节点从a变成其父节点b
  • 继续从四种情况中选择符合的规则来调整。

CASE 3 :如果关注节点是 a ,它的兄弟节点 c 是黑色, c 的左子节点 d 是红色, c 的右子节点 e 是黑色,我们就依次进行下面的操作:
  • 围绕关注节点a的兄弟节点c右旋;
  • 节点c和节点d交换颜色;
  • 关注节点不变;
  • 跳转到CASE 4,继续调整。

CASE 4 :如果关注节点 a 的兄弟节点 c 是黑色的,并且 c 的右子节点是红色的,我们就依次进行下面的操作:
  • 围绕关注节点a的父节点b左旋;
  • 将关注节点a的兄弟节点c的颜色,跟关注节点a的父节点b设置成相同的颜色;
  • 将关注节点a的父节点b的颜色设置为黑色;
  • 从关注节点a中去掉一个黑色,节点a就变成了单纯的红色或者黑色;
  • 将关注节点a的叔叔节点e设置为黑色;
  • 调整结束。

讨论

一:我说的 case3 的情况是表示老师的画的那个图, case3 图的例子根节点到左边叶子节点只经过 2 个黑色节点,到右边叶子节点却经过了 3 个黑色节点。
二:我这里就大概说下吧(一家之言,自己的一点经验,也希望别的同学来一起讨论):
1. 左旋右旋这个,个人还是认为要画图,不画图我自己也写不出那个代码 …… 哈哈。
2. 说到插入删除的算法,我说用到了递推,就比如插入的 CASE1 的情况, CASE1的处理之后,关注节点从本身变成了他的祖父节点(红色节点),这就是往根节点递推。不过我认为 CASE1 处理过一次之后,不一定会进入 case2 或者 case3 ,是有可能还在 case1 的。
换句话说,就是可以在 case1 的情况下,一直往根节点走,因为当前节点永远是红色,所以在最后要把根节点涂黑。同时,只要进入到 case2,case3的情况,就是变成平衡二叉树的单旋和双旋的情况,双旋的处理逻辑就是把双旋变成单旋(比如先右后左旋就是把树变成 左撇子 ”)。这个就变成了单左旋能一步到位处理的平衡了,这个就是归纳。把未知情况转化为已知,如果我没有记错的话,数学归纳法的核心思想就是递推和归纳。
3.其实我们只要记住,除了关注的节点所在的子树,其他的子树本身都是一颗红黑树,他们是满足红黑树的所有特征的。当关注节点往根节点递推时,这个时候关注节点的子树也已经满足了红黑树的定义,我们就不用再去特别关注子树的特征。只要注意关注节点往上的部分。这样就能把问题简化,思考的时候思路会清晰一些。
4. 再说到删除算法,我看到很多同学没理解为什么要红 - 黑,黑 - 黑节点的出现。这里我的看法是,红黑树最不好控制的其实是最后一个的性质 4(每个节点,从该节点到达其可达叶子节点的所有路径,都包含相同数目的黑色节点),因为你永远不知道别的子树到底有多少个黑色节点。这里加入红 - 黑,黑 -黑节点就可以控制红黑树满足性质 4 ,到时候要恢复颜色,只要去掉多余的黑色即可。
接下来的处理思路就是要满足: 1. 每个节点不是红的就是黑的, 2. 相邻节点不能是红的。这个思路计时变复杂为简单。
删除的 case1 情况,并没有真正处理,而且为了进入接下来的 case2,case3,case4 ,这里又是之前说到的归纳思想。 case2的情况又是一个递推思路,关注节点往根节点递推,让其左右子树都满足红黑树的定义。因为往上推,右子树多了一个黑色节点,就把关注节点的兄弟节点变红,使其满足性质 4.
删除的 case3 是为了进入 case4 ,提前变色的原因和 case2 是一样的,都是为了满足性质 4 。同样是归纳推理的思路。都要记住一点,各种 case下的其他子树节点都满足红黑树的定义,需要分类讨论的,都在这几种 case 情况中了。
4.最后的建议,其实说了这么多,很多的表达都不太清楚,但是个人感觉,数学基础好的同学,理解红黑树会好一些,学习的时候多画画图,人对图形的敏感肯定比文字高,另外的就是大家可以去看看源码,本人是做 java 开发的, jdk1.8 treemap就是用红黑树实现的,跟着源码多看看,跟着老师的说明或者百度上的教程思考,动笔画画图,都能理解的。我自己看 jdk源码的也是看了将近两个月才大概明白(因为也在上班,只有晚上有一些时间来看看代码)。学习 的过程中要耐心,学习红黑树本身也不是为了 默写 ,而是去学习思想,锻炼思维,复杂问题简单化,新问题转化为已解决过的问题等等。其实说到最后, 都是用到了数学的思维,这些思维都会在潜移默化中影响到自己。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值