手把手带你学并查集
1 并查集的定义
并查集是一种维护集合的数据结构。它的名字“并”、“查”、“集”分别取自Union(合并)、Find(查找)和Set(集合)这三个单词。其中Union指合并两个集合的操作,Find指查找两个元素是否属于同一集合的操作。
应用场景:并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。比如说有若干个家族,每个家族人员都很庞大,要判断任意两个人是否有亲戚关系,确实不容易,这个时候就可以运用并查集,按照这些家族中已有的人员关系逐渐建立关系网,建立关系网后就能判断任意给出的两个人是否具有亲戚关系。
![a0529628a2742279091898b40580b1a9.png](https://i-blog.csdnimg.cn/blog_migrate/8934119e18f4f9f861faa8d135f45a11.png)
实现过程:并查集是如何实现的呢?其实就是用一个数组:int parent[N]。其中parent[i]表示元素i的父亲节点,而父亲节点本身也是这个集合内的元素(0<=i<=N-1)。例如parent[2]=1就表示元素2的父亲节点为1,特别地parent[i]=i表示元素i是该集合的根节点。我们来用并查集数组表示上图各节点之间的关系:
parent[0]=0;
parent[1]=1; parent[2]=1; parent[3]=2; parent[4]=2;
parent[5]=5;parent[6]=6
2 并查集的基本操作
(1)初始化
一开始,每个元素都是独立的集合,因此需要令每个parent[i]=i;
public UF(int count){
this.count=count;
parent=new int[count];
for(int i=0;i parent[i]=i;//先默认每个节点的父节点是自己
}
}
(2)查找
同一个集合中只存在一个根节点,因此查找操作就是对给定的节点寻找其根节点的过程。
private int find(int x){
while(parent[x]!=x){//如果不是根节点,继续循环
x=parent[x];//获得自己的父节点
}
return x;
}
(3)合并
合并就是指把两个集合合并成一个集合。
private void union(int p,int q){
int rootP=find(p);//查找p节点的根节点,记为rootP
int rootQ=find(q);//查找q节点的根节点,记为rootQ
if(rootP==rootQ)return;
parent[rootP]=rootQ;
}
3 路径压缩
为什么进行路径压缩?上面讲解的并查集查找函数是没有经过优化的,在极端条件下效率较低。现在考虑一种情况,即题目中给出的元素数量很多并且形成一条链,那么查找函数的效率会非常低。如图,总共有10^5个元素形成一条链,那么假设要进行10^5次查询,且每次查询都查询最后面的节点的根节点,那么每次都要花费10^5的计算量查找。
如何来优化呢?先看一个例子, parent[1]=1;
parent[2]=1;
parent[3]=2;
parent[4]=3;
其实它等价于
parent[1]=1;
parent[2]=1;
parent[3]=1;
parent[4]=1;
我们可以按照这个思路,加快查找根节点的速度,这就是所谓的路径压缩。
private int find(int x){
//在查找过程中把路径上的各个节点的父节点设置为根节点
if(x==parent[x])return x;
//找到根节点
int fx = find(parent[x]);
parent[x] = fx;
return fx;
}
分析完之后,我们一起来看看完整代码怎么写
class UF{
int count;
int[] parent;
public UF(int count){
this.count=count;
parent=new int[count];
for(int i=0;i parent[i]=i;
}
}
private int find(int x){
if(x==parent[x])return x;
int fx = find(parent[x]);
parent[x] = fx;
return fx;
}
private void union(int p,int q){
int rootP=find(p);
int rootQ=find(q);
if(rootP==rootQ)return;
else parent[rootP]=rootQ;
}
private boolean connect(int p,int q){
int rootP=find(p);
int rootQ=find(q);
return rootP==rootQ;
}
private int count(){
return this.count;
}
}
4 一起来A个题目
结合一个题目来看看如何运用并查集解决问题。
班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。
输入:[[1,1,0], [1,1,0], [0,0,1]] 输出:2
解释:已知学生 0 和学生 1 互为朋友,他们在一个朋友圈。第2个学生自己在一个朋友圈。所以返回 2 。
输入:[[1,1,0], [1,1,1], [0,1,1]] 输出:1
解释:已知学生 0 和学生 1 互为朋友,学生 1 和学生 2 互为朋友,所以学生 0 和学生 2 也是朋友,所以他们三个在一个朋友圈,返回 1 。
class Solution{
//内部并查集类,一般是固定写法
class UF{
int count;
int[] parent;
public UF(int count){
this.count=count;
parent=new int[count];
for(int i=0;i parent[i]=i;
}
}
private int find(int x){
if(x==parent[x])return x;
int fx = find(parent[x]);
parent[x] = fx;
return fx;
}
private void union(int p,int q){
int rootP=find(p);
int rootQ=find(q);
if(rootP==rootQ)return;
else parent[rootP]=rootQ;
}
private boolean connect(int p,int q){
int rootP=find(p);
int rootQ=find(q);
return rootP==rootQ;
}
private int count(){
return this.count;
}
}
public int findCircleNum(int[][] M) {
int n=M.length;
if(n==0)return 0;
UF uf=new UF(n);
for(int i=0;i for(int j=i+1;j if(M[i][j]==1){
//两个学生有关系,则进行链接
uf.union(i,j);
}
}
}
return uf.count();
}
}