并查集总结--逐步分析,并查集没有那么难

本文详细介绍了并查集的概念,重点讲解动态连通性、实现思路及优化方法,包括路径压缩,旨在帮助读者深入理解并查集,并通过实际应用举例展示其在解决朋友圈问题中的应用。
摘要由CSDN通过智能技术生成

并查集

关于并查集看到一个有意思的解释,可以很好的帮助理解并查集。并查集详解(超级简单有趣~~就学会了)

还有一篇文章对并查集有很详细的解释,写的清楚明白,Union-Find算法详解

根据这两篇文章的内容,写写自己的理解。

一、动态连通性

先解释下什么叫做动态连通性。

动态连通性就是把原来相互独立的节点,通过某一种【等价关系】将两个节点连接起来。这里的【等价关系】就被成为“连通”。一般等价关系都具有三个性质:

  • 自反性:节点 p 和 p 是连通的;
  • 对称性:如果节点 p 和 q 连通,那么 q 和 p 也连通;
  • 传递性:如果节点 p 和 q 连通,q 和 r 连通,那么 p 和 r 连通。

Union-find算法主要要实现这两个API:

class UnionFind{
   
    //将p和q连接
    public void union(int p,int q);
    //判断p和q是否连通
    public boolean connected(int p,int q);    
    //返回图中有多少连通分量    
    public int count();
}

举个例子:
假设有一个很大很大的湖,这个湖里有10个小岛,给这些岛从0~9编号。开始的时候,这10个小岛两两之间都没有桥,人无法通过步行从一个岛上走到另一个岛上(请不要考虑船之类的水上交通工具或者岛之间的距离,这里只是为了说明意思),那么这10个岛就是相互独立的,就可以说这10个岛都是不连通的。

Union-find算法中需要实现的方法可以这么理解:

  • union表示:连接节点p和q,就是在两个岛之间建立一座桥,让两个岛之间建立连接。
  • connected表示:判断两个岛之间是否建立了连接,建立了连接就返回true,否则返回false。
  • count表示:记录湖中有几块岛屿是独立的。

注意,如果岛1和岛2之间建了桥,即调用union(1,2)建立连接,那么人就可以通过桥步行从岛1走到岛2上,岛1和岛2就不是独立的。岛3和岛1、岛2都没有建桥,那么岛3和岛1岛2之间就是独立的。

在开始的情况下,10个岛之间都没有建立连接,调用connected都会返回false,独立的岛屿数是10个,即连通分量是10。

现在调用union(0,1),即岛0和岛1建立了连接,connected(0,1)就会返回true,连通分量降为9。

再调用union(1,2),这时岛0、岛1、岛2之间建立了连接,connected(0,2)也会返回true,连通分量会降为8.

二、实现思路

我们把这个10个岛画在纸上在这里插入图片描述

为了表示连通性,我们需要给每个节点设置一个指针,指向它的父亲节点,不需要复杂的数据结构,只需要创建一个长度为n的数组parent[],这里的n指的是节点数。

parent[i] = j 表示:节点 i 的父亲节点是 j 。

如果是根节点,它的指针就指向自己,即parent[root] = root;

例如:开始时,各个岛屿都是相互独立的,那么各个节点的指针都指向自己在这里插入图片描述

创建UnionFind,并初始化

class UnionFind{
       	
	//记录连通分量    
	private int count;    
	//记录父亲节点的数组,节点x的父亲节点是parent[x]    
	private int[] parent;
	    
	//构造函数,n为节点总数,即初始状态    
	public UnionFind(int n){
           
		//开始互相之间都不连通,连通量为n        
		this.count = n;        
		//初始的时候,各个节点的父亲节点都是自己,所以parent[i]=i        
		parent = new int[n];        
		for (int i = 0; i < n; i++){
         
		      parent[i] = i;        
		}    
	}
	
    	//...
}

假如我们希望两个节点之间是连通,只需要让其中一个节点的父亲节点设为另一个节点即可,就是说,假设连通节点 p 和 q ,令parent[p] = q,多次连接之后,就会发现,连通的部分就像一棵树,如图
在这里插入图片描述

当我们希望两个节点连通,也可以让一个节点的根节点连接到另外一个节点的根节点上即可,这是一种比较普遍的方式。这样的话,我们首先需要找到这两个节点的根节点(注意:一个节点的根节点不一定是这个节点的父亲节点,可能是它的爷爷的爷爷…节点,直到找到那个父亲节点是自己的节点),让其中一个根节点成为另一个根节点的父亲节点。

//查找节点x的根节点,如果x的父亲节点不是x本身,那么就将x的父亲节点赋值给x,一层一层地向上找,直到x的父亲节点是x本身,即parent[x] = x
private int find(int x){
       
	while(parent[x] != x ){
          
		 x = parent[x];    
	}    
	return x;
}
//找到p,q的根节点,如果两者的根节点相同,说明两者已经是连通的了,无需再连通,直接返回;
//如果两个根节点不是同一个,那么将其中一个根节点连接到另外一个节点上,即让其中一个节点的父亲节点等于另一个节点
public void union(int p, int q){
       
	int rootP = find(p);   
	int rootQ = find(q);   
	if (rootP == rootQ) 
		return;    
	//将rootP的父亲节点设置为rootQ    
	parent[rootP] = rootQ;    
	//将两个互相独立的岛屿,连接到一起,那么连通分量就会少一,即count--;    
	count--;
}
//返回当前的连通分量个数
public int count(){
       
	return count;
}

至于connected的实现,如果节点 p 和节点 q 连通,那么他们肯定具有相同的根节点,即

public boolean connented(int p , int q){
      
	int rootP = find(p);    
	int rootQ = find(q);    
	return rootP == rootQ;
}

基本上,Union-Find的算法算是基本完成了。

关于复杂度,我们可以看到union和connected的复杂度基本来源于find,find从某个子节点向上查找根节点时,其时间复杂度就是树的高度,最坏的情况下是树退化成一个链表,需要遍历所有节点,时间复杂度是O(n)的。这个复杂度并不是很好,我们期望可以降低复杂度。既然时间复杂度是树的高度,那么如果树是比较平衡点的结构,那么复杂度是不是就降低了呢?

三、优化代码

我们可以看出,导致树结构不平衡的情况是,我们利用union函数,暴力地将节点 p 所在的树连通到节点 q 的根节点上,并没有注意树结构是否平衡,容易造成某一分支过长的情况。借用labuladong的一幅图
在这里插入图片描述

长此以往,树只会越来越不平衡,labuladong的详解里写到,采用一个数组来记录树的‘重量’,即树的节点数。在连接根节点的时候,比较下树的节点数,节点数较小的树连接到节点数较大的树的下面,可以避免不平衡的情况。


                
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值