Java 集合扩展系列(一) | 图框架
Java集合扩展系列(二) |索引堆
Java集合扩展系列(三) |并查集
1、什么是并查集
首先并查集是一个集合一种数据结构, 主要有两个操作,分别是
- 合并(Union):把两个不相交的集合合并为一个集合。
- 查询(Find):判断两个元素是否在同一个集合中。
主要用于处理不相交集合的合并及判断连接问题。
2、并查集能做什么
- 判断连接问题 (社交网络、关系网)
- 最近公共祖先
- 渗滤
- 图像处理
- 有限状态自动机的等价性
- Hinley-Milner 的多态类型推断
- Kruskal 的最小生成树算法
- 游戏(围棋、十六进制)
- 在 Fortran 中的编译语句的等价性问题
2、并查集实现结构
并查集的结构有点类似树型结构, 不过与树不同, 指针是指向父节点。并且里面会存在多颗树。 如下图
3、算法原理
3.1、合并(Union)
即将两个节点合并归属为同一个集合
。 假设有两个节点A 和 B, 一般不是将A 和 到 B, 就是将B合并到A。 在实现上其实也就是将 A的根节点的parent指针指向节点B的根节点即可。 不过为了维持这颗树结构的平衡高度, 一般由较矮的节点树 和合并到 较高的节点树。
如下图, 如果要合并 g 和 b, 先找到g和b的根节点a和e, 然后把e的父指针指向a即可完成合并。
3.2、查询(Find)
即判断两个节点是否有关系
, 通过parent指针不断向上寻找该节点的根节点, 然后判断它们两个的根节点是否一样
即可, 如果一样说明属于同一个集合, 反之不是。
虽然一般查找根节点这个算法非常简单, 但是一般为了性能优化, 减少每次向上搜索的深度, 在查找find根节点的过程中会进行路径压缩。
压缩路径过程
就是, 在向上寻找的过程中如果发现父节点不是根节点, 就把父指针指向爷爷节点, 然后自己直接跳到爷爷节点进行下一次判断,依次类推。 当遍历到根节点后, 整颗路径树就会变得非常的矮壮, 大大降低的搜索的深度。
4、代码实现
/**
* 并查集
* @author burukeyou
*/
public class UnionFind<T> {
//parent[i] = x, 表示节点i所指向的父节点为x
private int[] parent;
// rank[i]表示以i为根的集合所表示的树的层数。 主要用来在合并集合时作参考,不代表100%的准确性
private int[] rank;
private Integer nodeKeyIndex = -1;
// 当前并查集存在的节点
private final Set<T> nodeSet = new HashSet<>();
// Map<节点Key, 实际节点>
private final Map<Integer,T> keyNodeMap = new HashMap<>();
// Map<实际节点, 节点Key>
private final Map<T,Integer> nodeKeyMap = new HashMap<>();
public UnionFind(int size) {
parent = new int[size];
rank = new int[size];
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for( int i = 0 ; i < size ; i ++ ){
parent[i] = i;
rank[i] = 1;
}
}
public int getSize() {
return parent.length;
}
// 查找cur节点的所属根节点
private int findRootNode(int cur) {
if(cur < 0 || cur >= parent.length){
throw new IllegalArgumentException("cur Index Out Of bounds");
}
// 当父指针指向自己的就是根节点
while (cur != parent[cur]){
//压缩路径,指向爷爷节点,以此类推
parent[cur] = parent[parent[cur]];
// 更新当前迭代的指针
cur = parent[cur];
}
return cur;
}
// 给新加入到并查集的节点 分配唯一Key
private Integer distributeNodeKey(T node){
if (!nodeSet.contains(node)){
nodeSet.add(node);
nodeKeyIndex++;
keyNodeMap.put(nodeKeyIndex,node);
nodeKeyMap.put(node,nodeKeyIndex);
}
return nodeKeyMap.get(node);
}
//查看元素x和元素y是否所属一个集合
public boolean isConnected(T x , T y){
Integer xKey = nodeKeyMap.get(x);
Integer yKey = nodeKeyMap.get(y);
if (xKey == null || yKey == null){
// 任意一个为空,代表没有加入并查集肯定没关系
return false;
}
// 判断根节点是否一样即可
return findRootNode(xKey) == findRootNode(yKey);
}
public void union(T x, T y) {
Integer xKey = distributeNodeKey(x);
Integer yKey = distributeNodeKey(y);
union(xKey,yKey);
}
// 合并元素x和元素y所属的集合,
private void union(int x, int y) {
int xRoot = findRootNode(x);
int yRoot = findRootNode(y);
if(xRoot == yRoot){
// 已经属于同一个集合不需要再合并
return;
}
// 将rank低的集合合并到rank高的集合上
if(rank[xRoot] < rank[yRoot]){
//低深度向高深度树聚合时, 直接低深度树根节点指向 搞深度树的根结点即可
parent[xRoot] = yRoot;
}else if (rank[xRoot] > rank[yRoot]){
// 同上
parent[yRoot] = xRoot;
}else {
//深度一样时,x合并到y即可。 然后 y树层级加1
parent[xRoot] = yRoot;
rank[yRoot] += 1;
}
}
}
复制代码
5、测试案例
- 有以下社交关系网, 判断某两个人是否有关系
// 创建并查集
UnionFind<String> unionFind = new UnionFind<String>(30);
/*
假设有关系网
小明 ----- 小白 ----- 二狗 ---张三
| /
小新
小强 ---- 李四
*/
unionFind.union("小明","小白");
unionFind.union("小白","小新");
unionFind.union("小白","二狗");
unionFind.union("二狗","张三");
unionFind.union("张三","李四");
// 判断小新 和 张三 是否有间接好友关系 true
boolean connected = unionFind.isConnected("小新", "张三");
// 判断小新 和 小强 是否有间接好友关系 false
boolean connected1 = unionFind.isConnected("小新", "小强");
System.out.println();
复制代码