首页 > 代码库 > 算法导论读书笔记(13)

算法导论读书笔记(13)

<style></style>

算法导论读书笔记(13)

目录

  • 红黑树
  • 旋转
  • 插入
    • 情况1 : z 的叔父结点 y 是红色的
    • 情况2 : z 的叔父结点 y 是黑色的,而且 z 是右孩子
    • 情况3 : z 的叔父结点 y 是黑色的,而且 z 是左孩子
  • 删除
    • 情况1 : x 的兄弟 w 是红色的
    • 情况2 : x 的兄弟 w 是黑色的,且 w 的两个孩子都是黑色的
    • 情况3 : x 的兄弟 w 是黑色的, w 的左孩子是红色的,右孩子是黑色的
    • 情况4 : x 的兄弟 w 是黑色的,且 w 的右孩子是红色的

红黑树

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

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

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

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

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

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

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

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

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

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

旋转

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

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


当在某个结点 x 上做左旋时,我们假设它的右孩子 y 不是 T.nilx 可以为树内任意右孩子不是 T.nil 的结点。左旋以 xy 之间的链为“支轴”进行。它使 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 内,假设 zkey 域已经事先被赋值。

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-INSERTRB-INSERT 之间有四处不同。首先,在 TREE-INSERT 内的所有的 NIL 都被 T.nil 代替。其次,在 RB-INSERT 的第16,17行中,设置 z.leftz.rightT.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.pz 的祖父结点 z.p.p 的左孩子还是右孩子。这里只讨论 z.p 是左孩子的情况。

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

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

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

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

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

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

删除

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

首先,我们需要自定义一个类似于 TREE-DELETE 中调用的 TRANSPLANT 的子程序。该过程接收三个参数,红黑树 T 以及两棵子树 uv 。过程用子树 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-TRANSPLANTTRANSPLANT 有两点不同。首先,第1行使用哨兵 T.nil 替代 NIL 。其次,第7行的赋值语句不再需要条件。

过程 RB-DELETETREE-DELETE 类似,但是多了些代码。有些代码用于跟踪记录可能破坏红黑性质的结点 y 的状态。如果待删除的结点 z 的孩子结点少于两个,那么可以直接从树中删除 z ,并让 y 等于 z 。如果待删除的结点 z 有两个孩子,令 yz 的后继,并用 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-DELETETREE-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 也一样。除非 zy 的父结点(此时 z 有两个孩子且 y 是它的右孩子)。对 x.p 的赋值操作在过程 RB-TRANSPLANT 中第7行执行(通过观察可以看出来,在第5,8和16行被调用的 RB-TRANSPLANT ,其传递的第二个参数就是 x )。
  • 最后,如果结点 y 是黑色的,我们可能会破坏某些红黑性质,这就需要调用 RB-DELETE-FIXUP 来保持红黑性质。

RB-DELETE 中,如果被删除的结点 y 是黑色的,则会产生三个问题。首先,如果 y 原来是根结点,而 y 的某个红色孩子成为了新的根,这就违反了性质2。其次,如果 xx.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 必须有红色孩子,我们可以改变 wx.p 的颜色,再对 x.p 做一次左旋,而且红黑性质得以继续保持, x 的新兄弟是旋转之前 w 的某个孩子,其颜色为黑色。这样,情况1就转换成情况2,3或4。

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

RB-DELETE-FIXUP 第10~11行和上图b。因为 w 和两个孩子都是黑色的,故从 xw 上各去掉一重黑色,从而 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 会在测试其循环条件时结束。