红黑树总结
本文主要来源于维基百科以及<算法导论> 笔者对原文一些说法语句进行了调整,增加笔者的一些理解
1.红黑树概览
红黑树是一种很有意思的平衡检索树。它的统计性能要好于平衡二叉树,因此,红黑树在很多地方都有应用。在C++ STL中,很多部分(目前包括set, multiset, map, multimap)应用了红黑树的变体(SGI STL中的红黑树有一些变化,这些修改提供了更好的性能,以及对set操作的支持)。
2.关于红黑树的性质
红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树增加了如下的额外性质:
- 性质1. 节点是红色或黑色。
- 性质2. 根是黑色。
- 性质3. 所有叶子都是黑色(叶子是NIL节点)。
- 性质4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
- 性质5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。
这些性质强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。主要原因在于,属性4导致了路径不能有两个毗连的红色节点,最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点,根据属性5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。下图为典型的红黑树:
说明:图中的 "nil 叶子" 或"空(null)叶子"不包含数据而只充当树在此结束的指示,这些节点在绘图中经常被省略,导致了这些树好像同上述原则相矛盾,而实际上不是这样. 另外,在一棵红黑树中null节点是很多的,如果每个null节点都用一个真的节点的话那就太浪费空间了,我们可以对所有的null节点都使用同一个节点,这样可以简化算法又能节约空间.
3.红黑树的修改操作
关于旋转操作参看 http://www.cnblogs.com/biyeymyhjob/archive/2012/07/24/2606718.html
以下代码仿造维基百科的介绍和Linux kernel 2.6.7代码结构而编写
关于红黑树的结点节码:
struct rb_node { struct rb_node *rb_parent; int rb_color; #define RB_RED 0 #define RB_BLACK 1 struct rb_node *rb_right; struct rb_node *rb_left; }; struct rb_root { struct rb_node *rb_node; };
关于找到当前节点的祖父节点和叔节点操作函数:
struct rb_node *grandparent(struct rb_node *node) { return node->rb_parent->rb_parent; } struct rb_node *uncle(struct rb_node *node) { if (node->rb_parent == grandparent(n)->rb_left) return grandparent(n)->rb_right; else return grandparent(n)->rb_left; }
关于红黑树的左旋操作代码:
void rotate_left(struct rb_node *node) { struct *new=node->rb_right,*g=node->rb_parent; g->rb_left=new; node->rb_parent=new; node->rb_right=new->rb_left; if(new->rb_left != null) new->rb_left->rb_parent=node; new->rb_parent=g; new->rb_left=node; }
关于红黑树的右旋操作代码:
void rotate_right(struct rb_node *node) { struct *new=node->rb_left,*g=node->rb_parent; g->rb_right=new; node->rb_parent=new; node->rb_left=new->rb_right; if(new->rb_right != null) new->rb_right->rb_parent=node; new->rb_parent=g; new->rb_right=node; }
1).插入
注:以下插入操作的代码实际上为新节点插入后所执行的调整代码,并没有写出真实的插入操作,真实的插入操作实际上与二叉排序树相类似,这里不再赘述,
以下所有操作,在新插入节点后,首先将新节点设为红色。
首先分析当插入时,对红黑树性质的影响
性质1和性质3总是保持着。
性质2只在向空树插入红色节点时候收到威胁
性质4只在增加红色节点、重绘黑色节点为红色,或做旋转时受到威胁。
性质5只在增加黑色节点、重绘红色节点为黑色,或做旋转时受到威胁。
CASE 1: 如果插入的节点是根节点,也就是说初始的红黑树为空,这是最简单的情况,直接将该节点标识成黑色既能保持红黑树性质
void rb_insert_fixcase1(struct rb_node *node) { if (node->rb_parent == NULL) node->rb_color = RB_BLACK; else rb_insert_case2(node); }
CASE 2:如果新插入的节点不是根节点且为红色,又新插入的节点的父节点为黑色的话,这样插入后没有破坏性质4和5,即原节点为黑色一定会保持性质4,二插入新节点为红结点不违反性质5.
void rb_insert_fixcase2(struct rb_node *node) { if (node->rb_parent->rb_color == RB_BLACK) return; else rb_insert_fixcase3(node); }
CASE 3:当父节点为红色节点P且祖父节点U的另一个子节点(叔节点)U同为红色的情况下(此时父节点的父节点一定存在,否则插入前就已不是红黑树,此时又分为父节点是祖父节点的左子或者右子,对于对称性,只要解开一个方向就可以了,这里只考虑父节点为祖父左子的情况),插入的新节点N,此时违反性质4。
调整方案,如下图可以将父节点P和叔叔节点U重绘为黑色并重绘祖父节点为红色,这样可以保持性质5。现在我们的新节点N有了一个黑色的父节点P,因为通过父节点P或叔父节点U的任何路径都必定通过祖父节点G,在这些路径上的黑节点数目没有改变,但是,红色的祖父节点G的父节点也有可能是红色的,这就违反了性质4,为了解决这个问题,我们在祖父节点G上递归地进行情形1的整个过程。(把G当成是新加入的节点进行各种情况的检查)
void rb_insert_fixcase3(struct rb_node *node) { if (uncle(node) != NULL && uncle(node)->rb_color == RED) { node->rb_parent->rb_color = RB_BLACK; uncle(node)->rb_color = RB_BLACK; grandparent(node)->rb_color = RB_RED; rb_insert_fixcase1(grandparent(node)); } else rb_insert_fixcase4(n); }
CASE 4:当父节点P为红色节点且祖父节点的另一个子节点(叔节点)U为黑色或者缺失(可以判定出祖父节点G是黑色,如果祖父节点为红色,则插入前的红黑树违反性质4),同时新节点N是其父节点的右子节点,而父节点P又是其父节点G的左子节点,此时违反性质4,需要调整,如果将新节点N置为黑色有违反性质5,
调整方案:先进行一次左旋转调换新节点N和其父节点P的角色; 接着,按照CASE 5处理以前的父节点P(即在CASE5里面的新节点N)以解决仍然失效的性质4。注意这个改变会导致某些路径(比如图中节点P和节点N叶子节点)但由于这两个节点都是红色的,不违反性质5。如下图
与CASE 4相类似的情况就是,新节点N是其父节点左子节点,而父节点P又是其父节点G的由子节点,其他条件同上,只是将操作过程中的"左","右"相调换,于是有下图:
void rb_insert_fixcase4(struct rb_node *node) { if (node == node->rb_parent->rb_right && node->rb_parent == grandparent(node)->rb_left) { rotate_left(node->rb_parent); node = node->rb_left; } else if (node == node->rb_parent->rb_left && node->rb_parent == grandparent(node)->rb_right) { rotate_right(node->rb_parent); node = node->rb_right; } rb_insert_fixcase5(node); }
CASE 5:当父节点P为红色节点且祖父节点的另一个子节点(叔节点)U为黑色或者缺失(可以判定出祖父节点G是黑色,如果祖父节点为红色,则插入前的红黑树违反性质4),同时新节点N是其父节点的左子节点,而父节点P又是其父节点G的左子节点,此时违反性质4,需要调整,如果将新节点N置为黑色有违反性质5,则有以下调整方案
调整方案:对祖父节点G进行一次右旋转,旋转后,以前的父节点P现在是新节点N和以前的祖父节点G的父节点,然后切换节点P和节点G的颜色,即将P置为黑色,将G置为红色,这样保持了性质4和5,如下图
与CASE5相似的情况:新节点N是其父节点的右子节点,而父节点P又是其父节点G的右子节点,其他条件同上,操作类似,只是将操作过程中的"左","右"相调换,于是有下图:
void rb_insert_fixcase5(struct rb_node *node) { node->rb_parent->rb_color = RB_BLACK; grandparent(node)->rb_color = RB_RED; if (node == node->rb_parent->rb_left && node->rb_parent == grandparent(node)->rb_left) { rotate_right(grandparent(node)); } else if(node == node->rb_parent->rb_right && node->rb_parent == grandparent(node)->rb_right) { rotate_left(grandparent(node)); } }
下面给出网上对红黑树插入操作的总结
2).删除
简单删除
如果需要删除的节点有两个儿子,那么问题可以被转化成删除只有一个子节点的问题(为了表述方便,这里所指的子节点,为非叶子节点的子节点(NIL节点)),转换方法可按照对于一般排序二叉查找树的删除操作,在删除带有两个非叶子儿子的节点的时候,我们找到要么在它的左子树中的最大元素、要么在它的右子树中的最小元素,并把它的值转移到要删除的节点中。我们接着删除我们从中复制出值的那个节点,它必定有少于两个非叶子节点。因为只是复制了一个值而不违反任何属性,这就把问题简化为如何删除最多有一个儿子的节点的问题。不必不关心这个节点是最初要删除的节点还是我们从中复制出值的那个节点。
接下来部分中,我们只需要讨论删除只有一个子节点的情况(如果它两个儿子都为空,即均为叶子,我们任意将其中一个看作它的儿子)。
如果我们删除一个红色节点(此时该节点的儿子将都为叶子节点),它的父亲和儿子一定是黑色的。所以我们可以简单的用它的黑色儿子替换它,并不会破坏性质3和4。通过被删除节点的所有路径只是少了一个红色节点,这样可以继续保证性质5。
如果在被删除节点是黑色而它的儿子是红色的时候。如果只是去除这个黑色节点,用它的红色儿子顶替上来的话,会破坏性质5,但是如果重绘它的儿子为黑色,则曾经通过它的所有路径将通过它的黑色儿子,这样可以继续保持性质5。
复杂删除
注:以下删除操作的代码实际上为删除后所执行的调整代码,并没有写出真实的删除操作,真实的插入操作实际上已经在简单删除中说明了思想,实际上与排序二叉树删除相似,这里不再列出详细代码
需要详细讨论的是在要删除的节点和它的儿子二者都是黑色的时候,这是一种复杂的情况。我们首先把要删除的节点替换为它的节点,注意:以下图中的情况均为替换后的结果,这里称替换节点为N,称呼它的兄弟(它父亲的另一个子节点)为S。在下面的示意图中,我们还是使用P称呼N的父亲,SL称呼S的左儿子,SR称呼S的右儿子。
找到一节点兄弟结点的函数:
struct rb_node *sibling(struct rb_node *node) { if (node == node->rb_parent->rb_left) return node->rb_parent->rb_right; else return node->rb_parent->rb_left; }
再次强调 以下讨论均为节点N和它初始的父亲均是黑色的情况,删除它的父亲导致通过 N 的路径都比不通过它的路径少了一个黑色节点,这导致此红黑树违反了性质 4,树需要被重新平衡。
另外在CASE 2、CASE 5和CASE 6下,假定 N 是它父亲的左子节点。如果它是它的父节点右子节点,则在这些子节点情况下的左和右应对调即可,这里不再赘述。
CASE 1:N是新的根,这种情况非常简单,已经符合所有红黑树性质。
void rb_delete_case1(struct rb_node *node) { if (node->rb_parent != NULL) rb_delete_case2(node); }
CASE 2:S 是红色。
调整:首先在N的父节点P上做左旋转,把红色兄弟转换成N的祖父节点。我们接着对调 N 的父亲节点P和祖父节点S的颜色,这样就可以保持所有的路径仍然有相同数目的黑色节点,现在 N 有了一个黑色的兄弟节点SL和一个红色的父亲节点P,所以我们可以接下去按 CASE 4、CASE 5或CASE 6情况来处理。(它的新兄弟SL是黑色因为它曾是红色S的一个子节点。)
void rb_delete_case2(struct rb_node *node) { struct rb_node *s = sibling(node); if (s->rb_color == RB_RED) { node->rb_parent->rb_color = RB_RED; s->rb_color = RB_BLACK; if (node == node->rb_parent->rb_left) rotate_left(node->rb_parent); else rotate_right(node->rb_parent); } rb_delete_case3(node); }
CASE 3:N 的父节点P、兄弟结点S 和 S 的子节点都是黑色的。
调整:在这种情况下,简单的重绘 S 为红色,这样与这篇文章删除操作之前的红黑树相比,通过S的所有路径,与通过节点P左子节点的路径,均少一个黑色节点,虽然P左右路径黑色节点数是一样的,但是,通过 P 的所有路径现在比不通过 P 的路径少了一个黑色节点,所以仍旧违反性质4。
所以继续调整,就是要重新回到CASE 1 、CASE 2或者CASE 3,逐层向上进行调整,最后使所有路径的黑色节点均比原来少于一个黑色节点,调整结束后,红黑树重新符合所有性质
void rb_delete_case3(struct rb_node *node) { struct rb_node *s = sibling(node); if ((node->rb_parent->rb_color == RB_BLACK) && (s->rb_color == RB_BLACK) && (s->rb_left->rb_color == RB_BLACK) && s->rb_right->rb_color == RB_BLACK)) { s->rb_color = RB_RED; rb_delete_case1(node->rb_parent); } else rb_delete_case4(node); }
CASE 4:S 和 S 的子节点都是黑色,但是 N 的父节点是红色。
调整:简单的交换 N 的兄弟节点S和父亲节点P的颜色,这不影响不通过 N 的路径的黑色节点的数目,但是它使通过 N 的路径上对黑色节点数目增加了1,添补了在这些路径上删除的黑色节点,这样就使红黑树符合了性质4。
void rb_delete_case4(struct rb_node *node) { struct rb_node *s = sibling(node); if ((node->rb_parent->rb_color == RB_RED) && (s->rb_color == RB_BLACK) && (s->rb_left->rb_color == RB_BLACK) && (s->rb_right->rb_color == RB_BLACK)) { s->rb_color = RB_RED; node->rb_parent->rb_color = RB_BLACK; } else rb_delete_case5(node); }
CASE 5:S 是黑色节点,S 的左子节点SL是红色,S 的右子节点是黑色节点,而 N 是S节点父亲的左儿子。
调整:在 S 上做右旋转,这样 S 的左子节点成为 S 的父节点和 N 的新兄弟节点,然后交换 S 和它的新父亲SL的颜色,这样调整后,所有路径与之前相比仍有同样数目的黑色节点,但是现在 N 有了一个右子节点是红色的黑色兄弟SL,下面进入CASE 6。
N 和它的父亲节点P都不受CASE 5变换的影响。
void rb_delete_case5(struct rb_node *node) { struct rb_node *s = sibling(node); if (s->rb_color == RB_BLACK) { if ((node == node->rb_parent->rb_left) && (s->rb_right->rb_color == RB_BLACK) && (s->rb_left->rb_color == RB_RED)) { s->rb_color = RB_RED; s->rb_left->rb_color = RB_BLACK; rotate_right(s); } else if ((n == n->rb_parent->rb_right) && (s->rb_left->rb_color == RB_BLACK) && (s->rb_right->rb_color == RB_RED)) { s->rb_color = RB_RED; s->rb_right->rb_color = RB_BLACK; rotate_left(s); } } rb_delete_case6(node); }
CASE 6:S 是黑色节点,S 的右子节点是红色节点,而 N 是它父亲的左子节点,S的父节点颜色不确定。
调整:我们在 N 的父节点P上做左旋转,这样 S 成为 N 的父节点P和 S 的右子节点的父节点,然后交换 N 的父节点P和 S 的颜色,并使 S 的右子节点为黑色,
此时,N 现在增加了一个黑色祖先: 要么 N 的父节点P变成黑色,要么它是黑色而 S 被增加为一个黑色祖父,总之通过 N 的路径相比调整之前都增加了一个黑色节点。
此时,如果一个路径不经过 N,则有两种可能性:
- 这个路径通过 N 的新兄弟节点,那么它以前和现在都必定通过 S 和 N 的父亲节点P,而它们只是交换了颜色,所以路径保持了同样数目的黑色节点。
- 这个路径通过 N 的新叔父节点---现在的S 的右子节点SR,那么它以前通过 S、S 的父节点P和 S 的右子节点SR,但是现在只通过 S和SR,并且此时SR被从红色改变为黑色最终的合成效果是这个路径(S->SR)黑色节点数目仍然不变。
从以上可以看出,在任何情况下,在这些路径上的黑色节点数目都没有改变,所以前面CASE 6简单操作恢复了性质 4。
在示意图中的白色节点可以是红色或黑色,但是在变换前后都必须指定相同的颜色
void rb_delete_case6(struct rb_node *node) { struct rb_node *s = sibling(node); s->rb_color = node->rb_parent->rb_color; node->rb_parent->rb_color = RB_BLACK; if (node == node->rb_parent->rb_left) { s->rb_right->rb_color = RB_BLACK; rotate_left(node->rb_parent); } else { s->rb_left->rb_color = RB_BLACK; rotate_right(node->rb_parent); } }
【推荐】用1%的研发投入,搭载3倍性能的网易视频云技术
【推荐】融云发布 App 社交化白皮书 IM 提升活跃超 8 倍
· 黑客Jayson E.Street讲述:我如何一边搭飞机一边抢银行
· 晨飞雁通讯 乐视移动的新牺牲品?
· “C轮死”魔咒?我们盘点了已获C轮融资的互金企业,看他们过得怎么样
· 向越来越多的HTTPS致敬!
· 腾讯增持58同城118.81万股 早已为第一大股东
» 更多新闻...
· 技术的正宗与野路子
· 陈皓:什么是工程师文化?
· 没那么难,谈CSS的设计模式
· 程序猿媳妇儿注意事项