并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题(即所谓的并、查)。比如说,我们可以用并查集来判断一个森林中有几棵树、某个节点是否属于某棵树等。
例如:在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合, 然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中。其特点是看似并不复杂,但数据量极大, 若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高, 根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
本文先通过哈希表实现并查集结构和提供的操作,然后再通过数组实现,数组实现的效率比较高,一般在解题中使用。
通过哈希表实现并查集
本文并查集结构提供的二个操作是:
1)查询二个样本是否在同一个集合
boolean isSameSet(V a,V b);
2)把a所在集合的全体 和 b所在集合的全体 合并成一个集合
void union(V a,V b);
首先将给定样本V类型包成Node
public static class Node<V> {//给样本v类型包了一层成Node
V value;
public Node(V v) {
value = v;
}
}
定义并查集类
//并查集结构
public static class UnionFind<V> {
//一张表:从a -> Node(a) ,b -> Node(b) 从对象 到 对象包装成Node的 map
public HashMap<V, Node<V>> nodes;
public HashMap<Node<V>, Node<V>> parents;//不想在Node上增加指针,用这个表可以做到指针的作用;从孩子节点可以找到父节点
public HashMap<Node<V>, Integer> sizeMap;//只有一个节点是代表节点,才会在sizeMap中记录这个节点所在集合的大小
//代表节点是一个集合最上面的节点
//并查集的初始化
public UnionFind(List<V> values) { //将样本中每个对象都建成一个小集合
nodes = new HashMap<>();
parents = new HashMap<>();
sizeMap = new HashMap<>();
for (V cur : values) {
Node<V> node = new Node<>(cur);
nodes.put(cur, node);
parents.put(node, node); //初始时集合只有一个节点,那么这个节点的父亲节点就是字节(即那个指针循环指向自己)
sizeMap.put(node, 1); //初始时集合只有一个节点,那么这个节点就是代表节点,会在sizeMap中记录这个节点所在集合的大小
}
}
//findFather: 给你一个节点,请你往上到不能再往上,把代表节点返回
public Node<V> findFather(Node<V> cur) {
/* 重要优化:在从一个节点找到往上找到代表节点的过程中,把过程中的每个节点的挂到代表节点上面。 */
Stack<Node<V>> path = new Stack<>();
while (cur != parents.get(cur)) { //cur == parents.get(cur)说明找到了代表节点,否则表示没找到
path.push(cur); //沿途遇见的节点入栈
cur = parents.get(cur);
}//循环退出时候,cur就是代表结点
while (!path.isEmpty()) {
parents.put(path.pop(), cur); //优化:沿途每个节点全部挂到代表节点上面
}
return cur;//返回代表节点
}
//isSameSet:查询a样本和b样本在不在一个集合
public boolean isSameSet(V a, V b) {
return findFather(nodes.get(a)) == findFather(nodes.get(b));
//通过nodes这个map找到a和b的node,然后通过findFather找到他们的代表节点,然后判断代表结点是不是相等
}
//union:将a所在集合的全体 和 b所在集合的全体 合成一个集合
public void union(V a, V b) {
Node<V> aHead = findFather(nodes.get(a));//获取a样本所在集合的代表结点
Node<V> bHead = findFather(nodes.get(b)); //获取b样本所在集合的代表节点
if (aHead != bHead) {//如果二者的代表节点不是同一个表示二者不在同一个集合,需要进行union操作。
int aSetSize = sizeMap.get(aHead);//获取代表节点所在集合的全部个数
int bSetSize = sizeMap.get(bHead);
Node<V> big = aSetSize >= bSetSize ? aHead : bHead;
Node<V> small = big == aHead ? bHead : aHead;//大小集合代表节点重定向
//小集合代表节点往上直接指向大集合代表节点的头
parents.put(small, big);//小集合代表结点的父亲设置为大集合代表节点
sizeMap.put(big, aSetSize + bSetSize); //大集合的大小将变成二个集合的和
sizeMap.remove(small); //small挂在了大集合代表节点下面,已经不是代表节点,不需要保存记录了,可以删除。
//remove的愿意你是sizeMap中只存放代表结点的相关大小记录值
} //如果二者的代表节点相等就表示都在一个集合了,不用做任何事
}
public int sets() {
return sizeMap.size();
//可以通过sizeMap中代表节点的数量判断并查集中集合的数量
}
}
通过数组实现并查集解leetcode547省份数量 :
public static int findCircleNum(int[][] M) {
int N = M.length;
// {0} {1} {2} {N-1}每个都成一个集合初始化好
UnionFind unionFind = new UnionFind(N); //
for (int i = 0; i < N; i++) { //i=0是行号
for (int j = i + 1; j < N; j++) { //j=i+1是列号
//只遍历右上半区:因为矩阵是对称的,且对角线上肯定是1
if (M[i][j] == 1) { // i和j互相认识
unionFind.union(i, j);//i背后的集合 和 j背后的集合 合并成一个集合
}
}
}//这个双重循环跑完,那么该合并的都合并了。
return unionFind.sets();//返回并查集里面有多少集合
}
数组实现并查集结构:
public static class UnionFind {
private int[] parent; // parent[i] = k : i的父亲是k
private int[] size; //size[i]=k:如果i是代表节点,size[i]才有意义,否则无意义; i所在的集合大小是多少
private int[] help; // 辅助结构:用作栈存放元素
private int sets; // 记录并查集一共有多少个集合
public UnionFind(int N) {
parent = new int[N];
size = new int[N];
help = new int[N];
sets = N;
for (int i = 0; i < N; i++) {//0 ~N-1每个都变成自己的集合
parent[i] = i;//当前数的父亲是自己
size[i] = 1;//当前代表节点所在集合的大小
}
}
// 从i开始一直往上,往上到不能再往上,返回代表节点(同时过程中要做路径压缩)
private int find(int i) {//从i开始一直往上,往上到
int hi = 0;
while (i != parent[i]) {//i只要不等于自己的父亲,i就一直往上
help[hi++] = i;//把i寻找代表节点的过程中遇到的所有节点都记录到栈中
i = parent[i];
}//循环退出后表示找到了代表节点i
for (hi--; hi >= 0; hi--) {
parent[help[hi]] = i;//将栈中记录的所有数组的父节点在parent中设置为i
}
return i;
}
public void union(int i, int j) {
int f1 = find(i);//获得i所在集合的代表节点
int f2 = find(j);//获得j所在集合的代表结点
if (f1 != f2) {//i和j不在同一个结合,需要合并这二个集合
//小集合挂到大集合的下面,能让树的增长变慢
if (size[f1] >= size[f2]) {
size[f1] += size[f2];
parent[f2] = f1;//小集合的父亲设置为大集合的代表节点
} else {
size[f2] += size[f1];
parent[f1] = f2;
}
sets--;//二个集合合并成一个集合后,集合数量减少1
}
//如果f1和f2相等说明i和j在同一个集合中
}
public int sets() {
return sets;
}
}