算法导论读书笔记(13)

算法导论读书笔记(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
      的右孩子是红色的

红黑树

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

树中每个结点包含五个域: colorkeyleftright
p 。如果某结点没有一个子结点或父结点,则该结点相应的指针为 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-INSERT
TREE-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-INSERT
RB-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.p
z 的祖父结点 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 都着为黑色以解决 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 是黑色的。这两种情况通过 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
会在测试其循环条件时结束。

算法导论读书笔记(13),布布扣,bubuko.com

时间: 2024-12-21 18:19:30

算法导论读书笔记(13)的相关文章

算法导论读书笔记之钢条切割问题

算法导论读书笔记之钢条切割问题 巧若拙(欢迎转载,但请注明出处:http://blog.csdn.net/qiaoruozhuo) 给定一段长度为n英寸的钢条和一个价格表 pi (i=1,2, -,n),求切割钢条的方案,使得销售收益rn最大.注意,如果长度为n英寸的钢条价格pn足够大,最优解可能就是完全不需要切割. 若钢条的长度为i,则钢条的价格为Pi,如何对给定长度的钢条进行切割能得到最大收益? 长度i   1   2    3   4     5      6     7     8  

算法导论读书笔记(18)

算法导论读书笔记(18) 目录 最长公共子序列 步骤1:描述最长公共子序列的特征 步骤2:一个递归解 步骤3:计算LCS的长度 步骤4:构造LCS LCS问题的简单Java实现 最长公共子序列 某给定序列的子序列,就是将给定序列中零个或多个元素去掉后得到的结果.其形式化定义如下:给定一个序列 X = < x1 , x2 , - , xm >,另一个序列 Z = < z1 , z2 , - , zk >,如果 Z 满足如下条件则称 Z 为 X 的 子序列 (subsequence),

算法导论读书笔记(15) - 红黑树的具体实现

算法导论读书笔记(15) - 红黑树的具体实现 目录 红黑树的简单Java实现 红黑树的简单Java实现 /** * 红黑树 * * 部分代码参考自TreeMap源码 */ public class RedBlackTree<T> { protected TreeNode<T> root = null; private final Comparator<? super T> comparator; private int size = 0; private static

算法导论读书笔记(17)

算法导论读书笔记(17) 目录 动态规划概述 钢条切割 自顶向下的递归实现 使用动态规划解决钢条切割问题 子问题图 重构解 钢条切割问题的简单Java实现 动态规划概述 和分治法一样, 动态规划 (dynamic programming)是通过组合子问题的解而解决整个问题的.分治法是将问题划分成一些独立的子问题,递归地求解各子问题,然后合并子问题的解而得到原问题的解.与此不同,动态规划适用于子问题并不独立的情况,即各子问题包含公共的子子问题.在这种情况下,分治法会重复地求解公共的子子问题.而动态

算法导论读书笔记(16)

算法导论读书笔记(16) 目录 动态顺序统计 检索具有给定排序的元素 确定一个元素的秩 区间树 步骤1:基础数据结构 步骤2:附加信息 步骤3:维护信息 步骤4:设计新操作 动态顺序统计 之前介绍过 顺序统计 的概念.在一个无序的集合中,任意的顺序统计量都可以在 O ( n )时间内找到.而这里我们将介绍如何在 O ( lg n )时间内确定任意的顺序统计量. 下图显示的是一种支持快速顺序统计量操作的数据结构.一棵 顺序统计树 T 通过在红黑树的每个结点中存入附加信息而成.在一个结点 x 内,增

算法导论读书笔记(14) - 二叉查找树的具体实现

算法导论读书笔记(14) - 二叉查找树的具体实现 目录 二叉查找树的简单Java实现 二叉查找树的简单Java实现 /** * 二叉查找树 * 部分代码参考自TreeMap的源码 */ public class BinarySearchTree<T> { protected TreeNode<T> root = null; private final Comparator<? super T> comparator; private int size = 0; pub

算法导论读书笔记-第十四章-数据结构的扩张

算法导论第14章 数据结构的扩张 一些工程应用需要的只是标准数据结构, 但也有许多其他的应用需要对现有数据结构进行少许的创新和改造, 但是只在很少情况下需要创造出全新类型的数据结构, 更经常的是通过存储额外信息的方法来扩张一种标准的数据结构, 然后对这种数据结构编写新的操作来支持所需要的应用. 但是对数据结构的扩张并不总是简单直接的, 因为新的信息必须要能被该数据结构上的常规操作更新和维护. 14.1 动态顺序统计 顺序统计树(order-static tree) : 在红黑树的基础上, 在每个

平摊分析 --- 算法导论读书笔记

我们经常会说一个算法快不快,这个可以由实验得出,也可以通过分析复杂度得出.实验需要大量不同的输入才更全面准确,否则片面地看某个输入下的表现,是比较偏颇的.分析复杂度(通常分析最坏,因为平均涉及输入的概率分布,依靠假设或者实验和经验)有时候并不是一个简单的事,简单的情况是遍历 for(int i = 0; i != n; i++) 的这种情况,显然是O(n)的复杂度.但是一些复杂的情况就比较难办了,举例来说: a.   栈操作:  除了PUSH,POP,添加一个操作叫MULTIPOP. MULTI

字符串匹配问题 ---- 算法导论读书笔记

字符串匹配是一个很常见的问题,可以扩展为模式的识别,解决字符串问题的思想被广泛地应用.介绍四种解决该问题的办法,包括:最朴素的遍历法,Rabin-Karp算法,自动机机匹配,Knuth-Morris-Pratt算法即耳熟能详的KMP. 在一开始,先对时间复杂度做出一个总扩(从大到小):[1]朴素法:O( (n-m+1)m ):[2]Rabin-Karp:预处理:O(m),匹配:最坏O( (n-m+1)m ),但是平均和实际中比这个好得多:[3]自动机:预处理O(m|Σ|),匹配O(n):[4]K