并查集(Union Find)
父类设计
package cn.tiger.union;
/**
* @author FatTiger
* @date 2020-12-08 17:16
**/
public abstract class UnionFind {
protected int parents[];
public UnionFind(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("capacity must be >= 1");
}
parents = new int[capacity];
for (int i = 0; i <parents.length ; i++) {
parents[i] = i;
}
}
/**
* 查找v所属的集合(根节点)
* @param v
* @return int
* @author FatTiger
* @date 2020/12/8 17:22
*/
public abstract int find(int v);
/**
* 检查两个参数是否属于同一个集合
* @param v1
* @param v2
* @return boolean
* @author FatTiger
* @date 2020/12/8 17:25
*/
public boolean isSame(int v1, int v2) {
return find(v1) == find(v2);
}
/**
* 合并v1,v2两个集合
* @param v1
* @param v2
* @return void
* @author FatTiger
* @date 2020/12/8 17:26
*/
public abstract void union(int v1, int v2);
protected void rangeCheck(int v) {
if (v < 0 || v >= parents.length) {
throw new IllegalArgumentException("v is out of bounds");
}
}
}
需求分析
-
假设有n个村庄,有些村庄之间有连接的路,有些村庄之间没有连接的路
-
设计一个数据结构,能够快速执行2个操作
- 查询2个村庄之间是否有连接的路
- 连接两个村庄
-
数组、链表、平衡二叉树、集合?
- 查询、连接的时间复杂度都是:O(n)
-
并查集能够办到查询、连接的均摊时间复杂度都是O(α(n)),α(n) < 5
-
并查集非常适合解决这类“连接”问题
并查集有两个核心操作
- 查找(find):查找元素所在的集合(这里的集合并不是特指Set这种数据结构,是指广义的数据集合)
- 合并(union):将两个元素所在的集合合并为一个集合
有两种常见的实现思路
- Quick Find
- 查找的时间复杂度:O(1)
- 合并的时间复杂度:O(n)
- Qucik Union
- 查找的时间复杂度:O(logn),可以优化至O(α(n)),α(n) < 5
- 合并的时间复杂度:O(logn),可以优化至O(α(n)),α(n) < 5
如何存储数据
- 假设并查集处理的数据都是整形,那么可以用整形数组来存储数据
接口定义
初始化
- 初始化时,每个元素各自属于一个单元素集合
QucikFind
Union
将左边集合的所有父节点直接赋值为右边的父节点
总的来说就是让v1所在集合的所有元素都指向v2的根节点
Find
直接返回v所在的父节点即可
代码实现
package cn.tiger.union;
/**
* @author FatTiger
* @date 2020-12-08 17:16
**/
public class UnionFind {
protected int parents[];
public UnionFind(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("capacity must be >= 1");
}
parents = new int[capacity];
for (int i = 0; i <parents.length ; i++) {
parents[i] = i;
}
}
/**
* 查找v所属的集合(根节点)
* @param v
* @return int
* @author FatTiger
* @date 2020/12/8 17:22
*/
public int find(int v) {
rangeCheck(v);
return parents[v];
}
/**
* 检查两个参数是否属于同一个集合
* @param v1
* @param v2
* @return boolean
* @author FatTiger
* @date 2020/12/8 17:25
*/
public boolean isSame(int v1, int v2) {
return find(v1) == find(v2);
}
/**
* 合并v1,v2两个集合
* @param v1
* @param v2
* @return void
* @author FatTiger
* @date 2020/12/8 17:26
*/
public void union(int v1, int v2) {
int p1 = find(v1);
int p2 = find(v2);
if (p1 == p2) return;
for (int i = 0; i < parents.length; i++) {
if (parents[i] == p1) {
parents[i] = p2;
}
}
}
private void rangeCheck(int v) {
if (v < 0 || v >= parents.length) {
throw new IllegalArgumentException("v is out of bounds");
}
}
}
QuickUnion
Union
将右边的根节点作为左边的根节点,不影响原来左边的父子关系,只是多了个祖先或者兄弟
总的来说就是:让v1的根节点指向v2的根节点
Find
不断向上找父节点,找到根节点为止,返回根节点
代码实现
package cn.tiger.union;
/**
* @author FatTiger
* @date 2020-12-08 17:41
**/
public class UnionFind_QU extends UnionFind {
public UnionFind_QU(int capacity) {
super(capacity);
}
@Override
public int find(int v) {
rangeCheck(v);
while (v != parents[v]) {
v = parents[v];
}
return v;
}
@Override
public void union(int v1, int v2) {
int p1 = find(v1);
int p2 = find(v2);
if (p1 == p2) return;
parents[p1] = p2;
}
}
优化
- 在Union的过程中,可能会出现树不平衡的情况,甚至退化成链表
-
有两种常见的优化方案
- 基于size的优化:元素少的树 嫁接到 元素多的树下
- 基于rank的优化:矮的树 嫁接到 高的树下
基于size的优化
public class UnionFind_QU_S extends UnionFind_QU {
private int sizes[];
public UnionFind_QU_S(int capacity) {
super(capacity);
sizes = new int[capacity];
Arrays.fill(sizes, 1);
}
/**
* 合并v1,v2两个集合
* @param v1
* @param v2
* @return void
* @author FatTiger
* @date 2020/12/8 17:26
*/
@Override
public void union(int v1, int v2) {
int p1 = find(v1);
int p2 = find(v2);
if (p1 == p2) return;
if (sizes[p1] < sizes[p2]) {
parents[p1] = p2;
sizes[p2] += sizes[p1];
} else {
parents[p2] = p1;
sizes[p1] += sizes[p2];
}
}
}
基于rank的优化
public class UnionFind_QU_R extends UnionFind_QU {
private int ranks[];
public UnionFind_QU_R(int capacity) {
super(capacity);
ranks = new int[capacity];
Arrays.fill(ranks, 1);
}
/**
* 合并v1,v2两个集合
* @param v1
* @param v2
* @return void
* @author FatTiger
* @date 2020/12/8 17:26
*/
@Override
public void union(int v1, int v2) {
int p1 = find(v1);
int p2 = find(v2);
if (p1 == p2) return;
if (ranks[p1] < ranks[p2]) {
parents[p1] = p2;
} else if (ranks[p1] > ranks[p2]) {
parents[p2] = p1;
} else {
parents[p1] = p2;
ranks[p2] += 1;
}
}
}
路径压缩
public class UnionFind_QU_R_PC extends UnionFind_QU_R {
public UnionFind_QU_R_PC(int capacity) {
super(capacity);
}
@Override
public int find(int v) {
rangeCheck(v);
if (parents[v] != v) {
parents[v] = find(parents[v]);
}
return parents[v];
}
}
还有两种更优的做法,不但能降低树高,实现成本也比路径压缩低
- 路径分裂
- 路径减半
路径分裂
使路径上的每个节点都指向其祖父节点(parent的parent)
public class UnionFind_QU_R_PS extends UnionFind_QU_R {
public UnionFind_QU_R_PS(int capacity) {
super(capacity);
}
@Override
public int find(int v) {
rangeCheck(v);
if (parents[v] != v) {
int p = parents[v];
parents[v] = parents[parents[v]];
v = p;
}
return v;
}
}
路径减半
使路径上每隔一个节点就指向其祖父节点(parent的parent)
public class UnionFind_QU_R_PH extends UnionFind_QU_R {
public UnionFind_QU_R_PH(int capacity) {
super(capacity);
}
@Override
public int find(int v) {
rangeCheck(v);
if (parents[v] != v) {
parents[v] = parents[parents[v]];
v = parents[v];
}
return v;
}
}
总结
如果使用路径压缩、分裂或减半+基于rank或者size的优化
可以确保每个操作的均摊时间复杂度为O(α(n)),α(n) < 5
推荐搭配:
- Quick Union
- 基于rank的优化
- 路径减半或路径分裂