算法导论读书笔记(13)

http://www.cnblogs.com/sungoshawk/p/3740411.html

算法导论读书笔记(13)

红黑树

红黑树 是一种二叉查找树,但在每个结点上增加了一个存储位表示结点的颜色,可以是 RED 或 BLACK 。通过对任何一条从根到叶子的路径上的各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。红黑树(red-black tree)是许多“平衡的”查找树中的一种,它能保证在最坏情况下,基本的动态集合操作的时间为 O ( lg n )。

树中每个结点包含五个域: color , key , left , right 和 p 。如果某结点没有一个子结点或父结点,则该结点相应的指针为 NIL 。我们将这些 NIL 视为指向二叉查找树外结点(叶子)的指针,而把带关键字的结点视为树的内结点。

一棵红黑树需要满足下面的 红黑性质 :

  1. 每个结点或是红的,或是黑的。
  2. 根结点是黑的。
  3. 每个叶结点( NIL )是黑的。
  4. 如果一个结点是红的,则它的两个孩子都是黑的。
  5. 对每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑结点。

下图给出了一棵红黑树的例子。

为了便于处理红黑树代码中的边界条件,我们采用一个哨兵来代表 NIL 。对一棵红黑树来说,哨兵 T.nil 是一个与树内普通结点有相同域的对象。它的 color域为 BLACK ,而其他域可以设为任意允许的值。如下图所示,所有指向 NIL 的指针都被替换成指向哨兵 T.nil 的指针。

使用哨兵后,可以将结点 x 的 NIL 孩子视为一个其父结点为 x 的普通结点。这里我们用一个哨兵 T.nil 来代表所有的 NIL (所有的叶子以及根部的父结点)。

通常我们将注意力放在红黑树的内部结点上,因为它们存储了关键字的值。因此本文其余部分都将忽略红黑树的叶子,如下图所示。

从某个结点 x 出发(不包括该结点)到达一个叶结点的任意一条路径上,黑色结点的个数称为该结点 x 的 黑高度 ,用bh( x )表示。红黑树的黑高度定义为其根结点的黑高度。

引理
一棵有 n 个内结点的红黑树的高度至多为2lg( n + 1 )。

旋转

当在含 n 个关键字的红黑树上运行时,查找树操作 TREE-INSERT 和 TREE-DELETE 的时间为 O ( lg n )。由于这两个操作对树做了修改,结果可能违反了红黑树的性质,为保持红黑树的性质,就要改变树中某些结点的颜色和指针结构。

指针结构的修改是通过 旋转 来完成的,这是一种能保持二叉查找树性质的查找树局部操作。下图给出了两种旋转:左旋和右旋。


当在某个结点 x 上做左旋时,我们假设它的右孩子 y 不是 T.nil ; x 可以为树内任意右孩子不是 T.nil 的结点。左旋以 x 到 y 之间的链为“支轴”进行。它使 y称为该子树新的根, x 成为 y 的左孩子,而 y 的左孩子则成为 x 的右孩子。

在 LEFT-ROTATE 的伪码中,假设 x.right != T.nil ,并且根的父结点是 T.nil 。

LEFT-ROTATE(T, x)
1  y = x.right            // set y
2  x.right = y.left       // turn y's left subtree into s's right subtree
3  if y.left != T.nil
4      y.left.p = x
5  y.p = x.p
6  if x.p == T.nil
7      T.root = y
8  elseif x == x.p.left
9      x.p.left = y
10 else
11     x.p.right = y
12 y.left = x             // put x on y's left
13 x.p = y

下图显示了 LEFT-ROTATE 的操作过程。 RIGHT-ROTATE 的程序是对称的。它们都在 O ( 1 )时间内完成。

RIGHT-ROTATE(T, x)
1  y = x.left
2  x.left = y.right
3  if y.right != T.nil
4      y.right.p = x
5  y.p = x.p
6  if x.p == T.nil
7      T.root = y
8  elseif x == x.p.left
9      x.p.left = y
10 else
11     x.p.right = right
12 y.right = x
13 x.p = y

插入

向一棵含 n 个结点的红黑树 T 中插入一个新结点 z 的操作可在 O ( lg n )时间内完成。首先将结点 z 插入树 T 中,就好像 T 是一棵普通的二叉查找树一样,然后将 z 着为红色。为保证红黑性质,这里要调用一个辅助程序 RB-INSERT-FIXUP 来对结点重新着色并旋转。调用 RB-INSERT 会将 z 插入红黑树 T内,假设 z 的 key 域已经事先被赋值。

RB-INSERT(T, z)
1  y = T.nil
2  x = T.root
3  while x != T.nil
4      y = x
5      if z.key < x.key
6          x = x.left
7      else
8          x = x.right
9  z.p = y
10 if y == T.nil
11     T.root = z
12 elseif z.key < y.key
13     y.left = z
14 else
15     y.right = z
16 z.left = T.nil
17 z.right = T.nil
18 z.color = RED
19 RB-INSERT-FIXUP(T, z)

过程 RB-INSERT 的运行时间为 O ( lg n )。过程 TREE-INSERT 和 RB-INSERT 之间有四处不同。首先,在 TREE-INSERT 内的所有的 NIL 都被 T.nil 代替。其次,在 RB-INSERT 的第16,17行中,设置 z.left 和 z.right 为 T.nil ,来保持正确的树结构。第三,在第18行将 z 着为红色。第四,在最后一行,调用 RB-INSERT-FIXUP 来保持红黑性质。

RB-INSERT-FIXUP(T, z)
1  while z.p.color == RED
2      if z.p == z.p.p.left                              // z的父结点是其父结点的左孩子
3          y = z.p.p.right                               // 令y为z的叔父结点
4          if y.color == RED
5              z.p.color = BLACK                         // case 1
6              y.color = BLACK                           // case 1
7              z.p.p.color = RED                         // case 1
8              z = z.p.p                                 // case 1
9          else
10             if z == z.p.right
11                 z = z.p                               // case 2
12                 LEFT-ROTATE(T, z)                     // case 2
13             z.p.color = BLACK                         // case 3
14             z.p.p.color = RED                         // case 3
15             RIGHT-ROTATE(T, z.p.p)                    // case 3
16     else                                              // z的父结点是其父结点的右孩子
17         y = z.p.p.left                                // 令y为z的叔父结点
18         if y.color = RED
19             z.p.color = BLACK
20             y.color = BLACK
21             z.p.p.color = RED
22             z = z.p.p
23         else
24             if z = z.p.left
25                 z = z.p
26                 RIGHT-ROTATE(T, z)
27             z.p.color = BLACK
28             z.p.p.color = RED
29             LEFT-ROTATE(T, z.p.p)
30 T.root.color = BLACK

下图显示了在一棵红黑树上 RB-INSERT-FIXUP 是如何操作的。

要理解 RB-INSERT-FIXUP 的工作过程,需要分三个主要步骤来分析其代码。首先,确定当结点 z 被插入并着色为红色后,红黑性质有哪些不能保持。其次,分析 while 循环的总目标。最后,具体分析 while 循环中的三种情况。

在调用 RB-INSERT-FIXUP 时,红黑性质中的性质1和性质3会继续成立,因为新插入结点的子女都是哨兵 T.nil 。性质5也会成立,因为结点 z 代替了(黑色)哨兵,且结点 z 本身是具有哨兵子女的红色结点。因此,可能被破坏的就是性质2和性质4。这是因为 z 被着为红色,如果 z 是根结点则破坏了性质2,如果 z 的父结点是红色则破坏了性质4。上图a显示在结点 z 被插入后性质4被破坏。

要保持树的红黑性质,实际上一共要考虑六种情况,但其中三种与另外三种是对称的,区别在于 z 的父结点 z.p 是 z 的祖父结点 z.p.p 的左孩子还是右孩子。这里只讨论 z.p 是左孩子的情况。

上面伪码中情况1和情况2,3的区别在于 z 的叔父结点的颜色有所不同。如果 y 是红色,则执行情况1。否则,控制转移到情况2和情况3上。在所有三种情况中, z 的祖父 z.p.p 都是黑色的,因为它的父结点 z.p 是红色的,故性质4只在 z 和 z.p 之间被破坏了。

情况1 : z 的叔父结点 y 是红色的

下图显示的是情况1(第5~8行)的状况。只有在 z.p 和 y 都是红色的时候才执行。既然 z.p.p 是黑色的,我们可以将 z.p 和 y 都着为黑色以解决 z 和 z.p都是红色的问题,将 z.p.p 着为红色以保持性质5。然后把 z.p.p 当作新增的结点 z 来重复 while 循环。指针 z 在树中上移两层。

情况2 : z 的叔父结点 y 是黑色的,而且 z 是右孩子

情况3 : z 的叔父结点 y 是黑色的,而且 z 是左孩子

在情况2和情况3中, z 的叔父结点 y 是黑色的。这两种情况通过 z 是 z.p 的左孩子还是右孩子来区别。在情况2中,结点 z 是其父结点的右孩子。我们立刻使用一个左旋来将此状况转变为情况3,此时结点 z 成为左孩子。因为 z 和 z.p 都是红色的,所以所做的旋转对结点的黑高度和性质5都无影响。至此, z的叔父结点 y 总是黑色的,另外 z.p.p 存在且其身份保持不变。在情况3中,要改变某些结点的颜色,并作一次右旋以保持性质5。这样,由于在一行中不再有两个连续的红色结点,所有的处理到此结束。

删除

和 n 个结点的红黑树上的其它基本操作一样,对一个结点的删除要花 O ( lg n )时间。

首先,我们需要自定义一个类似于 TREE-DELETE 中调用的 TRANSPLANT 的子程序。该过程接收三个参数,红黑树 T 以及两棵子树 u , v 。过程用子树 v 来替代子树 u 在树中的位置。

RB-TRANSPLANT(T, u, v)
1 if u.p == T.nil
2     T.root = v
3 elseif u == u.p.left
4     u.p.left = v
5 else
6     u.p.right = v
7 v.p = u.p

过程 RB-TRANSPLANT 和 TRANSPLANT 有两点不同。首先,第1行使用哨兵 T.nil 替代 NIL 。其次,第7行的赋值语句不再需要条件。

过程 RB-DELETE 同 TREE-DELETE 类似,但是多了些代码。有些代码用于跟踪记录可能破坏红黑性质的结点 y 的状态。如果待删除的结点 z 的孩子结点少于两个,那么可以直接从树中删除 z ,并让 y 等于 z 。如果待删除的结点 z 有两个孩子,令 y 为 z 的后继,并用 y 替代 z 在树中的位置。我们还要记住 y 在删除或移动之前的颜色。由于结点 x 也可能破坏树的红黑性质,我们也需要跟踪记录下这个占据了结点 y 最初位置的结点 x 的状态。删除结点 z 后,过程RB-DELETE 还要调用 RB-DELETE-FIXUP 以保持红黑性质。

RB-DELETE(T, z)
1  y = z
2  y-original-color = y.color
3  if z.left == T.nil
4      x = z.right
5      RB-TRANSPLANT(T, z, z.right)
6  elseif z.right == T.nil
7      x = z.left
8      RB-TRANSPLANT(T, z, z.left)
9  else
10     y = TREE-MINIMUM(z.right)
11     y-original-color = y.color
12     x = y.right
13     if y.p == z
14         x.p = y
15     else
16         RB-TRANSPLANT(T, y, y.right)
17         y.right = z.right
18         y.right.p = y
19     RB-TRANSPLANT(T, z, y)
20     y.left = z.left
21     y.left.p = y
22     y.color = z.color
23 if y-original-color == BLACK
24     RB-DELETE-FIXUP(T, x)

RB-DELETE 和 TREE-DELETE 主要的不同之处罗列如下:

  • 我们维护了一个结点 y 。第1行令 y 指向了结点 z (此时 z 为待删结点且它的孩子结点少于两个)。当 z 有两个孩子结点时,第10行令 y 指向 z 的后继,然后 y 会取代 z 在树中的位置。
  • 由于 y 的颜色可能发生变化,变量 y-original-color 保存了 y 在发生改变之前的颜色。在为 y 赋值后,第2行和第10行立刻设置了该变量。如果 z 有两个孩子结点,那么 y != z 并且 y 会占据结点 z 在红黑树中的初始位置;第22行将 y 的颜色设置成和 z 一样。我们需要保存 y 的初始颜色以便在过程RB-DELETE 结尾处做测试;如果它是黑色的,那么删除或移动结点 y 就会破坏红黑性质。
  • 我们还要跟踪记录结点 x 的状态。第4,7和12行的赋值语句令 x 指向 y 的孩子结点或哨兵 T.nil 。
  • 一旦结点 x 移入 y 的初始位置,属性 x.p 总是指向 y 的父结点,哪怕 x 是哨兵 T.nil 也一样。除非 z 是 y 的父结点(此时 z 有两个孩子且 y 是它的右孩子)。对 x.p 的赋值操作在过程 RB-TRANSPLANT 中第7行执行(通过观察可以看出来,在第5,8和16行被调用的 RB-TRANSPLANT ,其传递的第二个参数就是 x )。
  • 最后,如果结点 y 是黑色的,我们可能会破坏某些红黑性质,这就需要调用 RB-DELETE-FIXUP 来保持红黑性质。

在 RB-DELETE 中,如果被删除的结点 y 是黑色的,则会产生三个问题。首先,如果 y 原来是根结点,而 y 的某个红色孩子成为了新的根,这就违反了性质2。其次,如果 x 和 x.p 都是红色的,就违反了性质4。第三,删除 y 可能导致其路径上黑结点的个数少1,这就违反了性质5。补救的一个办法就是把结点 x视为还有额外一重黑色。即,如果将任意包含结点 x 的路径上黑结点个数加1,则性质5成立。当将黑结点 y 删除时,将其黑色“下推”至其子结点。这样问题变为结点 x 可能既不是红色,也不是黑色,从而违反了性质1。这时需要调用 RB-DELETE-FIXUP 来纠正。

RB-DELETE-FIXUP(T, x)
1  while x != T.root and x.color == BLACK
2      if x == x.p.left
3          w = x.p.right
4          if w.color == RED
5              w.color = BLACK                                          // case 1
6              x.p.color = RED                                          // case 1
7              LEFT-ROTATE(T, x.p)                                      // case 1
8              w = x.p.right                                            // case 1
9          if w.left.color == BLACK and w.right.color == BLACK
10             w.color = RED                                            // case 2
11             x = x.p                                                  // case 2
12         else
13             if w.right.color == BLACK
14                 w.left.color = BLACK                                 // case 3
15                 w.color = RED                                        // case 3
16                 RIGHT-ROTATE(T, w)                                   // case 3
17                 w = x.p.right                                        // case 3
18             w.color = x.p.color                                      // case 4
19             x.p.color = BLACK                                        // case 4
20             w.right.color = BLACK                                    // case 4
21             LEFT-ROTATE(T, x.p)                                      // case 4
22             x = T.root                                               // case 4
23     else (same as then clause with "right" and "left" exchanged)
24 x.color = BLACK

过程 RB-DELETE-FIXUP 可以恢复性质1,2和4。这里仅说明性质1。过程中 while 循环的目标是将额外的黑色沿树上移,直到:

  1. x 指向一个红黑结点,此时,在第24行,将 x 着为黑色;
  2. x 指向根,这是可以简单地消除额外的黑色,或者
  3. 做必要的旋转和颜色改变。

在 while 循环中, x 总是指向具有双重黑色的那个非根结点。用 w 表示 x 的兄弟。算法中的四种情况在下图中加以说明。首先要说明的是在每种情况中的变换是如何保持性质5的。关键思想就在每种情况下,从(其包括)子树的根到每棵子树之间的黑结点个数(包括 x 的额外黑色)并不被变换所改变。因此,性质5在变换之前成立,之后依然成立。

情况1 : x 的兄弟 w 是红色的

见 RB-DELETE-FIXUP 第5~8行和上图a。因为 w 必须有红色孩子,我们可以改变 w 和 x.p 的颜色,再对 x.p 做一次左旋,而且红黑性质得以继续保持, x的新兄弟是旋转之前 w 的某个孩子,其颜色为黑色。这样,情况1就转换成情况2,3或4。

情况2 : x 的兄弟 w 是黑色的,且 w 的两个孩子都是黑色的

见 RB-DELETE-FIXUP 第10~11行和上图b。因为 w 和两个孩子都是黑色的,故从 x 和 w 上各去掉一重黑色,从而 x 只有一重黑色而 w 为红色。为了补偿去掉的黑色,需要在原 x.p 内新增一重额外黑色。然后新结点 x 在最后被着为黑色。

情况3 : x 的兄弟 w 是黑色的, w 的左孩子是红色的,右孩子是黑色的

见 RB-DELETE-FIXUP 第14~17行和上图c。此时可以交换 w 和其左孩子 w.left 的颜色,并对 w 右旋,而红黑性质依然保持,且从情况3转换成了情况1。

情况4 : x 的兄弟 w 是黑色的,且 w 的右孩子是红色的

见 RB-DELETE-FIXUP 第18~22行和上图d。通过做颜色的修改并对 x.p 做一次左旋,可以去掉 x 的额外黑色并把它变成单独黑色。将 x 置为根后, while 会在测试其循环条件时结束。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章