并查集原理与API设计

目录

前言:

原理:

并查集API设计 

1.​UF(int N)构造方法实现

2.union(int p,int q)合并方法实现  

 代码实现:

UF_Tree算法优化

 1 UF_Tree API设计

路径压缩

案例-畅通工程


前言:

上一章我给大家介绍了红黑树这种树结构,以及实现原理,在这一章我给大家介绍简单的并查集,它也是一种树结构,但相对于二叉查找树红黑树等,会相对比较简单,好理解。

原理:

并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。

并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。

并查集结构
并查集也是一种树型结构,但这棵树跟我们之前讲的二叉树、红黑树、 B 树等都不一样,这种树的要求比较简单:
1. 每个元素都唯一的对应一个结点;
2. 每一组数据中的多个元素都在同一颗树中;
3. 一个组中的数据对应的树和另外一个组中的数据对应的树之间没有任何联系;
4. 元素在树中并没有子父级关系的硬性要求;

 

 

并查集API设计 

1.​UF(int N)构造方法实现

1. 初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为 N 个组;
2. 初始化数组 eleAndGroup
3. eleAndGroup 数组的索引看做是每个结点存储的元素,把 eleAndGroup 数组每个索引处的值看做是该结点 所在的分组,那么初始化情况下,i索引处存储的值就是 i

2.union(int p,int q)合并方法实现  

1. 如果pq已经在同一个分组中,则无需合并

2. 如果 p q 不在同一个分组,则只需要将 p 元素所在组的所有的元素的组标识符修改为 q 元素所在组的标识符即 可
3. 分组数量 -1

 

 代码实现:

public class UF {
	//记录结点元素和该元素所在分组的标识
	private int[] eleAndGroup;
	//记录分组的组数
	private int count;
	//	构造方法
	public UF(int N) {
		//初始情况下,每个元素都在一个独立的分组中,所以,初始情况下,并查集中的数据默认分为N个组
		this.count=N;
		//初始化数组
		this.eleAndGroup=new int[N];
		//把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该结点所在的分组,那么初始化情况下,i索引处存储的值就是i
		for(int i=0;i<eleAndGroup.length;i++) {
			eleAndGroup[i]=i;
		}
	}
	//获取当前并查集中的数据有多少个分组
	public int count() {
		return this.count;
	}
	//元素p所在分组的标识符
	public int find(int p) {
		return this.eleAndGroup[p];
	}
	//判断并查集中元素p和元素q是否在同一分组中
	public boolean connected(int p,int q) {
		return find(p)==find(q);
	}
	//把p元素所在分组和q元素所在分组合并
	public void union(int p,int q) {
		//如果p和q已经在同一个分组中,则无需合并;
		if(connected(p,q)) {
			return;
		}
		//如果p和q不在同一个分组,则只需要将p元素所在组的所有的元素的组标识符修改为q元素所在组的标识 符即可
		int pGroup=find(p);
		int qGroup=find(q);
		for(int i=0;i<eleAndGroup.length;i++) {
			if(find(i)==pGroup) {
				eleAndGroup[i]=qGroup;
			}
		}
		//分组数量-1
		count--;
	}

}

大家大脑想想要想把一个N个小组,全部联合在一起那我们至少要多少次使用union方法?想必大家都知道是N-1次,而我们在union方法中又使用for循环,也就是说将所有的分组联合到一起,合并算法的时间复杂度为O(n^2)。如果是这样那么可能对于很少的分组情况可能会相对而言要好一点,那么对于像计算网络问题互联可能拥有上千万而言,那就是不乐观的。所以我们需要对算法进行优 化。

UF_Tree算法优化

为了提升 union 算法的性能,我们需要重新设计 fifind 方法和 union 方法的实现,此时我们先需要对我们的之前数据结构中的eleAndGourp 数组的含义进行重新设定:
1. 我们仍然让 eleAndGroup 数组的索引作为某个结点的元素;
2.eleAndGroup[i] 的值不再是当前结点所在的分组标识,而是该结点的父结点;

 1 UF_Tree API设计

1. fifind(int p) 查询方法实现
1. 判断当前元素 p 的父结点 eleAndGroup[p] 是不是自己,如果是自己则证明已经是根结点了;
2. 如果当前元素 p 的父结点不是自己,则让 p=eleAndGroup[p] ,继续找父结点的父结点 , 直到找到根结点为止;
2.union(int p,int q) 合并方法实现
1. 找到 p 元素所在树的根结点
2. 找到 q 元素所在树的根结点
3. 如果 p q 已经在同一个树中,则无需合并;
4. 如果 p q 不在同一个分组,则只需要将 p 元素所在树根结点的父结点设置为 q 元素的根结点即可;
5. 分组数量 -1

 

public class UF_Tree {
	private int[] eleAndGroup;
	private int count;
	public UF_Tree(int N) {
		this.count=N;
		this.eleAndGroup=new int[N];
		for(int i=0;i<eleAndGroup.length;i++) {
			eleAndGroup[i]=i;
		}
	}
	public int count() {
		return this.count;
	}
	
	public boolean connected(int p,int q) {
		return find(p)==find(q);
	}
	//元素p所在分组的标识符
	public int find(int p) {
		while(true) {
			//判断当前元素p的父结点eleAndGroup[p]是不是自己,如果是自己则证明已经是根结点了;
			if(p==eleAndGroup[p]) {
				return p;
			}
			//如果当前元素p的父结点不是自己,则让p=eleAndGroup[p],继续找父结点的父结点,直到找到根 结点为止;
			p=eleAndGroup[p];
		}
	}
	public void union(int p,int q) {
		//找到p元素所在树的根结点
		int proot=find(p);
		//找到q元素所在树的根结点
		int qroot=find(q);
		//如果p和q已经在同一个树中,则无需合并;
		if(proot==qroot) {
			return;
		}
		//如果p和q不在同一个分组,则只需要将p元素所在树根结点的父结点设置为q元素的根结点即可;
		eleAndGroup[p]=qroot;
		//分组数量-1
		this.count--;
		
	}
}

 

优化后的性能分析
我们优化后的算法 union ,如果要把并查集中所有的数据连通,仍然至少要调用 N-1 union 方法,但是,我们发现 union方法中已经没有了 for 循环,所以 union 算法的时间复杂度由 O(N^2) 变为了 O(N)
但是这个算法仍然有问题,因为我们之前不仅修改了 union 算法,还修改了 find 算法。我们修改前的 fifind 算法的时 间复杂度在任何情况下都为O(1) ,但修改后的 fifind 算法在最坏情况下是 O(N)

union方法中调用了fifind方法,所以在最坏情况下union算法的时间复杂度仍然为O(N^2)

路径压缩

 

UF_Tree 中最坏情况下 union 算法的时间复杂度为 O(N^2) ,其最主要的问题在于最坏情况下,树的深度和数组的大 小一样,如果我们能够通过一些算法让合并时,生成的树的深度尽可能的小,就可以优化fifind 方法。
之前我们在 union 算法中,合并树的时候将任意的一棵树连接到了另外一棵树,这种合并方法是比较暴力的,如果 我们把并查集中每一棵树的大小记录下来,然后在每次合并树的时候,把较小的树连接到较大的树上,就可以减小 树的深度
只要我们保证每次合并,都能把小树合并到大树上,就能够压缩合并后新树的路径,这样就能提高 fifind 方法的效 率。为了完成这个需求,我们需要另外一个数组来记录存储每个根结点对应的树中元素的个数,并且需要一些代码 调整数组中的值。
public class UF_Tree_Weighted {
	//记录结点元素和该元素所的父结点
	private int[] eleAndGroup;
	//记录并查集中数据的分组个数
	private int count;
	//存储每个根结点对应的树中元素的个数
	private int[] sz;
	public UF_Tree_Weighted(int N) {
		this.count=N;
		this.eleAndGroup=new int[N];
//		/把eleAndGroup数组的索引看做是每个结点存储的元素,把eleAndGroup数组每个索引处的值看做是该 结点的父结点,那么初始化情况下,i索引处存储的值就是i
		for(int i=0;i<eleAndGroup.length;i++) {
			eleAndGroup[i]=i;
		}
		//把sz数组中所有的元素初始化为1,默认情况下,每个结点都是一个独立的树,每个树中只有一个元素
		this.sz=new int[N];
		for(int i=0;i<sz.length;i++) {
			sz[i]=1;
		}
	}
	public int count() {
		return this.count;
	}
	
	public boolean connected(int p,int q) {
		return find(p)==find(q);
	}
	public int find(int p) {
		while(true) {
			if(p==eleAndGroup[p]) {
				return p;
			}
			p=eleAndGroup[p];
		}
	}
	public void union(int p,int q) {
		int proot=find(p);
		int qroot=find(q);
		//如果p和q已经在同一个树中,则无需合并;
		if(proot==qroot) {
			return;
		}
		//如果p和q不在同一个分组,比较p所在树的元素个数和q所在树的元素个数,把较小的树合并到较大的树
		if(sz[proot]<sz[qroot]) {
			//重新调整较大树的元素个数
			eleAndGroup[proot]=qroot;
			sz[qroot]+=sz[proot];
		}else {
			eleAndGroup[qroot]=proot;
			sz[proot]=sz[qroot];
		}
		//分组数量-1
		this.count--;
		
	}

}

案例-畅通工程

题目概述:
某省调查城镇交通状况,得到现有城镇道路统计表,表中列出了每条道路直接连通的城镇。省政府 畅通工程 的目 标是使全省任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要互相间接通过道路可达即可)。问 最少还需要建设多少条道路?
在我们的测试数据文件夹中有一个 trffiffiffic_project.txt 文件,它就是诚征道路统计表,下面是对数据的解释:

总共有20个城市,目前已经修改好了7条道路,问还需要修建多少条道路,才能让这20个城市之间全部相通?

解题思路:
1. 创建一个并查集 UF_Tree_Weighted(20);
2. 分别调用 union(0,1),union(6,9),union(3,8),union(5,11),union(2,12),union(6,10),union(4,8) ,表示已经修建好的 道路把对应的城市连接起来;
3. 如果城市全部连接起来,那么并查集中剩余的分组数目为 1 ,所有的城市都在一个树中,所以,只需要获取当前 并查集中剩余的数目,减去1 ,就是还需要修建的道路数目;
代码实现
public class Traffic_Project_Test {

	public static void main(String[] args) throws IOException{
		BufferedReader br=new BufferedReader(new InputStreamReader(Traffic_Project.class.getClassLoader().getResourceAsStream("traffic_projec t.txt")));
        读取城市数目,初始化并查集
		int totalNumber=Integer.parseInt(br.readLine());
		UF_Tree_Weighted uf=new UF_Tree_Weighted(totalNumber);
        //读取已经修建好的道路数目
		int roadNumber=Integer.parseInt(br.readLine());
    //循环读取已经修建好的道路,并调用union方法
		for(int i=0;i<roadNumber;i++) {
			String line=br.readLine();
			String[] s=line.split(" ");
			int p=Integer.parseInt(s[0]);
			int q=Integer.parseInt(s[1]);
			uf.union(p, q);
		}
    //至少需要修建路的数量=获取剩余的分组数量-1;
		int roads=uf.count()-1;
		System.out.println("还需要修建"+roads+"才能畅通工程");
	}

}

并查集的讲解到这也就结束了,那我们在下期再见!!!

活动地址:CSDN21天学习挑战赛

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。它主要包含两个操作:合并和查找。 合并操作将两个集合合并为一个集合,即将两个集合的根节点连接在一起。这可以通过将一个集合的根节点指向另一个集合的根节点来实现。 查找操作用于确定一个元素所属的集合。它通过沿着元素的父节点指针向上查找,直到找到根节点为止。根节点的特点是它的父节点指针指向自己。 并查集的主要应用之一是求解最小生成树问题。最小生成树是一个连通图的极小连通子图,且包含图中的所有节点,但是边的权重之和最小。在求解最小生成树问题时,可以使用并查集来判断两个节点是否属于同一个集合,以避免形成环路。 以下是一个使用C++实现的并查集的代码示例: ```cpp #include <iostream> #include <vector> using namespace std; vector<int> parent; // 初始化并查集 void init(int n) { parent.resize(n); for (int i = 0; i < n; i++) { parent[i] = i; } } // 查找元素所属的集合 int find(int x) { if (parent[x] != x) { parent[x] = find(parent[x]); } return parent[x]; } // 合并两个集合 void merge(int a, int b) { parent[find(a)] = find(b); } int main() { int n = 5; // 图中节点的个数 init(n); // 合并节点 0 和节点 1 所在的集合 merge(0, 1); // 合并节点 2 和节点 3 所在的集合 merge(2, 3); // 判断节点 0 和节点 3 是否属于同一个集合 if (find(0) == find(3)) { cout << "节点 0 和节点 3 属于同一个集合" << endl; } else { cout << "节点 0 和节点 3 不属于同一个集合" << endl; } return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Luck&Strive

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值