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` 操作时可以更高效地连接两个集合,而不必遍历整个链表。