需求分析

假设场景: 有n个村庄,有些村庄之间有连接的路,有些村庄并没有连接的路。

union在java怎么用 java中union_数据结构

设计一个数据结构,能快速实现以下两个操作:
(1)查询任意两个村庄之间是否有连接的路。
(2)连接任意两个村庄。
由此给出并查集支持的两个操作:
(1)合并(Union):把两个不相交的集合合并为一个集合。
(2)查询(Find):查询两个元素是否在同一个集合中。
平衡二叉树、集合、数组、链表也能实现上述需求,但这些工具能实现的功能更多,与专注于实现上述特定需求的并查集来说效率并不理想。
并查集的实现有QuickFind、QuickUnion两种方式,本文采取后者实现。QuickUnion - 底层是数组,逻辑层面是树。

首先初始化每个节点的父节点为自己 --初始化每个“村庄”为一个单独集合,索引对应“村庄”编号,数组值对应“村庄”父节点。

union在java怎么用 java中union_算法_02

for (int i = 0; i < parents.length; i++) {
		parents[i] = i;
	}

定义查找方法 – 给定一个点,查看这个点所属集合。实现:从给定点不断往上探,直到到达根节点,并返回根节点。一个点是否在哪个集合的标准就是看其根节点是谁。

public int find(int v) {
		rangCheck(v); //对索引进行合法性检查
		while(v != parents[v]) {
			v = parents[v];
		}
		return v;
	}

定义合并方法 – 给定两个点并合并。实现:将一边的根节点嫁接到另一边的根节点(这里默认左边根节点嫁接到右边根节点下)。

以1为根节点的集合与以2为根节点的集合合并为例,直接将根节点1嫁接到根节点2上。

union在java怎么用 java中union_父节点_03

public void union(int v1, int v2) {
		//拿到V1,v2的根节点
		int rt1 = find(v1);
		int rt2 = find(v2);
		if(rt1 == rt2) return; //v1,v2在同一集合
		
		parents[rt1] = rt2; 
	}

定义判断方法 – 判断给定的两个点是否在同一集合。实现:两个点的根节点相同即在同一集合,否则不在同一集合。

public boolean isTogether(int v1, int v2) {
		return find(v1) == find(v2); 
	}

优化

有时树会退化成链表,导致并查集效率降低。

union在java怎么用 java中union_算法_04


优化方法很多,这里使用路径减半(Path Having)优化。实现:使路径上每隔一个节点就指向其祖父节点。

public int findPH(int v) {//优化后下次查找效率就会高很多
		rangCheck(v);
		while(v != parents[v]) {
			parents[v] = parents[parents[v]];
			v = parents[v]; 
		}
		return v;
	}

优化后,形成了如下树结构:

union在java怎么用 java中union_java_05


[注] 优化后并不影响union和isTogether方法。

最后,提供完整代码供参考。

/**
 * 实现:底层数组、逻辑上树
 * @author Asus
 */
public class QuickUnion {
	private int[] parents; //索引对应各个节点,索引对应的值为结点的父节点
	public QuickUnion(int n) {
		this.parents = new int[n];
		//初始化每个节点父节点为自己
		for (int i = 0; i < parents.length; i++) {
			parents[i] = i;
		}
	}
	/**
	 * @return 返回根节点
	 */
	public int find(int v) {
		rangCheck(v); //对索引进行合法性检查
		while(v != parents[v]) {
			v = parents[v];
		}
		return v;
	}
	/**
	 * 优化:路径减半 - 使路径上每隔一个节点就指向其祖父节点
	 */
	public int findPH(int v) {
		rangCheck(v);
		while(v != parents[v]) {
			v = parents[parents[v]]; //让v的祖父节点变成其父节点
		}
		return v;
	}
	/**
	 * 合并两个集合
	 */
	public void union(int v1, int v2) {
		//拿到V1,v2的根节点
		int rt1 = find(v1);
		int rt2 = find(v2);
		parents[rt1] = rt2; //默认左边根节点嫁接到右边根节点下
	}
	/**
	 * 判断v1,v2是否在同一集合
	 */
	public boolean isTogether(int v1, int v2) {
		return find(v1) == find(v2); //如果v1,v2的根节点是同一个则在同一个集合
	}
	
	private void rangCheck(int v) {
		if(v < 0 || v >= parents.length)
			throw new IllegalArgumentException("index out of bounds");
	}
	
}