假设有n个村庄,有些村庄之间有连接的路,有些村庄之间没有:
设计一个数据结构,能够快速执行2个操作
对于上面这两个需求,可以用我们已知的一些数据结构进行完成(数组、链表、平衡二叉树、集合),比如用一个数组维护所有有连接的村庄。不过这样查询find和连接union的时间复杂度都为O(n)级别的。
并查集也叫做不相交集合 Disjoint Set,其包括两个核心操作:
有两种常见的实现思路:
如果并查集处理的数据都是整型,那么可以用整型数组来存储数据。
我们用parents[] 数组存储,其中parents[i]
表示i号元素的父节点的下标。 那么如何看他们是不是属于一个集合呢? 看他们属是否具有相同的根节点。也就是说,一棵树表示一个集合。
因此,并查集是可以用数组实现的树形结构(二叉堆、优先级队列也是可以用数组实现的树形结构)
/*
查找v所属的集合,返回根节点
*/
int find(int v);
/*
合并v1,v2所属的集合
*/
void union(int v1, int v2);
/*
检查v1,v2是否属于同一个集合,即查看根节点是否相同
*/
boolean isSame(int v1,int v2){
return find(v1) == find(v2);
}
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 < capacity ; i ++)
parents[i] = i;
}
...
parent[a]
点改为右边b的父节点parent[b]
我们发现,这种流程下来,数组里面存着的元素都是根节点,因此find函数直接返回parentsp[v]
.
也就是像上图那样,这棵树很扁(高度最多为2)
这样就很能体现其find快的优势,因为其父节点就是该集合的根节点,直接存在parents[i]
当中,如上图中:
步骤在前面已经讲明,那么代码如何实现的呢?
下面是quick find 模式的并查集数据结构:
/**
* 并查集抽象基类
* @author lowfree
*
*/
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 < capacity ; i ++)
parents[i] = i;
}
/**
* 查找所属于的集合,返回根节点
* @param v
* @return
*/
public abstract int find(int v) ;
protected void rangeCheck(int v) {
if(v < 0 || v > parents.length)
throw new IllegalArgumentException("v should in the range of capactiy!");
}
/**
* 检查v1和v2是否属于相同集合,即检查根节点是否相等
* @param v1
* @param v2
* @return
*/
public boolean isSame(int v1 , int v2) {
return find(v1) == find(v2);
}
/**
* 合并两个集合,规定是将左边的置到右边的集合中
* @param v1
* @param v2
*/
public abstract void union(int v1 , int v2);
}
/**
* 并查集 quick find
* @author lowfree
*/
public class UnionFind_QF extends UnionFind{
public UnionFind_QF(int capacity) {
super(capacity);
}
/*
父节点就是根节点
*/
//O(1)
public int find(int v) {
rangeCheck(v);
return parents[v];
}
/*
将v1所在集合的所有元素,一个一个地嫁接到v2的父节点去
*/
//O(n)
public void union(int v1 , int v2) {
int p1 = parents[v1];
int p2 = parents[v2];
//本来就是一个集合了
if(p1 == p2) return;
parents[v1] = p2;
for(int i = 0 ; i < parents.length ; i ++) {
if(parents[i] == p1)
parents[i] = p2;
}
}
合并操作步骤:找到两个根节点, 我们规定将左边a的根节点 “指向”(的父节点置为) 右边b的根节点
相比前面的quick find 模式
find操作需要不断地循环往上找根节点
这种情况下,那么代码如何实现的呢?
/**
* 并查集 quick union
* @author lowfree
*
*/
public class UnionFind_QU extends UnionFind{
public UnionFind_QU(int capacity) {
super(capacity);
// TODO Auto-generated constructor stub
}
/*
通过parent链条不断地向上找
*/
//O(logn)
@Override
public int find(int v) {
rangeCheck(v);
//不断向上地去找根节点
while(v != parents[v])
v = parents[v];
return v;
}
/*
将v1的根节点嫁接到v2的根节点上
*/
//O(logn)
@Override
public void union(int v1, int v2) {
//左边根节点
int p1 = find(v1);
//右边根节点
int p2 = find(v2);
if(p1 == p2) return;
//左边根节点的父节点变为右边根节点
parents[p1] = p2;
}
}
首先两个函数:
find(v)
: 寻找出v所在集合,即寻找v的根节点union(v1,v2)
:将v1,v2所在集合连接起来然后两个模式
find(v)
:寻找根节点即是父节点,所以直接返回parent[v]
即可union(v1,v2)
:将v1所在集合中的所有元素,一个一个地嫁接到v2所指的父节点上去find(v)
: 不断地向上连接去找根节点union(v1,v2)
:将v1的根节点直接嫁接到v2的根节点上去在Union的过程中,可能会出现树不平衡的情况,甚至退化成链表
有2种常见的优化方案
/**
* 并查集 quick union
* 基于Size的优化
* @author lowfree
*
*/
public class UnionFind_QU_S extends UnionFind_QU{
private int[] sizes; //维护集合大小的数组
public UnionFind_QU_S(int capacity) {
super(capacity);
sizes = new int[capacity]; //初始化时,各个集合的size都为1
for(int i = 0 ; i < sizes.length; i ++)
sizes[i] = 1;
}
//find函数不变
@Override
public int find(int v) {
rangeCheck(v);
//通过parent链条不断地向上找
while(v != parents[v])
v = parents[v];
return v;
}
//O(logn)
@Override
public void union(int v1, int v2) {
//左边根节点
int p1 = find(v1);
//右边根节点
int p2 = find(v2);
if(p1 == p2) return;
//将size小的集合嫁接到大的集合根节点上
if(sizes[p1] <sizes[p2]) {
parents[p1] = p2;
sizes[p2] += sizes[p1];
}else {
parents[p2] = p1;
sizes[p1] += sizes[p2];
}
}
}
ranks数组,每次将矮的树嫁接到高的上面去
union(1,4) :
/**
* 并查集 quick union
* 基于Rank的优化
* @author lowfree
*
*/
public class UnionFind_QU_R extends UnionFind_QU{
private int[] ranks;
public UnionFind_QU_R(int capacity) {
super(capacity);
ranks = new int[capacity];
for(int i = 0 ; i < ranks.length ; i++)
ranks[i] =1;
}
//find函数不变
@Override
public int find(int v) {
rangeCheck(v);
//通过parent链条不断地向上找
while(v != parents[v])
v = parents[v];
return v;
}
//将v1所在集合的所有元素都嫁接到v2上
//O(logn)
@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;
//这里不需要对ranks改动
}else if(ranks[p2] < ranks[1]) {
parents[p2] = p1;
}else { //两树一样高
parents[p1] = p2;
ranks[p2] ++; //此时高度会加1
}
}
}
/**
* 并查集 quick union
* 基于Rank的优化
* 路径压缩
* @author lowfree
*
*/
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] != parents[v]) {
parents[v] = find(parents[v]);
}
return parents[v];
}
//union不变
}
路径分裂:使路径.上的每个节点都指向其祖父节点(parent的parent)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u2hrKsLL-1590674681455)(union set.assets/image-20200528214325616.png)]
@Override
public int find(int v) {
rangeCheck(v);
while(v != parents[v]) {
int p = parents[v]; //先要保存一下其父亲,否则等下节点丢失
parents[v]= parents[parents[v]]; //v指向祖父节点
v = p;
}
return parents[v];
}
路径减半:使路径上每隔一个节点就指向其祖父节点(parent的parent)
/**
* 并查集 quick union
* 基于Rank的优化
* 路径分裂
* @author lowfree
*
*/
public class UnionFind_QU_R_PH extends UnionFind_QU_R{
public UnionFind_QU_R_PH(int capacity) {
super(capacity);
// TODO Auto-generated constructor stub
}
@Override
public int find(int v) {
rangeCheck(v);
while(v != parents[v]) {
parents[v]= parents[parents[v]]; //v指向祖父节点
v = parents[v]; //v 变为它的祖父节点
}
return parents[v];
}
//union不变
}
使用路径压缩,分裂或减半 + 基于rank或者size的优化可以确保每个操作的均摊时间复杂度为O(α(n)),α(n)<5
建议的搭配
nionFind_QU_R_PH extends UnionFind_QU_R{
public UnionFind_QU_R_PH(int capacity) {
super(capacity);
}
@Override
public int find(int v) {
rangeCheck(v);
while(v != parents[v]) {
parents[v]= parents[parents[v]]; //v指向祖父节点
v = parents[v]; //v 变为它的祖父节点
}
return parents[v];
}
//union不变
}