注:本文全部内容均翻译自维基百科,部份内容为原创。html
强烈建议阅读文章末尾的参考资料。node
红黑树是自平衡的二叉搜索树,是计算机科学中的一种数据结构。git
平衡是指全部叶子的深度基本相同(彻底相等的状况并很少见,因此只能趋向于相等) 。算法
二叉搜索树是指,节点最多有两个儿子,且左子树中全部节点都小于右子树。数据结构
树中节点有改动时,经过调整节点顺序(旋转),从新给节点染色,使节点知足某种特殊的性质来保持平衡。svg
旋转和染色过程确定通过特殊设计能够高效的完成。函数
它不是彻底平衡的二叉树,但能保证搜索操做在O(log n)的时间复杂度内完成(n是树中节点总数)。oop
插入、删除以及旋转、染色操做都是O(log n)的时间复杂度。ui
每一个节点只须要用一位(bit)保存颜色(仅为红、黑两种)属性,除此之外,红黑树不须要保存其余信息,this
因此红黑树与普通二叉搜索树(BST)的内存开销基本同样,不会占用太多内存。
The original data structure was invented in 1972 by Rudolf Bayer[2] and named "symmetric binary B-tree," but acquired its modern name in a paper in 1978 byLeonidas J. Guibas and Robert Sedgewick entitled "A Dichromatic Framework for Balanced Trees".[3] The color "red" was chosen because it was the best-looking color produced by the color laser printer available to the authors while working at Xerox PARC.[4]
A red–black tree is a special type of binary tree, used in computer science to organize pieces of comparable data, such as text fragments or numbers.
The leaf nodes of red–black trees do not contain data. These leaves need not be explicit in computer memory—a null child pointer can encode the fact that this child is a leaf—but it simplifies some algorithms for operating on red–black trees if the leaves really are explicit nodes. To save memory, sometimes a singlesentinel node performs the role of all leaf nodes; all references from internal nodes to leaf nodes then point to the sentinel node.
Red–black trees, like all binary search trees, allow efficient in-order traversal (that is: in the order Left–Root–Right) of their elements. The search-time results from the traversal from root to leaf, and therefore a balanced tree of n nodes, having the least possible tree height, results in O(log n) search time.
上图是一棵普通的红黑树
除了二叉树的基本要求外,红黑树必须知足如下几点性质。
这些约束使红黑树具备这样一个关键属性:从根节点到最远的叶子节点的路径长与到最近的叶子节点的路径长度相差不会超过2。 由于红黑树是近似平衡的。
另外,插入、删除和查找操做与树的高度成正比,因此红黑树的最坏状况,效率仍然很高。(不像普通的二叉搜索树那么慢)
解释一下为何有这样好的效果。注意性质4和性质5。假设一个红黑树T,其到叶节点的最短路径确定所有是黑色节点(共B个),最长路径确定有相同个黑色节点(性质5:黑色节点的数量是相等),另外会多几个红色节点。性质4(红色节点必须有两个黑色儿子节点)能保证不会再现两个连续的红色节点。因此最长的路径长度应该是2B个节点,其中B个红色,B个黑色。
最短的路径中所有是黑色节点,最长的路径中既有黑色又有红色节点。
由于这两个路径中黑色节点个数是同样的,并且不会出现两个连续的红色节点,因此最长的路径可能会出现红黑相间的节点。也就是说,树中任意两条路径中的节点数相差不会超过一倍。
好比下图:
将以前的红黑树当作B树,就是如今这个样子。
红黑树能够看做是,每一个节点簇包含1到3个关键值(Key)的四阶B树,因此就有2到4个子节点的指针。
这个B树中每一个节点簇中包含左键(LeftKey)、 中键(MidKey)、右键(RightKey),中键(MidKey)与红黑树中的黑色节点对应,另外两个左右键(LeftKey,RightKey)与红黑树中的红色节点对应。
还能够把此图当作是红色节点向上移动一个高度的红黑树。因此红色节点就与它黑色的父亲节点平行,组成一个 B树节点簇。
这时,会发如今B树中,全部红黑树的叶子节点都神奇的达到了相同的高度。
红黑树的结构与4阶B树(最少1个Key,最多3个Key的B树)是相同的。
4阶B树与红黑树的对应转换关系(图片引用自LLRB):
经过4阶B树能够很容易理解红黑树的插入、删除操做。
任一点插入B树,插入点确定落在叶子节点簇上。若是节点簇有空间,那么插入完成;若是没有空间,则从当前节点簇中选出一个空闲的键值,将其放入父节点簇中。
从B树中删除任一点的问题,能够只考虑删除最大键值或者删除最小键值的状况。缘由能够参考二叉搜索树的删除操做。
因此删除时,删除点也会落在叶子节点簇上。若是节点簇还有剩余键值,那么删除完成;若是节点簇没有剩余节点,则从其父节点簇中选出任一键值补充至当前节点簇。而后在父节点递归进行删除操做。
简单来讲,删除或插入节点时,所作的调整操做都是为了保持4阶B树的整体高度是一致的。
红黑树的查找操做与二叉搜索树BST彻底一致。可是插入和删除算法会破坏红黑树的性质。因此对红黑树执行删除、插入操做后须要调整使其恢复红黑树性质。调整过程仅须要少许的染色(O(log n) 或者 O(1)的复杂度)和至多3次的旋转操做(插入仅需2次)。虽然这样会使插入、删除操做很复杂,但其时间复杂度仍然在O(log n)之内。
建议不看下文描述的状况下,先在本身脑海中思考一下插入、删除操做后,如何调整树节点使其保持平衡状态(对应4阶B树的形状进行调整)。
有了本身的想法后,再对照文章的描述,会有更清晰的理解。
图示左旋(Left rotation)右旋(Rgith rotation)
插入操做与二叉搜索树同样,新节点确定会做为树中的叶子节点的儿子加入(详见二叉搜索树相关说明),不过为了恢复红黑树性质,还须要作些染色、旋转等调整操做。另外须要注意的是,红黑树叶子节点是黑色的NIL节点,因此通常用带有两个黑色NIL儿子的新节点直接替换原先的NIL叶子节点,为了方便后续的调整操做,新节点都是默认的红色。
注:插入节点后的调整操做,主要目的是保证树的整体高度不发生改变(使插入点为红色进入树中);若是必定要改变树的高度(插入点没法调整为红色),那么全部操做的目的是使树的总体高度增加1个单位,而不是仅某一子树增加1个高度。
具体如何进行调整要看新节点周围的节点颜色进行处理。下面是须要注意的几种状况:
注意:咱们使用New表示当前新插入的红色节点,Parent表示N的父亲节点,Grandparent表示N的爷爷节点,Uncle表示N的叔叔节点。另外,插入过程会发生递归循环(见case3),因此刚才定义的节点角色并不会绝对固定于某一点,会根据状况(case)进行交换,但每一个状况(case)的调整过程,角色确定保持不变。
后面的图示说明中,节点的颜色都与具体case相关。三角形通常表示未知深度的子树。顶部带有一个小黑点的三角形表示子树的根是黑色,不然子树的根是不肯定的颜色。
每种case都使用C语言代码展现。使用下面的节点获取叔叔节点与爷爷节点。
struct node *grandparent(struct node *n) { if ((n != NULL) && (n->parent != NULL)) return n->parent->parent; else return NULL; }struct node *uncle(struct node *n) { struct node *g = grandparent(n); if (g == NULL) return NULL; // No grandparent means no uncle if (n->parent == g->left) return g->right; else return g->left; }
Case1:当前节点N是树中的根节点的状况。这时,将节点直接染成黑色以知足性质2(根节点是黑色)。
因为N是根节点,因此这样确定也不会破坏性质5(从任一节点出发到叶子节点的路径中黑色节点的数量相等)。
void insert_case1(struct node *n) { if (n->parent == NULL) n->color = BLACK; else insert_case2(n); }
Case2:当前节点的父亲P是黑色的状况。这时,性质4(红色节点必须有两个黑色儿子节点)不会被破坏。性质5(从任一节点出发到其每一个叶子节点的路径,黑色节点的数量是相等的)也仍然知足,由于节点N是红色,但N还有两个黑色的叶子节点NIL,全部经过N的路径上,仍然保持和原来相同的黑色节点个数。
void insert_case2(struct node *n) { if (n->parent->color == BLACK) return; /* Tree is still valid */ else insert_case3(n); }
Case3:当前节点的父亲P和叔叔U都是红色的状况。这时,将P、U都染成黑色,而G染成红色以知足性质5(从任一节点出发到其每一个叶子节点的路径,黑色节点的数量是相等的)。如今,当前的红色节点N有一个黑色的父亲,并且全部通过父亲和叔叔节点的路径仍然保持与原来相同的节点个数。可是爷爷节点G可能会违反性质2(根节点必须是黑色)或者性质4(红色节点必须有两个黑色儿子节点)(在G节点的父亲也是红色节点时,会破坏性质4)。要修复这个问题,能够对节点G递归执行Case1的操做(能够这样理解,把G看成是新插入的红色节点,对G执行调整操做。由于G的两个子树是平衡的)。这里是尾递归调用,因此也可使用循环的方法实现。由于这以后确定会执行一次旋转操做,并且确定提常数级的旋转次数。
注:由于P是红色的,因此N确定还有一个爷爷节点G。若是N没有爷爷节点,那P节点就是根节点,应该是黑色才对。因而可知,N还会有一个叔叔节点U,但U也多是叶子节点(NIL),具体状况见Case4和Case5
void insert_case3(struct node *n) { struct node *u = uncle(n), *g; if ((u != NULL) && (u->color == RED)) { n->parent->color = BLACK; u->color = BLACK; g = grandparent(n); g->color = RED; insert_case1(g); } else { insert_case4(n); } }
Case4:父亲P是红色,叔叔U是黑色,而且N是P的右孩子,P是G的左孩子的状况。
这时,对节点P执行左旋操做,使P变成N的左孩子,N变成G的左孩子,也就是说进入了Case5 的状况。
旋转操做完成以后,性质4(红色节点必须有两个黑色儿子节点)仍然不知足。而性质5(从任一节点出发到其每一个叶子节点的路径,黑色节点的数量是相等的)是仍然保持的,由于旋转操做使节点G出发到子树1的路径上多了一个节点N,G到子树2的路径上多了一个节点P,G到子树3的路径上少了一个节点P,并且P、N是红色,不会影响路径中黑色节点的数量。
因为旋转操做后,性质4(红色节点必须有两个黑色儿子节点)仍然不知足,因此咱们直接进入Case5处理。
注:
Case4的主要目的就是将当前状况转换到Case5进行处理。
Case4的说明和图示中,咱们仅提到了N是右孩子,P是左孩子的状况;另外N是左孩子,P是右孩子的状况没有说明。由于这两种状况处理方法是类似的。不过在C代码中包括了两种状况的处理。
void insert_case4(struct node *n) { struct node *g = grandparent(n); if ((n == n->parent->right) && (n->parent == g->left)) { rotate_left(n->parent); /* * rotate_left can be the below because of already having *g = grandparent(n) * * struct node *saved_p=g->left, *saved_left_n=n->left; * g->left=n; * n->left=saved_p; * saved_p->right=saved_left_n; * * and modify the parent's nodes properly */ n = n->left; } else if ((n == n->parent->left) && (n->parent == g->right)) { rotate_right(n->parent); /* * rotate_right can be the below to take advantage of already having *g = grandparent(n) * * struct node *saved_p=g->right, *saved_right_n=n->right; * g->right=n; * n->right=saved_p; * saved_p->left=saved_right_n; * */ n = n->right; } insert_case5(n); }
Case5:父亲P是红色,但叔叔U是黑色, N是左孩子,P也是左孩子的状况。
此时,对节点G执行一次右旋。使P成为N和G的父节点。已知G是黑色(P是红色,为了避免破坏性质4(红色节点必须有两个黑色儿子节点),G确定是黑色),因此将G染成红色,P染成黑色。此时,既知足性质4(红色节点必须有两个黑色儿子节点),也知足性质5(从任一节点出发到其每一个叶子节点的路径,黑色节点的数量是相等的)。惟一的改变是原来通过G节点的路径,如今所有都会通过P节点。
void insert_case5(struct node *n) { struct node *g = grandparent(n); n->parent->color = BLACK; g->color = RED; if (n == n->parent->left) rotate_right(g); else rotate_left(g); }
注:到此为止,插入操做的调整都结束了。
注:理解删除操做的重点是,黑色节点删除后,儿子节点中有红色的则从儿子树中选一节点填补被删除后的空缺;不然,从兄弟子树中选择一个节点填补空缺;再不然,就将问题递归到父亲节点处理。跟继承皇位的办法类似
在普通二叉搜索树中删除一个含有两个非叶子儿子的节点时,咱们会先找到此节点左子树中最大的节点(也叫前驱),或者右子树中的最小节点(也叫后继),将找到的节点值替换到当前被删除节点的位置,而后删除前驱或者后继节点(详见这里)。这里被删除的节点,至多有一个非叶子节点。由于替换节点值的操做不会破坏红黑树的性质,因此删除红黑树任一节点的问题就简化为,删除一个含有至多一个非叶子儿子的状况。
后面的讨论过程当中,咱们将这个被删除的节点(含至多一个非叶子儿子)标记为M。M惟一的一个非叶子儿子咱们称之为C,若是M的儿子都是叶子节点,那么两个叶子均可称为C,不作区分。
若是M是红色节点,只要用儿子C直接替换到M的位置便可(这仅发生在M有两个叶子节点的状况,由于假设M有一个黑色的儿子CL,CL不是叶子节点,因此CL还有两个黑色的叶子CLL、CLR,M的另一个儿子是叶子节点CR。那么M节点违反性质5(从任一节点出发到其每一个叶子节点的路径,黑色节点的数量是相等的),因此C是黑色时确定是叶子)。由于原来通过被删除的红色节点的全部路径中,仅少了一个红色节点,且M的父亲和儿子确定是黑色,因此性质3(叶节点(NIL)是黑色的)和性质4(红色节点必须有两个黑色儿子节点)和性质5(从任一节点出发到其每一个叶子节点的路径,黑色节点的数量是相等的)不受影响,
还有一种简单的状况是,M是黑色,C是红色时,若是只是用C替换到M的位置,可能会破坏性质4(红色节点必须有两个黑色儿子节点)和性质5(从任一节点出发到其每一个叶子节点的路径,黑色节点的数量是相等的)。因此,只要再把C染成黑色,那么原有性质所有不受影响。
比较复杂的状况是M和C都是黑色时(这仅发生在M有两个叶子的状况,具体缘由上一段已经说明)。仍然将C节点直接替换到M的位置,不过咱们将位置新位置的C称为N,N的兄弟节点(之前是M的兄弟)称为Sibling。在下面的图示中,咱们会使用P表示N的父亲(之前是M的父亲),SL表示S的左儿子,SR表示S的右儿子(S确定不是叶子节点,由于M和C是黑色,因此P的儿子节点中,M所在子树高度为2,因此S所在子树高度也是2,因此S确定不是叶子)。
注:下面各类状况中,咱们可能交换(改变)各个节点的角色。但在每种状况处理中角色名称是固定不变的。
图示中的节点不会覆盖全部可能的颜色,只是为了方便描述任举一例。白色节点表示未知的颜色(多是红色也多是黑色) 。
使用此函数获取兄弟节点
struct node *sibling(struct node *n) { if (n == n->parent->left) return n->parent->right; else return n->parent->left; }
Note: In order that the tree remains well-defined, we need that every null leaf remains a leaf after all transformations (that it will not have any children). If the node we are deleting has a non-leaf (non-null) child N, it is easy to see that the property is satisfied. If, on the other hand, N would be a null leaf, it can be verified from the diagrams (or code) for all the cases that the property is satisfied as well.
下面的代码用来处理刚才说的几种简单状况。函数replace_node()将节点child替换到节点n的位置(替换值,而不改变颜色)。另外,为了操做方便,下面的代码中使用一个真实的Node表示叶子节点(不是用NULL表示叶子)。这种表示方法不影响以前处理插入操做的代码。
void delete_one_child(struct node *n) { /* * Precondition: n has at most one non-null child. */ struct node *child = is_leaf(n->right) ? n->left : n->right; replace_node(n, child); if (n->color == BLACK) { if (child->color == RED) child->color = BLACK; else delete_case1(child); } free(n); }
Note: If N is a null leaf and we do not want to represent null leaves as actual node objects, we can modify the algorithm by first calling delete_case1() on its parent (the node that we delete, n
in the code above) and deleting it afterwards. We can do this because the parent is black, so it behaves in the same way as a null leaf (and is sometimes called a 'phantom' leaf). And we can safely delete it at the end as n
will remain a leaf after all operations, as shown above.
若是N和它原来的父亲(M)都是黑色,那么删除操做会使全部通过N节点的路径都缺乏一个黑色节点。由于性质5(从任一节点出发到其每一个叶子节点的路径,黑色节点的数量是相等的)被破坏,树须要进行调整以保持平衡。下面详细说一下须要考虑的几种状况。
Case1:N是根节点。此时什么也不须要作。由于每条路径都少了一个黑色节点,并且根是黑色的,因此全部性质都没有被破坏。
void delete_case1(struct node *n) { if (n->parent != NULL) delete_case2(n); }
注:在状况二、五、6中,咱们都假定N是P的左儿子。对于N是右儿子的状况,处理方法也不复杂,只要将左右对调就好了。在示例代码中允份考虑了这些状况。
Case2:S是红色。
这时,咱们交换P和S的颜色,而后对P执行左旋操做,使S成为N的爷爷。注意,P节点确定是黑色,由于P的儿子S是红色。此时,全部路径上的黑色节点个数没有变化,而N节点如今的兄弟SL变成了黑色,N节点如今的父亲P变成了红色。接下来,咱们能够交给Case四、五、6继续处理。在下面的状况中,咱们会将N的新兄弟SL仍然称作S。
void delete_case2(struct node *n) { struct node *s = sibling(n); if (s->color == RED) { n->parent->color = RED; s->color = BLACK; if (n == n->parent->left) rotate_left(n->parent); else rotate_right(n->parent); } delete_case3(n); }
Case3:P、S和S的儿子都是黑色的状况。
直接将S染成红色。这样刚好使通过S点的路径上也少一个黑色节点,而通过N节点的路径因为以前的删除操做,如今也是少一个黑色节点的状态。顺其天然的,S、N是P的儿子,因此如今通过P点的路径相比原来少了一个黑色节点。这么作至关于,把原先存在于N节点的不平衡状态上移到了P节点,如今P节点不知足性质5(从任一节点出发到其每一个叶子节点的路径,黑色节点的数量是相等的)。咱们能够将P点交给Case1处理,这样就造成一个递归。
void delete_case3(struct node *n) { struct node *s = sibling(n); if ((n->parent->color == BLACK) && (s->color == BLACK) && (s->left->color == BLACK) && (s->right->color == BLACK)) { s->color = RED; delete_case1(n->parent); } else delete_case4(n); }
Case4:S和S的儿子是黑色,但P是红色的状况。
此时,只要把P和S点的颜色互换一下,便可使树恢复平衡状态。原先通过S点的路径,黑色节点数量仍然保持不变;而原先通过N点的路径,如今多了一个黑色节点P,正好弥补了删除节点M后缺乏一个黑色节点的问题。
void delete_case4(struct node *n) { struct node *s = sibling(n); if ((n->parent->color == RED) && (s->color == BLACK) && (s->left->color == BLACK) && (s->right->color == BLACK)) { s->color = RED; n->parent->color = BLACK; } else delete_case5(n); }
Case5:P点颜色任意,S点是黑色,S左儿子是红色,右儿子是黑色。N是P的左儿子(S是P的右儿子)的状况。
此时,咱们对S点执行右旋转,使得S的左儿子SL,既是S的父亲,也是N的兄弟。同时交换S和SL的颜色,这样全部路径中黑色节点的数量没有变化。
如今N点的兄弟节点S就有了一个红色的右儿子,由于咱们能够直接进入Case6处理。
此次转换对于P和N点没有什么影响。(须要再次说明的是,Case6中,咱们把N的新兄弟仍然称为S)
void delete_case5(struct node *n) { struct node *s = sibling(n); if (s->color == BLACK) { /* this if statement is trivial, due to case 2 (even though case 2 changed the sibling to a sibling's child, the sibling's child can't be red, since no red parent can have a red child). */ /* the following statements just force the red to be on the left of the left of the parent, or right of the right, so case six will rotate correctly. */ if ((n == n->parent->left) && (s->right->color == BLACK) && (s->left->color == RED)) { /* this last test is trivial too due to cases 2-4. */ s->color = RED; s->left->color = BLACK; rotate_right(s); } else if ((n == n->parent->right) && (s->left->color == BLACK) && (s->right->color == RED)) {/* this last test is trivial too due to cases 2-4. */ s->color = RED; s->right->color = BLACK; rotate_left(s); } } delete_case6(n); }
Case6:P点颜色任意,S点是黑色,S的右儿子是红色。N是P的左儿子(S是P的右儿子)的状况。
此时,咱们对P点执行左旋转,使S成为P的父亲(同时仍是SR的父亲)。
同时,交换P、S的颜色,并将SR染成黑色。此时S节点的左右子树恢复了平衡,且与删除节点M前有相同的黑色节点数。性质4(红色节点必须有两个黑色儿子节点)和性质5(从任一节点出发到其每一个叶子节点的路径,黑色节点的数量是相等的)也已经恢复正常。
不管P节点先前是什么颜色,N都比以前多了一个黑色祖先。假设P先前是红色,如今P被染成了黑色;假设P先前就是黑色,如今P又多了同样黑色的父节点S,因此通过N的路径中,增长了一个黑色节点。
同时,还要说明一下不通过N点的路径的变化状况,一共有两种可能:
综上,这些路径中黑色节点数量都没有改变。由于,咱们修复了性质4(红色节点必须有两个黑色儿子节点)和性质5(从任一节点出发到其每一个叶子节点的路径,黑色节点的数量是相等的)。图示中的白色节点能够是任意颜色(红或黑),只要调整先后保持一致便可。
void delete_case6(struct node *n) { struct node *s = sibling(n); s->color = n->parent->color; n->parent->color = BLACK; if (n == n->parent->left) { s->right->color = BLACK; rotate_left(n->parent); } else { s->left->color = BLACK; rotate_right(n->parent); } }
须要强调的是,这里的函数使用的是尾部递归,因此算法是原地算法。上面的算法中,除了删除算法的Case3之外,全部case都是按次序执行。this is the only case where an in-place implementation will effectively loop (after only one rotation in case 3).
另外,尾递归不会发生在儿子节点,通常都是从儿子节点向上,也就是向父亲节点递归。并且递归次数不会超过O(log n)次(n是删除节点前的节点总数)。只要在Case2中发生旋转操做(这也是Case1到Case2间的循环过程当中惟一可能发生的旋转),N的父亲就会变成红色,因此循环会当即中止。所以循环过程最多发生一次旋转。退出循环后最多发生两次旋转(Case五、Case6中)。也就是说,红黑树的删除操做,总共不会超过3次旋转。
注:更深刻的“渐进边界的证实”等其余内容这里就略过不译了。
老外写的左倾红黑树,是一种比本文所述的更好理解,且效率更高的红黑树。2008年发表。
http://www.cs.princeton.edu/~rs/talks/LLRB/RedBlack.pdf
维基百科原文(强烈建议看英文版)
http://en.wikipedia.org/wiki/Red%E2%80%93black_tree
看得见的红黑树(对红黑树的调整过程有不理解的地方,用此网址模拟一遍插入删除操做,会让思路更清晰)
https://www.cs.usfca.edu/~galles/visualization/RedBlack.html