红黑树
简介有一个平衡二叉树:红黑树(Red Black Tree),由红黑树Rudolf Bayer至1972明,当时被称为平衡二叉B树(symmetric binary B-trees),1978年被Leonidas J. Guibas 和Robert Sedgewick改成一个比較摩登的名字:红黑树。
红黑树和之前所讲的AVL树相似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。自从红黑树出来后,AVL树就被放到了博物馆里,据说是红黑树有更好的效率,更高的统计性能。
只是在我了解了红黑树的实现原理后。并不相信这是真的,关于这一点我们会在后面进行讨论。
红黑树和AVL树的差别在于它使用颜色来标识结点的高度,它所追求的是局部平衡而不是AVL树中的很严格的平衡。之前我们在解说AVL树时。已经领教过AVL树的复杂,但AVL树的复杂比起红黑树来说简直是小巫见大巫。
红黑树是真正的变态级数据结构。
首先来一个Silverlight做的红黑树的动画,它有助于帮你理解什么是红黑树。
这里须要注意,必须安装Silverlight 2.0 RTW 才干正常执行游戏,下载地址:
http://www.microsoft.com/silverlight/resources/install.aspx?
使用注意事项:
l 结点仅仅接收整数,假设在加入和删除操作中输入非法字串,则会随机加入或删除一个0~99之间的整数。
l 能够不在编辑框中输入数字,直接单击加入和删除button进行加入和删除操作。
l 能够拖动拖动条控制动画速度。
红黑树的平衡
红黑树首先是一棵二叉查找树,它每一个结点都被标上了颜色(红色或黑色),红黑树满足下面5个性质:
1、 每一个结点的颜色仅仅能是红色或黑色。
2、 根结点是黑色的。
3、 每一个叶子结点都带有两个空的黑色结点(被称为黑哨兵),假设一个结点n的仅仅有一个左孩子,那么n的右孩子是一个黑哨兵;假设结点n仅仅有一个右孩子,那么n的左孩子是一个黑哨兵。
4、 假设一个结点是红的,则它的两个儿子都是黑的。也就是说在一条路径上不能出现相邻的两个红色结点。
5、 对于每一个结点来说。从该结点到其子孙叶结点的全部路径上包括同样数目的黑结点。
红黑树的这5个性质中,第3点是比較难理解的。但它却很有必要。我们看图1中的左边这张图。假设不使用黑哨兵。它全然满足红黑树性质,结点50到两个叶结点8和叶结点82路径上的黑色结点数都为2个。
但假设增加黑哨兵后(如图1右图中的小黑圆点)。叶结点的个数变为8个黑哨兵,根结点50到这8个叶结点路径上的黑高度就不一样了,所以它并非一棵红黑树。
要看真正的红黑树请在以上动画中加入几个结点,看看是否满足以上性质。
红黑树的旋转操作
红黑树的旋转操作和AVL树一样。分为LL、RR、LR、RL四种旋转类型,区别在于旋转完毕后改变的是结点的颜色,而不是平衡因子。旋转动画演示请參考AVL这篇文章中的Flash动画:
http://www.cnblogs.com/abatei/archive/2008/11/17/1335031.html
红黑树上结点的插入
在讨论红黑树的插入操作之前必需要明确,不论什么一个即将插入的新结点的初始颜色都为红色。这一点非常easy理解。由于插入黑点会添加某条路径上黑结点的数目,从而导致整棵树黑高度的不平衡。但假设新结点父结点为红色时(如图2所看到的)。将会违返红黑树性质:一条路径上不能出现相邻的两个红色结点。这时就需要通过一系列操作来使红黑树保持平衡。
为了清楚地表示插入操作下面在结点中使用“新”字表示一个新插入的结点;使用“父”字表示新插入点的父结点。使用“叔”字表示“父”结点的兄弟结点。使用“祖”字表示“父”结点的父结点。
插入操作分为下面几种情况:
1、黑父
如图3所看到的。假设新点的父结点为黑色结点,那么插入一个红点将不会影响红黑树的平衡。此时插入操作完毕。
红黑树比AVL树优秀的地方之中的一个在于黑父的情况比較常见,从而使红黑树须要旋转的几率相对AVL树来说会少一些。
2.红父
假设新点的父结点为红色,这时就须要进行一系列操作以保证整棵树红黑性质。
如图3所看到的,因为父结点为红色,此时能够判定,祖父结点必然为黑色。这时须要依据叔父结点的颜色来决定做什么样的操作。青色结点表示颜色未知。
因为有可能须要根结点到新点的路径上进行多次旋转操作,而每次进行不平衡推断的起始点(我们可将其视为新点)都不一样。所以我们在此使用一个蓝色箭头指向这个起始点。并称之为判定点。
2.1 红叔
当叔父结点为红色时,如图4所看到的。无需进行旋转操作,仅仅要将父和叔结点变为黑色,将祖父结点变为红色就可以。但因为祖父结点的父结点有可能为红色,从而违反红黑树性质。此时必须将祖父结点作为新的判定点继续向上进行平衡操作。
须要注意,不管“父”在“叔”的左边还是右边。不管“新”是“父”的左孩子还是右孩子,它们的操作都全然一样。
2.2 黑叔
当叔父结点为黑色时,须要进行旋转,下面图示了全部的旋转可能
情形1:
情形2:
情形3:
情形4:
能够观察到,当旋转完毕后,新的旋转根所有为黑色。此时不须要再向上回溯进行平衡操作。插入操作完毕。须要注意。上面四张图的“叔”、“1”、“2”、“3”结点有可能为黑哨兵结点。
事实上红黑树的插入操作不是非常难,甚至比AVL树的插入操作还更简单些。
但删除操作就远远比AVL树复杂得多,以下就介绍红黑树的删除操作。
红黑树上结点的删除
红黑树本身是一棵二叉查找树,它的删除和二叉查找树的删除相似。首先要找到真正的删除点,当被删除结点n存在左右孩子时,真正的删除点应该是n的中序遍在前驱,关于这一点请复习二叉查找树的删除。如图9所看到的。当删除结点20时。实际被删除的结点应该为18。结点20的数据变为18。
所以能够判断出,在进行删除操作时,真正的删除点必然是仅仅有一个孩子或没有孩子的结点。而依据红黑树的性质能够得出下面两个结论:
1、 删除操作中真正被删除的必然是仅仅有一个红色孩子或没有孩子的结点。
2、 假设真正的删除点是一个红色结点,那么它必然是一个叶子结点。
理解这两点很重要,如图10所看到的,除了情况(a)外。其它任一种况结点N都无法满足红黑树性质。
在下面讨论中,我们使用蓝色箭头表示真正的删除点。它也是旋转操作过程中的第一个判定点。真正的删除点使用“旧”标注,旧点所在位置将被它的的孩子结点所代替(最多仅仅有一个孩子),我们使用“新”表示旧点的孩子结点。删除操作可分为下面几种情形:
1、旧点为红色结点
若旧点为红色结点。则它必然是叶子结点。直接删除就可以。如图11所看到的
2、一红一黑
当旧点为黑色结点,新点为红色结点时,将新点代替旧点位置后,将新点染成黑色就可以(如图12所看到的)。这里须要注意:旧点为红色,新点为黑色的情况不可能存在。
3、双黑
当旧点和新点都为黑色时(新点为空结点时。亦属于这样的情况),情况比較复杂,须要依据旧点兄弟结点的颜色来决定进行什么样的操作。我们使用“兄”来表示旧点的兄弟结点。这里可分为红兄和黑兄两种情况:
3.1 红兄
因为兄弟结点为红色。所以父结点必然为黑色。而旧点被删除后,新点代替了它的位置。
下图演示了两种可能的情况:
红兄的情况须要进行RR或LL型旋转。然后将父结点染成红色,兄结点染成黑色。然后又一次以新点为判定点进行平衡操作。我们能够观察到,旋转操作完毕后,判定点没有向上回溯。而是减少了一层,此时变成了黑兄的情况。
3.2 黑兄
黑兄的情况最为复杂。须要依据黑兄孩子结点(这里用“侄”表示)和父亲结点的颜色来决定做什么样的操作。
3.2.1 黑兄二黑侄红父
如图14所看到的,这样的情况比較简单,仅仅需将父结点变为黑色。兄结点变为黑色,新结点变为黑色就可以。删除操作到此结束。
3.2.2 黑兄二黑侄黑父
如图15所看到的。此时将父结点染成新结点的颜色。新结点染成黑色。兄结点染成红色就可以。当新结点为红色时,父结点被染成红色。此时须要以父结点为判定点继续向上进行平衡操作。
3.2.3 黑兄红侄
黑兄红侄有以下四种情形,以下分别进行图示:
情形1:
情形2:
情形3:
情形4:
由以上图例所看到的,看完以上四张图的兄弟有可能会有一个疑问,假设情形1和情形2中的两个侄子结点都为红色时。是该进行LL旋转还是进行LR旋转呢?答案是进行LL旋转。情形3和情形4则是优先进行RR旋转的判定。
教你透彻了解红黑树
作者 July、saturnman 2010年12月29日
本文參考:Google、算法导论、STL源代码剖析、计算机程序设计艺术。
本人声明:个人原创。转载请注明出处。
推荐阅读:Left-Leaning Red-Black Trees, Dagstuhl Workshop on Data Structures, Wadern, Germany, February, 2008.
------------------------------
红黑树系列,四篇文章于今日已经完毕。[二零一一年一月九日]
教你透彻了解红黑树:
http://blog.csdn.net/v_JULY_v/archive/2010/12/29/6105630.aspx
红黑树算法的层层剖析与逐步实现 [此文被推荐]
http://blog.csdn.net/v_JULY_v/archive/2010/12/31/6109153.aspx
教你彻底实现红黑树:红黑树的c源代码实现与剖析
http://blog.csdn.net/v_JULY_v/archive/2011/01/03/6114226.aspx
一步一图一代码。一定要让你真正彻底明确红黑树 [最后更新]
http://blog.csdn.net/v_JULY_v/archive/2011/01/09/6124989.aspx
------------------------------
一、红黑树的介绍
先来看下算法导论对R-B Tree的介绍:
红黑树,一种二叉查找树。但在每一个结点上添加一个存储位表示结点的颜色,能够是Red或Black。
通过对不论什么一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其
他路径长出俩倍,因而是接近平衡的。
前面说了。红黑树。是一种二叉查找树,既然是二叉查找树。那么它必满足二叉查找树的一般性质。
以下,在详细介绍红黑树之前。咱们先来了解下 二叉查找树的一般性质:
1.在一棵二叉查找树上。运行查找、插入、删除等操作,的时间复杂度为O(lgn)。
由于,一棵由n个结点,随机构造的二叉查找树的高度为lgn,所以顺理成章,一般操作的运行时间为O(lgn)。
//至于n个结点的二叉树高度为lgn的证明。可參考算法导论 第12章 二叉查找树 第12.4节。
2.但若是一棵具有n个结点的线性链。则此些操作最坏情况运行时间为O(n)。
而红黑树,能保证在最坏情况下,主要的动态几何操作的时间均为O(lgn)。
ok,我们知道。红黑树上每一个结点内含五个域。color,key,left,right。假设对应的指针域没有,则设为NIL。
一般的,红黑树。满足下面性质,即仅仅有满足下面所有性质的树,我们才称之为红黑树:
1)每一个结点要么是红的,要么是黑的。
2)根结点是黑的。
3)每一个叶结点,即空结点(NIL)是黑的。
4)假设一个结点是红的,那么它的俩个儿子都是黑的。
5)对每一个结点,从该结点到其子孙结点的全部路径上包括同样数目的黑结点。
下图所看到的,即是一颗红黑树:
此图忽略了叶子和根部的父结点。
二、树的旋转知识
当我们在对红黑树进行插入和删除等操作时。对树做了改动,那么可能会违背红黑树的性质。
为了保持红黑树的性质,我们能够通过对树进行旋转,即改动树种某些结点的颜色及指针结构。以达到对红黑树进行
插入、删除结点等操作时,红黑树依旧能保持它特有的性质(如上文所述的,五点性质)。
树的旋转,分为左旋和右旋。下面借助图来做形象的解释和介绍:
1.左旋
如上图所看到的:
当在某个结点pivot上,做左旋操作时,我们如果它的右孩子y不是NIL[T]。pivot能够为树内随意右孩子而不是NIL[T]的结点。
左旋以pivot到y之间的链为“支轴”进行。它使y成为该孩子树新的根,而y的左孩子b则成为pivot的右孩子。
来看算法导论对此操作的算法实现(以x取代上述的pivot):
LEFT-ROTATE(T, x)
1 y ← right[x] ▹ Set y.
2 right[x] ← left[y] ▹ Turn y's left subtree into x's right subtree.
3 p[left[y]] ← x
4 p[y] ← p[x] ▹ Link x's parent to y.
5 if p[x] = nil[T]
6 then root[T] ← y
7 else if x = left[p[x]]
8 then left[p[x]] ← y
9 else right[p[x]] ← y
10 left[y] ← x ▹ Put x on y's left.
11 p[x] ← y
2.右旋
右旋与左旋差点儿相同,再此不做具体介绍。
对于树的旋转,能保持不变的仅仅有原树的搜索性质,而原树的红黑性质则不能保持,
在红黑树的数据插入和删除后可利用旋转和颜色重涂来恢复树的红黑性质。
至于有些书如 STL源代码剖析有对双旋的描写叙述,事实上双旋仅仅是单旋的两次应用,并无新的内容,
因此这里就不再介绍了,并且左右旋也是相互对称的。仅仅要理解当中一种旋转就能够了。
三、红黑树插入、删除操作的详细实现
三、I、ok。接下来,咱们来详细了解红黑树的插入操作。
向一棵含有n个结点的红黑树插入一个新结点的操作能够在O(lgn)时间内完毕。
算法导论:
RB-INSERT(T, z)
1 y ← nil[T]
2 x ← root[T]
3 while x ≠ nil[T]
4 do y ← x
5 if key[z] < key[x]
6 then x ← left[x]
7 else x ← right[x]
8 p[z] ← y
9 if y = nil[T]
10 then root[T] ← z
11 else if key[z] < key[y]
12 then left[y] ← z
13 else right[y] ← z
14 left[z] ← nil[T]
15 right[z] ← nil[T]
16 color[z] ← RED
17 RB-INSERT-FIXUP(T, z)
咱们来详细分析下。此段代码:
RB-INSERT(T, z),将z插入红黑树T 之内。
为保证红黑性质在插入操作后依旧保持。上述代码调用了一个辅助程序RB-INSERT-FIXUP
来对结点进行又一次着色,并旋转。
14 left[z] ← nil[T]
15 right[z] ← nil[T] //保持正确的树结构
第16行。将z着为红色。因为将z着为红色可能会违背某一条红黑树的性质。
所以。在第17行,调用RB-INSERT-FIXUP(T,z)来保持红黑树的性质。
RB-INSERT-FIXUP(T, z)。例如以下所看到的:
1 while color[p[z]] = RED
2 do if p[z] = left[p[p[z]]]
3 then y ← right[p[p[z]]]
4 if color[y] = RED
5 then color[p[z]] ← BLACK ▹ Case 1
6 color[y] ← BLACK ▹ Case 1
7 color[p[p[z]]] ← RED ▹ Case 1
8 z ← p[p[z]] ▹ Case 1
9 else if z = right[p[z]]
10 then z ← p[z] ▹ Case 2
11 LEFT-ROTATE(T, z) ▹ Case 2
12 color[p[z]] ← BLACK ▹ Case 3
13 color[p[p[z]]] ← RED ▹ Case 3
14 RIGHT-ROTATE(T, p[p[z]]) ▹ Case 3
15 else (same as then clause
with "right" and "left" exchanged)
16 color[root[T]] ← BLACK
ok,參考一网友的言论,用自己的语言,再来详细解剖下上述俩段代码。
为了保证阐述清晰,我再写下红黑树的5个性质:
1)每一个结点要么是红的,要么是黑的。
2)根结点是黑的。
3)每一个叶结点,即空结点(NIL)是黑的。
4)假设一个结点是红的。那么它的俩个儿子都是黑的。
5)对每一个结点。从该结点到其子孙结点的全部路径上包括同样数目的黑结点。
在对红黑树进行插入操作时,我们一般总是插入红色的结点,由于这样能够在插入过程中尽量避免对树的调整。
那么,我们插入一个结点后,可能会使原树的哪些性质改变列?
由于,我们是依照二叉树的方式进行插入,因此元素的搜索性质不会改变。
假设插入的结点是根结点。性质2会被破坏,假设插入结点的父结点是红色。则会破坏性质4。
因此,总而言之,插入一个红色结点仅仅会破坏性质2或性质4。
我们的回复策略非常简单。
其一、把出现违背红黑树性质的结点向上移,假设能移到根结点,那么非常easy就能通过直接改动根结点来恢复红黑树的性质。直接通过改动根结点来恢复红黑树应满足的性质。
其二、穷举全部的可能性,之后把能归于同一类方法处理的归为同一类,不能直接处理的化归到以下的几种情况,
//注:下面情况3、4、5与上述算法导论上的代码RB-INSERT-FIXUP(T, z)。相相应:
情况1:插入的是根结点。
原树是空树,此情况仅仅会违反性质2。
对策:直接把此结点涂为黑色。
情况2:插入的结点的父结点是黑色。
此不会违反性质2和性质4,红黑树没有被破坏。
对策:什么也不做。
情况3:当前结点的父结点是红色且祖父结点的还有一个子结点(叔叔结点)是红色。
此时父结点的父结点一定存在。否则插入前就已不是红黑树。
与此同一时候。又分为父结点是祖父结点的左子还是右子,对于对称性。我们仅仅要解开一个方向就能够了。
在此,我们仅仅考虑父结点为祖父左子的情况。
同一时候。还能够分为当前结点是其父结点的左子还是右子,可是处理方式是一样的。
我们将此归为同一类。
对策:将当前节点的父节点和叔叔节点涂黑,祖父结点涂红,把当前结点指向祖父节点,从新的当前节点又一次開始算法。
针对情况3。变化前(图片来源:saturnman)[插入4节点]:
变化后:
情况4:当前节点的父节点是红色,叔叔节点是黑色,当前节点是其父节点的右子
对策:当前节点的父节点做为新的当前节点,以新当前节点为支点左旋。
例如以下图所看到的。变化前[插入7节点]:
变化后:
情况5:当前节点的父节点是红色,叔叔节点是黑色,当前节点是其父节点的左子
解法:父节点变为黑色。祖父节点变为红色,在祖父节点为支点右旋
例如以下图所看到的[插入2节点]
变化后:
==================
三、II、ok,接下来,咱们最后来了解,红黑树的删除操作:
算法导论一书,给的算法实现:
RB-DELETE(T, z) 单纯删除结点的总操作
1 if left[z] = nil[T] or right[z] = nil[T]
2 then y ← z
3 else y ← TREE-SUCCESSOR(z)
4 if left[y] ≠ nil[T]
5 then x ← left[y]
6 else x ← right[y]
7 p[x] ← p[y]
8 if p[y] = nil[T]
9 then root[T] ← x
10 else if y = left[p[y]]
11 then left[p[y]] ← x
12 else right[p[y]] ← x
13 if y 3≠ z
14 then key[z] ← key[y]
15 copy y's satellite data into z
16 if color[y] = BLACK
17 then RB-DELETE-FIXUP(T, x)
18 return y
RB-DELETE-FIXUP(T, x) 恢复与保持红黑性质的工作
1 while x ≠ root[T] and color[x] = BLACK
2 do if x = left[p[x]]
3 then w ← right[p[x]]
4 if color[w] = RED
5 then color[w] ← BLACK ▹ Case 1
6 color[p[x]] ← RED ▹ Case 1
7 LEFT-ROTATE(T, p[x]) ▹ Case 1
8 w ← right[p[x]] ▹ Case 1
9 if color[left[w]] = BLACK and color[right[w]] = BLACK
10 then color[w] ← RED ▹ Case 2
11 x p[x] ▹ Case 2
12 else if color[right[w]] = BLACK
13 then color[left[w]] ← BLACK ▹ Case 3
14 color[w] ← RED ▹ Case 3
15 RIGHT-ROTATE(T, w) ▹ Case 3
16 w ← right[p[x]] ▹ Case 3
17 color[w] ← color[p[x]] ▹ Case 4
18 color[p[x]] ← BLACK ▹ Case 4
19 color[right[w]] ← BLACK ▹ Case 4
20 LEFT-ROTATE(T, p[x]) ▹ Case 4
21 x ← root[T] ▹ Case 4
22 else (same as then clause with "right" and "left" exchanged)
23 color[x] ← BLACK
为了保证下面的介绍与阐述清晰,我第三次重写下红黑树的5个性质
1)每一个结点要么是红的。要么是黑的。
2)根结点是黑的。
3)每一个叶结点,即空结点(NIL)是黑的。
4)假设一个结点是红的,那么它的俩个儿子都是黑的。
5)对每一个结点,从该结点到其子孙结点的全部路径上包括同样数目的黑结点。
(相信。重述了3次,你应该有深刻记忆了。:D)
saturnman:
红黑树删除的几种情况:
-------------------------------------------------
博主提醒:
下面全部的操作,是针对红黑树已经删除结点之后。
为了恢复和保持红黑树原有的5点性质。所做的恢复工作。
前面,我已经说了。由于插入、或删除结点后,
可能会违背、或破坏红黑树的原有的性质,
所以为了使插入、或删除结点后的树依旧维持为一棵新的红黑树。
那就要做俩方面的工作:
1、部分结点颜色。又一次着色
2、调整部分指针的指向。即左旋、右旋。
而以下全部的文字,则是针对红黑树删除结点后,所做的修复红黑树性质的工作。
二零一一年一月七日更新。
------------------------------------------------------------
(注:下面的情况3、4、5、6,与上述算法导论之代码RB-DELETE-FIXUP(T, x) 恢复与保持
中case1,case2,case3,case4相相应。)
情况1:当前节点是红色
解法。直接把当前节点染成黑色,结束。
此时红黑树性质所有恢复。
情况2:当前节点是黑色且是根节点
解法:什么都不做,结束
情况3:当前节点是黑色,且兄弟节点为红色(此时父节点和兄弟节点的子节点分为黑)。
解法:把父节点染成红色,把兄弟结点染成黑色,之后又一次进入算法(我们仅仅讨论当前节点是其父节点左孩子时的情况)。
然后,针对父节点做一次左旋。此变换后原红黑树性质5不变。而把问题转化为兄弟节点为黑色的情况。
3.变化前:
3.变化后:
情况4:当前节点是黑色,且兄弟是黑色。且兄弟节点的两个子节点全为黑色。
解法:把当前节点和兄弟节点中抽取一重黑色追加到父节点上,把父节点当成新的当前节点。又一次进入算法。
(此变换后性质5不变)
4.变化前
4.变化后
情况5:当前节点颜色是黑色,兄弟节点是黑色,兄弟的左子是红色。右子是黑色。
解法:把兄弟结点染红,兄弟左子节点染黑,之后再在兄弟节点为支点解右旋,
之后又一次进入算法。此是把当前的情况转化为情况6,而性质5得以保持。
5.变化前:
5.变化后:
情况6:当前节点颜色是黑色。它的兄弟节点是黑色,可是兄弟节点的右子是红色,兄弟节点左子的颜色随意。
解法:把兄弟节点染成当前节点父节点的颜色,把当前节点父节点染成黑色,兄弟节点右子染成黑色,
之后以当前节点的父节点为支点进行左旋,此时算法结束。红黑树全部性质调整正确。
6.变化前:
6.变化后:
限于篇幅。不再过多赘述。
很多其它。可參考算法导论或下文我写的第二篇文章。
完。
July、二零一零年十二月二十九日初稿。三十日凌晨修订。行文3个小时以上。
----------------
今下午画红黑树画了好几个钟头,贴俩张图:
红黑树插入的3种情况:
红黑树删除的4种情况:
ok,仅仅贴俩张。很多其它,參考我写的关于红黑树的第二篇文章:
红黑树算法的层层剖析与逐步实现[推荐]
http://blog.csdn.net/v_JULY_v/archive/2010/12/31/6109153.aspx
这篇文章针对算法实现源代码分十层。层层、逐层剖析。相信,更清晰易懂。
//尤其文末针对红黑树的删除情况,层次清晰。
July、2010年12月31日、最后更新。