树与二叉树的应用——并查集

一、并查集的概念与问题描述

有n个元素从1到n编号,初始时,每一个元素在自己的类中,然后执行一系列Find和Union操作。操作Find(theElement)返回元素theElement所在类的唯一特征;而Union(classA, classB)把包含a和b的两个类合并,其中classA=Find(a)、classB=Find(b),且classA与classB不相等。本文使用将每个集合(类)表示为一棵树作为解决方案,以下给出作为一种简单的集合表示的并查集支持的三种操作:

  • Initial(S):将集合S中的每个元素都初始化为只有一个单元素的子集合。
  • Union(S, Root1, Root2):把集合S中的子集合Root2并入子集合Root1,要求Root1和Root2互不相交,否则不执行合并。
  • Find(S, x):查找集合S中单元素x所在的子集合,并返回该子集合的根结点。

二、并查集的存储结构

通常用树的双亲表示作为并查集的存储结构,每个子集合以一棵树表示(即树中每个非根结点都有一个指针指向其父结点,指向父结点的指针之所以需要,是因为查找操作需要向上搜索一棵树)。

所有表示子集合的树,构成表示全集合的森林,存放在双亲表示数组内。通常用数组元素下标代表元素名,用根结点的下标代表子集合名,根结点的双亲域为负数(可以设置为该子集合元素数量的相反数)。

示例:

1. 并查集的初始化

设有一个全集合为S={0, 1, 2, 3, 4, 5, 6, 7, 8, 9},初始化时每个元素自成一个单元素子集合,每个子集合的数组值为-1。如下图所示:

2. 用树表示并查集

经过一段时间的合并操作后,这些子集合合并为三个更大的子集合,即S1={0, 6, 7, 8},S2={1, 4, 9},S3={2, 3, 5}。此时并查集的树形和存储结构如下图所示:

3. S1∪S2可能的表示方法

为了合并两个子集合,只需将其中一个子集合根结点的双亲指针指向另一个集合的根结点。S1∪S2可能的表示方法如下图所示:


或者

三、并查集的求解策略

并查集问题的求解策略是:把每一个集合表示为一棵树。

查找时,我们把根元素作为集合标志符,因为每一个集合都有唯一的根,所以当且仅当 i 和 j 属于同一个集合时,有Find(i)=Find(j)。为了确定元素theElement属于哪个集合,我们从元素theElement的结点开始,沿着结点到其父结点向上移动,直到到根结点为止。

合并时,我们假设在调用语句Union(classA, classB)中,classA和classB分别是两个不同树集合的根(即classA不等于classB),为了把两个集合合并,我们让一棵树成为另一棵树的子树。

四、并查集的基本实现

并查集的结构定义如下:
【在采用树的双亲指针数组表示作为并查集的存储表示时,集合元素的编号从0到SIZE-1,其中SIZE是最大元素的个数。】

#define SIZE 100
int UFSets[SIZE];//集合元素数组(双亲指针数组)

1. 并查集的初始化操作

void Initial(int S[]){//S即为并查集
    for(int i = 0; i < SIZE; i++){//每个自成单元素集合
        s[i] = -1;
    }
}

2. 并查集的Find操作

在并查集S中查找并返回包含元素x的树的根。

void Find(int S[], int x){
    while(S[x] >= 0){//循环寻找x的根
        x = S[x];
    }
    return x;//根的S[]小于0
}

判断两个元素是否属于同一集合,只需分别找到它们的根,再比较根是否相同即可。

3. 并查集的Union操作

求两个不相交子集的并集。若将两个元素所在的集合合并为一个集合,则需要先找到两个元素的根,再令一颗子集树的根指向另一颗子集树的根。

void Union(int S[], int Root1, int Root2){
    if(Root1 == Root2){return;}//要求Root1与Root2是不同的集合
    else{
        S[Root2] = Root1;//将根Root2连接到另一根Root1下面
    }
}

Find操作和Union操作的时间复杂度分别为O(d)和O(1),其中d为树的深度。

五、并查集实现的优化

1. 合并函数Union的性能改进

在对根为 i 和根为 j 的树进行合并操作时,利用重量规则高度规则,可以提高并查集算法的性能。

  • 重量规则:若根为 i 的树的结点数少于根为 j 的树的结点数,则将 j 作为 i 的父结点;否则,将 i 作为 j 的父结点。
  • 高度规则:若根为 i 的树的高度少于根为 j 的树的高度,则将 j 作为 i 的父结点;否则,将 i 作为 j 的父结点。

改进办法(利用重量规则):做Union操作之前,首先判别子集中的成员数量,然后令成员少的根指向成员多的根,即把小树合并到大树,为此可令根结点的绝对值保存集合树中的成员数量。以下为具体代码实现:

void Union(int S[], int Root1, int Root2){
    if(Root1 == Root2){return;}
    if(S[Root2] > S[Root1]){//Root2的结点数更少
        S[Root1] += S[Root2];//累加集合树的结点总数
        S[Root2] = Root1;//Root2合并到Root1上
    }
    else{//Root1的结点数更少
        S[Root2] += S[Root1];//累加集合树的结点总数
        S[Root1] = Root2;//Root1合并到Root2上
    }
}

在极端情况下,n个元素构成的集合树的深度为n,则Find操作的最坏时间复杂度为O(n)。而采用上述方法构造得到的集合树,其深度不超过⌊log2n⌋+1。

示例:

2. 查找函数Find的性能改进

随着子集间的逐渐合并,集合树的深度越来越大,为了进一步减少确定元素所在集合的时间,可进一步对Find操作进行优化。

改进办法(利用路径紧缩):当所查元素 x 不在树的第二层时,在算法中增加一个“压缩路径”的功能,即将从根到元素 x 路径上的所有元素都变成根的孩子。以下为具体代码实现:

void Find(int S[], int x){
    int root = x;
    while(S[root] >= 0){//循环寻找x的根
        root = S[root];
    }
    while(x != root){//压缩路径
        int t = S[x];//t指向x的父结点
        S[x] = root;//x直接挂到根结点的下面
        x = t;
    }
    return root;//返回根结点编号
}

通过Find操作的“压缩路径”优化后,可使集合树的深度不超过O(α(n)),其中α(n)是一个增长极其缓慢的函数,对于常见的正整数n,通常α(n)<=4。

【拓展】:在改进Find函数的性能时,利用了路径压缩(path compression)过程缩短从元素x到根的查找路径,而该过程的实现有三种不同的途径——路径紧缩(path compaction)、路径分割(path splitting)和路径对折(path halving)。

  • 路径紧缩:从待查结点到根结点的路径上,所有结点的parent指针都被改为指向根结点。虽然路径紧缩增加了单个查找操作的时间,但它减少了此后查找操作的时间。以下为从结点K开始的路径紧缩:

  • 路径分割:从待查结点到根结点的路径上,除根结点与其子结点外,每个结点的parent指针都被改为指向各自的祖父。在路径分割时,只考虑从待查结点到根结点的一条路径就够了。以下为从结点K开始的路径分割:

  • 路径对折:从待查结点到根结点的路径上,除根结点与其子结点外,每隔一个结点,其parent指针都被改为指向各自的祖父。在路径对折中,指针改变的个数仅为路径分割中的一半。同样,在路径对折中只考虑从待查结点到根结点的一条路径就够了。以下为从结点K开始的路径对折:

六、有关并查集的例题

① 并查集的结构是一种( B )。
A. 二叉链表存储的二叉树
B. 双亲表示法存储的树
C. 顺序存储的二叉树
D. 孩子表示法存储的树

② 并查集中最核心的两个操作是: ①查找,查找两个元素是否属于同一个集合;②合并,如果两个元素不属于同一个集合,且所在的两个集合互不相交,则合并这两个集合。假设初始长度为10 (0~9) 的并查集,按 1-2、3-4、5-6、7-8、8-9、1-8、0-5、1-9 的顺序进行查找和合并操作,最终并查集共有( C )个集合。
A. 1
B. 2
C. 3
D. 4

③ 下列关于并查集的叙述中,( D )是错误的(注意,本题涉及图的考点)。
A. 并查集是用双亲表示法存储的树
B. 并查集可用于实现克鲁斯卡尔算法
C. 并查集可用于判断无向图的连通性
D. 在长度为 n 的并查集中进行查找操作的时间复杂度为 O(log2n)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值