并查集
维基百科解释
在计算机科学中,并查集(英文:Disjoint-set data structure,直译为不交集数据结构)是一种数据结构,用于处理一些不交集(Disjoint sets,一系列没有重复元素的集合)的合并及查询问题。并查集支持如下操作:
- 查询:查询某个元素属于哪个集合,通常是返回集合内的一个“代表元素”。这个操作是为了判断两个元素是否在同一个集合之中。
- 合并:将两个集合合并为一个。
- 添加:添加一个新集合,其中有一个新元素。添加操作不如查询和合并操作重要,常常被忽略。
由于支持查询和合并这两种操作,并查集在英文中也被称为联合-查找数据结构(Union-find data structure)或者合并-查找集合(Merge-find set)。
有两种常见的思路:
- Quick Find
查找(Find)的时间复杂度O(1)
合并(Union)的时间复杂度O(n)
- Quick Union
查找(Find)的时间复杂度O(a(n)) ,a(n)<5
合并(Union)的时间复杂度O(a(n)) ,a(n)<5
如何处理数据
- 假设并查集处理的数据都是整数,那么可以用整型数组来存储数据
- 因此,并查集是可以用数组实现的树形结构(二叉堆、优先级队列也是可以用数组实现的树形结构)
接口定义
int find(int v)
//默认是将v1合并到v2上去
void union(int v1,int v2)
boolean isSame(int v1,int v2)
初始化
private int[] parents;
public UnionFind(int capacity){
if(capacity<0){
throw new IllegalArgumentException("capacity must >=1");
}
parents = new int[capacity];
for(int i =0;i<parents.length;i++){
parents[i]=i;
}
}
Quick Find
public class UnionFind {
private int[] parents;
public UnionFind(int capacity){
if(capacity<0){
throw new IllegalArgumentException("capacity must >=1");
}
parents = new int[capacity];
for(int i =0;i<parents.length;i++){
parents[i]=i;
}
}
public int find(int v){
//具体实现上需要判断是否越界
return parents[v];
}
//默认是将v1合并到v2上去
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;
}
}
public boolean isSame(int v1,int v2){
return find(v1)==find(v2);
}
}
Quick Union
Quick Union 的union(v1,v2):是让v1的根节点指向v2的根节点
所以需要对find和union重写
public int find(int v){
//具体实现上需要判断是否越界
while(v!=parents[v]){
v = parents[v];
}
return v;
}
//默认是将v1合并到v2上去
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的优化:需要再设置一个sizes变量
元素少的树嫁接到元素多的数
public class UnionFind {
private int[] parents;
private int[] sizes;
public UnionFind(int capacity){
if(capacity<0){
throw new IllegalArgumentException("capacity must >=1");
}
parents = new int[capacity];
sizes = new int[capacity];
for(int i =0;i<parents.length;i++){
parents[i]=i;
sizes[i]=1;
}
}
public int find(int v){
//具体实现上需要判断是否越界
while(v!=parents[v]){
v = parents[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]){
parents[p1] =p2;
sizes[p2]+=sizes[p1];
} else{
parents[p2] = p1;
sizes[p1]+=sizes[p2];
}
}
public boolean isSame(int v1,int v2){
return find(v1)==find(v2);
}
}
基于rank的优化
矮的树嫁接到高的树
public class UnionFind {
private int[] parents;
private int[] ranks;
public UnionFind(int capacity){
if(capacity<0){
throw new IllegalArgumentException("capacity must >=1");
}
parents = new int[capacity];
ranks = new int[capacity];
for(int i =0;i<parents.length;i++){
parents[i]=i;
ranks[i]=1;
}
}
public int find(int v){
//具体实现上需要判断是否越界
while(v!=parents[v]){
v = parents[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]){
parents[p1] =p2;
} else if(ranks[p2]<ranks[p1]){
parents[p2] = p1;
} else{
parents[p1]=p2;
ranks[p2]++;
}
}
public boolean isSame(int v1,int v2){
return find(v1)==find(v2);
}
}
路径压缩(Path Compression)
虽然有了基于rank的优化,树会相对平衡一点
但是随着union次数的增多,树的高度依然会越来越高,导致find变慢,尤其是底层节点
路径压缩指的是 在find时使路径上的所有节点都指向根节点,从而降低树的高度
public int find(int v){
//具体实现上需要判断是否越界
while(parents[v]!=v){
parents[v] = find(parents[v]) ;
}
return parents[v];
}
路径分裂(Path Spliting)
使路径上的每个节点都指向其祖父节点(父亲的父亲)
public int find(int v){
//具体实现上需要判断是否越界
while(v !=parents[v]){
int parent = parents[v];
parents[v] = parents[parent] ;
v = parent;
}
return v;
}
路径减半(Path Halving)
使路径上每隔一个节点就指向其祖父节点
public int find(int v) {
while (v != parents[v]) {
parents[v] = parents[parents[v]];
v = parents[v];
}
return v;
}
总结
之前使用的都是整型类型,如果时其它的自定义类型也想使用并查集怎么办呢
- 通过一些方法将自定义类型转为整型后使用并查集(比如哈希值)
- 使用链表+映射(Map)