红黑树

红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它是在1972鲁道夫·贝尔发明的,他称之为"对称二叉B树",它现代的名字是在 Leo J. Guibas 和 Robert Sedgewick 于1978写的一篇论文中获得的。红黑树的操作有着良好的最坏情况运行时间,并且在实践中是高效的,它可以在O(log n)时间内做查找,插入和删除,这里的n是树中元素的数目。

一棵红黑树是指一棵满足下述性质的二叉查找树(binary search tree):
1. 
每个结点或者为黑色或者为红色。
2. 
根结点为黑色。
3. 
每个叶结点(NIL)都是黑色的。
4. 
如果一个结点是红色的,那么它的两个子节点都是黑色的(也就是说,不能有两个相邻的红色结点)。
5. 
对于每个结点,从该结点到其所有子孙叶结点的路径中所包含的黑色结点数量必须相同。


红黑树的每个节点上的属性除了有一个key3个指针:parentlchildrchild以外,还多了一个属性color,它只能是两种颜色:红或黑。


节点x的黑高度:从某个节点x到达一个叶结点的任意一条路径上包含的黑色结点(包括叶结点)数量。用bh(x)表示。另外规定叶结点的黑高度为0

定理:一棵含有n个内结点的红黑树的树高至多为2log(n+1) 
证明:先证以某一节点x为根的子树中至少包含2bh(x) - 1个内节点。用归纳法:
(1)x的高度为0,则x为一叶节点,以x为根的子树中包含2bh(x) - 1 = 20 - 1 = 0;
(2)考虑一个高度为正值的节点x,它是个内节点,且有两个子女,每个子女根据其自身的颜色是红或黑而有黑高度bh(x)或bh(x) - 1,由归纳假设,每个子女至少包含2(bh(x) - 1) - 1个内节点。所以,以节点x为根的子树中至少包含(2(bh(x) - 1) - 1) + (2(bh(x) - 1) - 1) + 1 = 2bh(x) - 1。命题得证。
设h为树的高度,根据性质4,从根到叶节点(不包括根)的任一条简单路径上,至少有一半的节点必是黑的。从而,根的黑高度至少为h / 2;故有n>= 2(h / 2) - 1 有lg(n + 1) >= h / 2 或(h <= 2lg(n +1))。
所以,命题得证。
由这个定理可知,动态集合操作Search, Minimum, Maximum, Successor, Predecessor可用红黑树在O(lg n)时间内实现。

 

 

红黑树的关键性质: 从根到叶子的最长路径不多于最短路径的两倍长,结果是这个树大致上是平衡的。基本的操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限决定了红黑树在最坏情况下都是高效的,这就不同于普通的二叉查找树。要知道为什么这些特性确保了这个结果,注意到属性4导致了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据属性5所有的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。

因为每一个红黑树也是一个特化的二叉查找树,因此红黑树上的只读操作与普通二叉查找树上的只读操作相同。然而,在红黑树上进行插入操作和删除操作会导致不再符合红黑树的性质。恢复红黑树的属性需要少量(O(log n))的颜色变更(实际是非常快速的)和不超过三次树旋转(对于插入操作是两次)。虽然插入和删除很复杂,但操作时间仍可以保持为 O(log n) 次。

在很多树数据结构的表示中,一个节点有可能只有一个子节点,而叶子节点包含数据。用这种范例表示红黑树是可能的,但是这会改变一些属性并使算法复杂。为此,本文中我们使用 "nil 叶子"或"空(null)叶子",如上图所示,它不包含数据而只充当树在此结束的指示。这些节点在绘图中经常被省略,导致了这些树好象同上述原则相矛盾,而实际上不是这样。与此有关的结论是所有节点都有两个子节点,尽管其中的一个或两个可能是空叶子。

 红黑树的插入操作

我们首先以二叉查找树的方法增加节点并标记它为红色(如果设为黑色,就会导致在该叶子所在的路径上多一个额外的黑节点,这个是很难调整的;如果将其设为红色节点,可能会导致出现两个连续红色节点的冲突,不过这可以通过颜色调换和树旋转来调整)。下面要进行什么操作取决于其他临近节点的颜色。同人类的家族树中一样,我们将使用术语叔父节点来指一个节点的父节点的兄弟节点。注意:

  • 性质1和性质3总是保持着。
  • 性质4只在增加红色节点、重绘黑色节点为红色,或做旋转时受到威胁。
  • 性质5只在增加黑色节点、重绘红色节点为黑色,或做旋转时受到威胁。

在下面的示意图中,将要插入的节点标为NN的父节点标为PN的祖父节点标为GN的叔父节点标为U。在图中展示的任何颜色要么是由它所处情形所作的假定,要么是这些假定所暗含的。

对于每一种情况,我们将使用 C语言编写的代码来展示。通过下列函数,可以找到一个节点的叔父和祖父节点:

 node grandparent(node n) {

     return n->parent->parent;

 }

 

 node uncle(node n) {

     if (n->parent == grandparent(n)->left)

         return grandparent(n)->right;

     else

         return grandparent(n)->left;

 }

情形1新节点N位于树的根上,没有父节点。在这种情形下,我们把它重绘为黑色以满足性质2。因为它在每个路径上对黑节点数目增加1,性质5符合。

 void insert_case1(node n) {

     if (n->parent == NULL)

         n->color = BLACK;

     else

         insert_case2(n);

 }

情形2新节点的父节点P是黑色,所以性质4没有失效(新节点是红色的)。在这种情形下,树仍是有效的。性质5受到威胁,因为新节点N有两个黑色叶子儿子;但是由于新节点N是红色,通过它的每个子节点的路径就都有同通过它所取代的黑色的叶子的路径同样数目的黑色节点,所以这个性质依然满足。

 void insert_case2(node n) {

     if (n->parent->color == BLACK)

         return; /* 树仍旧有效 */

     else

         insert_case3(n);

 }

注意: 在下列情形下我们假定新节点有祖父节点。因为父节点是红色;并且如果它是根,它就应当是黑色。新节还有一个叔父节点,尽管在下面的情形4和5它可能是叶子。

情形3如果父节点P和叔父节点U二者都是红色,则我们可以将它们两个重绘为黑色并重绘祖父节点G为红色(用来保持性质5)。现在我们的新节点N有了一个黑色的父节点P。因为通过父节点P或叔父节点U的任何路径都必定通过祖父节点G,在这些路径上的黑节点数目没有改变。但是,红色的祖父节点G的父节点也有可能是红色的,这就违反了性质4。为了解决这个问题,我们在祖父节点G上递归地进行情形1的整个过程(把G当成是新加入的节点进行各种情况的检查)

 void insert_case3(node n) {

     if (uncle(n) != NULL && uncle(n)->color == RED) {

         n->parent->color = BLACK;

         uncle(n)->color = BLACK;

         grandparent(n)->color = RED;

         insert_case1(grandparent(n));

     }

     else

         insert_case4(n);

 }

注意: 在余下的情形下,我们假定父节点P是其父亲G 的左子节点。如果它是右子节点,情形4情形5中的左和右应当对调。

情形4父节点P是红色而叔父节点U是黑色或缺少; 还有,新节点N是其父节点P的右子节点,而父节点P又是其父节点的左子节点。在这种情形下,我们进行一次左旋转调换新节点和其父节点的角色; 接着,我们按情形5处理以前的父节点P。这导致某些路径通过它们以前不通过的新节点N或父节点P中的一个,但是这两个节点都是红色的,所以性质5没有失效。

 void insert_case4(node n) {

     if (n == n->parent->right && n->parent == grandparent(n)->left) {

         rotate_left(n->parent);

         n = n->left;

     } else if (n == n->parent->left && n->parent == grandparent(n)->right) {

         rotate_right(n->parent);

         n = n->right;

     }

     insert_case5(n);

 }

情形5父节点P是红色而叔父节点U 是黑色或缺少,新节点N是其父节点的左子节点,而父节点P又是其父节点G的左子节点。在这种情形下,我们进行针对祖父节点P 的一次右旋转; 在旋转产生的树中,以前的父节点P现在是新节点N和以前的祖父节点G 的父节点。我们知道以前的祖父节点G是黑色,否则父节点P就不可能是红色。我们切换以前的父节点P和祖父节点G的颜色,结果的树满足性质4。性质5也仍然保持满足,因为通过这三个节点中任何一个的所有路径以前都通过祖父节点G ,现在它们都通过以前的父节点P。在各自的情形下,这都是三个节点中唯一的黑色节点。

 void insert_case5(node n) {

     n->parent->color = BLACK;

     grandparent(n)->color = RED;

     if (n == n->parent->left && n->parent == grandparent(n)->left) {

         rotate_right(grandparent(n));

     } else {

         /* Here, n == n->parent->right && n->parent == grandparent(n)->right */

         rotate_left(grandparent(n));

     }

 }

在最后附录“台大-红黑树.ppt”上,有红黑树的插入操作的详细讲解,可以参照理解。

 

 

 

 

 

红黑树节点的删除

如果需要删除的节点有两个儿子,那么问题可以被转化成删除另一个只有一个儿子的节点的问题(为了表述方便,这里所指的儿子,为非叶子节点的儿子)。对于二叉查找树,在删除带有两个非叶子儿子的节点的时候,我们找到要么在它的左子树中的最大元素、要么在它的右子树中的最小元素,并把它的值转移到要删除的节点中。接着删除我们从中复制出值的那个节点,它最多只有一个非空节点的儿子。因为只是复制了一个值而不违反任何属性,这就把问题简化为如何删除最多有一个儿子的节点的问题。它不关心这个节点是最初要删除的节点还是我们从中复制出值的那个节点。

在本文余下的部分中,我们只需要讨论删除只有一个儿子的节点。如果我们删除一个红色节点,它的父亲和儿子一定是黑色的。所以我们可以简单的用它的黑色儿子替换它,并不会破坏属性3和4。通过被删除节点的所有路径只是少了一个红色节点,这样可以继续保证属性5。另一种简单情况是在被删除节点是黑色而它的儿子是红色的时候。如果只是去除这个黑色节点,用它的红色儿子顶替上来的话,会破坏属性4,但是如果我们重绘它的儿子为黑色,则曾经通过它的所有路径将通过它的黑色儿子,这样可以继续保持属性4(参考后面PPT24页)。

需要进一步讨论的是在要删除的节点和它的儿子二者都是黑色的时候,这是一种复杂的情况。我们首先把要删除的节点替换为它的儿子。出于方便,称呼这个节点为N,称呼它的兄弟为S。在下面的示意图中,我们还是使用P称呼N的父亲,SL称呼S的左儿子,SR称呼S的右儿子。我们将使用下述函数找到兄弟节点:

 node sibling(node n) {

      if (n == n->parent->left)

          return n->parent->right;

      else

          return n->parent->left;

 }

我们可以使用下列代码进行上述的概要步骤,这里的函数 replace_node 替换 child 到 n 在树中的位置。出于方便,在本章节中的代码将假定空叶子被用不是 NULL 的实际节点对象来表示(在插入章节中的代码可以同任何一种表示一起工作)。

 void delete_one_child(node n) {

     /* Precondition: n has at most one non-null child */

     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);

 }

如果 N 和它初始的父亲是黑色,则删除它的父亲导致通过 N 的路径都比不通过它的路径少了一个黑色节点。因为这违反了属性 5,树需要被重新平衡。有几种情况需要考虑:

情况 1: N是新的根。在这种情况下,我们就做完了。我们从所有路径去除了一个黑色节点,而新根是黑色的,所以属性都保持着。

 void delete_case1(node n) {

     if (n->parent == NULL)

         return;

     else

         delete_case2(n);

 }

注意在情况2、5和6下,我们假定 N 是它父亲的左儿子。如果它是右儿子,则在这些情况下的左和右应当对调。

情况 2: S 是红色。在这种情况下我们在N的父亲上做左旋转,把红色兄弟转换成N的祖父。我们接着对调 N 的父亲和祖父的颜色。尽管所有的路径仍然有相同数目的黑色节点,现在 N 有了一个黑色的兄弟和一个红色的父亲,所以我们可以接下去按 4、5或6情况来处理。(它的新兄弟是黑色因为它是红色S的一个儿子。)

 void delete_case2(node n) {

     if (sibling(n)->color == RED) {

         n->parent->color = RED;

         sibling(n)->color = BLACK;

         if (n == n->parent->left)

             rotate_left(n->parent);

         else

             rotate_right(n->parent);

     }

     delete_case3(n);

 }

情况 3: N 的父亲、S 和 S 的儿子都是黑色的。在这种情况下,我们简单的重绘 S 为红色。结果是通过S的所有路径, 它们就是以前不通过 N 的那些路径,都少了一个黑色节点。因为删除 N的初始的父亲使通过 N 的所有路径少了一个黑色节点,这使事情都平衡了起来。但是,通过 P的所有路径现在比不通过 P 的路径少了一个黑色节点,所以仍然违反属性5。要修正这个问题,我们要从情况 1 开始,在 P 上做重新平衡处理。

 void delete_case3(node n) {

     if (n->parent->color == BLACK &&

         sibling(n)->color == BLACK &&

         sibling(n)->left->color == BLACK &&

         sibling(n)->right->color == BLACK)

     {

         sibling(n)->color = RED;

         delete_case1(n->parent);

     }

     else

         delete_case4(n);

 }

情况 4: S 和 S 的儿子都是黑色,但是 N 的父亲是红色。在这种情况下,我们简单的交换 N 的兄弟和父亲的颜色。这不影响不通过 S 的路径的黑色节点的数目,但是它在通过 N 的路径上对黑色节点数目增加了1,添补了在这些路径上删除的黑色节点。

 void delete_case4(node n) {

     if (n->parent->color == RED &&

         sibling(n)->color == BLACK &&

         sibling(n)->left->color == BLACK &&

         sibling(n)->right->color == BLACK)

     {

         sibling(n)->color = RED;

         n->parent->color = BLACK;

     }

     else

         delete_case5(n);

 }

情况 5: S是黑色,S的左儿子是红色,S的右儿子是黑色,而N是S的父亲的左儿子。在这种情况下我们在S上做右旋转,这样S的左儿子成为S的父亲和N的新兄弟。我们接着交换S和它的新父亲的颜色。所有路径仍有同样数目的黑色节点,但是现在N有了一个右儿子是红色的黑色兄弟,所以我们进入了情况 6。N和它的父亲都不受这个变换的影响。

 void delete_case5(node n) {

     if (n == n->parent->left &&

         sibling(n)->color == BLACK &&

         sibling(n)->left->color == RED &&

         sibling(n)->right->color == BLACK)

     {

         sibling(n)->color = RED;

         sibling(n)->left->color = BLACK;

         rotate_right(sibling(n));

     }

     else if (n == n->parent->right &&

              sibling(n)->color == BLACK &&

              sibling(n)->right->color == RED &&

              sibling(n)->left->color == BLACK)

     {

         sibling(n)->color = RED;

         sibling(n)->right->color = BLACK;

         rotate_left(sibling(n));

     }

     delete_case6(n);

 }

情况 6: S是黑色,S的右儿子是红色,而 N 是它父亲的左儿子。在这种情况下我们在 N 的父亲上做左旋转,这样S成为N的父亲与S的右儿子的父亲。我们接着交换 N 的父亲和 S 的颜色,并使S的右儿子为黑色。子树在它的根上的仍是同样的颜色。但是,通过 N 的路径增加了一个黑色节点。

此时,如果一个路径不通过 N,则有两种可能性:

  • 它通过 N 的新兄弟。那么它以前和现在都必定通过 S 和 N 的父亲,而它们只是交换了颜色。所以路径保持了同样数目的黑色节点。
  • 它通过 N 的新叔父--S的右儿子。那么它以前通过 S、S 的父亲和 S 的右儿子,但是现在只通过 S,它被假定为它以前的父亲的颜色,和 S 的右儿子,它被从红色改变为黑色。合成效果是这个路径通过了同样数目的黑色节点。

在任何情况下,在这些路径上的黑色节点数目都没有改变。所以我们恢复了属性5。在示意图中的白色节点可以是红色或黑色,但是在变换前后都必须指定相同的颜色。

 void delete_case6(node n) {

     sibling(n)->color = n->parent->color;

     n->parent->color = BLACK;

     if (n == n->parent->left) {

         /* Here, sibling(n)->color == BLACK &&

                  sibling(n)->right->color == RED */

         sibling(n)->right->color = BLACK;

         rotate_left(n->parent);

     }

     else

     {

         /* Here, sibling(n)->color == BLACK &&

                  sibling(n)->left->color == RED */

         sibling(n)->left->color = BLACK;

         rotate_right(n->parent);

     }

 }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值