需求分析
假设场景: 有n个村庄,有些村庄之间有连接的路,有些村庄并没有连接的路。
设计一个数据结构,能快速实现以下两个操作:
(1)查询任意两个村庄之间是否有连接的路。
(2)连接任意两个村庄。
由此给出并查集支持的两个操作:
(1)合并(Union):把两个不相交的集合合并为一个集合。
(2)查询(Find):查询两个元素是否在同一个集合中。
平衡二叉树、集合、数组、链表也能实现上述需求,但这些工具能实现的功能更多,与专注于实现上述特定需求的并查集来说效率并不理想。
并查集的实现有QuickFind、QuickUnion两种方式,本文采取后者实现。QuickUnion - 底层是数组,逻辑层面是树。
首先初始化每个节点的父节点为自己 --初始化每个“村庄”为一个单独集合,索引对应“村庄”编号,数组值对应“村庄”父节点。
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上。
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);
}
优化
有时树会退化成链表,导致并查集效率降低。
优化方法很多,这里使用路径减半(Path Having)优化。实现:使路径上每隔一个节点就指向其祖父节点。
public int findPH(int v) {//优化后下次查找效率就会高很多
rangCheck(v);
while(v != parents[v]) {
parents[v] = parents[parents[v]];
v = parents[v];
}
return v;
}
优化后,形成了如下树结构:
[注] 优化后并不影响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");
}
}