本系列文章为浙江大学陈越、何钦铭数据结构学习笔记,前面的系列文章链接如下:
数据结构基础:P1-基本概念
数据结构基础:P2.1-线性结构—>线性表
数据结构基础:P2.2-线性结构—>堆栈
数据结构基础:P2.3-线性结构—>队列
数据结构基础:P2.4-线性结构—>应用实例:多项式加法运算
数据结构基础:P2.5-线性结构—>应用实例:多项式乘法与加法运算-C实现
数据结构基础:P3.1-树(一)—>树与树的表示
数据结构基础:P3.2-树(一)—>二叉树及存储结构
数据结构基础:P3.3-树(一)—>二叉树的遍历
数据结构基础:P3.4-树(一)—>小白专场:树的同构-C语言实现
数据结构基础:P4.1-树(二)—>二叉搜索树
数据结构基础:P4.2-树(二)—>二叉平衡树
数据结构基础:P4.3-树(二)—>小白专场:是否同一棵二叉搜索树-C实现
数据结构基础:P4.4-树(二)—>线性结构之习题选讲:逆转链表
数据结构基础:P5.1-树(三)—>堆
数据结构基础:P5.2-树(三)—>哈夫曼树与哈夫曼编码
一、集合的表示及查找
集合主要的运算有以下四种:
1. 求两个集合的交集、并集
2. 求一个集合的补集
3. 求两个集合的差集
4. 给你一个元素,判断它属于哪个集合
1.1 并查集
下面我们来讨论一种很典型的集合运算:并查集
例子:有10台电脑{1,2,3,...,9,10},已知下列电脑之间已经实现了连接:
1和2,2和4,3和5,4和7,5和8,6和9,6和10
问: 2和7之间,5和9之间是否是连通的?
解决思路:
1、将10台电脑看成10个集合{1},{2},{3},...,{9},{10}
2、已知一种连接“x和y”,就将x和y对应的集合合并
3、查询“x和y是否是连通的“就是判别x和y是否属于同一个集合
这上面所做的主要操作就是下面两个,这样的一种问题就叫并查集。
1、把两个集合并在一起
2、查某个元素属于哪个集合
1.2 并查集的存储
对并查集这样的一个问题,我们首先要考虑一下集合怎么进行表示,也就说怎么存储一个集合。
我们可以用一个树来表示一个集合,一棵树是一个集合。如果是两个集合,那就是两棵树。树里的每个结点代表集合的元素,用树根来代表这个集合。所以想查某个元素属于哪个集合,你只要在这个树上面去找它的根结点是谁。如果要把两个集合并在一起,那么就是把两个树并在一起形成一个更大的树。
比方说我们有三个这样的集合
S1={1,2,4,7}
S2={3,5,8}
S3={6,9,10}
在并查操作过程当中,我们不存在已知一个结点找它儿子是谁。更重要的是要知道已知1个结点,去找它父亲是谁,因为根据这个我们可以很快找到根了。所以这种表示方法就不像我们前面二叉树是一个结点指向儿子,而是有一个结点指向父亲。这种表示方法叫做双亲表示方法,每个结点的指针都指向它的父亲。
对于上面三棵树,我们可以用链表存储,也可以直接用数组存储。
数组中每个元素的类型描述为:
typedef struct {
ElementType Data;
int Parent;
} SetType;
下面就是数组的表示方法。数组的每一个分量是个结构,结构包含Data和Parent。Parent是个位置,指向它父亲在数组中的下标值。比如5的父亲是3,在数组中位于第二个。1、3、6都是根结点,没有父结点,所以其Parent为-1。
查找操作:给你一个元素X,找它属于哪个集合,对应代码如下:
int Find( SetType S[ ], ElementType X )
{ /* 在数组S中查找值为X的元素所属的集合 */
/* MaxSize是全局变量,为数组S的最大长度 */
int i;
//找X
for ( i=0; i < MaxSize && S[i].Data != X; i++) ;
if( i >= MaxSize ) return -1; /* 未找到X,返回-1 */
//找父亲
for( ; S[i].Parent >= 0; i = S[i].Parent ) ;
return i; /* 找到X所属集合,返回树根结点在数组S中的下标 */
}
二、集合的并运算
集合的并运算操作步骤如下:
1、分别找到X1和X2两个元素所在集合树的根结点
2、如果它们不同根,则将其中一个根结点的父结点指针设置成另一个根结点的数组下标。
对应代码如下:
void Union( SetType S[ ], ElementType X1, ElementType X2 )
{
int Root1, Root2;
Root1 = Find(S, X1);
Root2 = Find(S, X2);
if( Root1 != Root2 )S[Root2].Parent = Root1;
}
面临的问题:
现在我要将上面例子中的Root2挂在Root1下面,此时Root2的父结点为Root1,Root2(结点4)的Parent也就修改为结点1的下标值0。
在这样挂的过程中,如果不断地做Union,这个树会变得越来越大。而且还有另外一种可能,就是这个树越来越高。刚才我们的集合1跟集合3都是两层的,并在一起就变成三层了。树越来越高了之后会导致我们的查找操作效率变差。
解决方案:
为了保证集合后面查找的效率,两个集合并在一起的时候,尽量把小的集合并到大的集合里面去,这样有可能高度不会增加,还是大的集合的那个高度。如果按照这个策略要把小的并得大的里面去,那我们一定要知道每个集合到底有多少个元素,怎么样有这样的一个信息?
方法1(造成空间浪费):
每个结点是个结构,包含两个分量:Data和Parent,我们可以增加一个分量用于记录集合的元素个数。但是我们只有根结点才需要记录这个集合有多少个元素,其他的结点都不用记录,那这就导致了空间浪费。
方法2(可行):
各个结点的Parent的值分为两种情况:一种是代表他父结点的下标,是大于等于零的这样的一个值。另外一种是代表根结点,用负数来表示。目前的根结点统一用-1表示,我们可以用另外一个负数来表示根结点,这个负数的绝对值代表这个树的总结点数。比如6那个结点的Parent为-3,代表它是根结点,且这棵树有3个结点。那么1那个结点的Parent为-7,代表它是根结点,且这棵树有7个结点。
三、C语言-集合的定义与并查操作
#define MAXN 1000 /* 集合最大元素个数 */
typedef int ElementType; /* 默认元素可以用非负整数表示 */
typedef int SetName; /* 默认用根结点的下标作为集合名称 */
typedef ElementType SetType[MAXN]; /* 假设集合元素下标从0开始 */
void Union( SetType S, SetName Root1, SetName Root2 )
{ /* 这里默认Root1和Root2是不同集合的根结点 */
/* 保证小集合并入大集合 */
if ( S[Root2] < S[Root1] ) { /* 如果集合2比较大 */
S[Root2] += S[Root1]; /* 集合1并入集合2 */
S[Root1] = Root2;
}
else { /* 如果集合1比较大 */
S[Root1] += S[Root2]; /* 集合2并入集合1 */
S[Root2] = Root1;
}
}
SetName Find( SetType S, ElementType X )
{ /* 默认集合元素全部初始化为-1 */
if ( S[X] < 0 ) /* 找到集合的根 */
return X;
else
return S[X] = Find( S, S[X] ); /* 路径压缩 */
}
四、小测验
1、已知a、b两个元素均是所在集合的根结点,且分别位于数组分量3和2位置上,其parent值分别为-3,-2。问:将这两个集合按集合大小合并后,a和b的parent值分别是多少?
A. -5,2
B. -5,3
C. -3,3
D. 2,-2
答案:B