1.并查集的基本概念
并查集是用于集合合并和元素查询的一种数据结构。主要有两个功能,一是合并元素到同一个集合中;二是查询一个元素属于哪一个集合。
因此,可以设计一个接口用来规定并查集的特性,具体的实现可以根据具体应用灵活处理。接口中元素x,y分别是只得元素的编号,而并不一定是真实的元素,真实的元素可以根据具体的应用进行确定。
class IDisjointset{
public:
virtual void init_set() = 0;
virtual void union_set(int x, int y) = 0;
virtual int find_set(int x) = 0;
};
2.采用map实现的并查集
如果使用一个map来实现并查集,实现接口,class CMdjs : public IDisjointset,函数的override申明就不再给出了,仅仅给出成员变量和构造函数。 显然,这种实现方式中,同一个集合中的元素应该有相同的groupid值。
class CMdjs:public IDisjointset{
public:
CMdjs(int n){
this.n=n;
}
private:
int n;
map<int,int>groupid;
};
一开始使用并查集时,需要将所有元素都看作独立的集合,即,为每个元素都分配一个group id.因此可以这样实现并查集的初始化:
void CMdjs::init_set(){
//n为元素的个数
for(int i=0;i<n;i++)groupid[i]=i;
}
对于合并操作,给定需要合并的元素x和y,不能简单的将x和y的groupid设为相同的值,因为如果 x属于集合A,y属于集合B,那么需要把B中所有的元素的groupid改成A的,而不是只改元素y的。
void CMdjs::union_set(int x,int y){
//将元素y对应的集合中的元素的groupid全部改为x的
const int gid=groupid[y];
for(int i=0;i<n;i++){
if(groupid[i]==gid) groupid[i]=groupid[x];
}
}
对于查询操作,给定元素,应该返回其属于哪一个集合,即需要给出groupid.
void CMdjs::find_set(int x){
return groupid[x];
}
3.实现的时间复杂度分析
对于初始化操作,显然需要O(n)的复杂度;
对于合并操作,如果考虑map的查询时间,需要O(nlogn)的复杂度;
对于查找操作,如果考虑map的查询时间,则需要O(logn)的复杂度。
4.两种实现并查集的思路
通过判断给出集合的方式,可以将并查集的实现方式分为两种,一种是采用串联式,一种是采用平行式。假设有123456一共6个节点,这6个节点是需要合并成一个集合的,现在假设123已经合并,456已经合并,现在得知2和5属于同一个集合,进行合并操作,可能有两种肯能的实现方式.
4.1 串联式实现方式
如下表。
节点 | 1 | 2 | 3 | 4 | 5 | 6 |
指向 | 2 | 3 | 3 | 5 | 6 | 6 |
这里实际上是一个路径表,表示1指向2,2指向3,这样就知道1、2、3是属于同一个集合的,这里指向的最终的节点就可以作为集合号,例如这里的3和6. 现在要将2和5划分到同一个集合,那么需要将2所在的集合和5所在的集合合并,而不是简单的把2指向5.因此需要先找到2和5属于哪个集合,查询得知属于集合3和集合6,那么将节点3指向节点6即可。
节点 | 1 | 2 | 3 | 4 | 5 | 6 |
指向 | 2 | 3 | 6 | 5 | 6 | 6 |
4.2平行式实现方式
如下表。
节点 | 1 | 2 | 3 | 4 | 5 | 6 |
集合号 | 1 | 1 | 1 | 4 | 4 | 4 |
平行事实现每次直接将所有的待合并的集合中的元素的集合号设定为另一个集合的集合号,这里如果要合并2和5所代表的集合,那么只需要查询得到2个5分别属于集合1和集合4,可以将集合4中的所有元素的集合号全部设定为1即可。
节点 | 1 | 2 | 3 | 4 | 5 | 6 |
集合号 | 1 | 1 | 1 | 1 | 1 | 1 |
显示上面采用的实现方式是第二种。
4.3两种实现方式比较
如果不考虑底层数据结构的查询时间(例如采用红黑树实现的map的查询时间是O(logn)),比较两种方式的时间复杂度比较如下表(空间复杂度相同):
串联式 平行式 查询 O(n) O(1) 合并 O(n) O(k)最差O(n)