算法导论 21.2 不相交集合的链表表示 习题解答

21.2-1使用链表表示和加权合并启发式策略,写出MAKE-SET、FIND-SET和UNION 操作的伪代码。并指定你在集合对象和表对象中所使用的属性。

这个操作的目的是创建一个包含单个元素 x 的新集合,并初始化相关的集合属性。每个元素 x 通过 .set 指向其所属的集合对象,而集合对象 S 包含 .head(指向集合的头部,即代表元素)、.tail(指向集合的尾部,即最后一个元素)和 .size(表示集合的大小)属性。

这样的实现在每次 MAKE-SET 操作后,都创建了一个只包含一个元素的集合,该元素既是集合的头部又是尾部,且集合大小为 1。

MAKE-SET(x):

  1. 创建一个节点 S 包含 .head、.tail 和 .size 属性。

  2. 将 x.set 设置为 S,表示 x 所属的集合为 S。

  3. 将 x.next 设置为 NIL,初始化 x 的下一个节点为 null。

  4. 将 S.head 设置为 x,表示 S 的头部为 x。

  5. 将 S.tail 设置为 x,表示 S 的尾部为 x。

  6. 将 S.size 设置为 1,表示 S 的大小为 1。

  7. 返回 S。

这个操作返回元素 x 所属的集合的头部,即集合的代表元素。

FIND-SET(x):

  返回 x.set.head

加权合并:将较短的集合连接到较长的集合。

UNION(x, y):

  root_x = FIND-SET(x)

  root_y = FIND-SET(y)

  如果 root_x ≠ root_y:

    如果 root_x.set.size < root_y.set.size:

      将 root_x.set.tail.next 设置为 root_y.set.head

      更新 root_y.set.head 到 root_y.set.tail 中的每个节点的 set 指针为 root_x.set

      root_x.set.tail = root_y.set.tail

      root_x.set.size += root_y.set.size

    否则:

      将 root_y.set.tail.next 设置为 root_x.set.head

      更新 root_x.set.head 到 root_x.set.tail 中的每个节点的 set 指针为 root_y.set

      root_y.set.tail = root_x.set.tail

      root_y.set.size += root_x.set.size

21.2-2给出下面程序的结果数据结构,并回答该程序中 FIND-SET 操作返回的答案。这里使用加权合并启发式策略的链表表示。

1 for i=1 to 16

2   MAKE-SET(xi)

3 for i=1 to 15 by 2

4   UNION(xi,xi+1)

5 for i=1 to 13 by 4

6   UNION(xi,xi+2)

7 UNION(x1,x5)

8 UNION(x11,x13)

9 UNION(x1,x10)

10 FIND-SET(x2)

11 FIND-SET(x9)

假定如果包含xi和xj集合有相同的大小,则 UNION(xi,xj)表示将xj所在的表链接到xi所在的表后。

Line 1 - Line 2:

创建16个不相交的集合,每个集合包含一个单独的元素。

结果数据结构:16个包含一个元素的不相交集合。

Line 3 - Line 4:

{1,2},{3,4},{5,6},{7,8},{9,10},{11,12},{13,14},{15,16}.

Line 5 - Line 6:

{1,2,3,4},{5,6,7,8},{9,10,11,12},{13,14,15,16}.

Line 7:

{1,2,3,4,5,6,7,8},{9,10,11,12},{13,14,15,16}.

Line 8:

{1,2,3,4,5,6,7,8},{9,10,11,12,13,14,15,16}.

Line 9:

{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}

FIND-SET(x2)和FIND-SET(x9)都会返回x1

21.2-3对定理 21.1的整体证明进行改造,得到使用链表表示和加权合并启发式策略下的MAKE-SET 和FIND-SET 的摊还时间上界为 O(1),以及 UNION 的摊还时间上界为O(lgn)。

在原始证明中,我们得到了整体的摊还时间上界为 O(n log n),其中路径压缩和加权合并在摊还分析中起到了关键作用。

在讨论三个函数的实际工作量时,这三个函数分别是 `MAKE-SET`、`FIND-SET` 和 `UNION`。下面讨论每个函数的实际工作量:

`MAKE-SET`函数:

   实际工作量:在 `MAKE-SET` 操作中,主要的工作是创建一个新的集合,其中包含一个元素,同时初始化集合对象的属性。

   时间复杂度:由于只涉及常数级别的工作,`MAKE-SET` 操作的实际工作量为 O(1)。

`FIND-SET`函数:

   实际工作量:`FIND-SET` 操作的主要工作是找到给定元素所属的集合,通常涉及路径压缩操作,将元素直接连接到集合的根节点上。

   时间复杂度:由于路径压缩和树的深度不会超过对数级别(通过启发式策略,如加权合并),`FIND-SET` 操作的实际工作量可以认为是 O(1)。

`UNION`函数:

   实际工作量:`UNION` 操作的主要工作是合并两个集合,涉及加权合并等策略,以确保树的平衡。

   时间复杂度: 由于加权合并等启发式策略的影响,`UNION` 操作的实际工作量为 O(log n)。

综合来看,`MAKE-SET` 和 `FIND-SET` 操作的实际工作量是常数级别的,而 `UNION` 操作的实际工作量受到加权合并启发式策略的影响,为 O(log n)。在整个并查集的操作序列中,`MAKE-SET` 和 `FIND-SET` 的实际工作量较小,而 `UNION` 的实际工作量在考虑到其摊还时间的情况下也能够保持较低水平。

路径压缩(Path Compression)和加权合并(Weighted Union)是并查集(Disjoint-Set Union,DSU)中两种用于优化性能的启发式策略。

路径压缩(Path Compression):

   目标:减小集合中元素的查找路径长度,提高 FIND-SET 操作的效率。

   操作:在执行 FIND-SET 操作时,将查找路径上的每个节点直接连接到集合的根节点,从而压缩路径长度。

   效果:路径压缩使得集合中的每个元素都直接指向根节点,从而减小了后续 FIND-SET 操作的时间复杂度。

加权合并(Weighted Union):

   目标:保持并查集中的平衡,避免出现高度不平衡的树结构,提高 UNION 操作的效率。

   操作:在执行 UNION 操作时,将高度较低的树连接到高度较高的树的根节点上。

   效果:加权合并通过考虑树的高度信息,确保在 UNION 操作中,较小的树被连接到较大的树上。这有助于避免出现树的高度过大,从而降低了后续 FIND-SET 操作的时间复杂度。

这两种启发式策略的综合使用可以显著提高并查集的整体性能。路径压缩主要优化 FIND-SET 操作,而加权合并主要优化 UNION 操作。在实际应用中,它们通常一起使用,以平衡并查集中的树结构并减小查找路径的长度,从而实现更高效的操作。

21.2-4请给出图 21-3 所示操作序列的一个运行时间的渐近紧确界,假定使用链表表示和加权合并启发式策略。(使用链表集合表示和UNION的简单实现,在n个对象上的2n一1个操作序列要 O(n^2)总时间,或者每个操作平均时间为θ(n))

操作            更新的对象数

MAKE-SET(x1)      1

MAKE-SET(x2)      1

MAKE-SET(xn)      1

UNION(x2,x1)      1

UNION(x3,x2)      2

UNION(x4,x3)      3

UNION(xn,xn-1)    n-1

如果每次调用 MAKE-SET 操作都耗费 Θ(1) 的时间,而每个 UNION 操作都是将一个大小为1的集合与另一个集合合并,那么每个 UNION 操作的时间复杂度为 Θ(1)。在这种情况下,进行 n 次 MAKE-SET 操作和 n-1 次 UNION 操作的总运行时间为 Θ(n)。

21.2-5 Gompers 教授猜想也许有可能在每个集合对象中仅使用一个指针,而不是两个指针(head 和tail),同时仍然保留每个链表元素的2个指针。请说明教授的猜想是有道理的,并通过描述如何使用一个链表来表示每个集合,使得每个操作与本章中描述的操作有相同的运行时间,来加以解释。同时描述这些操作是如何工作的。你的方法应该允许使用加权合并启发式策略,并与本节所描述的有相同效果。(提示:使用一个链表的尾作代表)

# 集合对象属性

Set:

  tail  # 表示链表的尾巴,即集合的根节点

  size  # 表示集合的大小

# 表对象属性

Node:

  key   # 表示节点的关键字

  next  # 指向下一个节点

  set   # 指向节点所属的集合

  rank  # 表示节点的秩,用于加权合并

# MAKE-SET操作

MAKE-SET(x):

  create a new set S with x as the only element

  x.set = S

  x.rank = 0

  S.tail = x

  S.size = 1

# FIND-SET操作

FIND-SET(x):

  if x.set.tail != x:

    # 路径压缩:将路径上的每个节点的set指向根节点

    old_tail = x.set.tail

    x.set.tail = FIND-SET(old_tail)  # 递归调用,找到根节点

  return x.set.tail

# UNION操作

UNION(x, y):

  root_x = FIND-SET(x)

  root_y = FIND-SET(y)

  if root_x != root_y:

    if root_x.rank < root_y.rank:

      # 将集合root_x链接到集合root_y的头部

      root_x.set.tail = root_y.set.tail

      root_x.set.size += root_y.set.size

    elif root_x.rank > root_y.rank:

      # 将集合root_y链接到集合root_x的头部

      root_y.set.tail = root_x.set.tail

      root_y.set.size += root_x.set.size

    else:

      # 将集合root_x链接到集合root_y的头部,更新秩

      root_x.set.tail = root_y.set.tail

      root_x.set.size += root_y.set.size

      root_y.rank += 1

初始状态:

集合1: `x`

集合2: `y`

集合3: `z`

MAKE-SET(x):

   创建一个新的链表,`tail` 指向 `x`。

   集合1: x (tail)

   集合2: y

   集合3: z

MAKE-SET(y):

   创建一个新的链表,`tail` 指向 `y`。

   集合1: x (tail)

   集合2: y (tail)

   集合3: z

MAKE-SET(z):

   创建一个新的链表,`tail` 指向 `z`。

   集合1: x (tail)

   集合2: y (tail)

   集合3: z (tail)

UNION(x, y):

   将 `y` 所在链表连接到 `x` 所在链表的尾部。

   集合1: x (tail) -> y

   集合2:

   集合3: z (tail)

UNION(y, z):

   将 `z` 所在链表连接到 `y` 所在链表的尾部。

   集合1: x (tail) -> y (tail) -> z

   集合2:

   集合3:

通过这个例子,我们可以看到每个集合的最后一个元素(`tail`)充当了该集合的根节点,同时每个元素仍然有两个指针,一个指向下一个元素,一个指向集合的根节点。这样,我们在只使用一个指针的情况下,仍然能够方便地执行并查集的基本操作,而不影响性能。

`FIND-SET` 操作用于确定给定元素所属的集合,并返回该集合的代表元素(根节点)。在这个特定的链表表示和加权合并启发式策略的情境下,`FIND-SET` 操作的目标是找到给定元素所在集合的根节点。

集合1: x (tail) -> y (tail) -> z

执行 FIND-SET(x):

   要查找元素 `x` 所在的集合,我们从 `x` 开始,通过链表的 `tail` 指针一直追溯到集合的根节点。

   在这个例子中,`x` 的 `tail` 指向 `y`,而 `y` 的 `tail` 指向 `z`,所以集合的根节点是 `z`。

   返回根节点 `z` 作为集合的代表元素。

执行 FIND-SET(z):

   对于元素 `z`,通过链表的 `tail` 指针追溯到集合的根节点。

   在这个例子中,`z` 本身就是集合的根节点。

   返回 `z` 作为集合的代表元素。

`FIND-SET` 操作的目标是找到元素所在集合的根节点,即代表元素。

通过链表的 `tail` 指针追溯到根节点,保持了在路径上执行路径压缩操作的能力,以确保后续的 `FIND-SET` 操作更加高效。

21.2-6假设对 UNION过程做一个简单的改动,在采用链表表示中拿掉让集合对象的 tail 指针总指向每个表的最后一个对象的要求。无论是使用还是不使用加权合并启发式策略,这个修改不应该改变 UNION过程的渐近运行时间。(提示:而不是把一个表链接到另一个表后面,将它们拼接在一起。

具体操作如下:

1. 将第二个链表(S2)插入到第一个链表(S1)的头部和元素之间。

2. 存储指向S1的第一个元素的指针。

3. 对于S2中的每个元素x,将x的头指针设置为S1的头指针。

4. 当达到S2的最后一个元素时,将其next指针设置为S1的第一个元素。

这个方法的关键是始终让S2充当较小的集合,这有助于加权合并启发式策略的性能。通过这种方式,“拼接”两个链表的操作在渐近时间上保持线性,不影响 `UNION` 操作的性能。这种优化使得在执行 `UNION` 操作时可以更高效地连接两个集合,而不必遍历整个链表。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值