由于该系列文章标题为数据结构,故书中的第七章排序就不再此分析了。直接跳到第八章的不相交ADT:这种数据结构实现例程仅需要几行代码。
1、等价关系。
在集合S上定义运算R(relation),对于a, b∈S,运算aRb返回类型为布尔型变量,即仅有 true 和 false 。当aRb = true 时,则称a、b有关系。
对于定义的该运算,有三条性质:(其中 aRb 则直接代表 aRb = true)
1、自反性:对所有的a∈S,均有aRa;
2、对称性:aRb的充要条件为bRa。
3、传递性:若aRb, bRc,则aRc。
例:抽象的运算: 电气连接(electrical connectivity)是等价关系,其中的所有连接通过金属导线完成。
2、动态等价性问题。
假设存在一种等价关系运算符 ~ 满足上文的等价关系,则会出现问题:对任意 a 、b,是否 a~b。
解决问题的方法是声明一个二维布尔数组,类似于表格,其存储着每两个元素之间 ~ 关系。但是这种方法有着定义不明显且隐秘的缺点。
因此,出现了第二种解决问题的方法:鉴于满足等价关系 ~ 的元素有 传递性的性质, 那么对于 aRb , aRc,则能推出bRc。这一点是十分重要的,由于这个性质,在S中所有能够与 a 等价的元素均可以被规划在一起,形成一个等价类( equivalence class) ,即在这个等价类中,所有的元素均等价,而在这个类外的元素均不与该类中的元素等价。
则问题 a与b是否等价就变成了a与b是否在同一个等价类中。
分析输入数据最初是由 N 个集合(数学术语)组成的集合(将这些若干小集合放在一起,原文为 collection)时的问题。在前提每个集合含有一个元素,而同时这些小集合的元素之间关系均为 false ,则会得到Si∩Sj =Ø,其中Si和Sj分别代表不同的小集合。 进一步分析则会得到这些集合不相交(disjoint)的结果。
对于这些集合,存在着两种运算:
Find: 返回包含给定元素的类;
添加关系 a~b:在添加之前,先通过Find a 和Find b,并且判断二者是否已经处于一个等价类中。若处于则不进行操作,相反。则进行Union操作。
Union操作:把含有 a 和 b 的两个等价类合并为一个新的等价类。即新建立一个集合Sk = Si∪Sj 并删除掉原来的集合 Si 和 Sj。
这项工作被称作 :Union / Find 算法。以下对该算法做一介绍。
由于无论是大集合还是小集合,都可能因为Union算法发生改变,因此算法是动态的(dynamic)。
同时,这个算法也是联机的(on-line)。因为当Find执行时给出答案才能进行Union操作。
在此,再说明一种脱机(off-line)算法,以便于区分:该算法(脱机)观察所有的Union和Find序列。它对每个Find给出的答案必须和所有执行到该Find例程的Union一致。答案就相当于算法返回的结果,即在执行该算法后新得到的大集合。
由上面的操作,可以注意到:我们不会进行元素的值的比较,而只会知道其所在的等价类的名字。而对于仅当两个元素属于同一个等价类的情况下,作用在这两个元素的Find例程会返回相同的名字:即寻找a、b两个元素所属的等价类,则a、b属于同一个等价类的情况下,有:Find(a) =Find(b)。
解决动态等价问题有两种方法:
一(这种方法能够让Find例程最坏以常数运行时间运行)、
首先,在一个数组中存储每一个等价类的名字,此时Find就是简单的O(1)查找。然而,对于Union(a,b)则不会那么简单:假设 a 在等价类 i 中而 b 在等价类 j 中。然后扫描该数组,将所有的 i 变为 j 。其中扫描花费时间 Θ(N)的时间,其中N为所有元素的数量。在最坏的情况下,即所有的元素中包含两个类,其中N-1个元素在等价类i中,另外一个在 j 中,那么则会连续进行N-1次Union操作。花费的时间就为Θ(N²)。若存在Ω(N²)次Find运算,则性能会很好,反之则所耗时间特别多。
二、将同一个等价类中的元素放在同一个链表中。
3、基本数据结构
现在讨论用 树 的形式实现该不相交ADT,则每一棵树则能表达一个等价类,同时用树的集合,即森林存储不同的等价类。而集合的名字则由根处的节点给出。同时,类似于链表的游标实现,我们也将此处的树存储在数组中,并有:数组中的每个成员P[ i ] 表示存储的元素为 i 的父亲。
下图给出了较为形象的例子。(图片来自:https://yq.aliyun.com/articles/297655)
但应当注意的是:下图中根节点存储的是树的深度。除了根之外,其他节点存储的其父亲的数组下标。
因为图中给出的是一种更为高级的方法,并不符合当前基本数据结构的描述。因此我们先假设根节点并没有存储树的深度,并对其进行简单的实现。
先考虑 Find 例程:对于特定元素X的Find例程,Find(X)返回包含树的根而完成,这表示Find例程运行的时间与X节点的深度成正比。
再考虑Union 例程:Union(a,b)合并两棵树,在例程中通过将一个接待你的根指向另一棵树的根节点来完成。
以下为C语言实现:、
#define NumSets 100
typedef int DisjSet[NumSets + 1];
typedef int SetType;
typedef int ElementType;
/*
DisjSet 是 the Disjoint Set 即 不相交ADT 的英文缩写
SetType是根节点的数组下标,而ElementType可以代表所有元素的数组下标,即SetType属于ElementType
注意: 对于元素(或者称作数组下标)X,S[X]存储着它的父亲的元素(或者说父亲的下标)
即我们可以把S[X]在此处看作求父节点下标的运算
*/
void Initialize(DisjSet S)
{
int i;
for (i = NumSets; i > 0; i--)S[i] = 0;
}
void SetUnion(DisjSet S, SetType Root1, SetType Root2)
{
S[Root2] = Root1;
//将第二个节点指向第一个节点
}
SetType Find(ElementType X, DisjSet S)
{
if (S[X] <= 0)return X; //当S[X]=0时,下标X就是根
else return Find(S[X], S);// 否则,对他的父节点进行Find操作
}
使用不相交ADT时,时刻要弄清楚数组下标和数组下标存储的元素之间的关系,千万不要混淆概念。
即:对于元素,或者数组下标X,S[X]存储的是其父节点的数组下标,或者当X为根节点时,存储的是0。