需求分析
假设有N个村庄,有些村庄之间有连接的路,有些村庄之间没有路
- 查询两个村庄之间是否有路
- 连接两个村庄
并查集非常适合解决这类“连接”相关的问题
实现
并查集又叫不相交集合(Disjoint Set),主要有两个核心操作:
- 查找(Find):查找元素所在的集合,查找这条路在哪个村庄
- 合并(Union):将两个元素所在的集合合并成一个集合,将两条路所在的村庄合并成一个村庄
如何存储数据
如果并查集处理的数据类型是整型,那么可以用整型数组来存储数据,如图:
因此,并查集是可以用数组来实现的树形结构。
元素初始化
初始化时每个元素各自属于一个单独的集合,如图:
代码:
private int[] array;
public UnionFind_QF(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("参数不合法");
}
array = new int[capacity];
for (int i = 0; i < capacity; i++) {
array[i] = i;
}
}
Quick Find
Union的过程
比如union(v1,v2),指的是让v1所在集合的所有元素都指向v2的根节点,看图:
代码:
/**
* union(v1,v2)
*/
public void union(int v1, int v2) {
int parentV1 = find(v1);
int parentV2 = find(v2);
if (parentV1 == parentV2) {
return;
}
for (int i = 0; i < array.length; i++) {
if (array[i] == parentV1) {
array[i] = parentV2;
}
}
}
public int find(int v) {
return array[v];
}
时间复杂度:O(n)
Find的过程
代码:
public int find(int v) {
return array[v];
}
时间复杂度:O(1)
Quick Union
Union的过程
比如union(v1,v2),指的是让v1的根节点指向v2的根节点。
代码:
private int[] array;
public QuickUnion_QU(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("参数不合法");
}
array = new int[capacity];
for (int i = 0; i < capacity; i++) {
array[i] = i;
}
}
public void union(int v1, int v2) {
//查找v1的根节点
int p1 = find(v1);
//查找v2的根节点
int p2 = find(v2);
if (p1 == p2) {
return;
}
//把v1的根节点设置为v2的根节点
array[p1] = p2;
}
时间复杂度:O(logn)
Find的过程
代码:
private int find(int v) {
while (v != array[v]) {
v = array[v];
}
return v;
}
时间复杂度:O(logn)
Quick Union的优化
在Union的过程中,可能出现树不平衡的情况,还有可能退化成链表,如图:
如果退化成链表的话,find()时间复杂度就成了O(n),效率很低。
两种优化方案:
- 基于size的优化,元素少的嫁接到元素多的树上去
- 基于rank的优化,高度低的嫁接到高度高的树上去
Quick Union-基于size的优化
将元素少的分支嫁接到元素多的分支上去,如图:
代码:
/**
* QuickUnion基于size的优化
*
* @author jun.zhang6
* @date 2020/9/30
*/
public class QuickUnion_size {
/**
* 存数据的数组
*/
private int[] array;
/**
* 存size的数组
*/
private int[] sizes;
public QuickUnion_size(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("capacity不合法");
}
array = new int[capacity];
sizes = new int[capacity];
for (int i = 0; i < capacity; i++) {
array[i] = i;
sizes[i] = 1;
}
}
/**
* 查找根节点
*/
public int find(int v) {
while (v != array[v]) {
v = array[v];
}
return v;
}
/**
* 将v1的根节点嫁接到v2的根节点
*/
public void union(int v1, int v2) {
int p1 = find(v1);
int p2 = find(v2);
if (p1 == p2) {
return;
}
if (sizes[p1] < sizes[p2]) {
array[p1] = p2;
sizes[p2] += sizes[p1];
} else {
array[p2] = p1;
sizes[p1] += sizes[p2];
}
}
}
但是基于size的优化还是会存在树不平衡的问题,如图:
Quick Union-基于rank的优化
将高度低的树嫁接到高度高的树上去,如图:
代码:
/**
* QuickUnion基于ranking的优化
*
* @author jun.zhang6
* @date 2020/9/30
*/
public class QuickUnion_ranking {
/**
* 存数据的数组
*/
private int[] array;
/**
* 存高度的数组
*/
private int[] ranks;
public QuickUnion_ranking(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("capacity不合法");
}
array = new int[capacity];
ranks = new int[capacity];
for (int i = 0; i < capacity; i++) {
array[i] = i;
ranks[i] = 1;
}
}
public int find(int v) {
while (v != array[v]) {
v = array[v];
}
return v;
}
/**
* 将v1的根节点嫁接到v2的根节点
*/
public void union(int v1, int v2) {
int p1 = find(v1);
int p2 = find(v2);
if (p1 == p2) {
return;
}
if (ranks[p1] < ranks[p2]) {
array[p1] = p2;
} else if (ranks[p1] > ranks[p2]) {
array[p2] = p1;
} else {
array[p1] = p2;
ranks[p2]++;
}
}
}
Quick Union-路径压缩
上面基于rank来合并集合,可以降低树的不平衡,但是随着数据合并的越来越多树的高度肯定也会越来越高,最后导致find()操作非常耗时,因为要一层层往上找,直到找到根节点为止。
路径压缩:在find()时,路径上所有的节点都指向根节点,从而降低树的高度。如图:
代码:
/**
* 路径压缩,降低树的高度,递归
*/
@Override
public int find(int v) {
if (v != array[v]) {
array[v] = find(array[v]);
}
return array[v];
}
路径压缩因为要把find()路径上所有的节点都指向根节点,所以实现成本高,还可以继续优化,可以使用路径分裂和路径减半。
Quick Union-路径分裂
使路径上的每个节点都指向其祖父节点,如图:
代码:
/**
* 路径分裂
*/
@Override
public int find(int v) {
while (v != array[v]) {
int parent = array[v];//parent = 2
array[v] = array[parent];//array[1] = 3
v = parent;//v = 2
}
return v;
}
Quick Union-路径减半
使路径上每隔一个节点就指向其祖父节点,如图:
代码:
/**
* 路径减半
*/
@Override
public int find(int v) {//v=1
while (v != array[v]) {
array[v] = array[array[v]];//array[1] = array[2]=3
v = array[v];//v=3
}
return v;
}
我们平时在使用时一般基于QuickUnion + rank优化 + 路径分裂/路径减半组合在一起使用。
自定义类型
之前我们时基于整数类型来实现的,如果自定义类型也想使用并查集,如何实现呢? 直接上代码:
package com.ymm.zhangj.dataStructure;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* 自定义的并查集
*
* @author jun.zhang6
* @date 2020/9/30
*/
public class CustomUnionFind<V> {
/**
* 每个值对应一个节点
*/
private Map<V, Node<V>> nodes = new HashMap<>();
/**
* 初始化
*/
public void init(V v) {
if (nodes.containsKey(v)) {
return;
}
nodes.put(v, new Node<>(v));
}
/**
* 找出v对应节点的根节点,路径减半
*/
public Node<V> findNode(V v) {
//获取v对应的节点
Node<V> node = nodes.get(v);
if (node == null) {
return null;
}
while (!Objects.equals(node.value, node.parent.value)) {
node.parent = node.parent.parent;
node = node.parent;
}
return node;
}
/**
* 查找v对应根节点的值
*/
public V find(V v) {
Node<V> node = findNode(v);
return node == null ? null : node.value;
}
public void union(V v1, V v2) {
//查找v1对应的根节点
Node<V> p1 = findNode(v1);
Node<V> p2 = findNode(v2);
if (p1 == null || p2 == null) {
return;
}
if (Objects.equals(p1.value, p2.value)) {
return;
}
if (p1.rank < p2.rank) {
p1.parent = p2;
} else if (p1.rank > p2.rank) {
p2.parent = p1;
} else {
p1.parent = p2;
p2.rank += 1;
}
}
/**
* 判断两个节点是否属于同一集合
*/
public boolean isSame(V v1, V v2) {
return Objects.equals(find(v1), find(v2));
}
/**
* 内部节点对象
*/
private static class Node<V> {
V value;
Node<V> parent = this;
int rank = 1;
public Node(V value) {
this.value = value;
}
}
}