参考文章:
红黑树原理:此篇逻辑清晰,但是红黑树的配图不行,没法根据图来进行实际的操作理解,本文的意图就是根据作者的思路进行图片的重新分析。
《算法导论》中文版,中文版翻译的马马虎虎,但是有些概念翻译的有点烂,在学习过程中会产生一些疑惑,需要及时更新自己的认知。
红黑树特性:
(1)每个节点要么红要么黑。
(2)根节点黑色。
(3)每个叶子节点是黑色。【叶子节点指的是NIL或者NULL的叶子节点】。
(4)如果一个节点是红色的,那么它的叶子节点必须是黑色的。
(5)从一个节点到改节点的叶子节点的所有路径上包含相同数目的黑节点。
注意点:第(5)点就是《算法导论》翻译的问题,我顺带去找了一下英文版本的解释:
Every path from a given node to any of its descendant NIL nodes contains the same number of black nodes.就是该节点到NIL节点(也就是叶子节点)的黑色节点数目相同。
示意图:(参考文章内此图就是有问题的,140节点下竟让是10和30,应该是作者配图出错了)
针对特性(5),我们可以数一下黑色节点数目是多少,【80】节点到任意一个【nil】节点的黑色数目都是3个。
关于实践复杂度什么的不去关注,暂时没想关注这些东西。省略掉这一部分;
红黑树操作基础---左旋和右旋:
在红黑树进行添加和删除之后,有可能当前的树不再是一个红黑树了,可能特性不满足,所以需要进行调整,而调整的时候除了颜色的更新,还有一个操作就是旋转,旋转包括左旋和右旋。
(1)左旋
左旋操作本质上就是将当前节点变成左节点(具体操作就是固定“当前节点X”,然后逆时针旋转其“右孩子节点Y”,使得Y变成X的父节点)。
算法导论中的伪代码如下:(我把判断部分单独分开,可以看的清楚一些)
//T:红黑树 //nil[T]:哨兵节点,类似空节点 //每个节点存在5个域:color(颜色),key(关键字),left(左指针),right(右指针),p(父指针) LEFT-ROTATE(T, x) y ← right[x] // 前提:假设x的右孩子为y。 right[x] ← left[y] // 将 “y的左孩子” 设为 “x的右孩子”。 p[left[y]] ← x // 将 “x” 设为 “y的左孩子的父亲”。 p[y] ← p[x] // 将 “x的父亲” 设为 “y的父亲” if p[x] = nil[T] then root[T] ← y // 情况1:如果 “x的父亲” 是空节点,则将y设为根节点 else if x = left[p[x]] then left[p[x]] ← y // 情况2:如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子” else right[p[x]] ← y // 情况3:x是它父节点的右孩子, 将y设为“x的父节点的右孩子” left[y] ← x // 将 “x”设为 “y的左孩子” p[x] ← y // 将 “x ”的父节点” 设为 “y”
(2)右旋
右旋操作本质上就是将当前节点变成右节点(具体操作就是固定“当前节点Y”,然后顺时针旋转其“左孩子节点X”,使得X变成Y的父节点)。
算法导论中的伪代码如下:
//T:红黑树 //nil[T]:哨兵节点,类似空节点 //每个节点存在5个域:color(颜色),key(关键字),left(左指针),right(右指针),p(父指针) RIGHT-ROTATE(T, y) x ← left[y] // 前提:这里假设y的左孩子为x。 left[y] ← right[x] // 将 “x的右孩子” 设为 “y的左孩子”。 p[right[x]] ← y // 将 “y” 设为 “x的右孩子的父亲”。 p[x] ← p[y] // 将 “y的父亲” 设为 “x的父亲” if p[y] = nil[T] then root[T] ← x // 情况1:如果 “y的父亲” 是空节点,则将x设为根节点 else if y = right[p[y]] then right[p[y]] ← x // 情况2:如果 y是它父节点的右孩子,则将x设为“y的父节点的右孩子” else left[p[y]] ← x // 情况3:y是它父节点的左孩子 将x设为“y的父节点的左孩子” right[x] ← y // 将 “y” 设为 “x的右孩子” p[y] ← x // 将 “y的父节点” 设为 “x”
红黑树操作基础---添加:
插入的步骤简单来划分可以分成3步:
(1)插入节点
因为红黑树其实也是二叉树,所以可以根据树的键值来确认插入的位置,然后插入节点。
(2)着色为红色
插入之后需要将节点着色为红色。为什么是红色?
因为需要保证特性(5),如果是黑色的话那么次路径肯定比其他路径多了一个黑色节点,着色就不满足要求了,所以直接着色为红色就没有特性(5)出现的问题。
(3)通过旋转或者再次着色,重新满足红黑树要求
5大特性,在着色之后还会出现哪些特性被影响呢?那就是特性(4),因为当前节点被着色为红色,如果父节点也是红色,立马就不满足要求。所以需要旋转和重新着色。
添加操作伪代码:
//待插入节点z RB-INSERT(T, z) y ← nil[T] // 新建节点“y”,将y设为空节点。 x ← root[T] // 设“红黑树T”的根节点为“x” while x ≠ nil[T] // 找出要插入的节点“z”在二叉树T中的位置“y” do y ← x if key[z] < key[x] then x ← left[x] else x ← right[x] p[z] ← y // 设置 “z的父亲” 为 “y” if y = nil[T] then root[T] ← z // 情况1:若y是空节点,则将z设为根 else if key[z] < key[y] then left[y] ← z // 情况2:若“z的key域” < “y的key域”,则将z设为“y的左孩子” else right[y] ← z // 情况3:“z的key域” >= “y的key域”,将z设为“y的右孩子” left[z] ← nil[T] // z的左孩子设为空 right[z] ← nil[T] // z的右孩子设为空。 //第一步完成:将“节点z插入到二叉树”中了。 color[z] ← RED // 将z着色为“红色” //第二步完成:着色。 RB-INSERT-FIXUP(T, z) // 通过RB-INSERT-FIXUP对红黑树的节点进行颜色修改以及旋转,让树T仍然是一颗红黑树 //第三步完成:旋转和着色。
修正操作伪代码:
RB-INSERT-FIXUP(T, z) while color[p[z]] = RED // 若“当前节点(z)的父节点是红色”,则进行以下处理。 do if p[z] = left[p[p[z]]] // 若“z的父节点”是“z的祖父节点的左孩子”,则进行以下处理。 then y ← right[p[p[z]]] // 将y设置为“z的叔叔节点(z的祖父节点的右孩子)” if color[y] = RED // Case 1条件:叔叔是红色 then color[p[z]] ← BLACK ▹ Case 1 // (01) 将“父节点”设为黑色。 color[y] ← BLACK ▹ Case 1 // (02) 将“叔叔节点”设为黑色。 color[p[p[z]]] ← RED ▹ Case 1 // (03) 将“祖父节点”设为“红色”。 z ← p[p[z]] ▹ Case 1 // (04) 将“祖父节点”设为“当前节点”(红色节点) else if z = right[p[z]] // Case 2条件:叔叔是黑色,且当前节点是右孩子 then z ← p[z] ▹ Case 2 // (01) 将“父节点”作为“新的当前节点”。 LEFT-ROTATE(T, z) ▹ Case 2 // (02) 以“新的当前节点”为支点进行左旋。 color[p[z]] ← BLACK ▹ Case 3 // Case 3条件:叔叔是黑色,且当前节点是左孩子。(01) 将“父节点”设为“黑色”。 color[p[p[z]]] ← RED ▹ Case 3 // (02) 将“祖父节点”设为“红色”。 RIGHT-ROTATE(T, p[p[z]]) ▹ Case 3 // (03) 以“祖父节点”为支点进行右旋。 else (same as then clause with "right" and "left" exchanged) // 若“z的父节点”是“z的祖父节点的右孩子”,将上面的操作中“right”和“left”交换位置,然后依次执行。 color[root[T]] ← BLACK
修正代码比较复杂,场景较多,但是涉及到的场景基本都是在保证一个红黑树不被破坏,一般就是性质(2),(4),(5)容易被破坏,所以需要左旋,右旋等操作。
修正操作中的场景总结:
伪代码中也列出来3个场景以及修正过程:
CaseA1:当前节点的父节点是红色,且当前节点的父节点的另一个节点(叔叔节点)也是红色。
处理策略---> (01)父节点设置黑色;
(02)叔叔节点设置黑色;
(03)祖父节点设置红色。
(04)设置祖父节点为当前节点。
CaseA2:当前节点的父节点是红色,叔叔节点是黑色且当前节点是父节点的右孩子。
处理策略---> (01)将父节点作为新的当前节点;(02)以当前节点为支点左旋;
CaseA3:当前节点的父节点是红色,叔叔节点是黑色且当前节点是父节点的左孩子。
处理策略---> (01)设置父节点为黑色;(02)设置祖父节点为红色;(03)以祖父节点为支点右旋;
当然,三种场景需要图的结合才能更好的看出具体操作,下面列出参考文章内的图,做了一点改正,(有些地方不对)。
需要注意伪代码中在进入do if的操作时需要一次判断,else的场景下需要将left和right互换,也就是相反的操作,但是场景还是原有的场景,本系列学习就称作CaseB1,CaseB2,CaseB3。
修正操作中的场景总结图展示:
1. 前提:
基础红黑树(图1-1);
2. 插入节点45:
45节点插入进去之后如图2-1,插入过程非常简单,因为本身红黑树就是有序了,所以只要查找到待插入位置,然后插入,着色就行。
2.1 针对插入45之后的第一次修正(满足CaseA1):
2.2 针对插入45之后的第二次修正(依然满足CaseA1):
第一次修正之后当前节点变成60,此时又出现了违背了特性(4),父节点和孩子节点都为红色。因此还需要继续修正。
2.3 针对插入45之后的第三次修正(都不满足Case的要求,直接执行最后的Root着色操作):
至此插入动作和修正操作结束,红黑树重新变为合法的。但是此次插入没有涉及到Case2和Case3的场景,需要再次分析一下这两个场景。
这里贴一下算法导论里的结论:这里在上面给的插入完整示例内也发现了,在执行CaseA2(CaseB2)或者CaseA3(CaseB3)之后就会变成一个合法的红黑树。不会超过2次,超过2次就要去看看是不是场景分析错了。
红黑树操作基础---删除:
红黑树的删除分成2步:
(1)删除节点
删除节点和普通二叉树一样,需要分成3种情况:
(1.1)叶子节点:直接删除;
(1.2)只有一个孩子:直接删除,并将孩子节点顶替该位置;
(1.3)有两个孩子:找出后继节点
(2)旋转和着色重新变为合法红黑树
删除操作伪代码:
RB-DELETE(T, z) if left[z] = nil[T] or right[z] = nil[T] then y ← z // 若“z的左孩子” 或 “z的右孩子”为空,则将“z”赋值给 “y”; else y ← TREE-SUCCESSOR(z) // 否则,将“z的后继节点”赋值给 “y”。 if left[y] ≠ nil[T] then x ← left[y] // 若“y的左孩子” 不为空,则将“y的左孩子” 赋值给 “x”; else x ← right[y] // 否则,“y的右孩子” 赋值给 “x”。 p[x] ← p[y] // 将“y的父节点” 设置为 “x的父节点” if p[y] = nil[T] then root[T] ← x // 情况1:若“y的父节点” 为空,则设置“x” 为 “根节点”。 else if y = left[p[y]] then left[p[y]] ← x // 情况2:若“y是它父节点的左孩子”,则设置“x” 为 “y的父节点的左孩子” else right[p[y]] ← x // 情况3:若“y是它父节点的右孩子”,则设置“x” 为 “y的父节点的右孩子” if y ≠ z then key[z] ← key[y] // 若“y的值” 和“z的值不等”(也就是z有两个孩子),则赋值给 “z”。注意:这里只拷贝z的值给y,而没有拷贝z的颜色!!! copy y's satellite data into z if color[y] = BLACK then RB-DELETE-FIXUP(T, x) // 若“y为黑节点”,则调用 return y
基于第一步的删除节点来简单归纳一下它可能违反的特性:
特性(2):如果删除y的是根,那么y的一个红色孩子成为了根。
特性(4):x和p[y]可能都是红色的。也就是删除的y是黑色,p[y]和x都是红色的。
特性(5):删除的y可能是黑色,导致此路径上黑色节点少1。
和插入一样,算法导论里还是在优先满足特性(5),这样在修正的时候就少一种不满足,怎么去保证的呢?
假定“x包含一个额外的黑色”,这样就解决了特性(5)的问题,but,why?
因为x节点原本是有颜色的,要么红,要么黑,当删除的y是黑色时,x节点上移时,该路径上就减少了一个黑色节点。所以我们假定x包含一个额外的黑色的话就解决了这个问题了,特性(5)满足了。
但是x节点现在就会出现两种颜色了,“红”+“黑”或者“黑”+“黑”;这就不满足特性(1)了,因此现在需要解决的问题就是如何去满足特性(1),特性(2),特性(4)。
修正操作伪代码:
RB-DELETE-FIXUP(T, x) while x ≠ root[T] and color[x] = BLACK do if x = left[p[x]] then w ← right[p[x]] // 若 “x”是“它父节点的左孩子”,则设置 “w”为“x的叔叔”(即x为它父节点的右孩子) if color[w] = RED // Case 1: x是“黑+黑”节点,x的兄弟节点是红色。(此时x的父节点和x的兄弟节点的子节点都是黑节点)。 then color[w] ← BLACK ▹ Case 1 // (01) 将x的兄弟节点设为“黑色”。 color[p[x]] ← RED ▹ Case 1 // (02) 将x的父节点设为“红色”。 LEFT-ROTATE(T, p[x]) ▹ Case 1 // (03) 对x的父节点进行左旋。 w ← right[p[x]] ▹ Case 1 // (04) 左旋后,重新设置x的兄弟节点。 if color[left[w]] = BLACK and color[right[w]] = BLACK // Case 2: x是“黑+黑”节点,x的兄弟节点是黑色,x的兄弟节点的两个孩子都是黑色。 then color[w] ← RED ▹ Case 2 // (01) 将x的兄弟节点设为“红色”。 x ← p[x] ▹ Case 2 // (02) 设置“x的父节点”为“新的x节点”。 else if color[right[w]] = BLACK // Case 3: x是“黑+黑”节点,x的兄弟节点是黑色;x的兄弟节点的左孩子是红色,右孩子是黑色的。 then color[left[w]] ← BLACK ▹ Case 3 // (01) 将x兄弟节点的左孩子设为“黑色”。 color[w] ← RED ▹ Case 3 // (02) 将x兄弟节点设为“红色”。 RIGHT-ROTATE(T, w) ▹ Case 3 // (03) 对x的兄弟节点进行右旋。 w ← right[p[x]] ▹ Case 3 // (04) 右旋后,重新设置x的兄弟节点。 else color[w] ← color[p[x]] ▹ Case 4 // Case 4: x是“黑+黑”节点,x的兄弟节点是黑色;x的兄弟节点的右孩子是红色的。(01) 将x父节点颜色 赋值给 x的兄弟节点。 color[p[x]] ← BLACK ▹ Case 4 // (02) 将x父节点设为“黑色”。 color[right[w]] ← BLACK ▹ Case 4 // (03) 将x兄弟节点的右子节设为“黑色”。 LEFT-ROTATE(T, p[x]) ▹ Case 4 // (04) 对x的父节点进行左旋。 x ← root[T] ▹ Case 4 // (05) 设置“x”为“根节点”。 else (same as then clause with "right" and "left" exchanged) // 若 “x”是“它父节点的右孩子”,将上面的操作中“right”和“left”交换位置,然后依次执行。 color[x] ← BLACK在case4的开始时手动添加了一个else,感觉这边缺少一个else,不然都变成case2执行之后必须要执行case4的感觉。而本质上是区分了case2,case3,case4的场景的。
核心理念:将额外的黑色沿树上移,直到出现下面三种场景:
(1)x指向一个红黑节点,此时在修复代码的最后一行直接将x单独着色(黑色)。
(2)x指向根,直接消除额外的黑色。
(3)做必要的旋转和颜色修改。
场景细分:
(1)x是“红”+“黑”节点;
处理方式:直接把x设为黑色,over。
(2)x是“黑”+“黑”节点,且x是根;
处理方式:什么都不用做,over。
(3)x是“黑”+“黑”节点,且x不是根;
处理方式:需要细分具体场景进行旋转和着色操作,最复杂。
Case1:x是“黑”+“黑”节点,x的兄弟节点w是红色(这种情况下x的父节点和w的子节点都是黑节点)。
处理策略---> (01)w设置成黑色。
(02)父节点设置成红色。
(03)左旋父节点。
(04)重新设置x的兄弟节点。
Case2:x是“黑”+“黑”节点,x的兄弟节点w是黑色,且w的两个孩子节点也是黑色。
处理策略---> (01)w设置成红色。
(02)设置x的父节点为新的x节点。
(03)父节点添加额外的黑色。(这个操作只是基于假定情况【一个节点具有黑+黑】下的产物)
此图中的一层黑色我标注为灰色,双层黑标注为纯黑用以区分。
Case2的操作意图分析:
X节点是“黑”+“黑”,如果我们将X节点的黑颜色转移至父节点中(如果父节点原来是红色,那么此时就是“红”+“黑”,如果父节点原本是黑色,那么此时就是“黑”+黑),但是此时,经过X节点的黑色节点数目没有发生变化(原本X是两个黑色,现在一个黑色跑父节点上),但是其兄弟节点w上的黑色数目多出一个(多出的一个就是父节点中的黑色)。所以需要将兄弟节点的颜色变成红色。(此意图比较难以理解的地方在于黑颜色上移)。
经过此操作:最后将父节点设置成当前节点,此时父节点的情况是“红”+“黑”或者“黑”+“黑”,红黑的话直接设置成黑色就行,黑黑的话需要进一步循环。
Case3:x是“黑”+“黑”节点,x的兄弟节点w是黑色,w的左孩子是红色的,右孩子是黑色的。
Case4:x是“黑”+“黑”节点,x的兄弟节点w是黑色,w的右孩子是红色的(主要目的是消除x节点上的多余的一层黑色)。
处理策略---> (01)x父节点的颜色设置给兄弟节点w。
(02)设置x的父节点为黑色。
(03)设置w节点的右节点为黑色。
(04)对x的父节点左旋。
(05)设置x为根节点。
注意下图中的修正结果不是一颗合法的红黑树,它只是红黑树的一个局部构造。
Case4的操作意图分析:
Case4的场景中是需要去除x节点中多余的一个黑色,Case4采用的是父节点左旋,但是左旋会带来很多问题:
(1)兄弟节点D的左孩子在Case4中无颜色要求,因此如果左孩子是红色的话,那么会违反特性(4)。
基于此,需要将父节点B设置成黑色,这样,特性(4)得以保全,此时我们来看特性(5)是不是保证的。当父节点B设置成黑色左旋之后。
(2)经过X节点(“黑”+“黑”)(也就是A节点)的黑色数目多出1来,因此此时只要将X节点上多出的一个黑去除掉就行。
(3)经过D节点左孩子(也就是C节点)的黑色数目保持一致(因为在左旋之前D是黑色的,无论C是红是黑,只要B是黑,数目依然保持一致)。
(4)经过D节点右孩子(也就是E节点)的黑色数目不一致了,因为B和D在左旋之前交换过颜色了,D现在是红色,做法是将E变成黑色,就解决问题了。
以上应该就是插入和删除的全部场景了,下面贴一下删除过程中的次数问题: