本文首发于我的Github博客
本文是数据结构预算法复习系列的第一篇博文,会介绍写作该系列博文的原因
本文复习了并查集的概念,基础的API,良好的实现(路径压缩与权重),简单的应用和变式
写作本篇博文的原因
- 生活所迫
- 懂的都懂,IT行业从业者(或者是考研保研),跑不了的
- 数据结构与算法是大学生活里花费的时间很多的部分,复习一下,也算是对自己的大学生涯有个交代
- 三次元的生活真的无聊,找点代码写,嘿嘿(
难道不能写工程项目吗?质问!)
并查集的概念与基础API
并查集的概念
假设拥有N个元素1…N,我们需要对其做两种基础的操作:
- 宣告某两个元素a, b处在同一个等价类里
- 判断某两个元素a, b是否在一个等价类里
用于处理这种问题,提供这两种操作接口的数据结构,可以被称为并查集
并查集基础的API
interface UnionFind {
void union(int a, int b);
boolean isConnected(int a, int b);
}
良好的实现
良好的实现往往将一个等价类以树的形式进行组织,使用一个数组int[] parent
,parent[i]
记录了元素i
的父亲节点
如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tY25IREj-1592185859107)(union-find-tree.png)]
isConnected(a, b)
接口即判断a
和b
是否在同一棵树内(是否有同样的根)union(a, b)
接口即需要将a
所属的树归并到b
所属的树下,或者将b
所属的树归并到a
所属的树下- 提高执行速度,就是想办法降低树的高度
路径压缩
路径压缩基本上能够完全解决并查集的接口执行速度问题
其基本思想是:
在沿着某条路径查询到
a
元素的根之后,将这条路径上所有的元素父亲,都直接设为根节点
这样,在以后查询的时候,查找到这条路径上的元素,只需要一次向上查询,就可以找到根节点了
路径压缩实现
public class PathCompressUnionFindImpl implements UnionFind {
int[] parent;
public PathCompressUnionFindImpl(int n) {
parent = new int[n];
for (int i = 0; i < parent.length; ++i) {
parent[i] = i;
}
}
private int find(int a) {
if (parent[a] == a) {
return a;
}
return parent[a] = find(parent[a]);
}
@Override
public void union(int a, int b) {
int rootA = find(a), rootB = find(b);
parent[rootA] = rootB;
}
@Override
public boolean isConnected(int a, int b) {
return find(a) == find(b);
}
}
加权并查集
虽然路径压缩基本能够解决并查集的执行速度问题,但是还有一个解决方案值得我们思考
加权并查集,其实是基于这样的观察:
在代表一个等价类的树的高度快速增长时,往往是由于将高的树合并成为了矮的树的子树
如果每次合并,都将矮的树合并成为高的树的子树,那么树的高度增长就会减缓
从数学的角度来看,减缓的原因是
每次当树的高度增加1,就意味着树的节点总数至少翻倍了
那么一共有N个节点,树的高度最多也就是lg N
加权并查集实现
public class WeightedUnionFindImpl implements UnionFind {
int[] parent, size;
private int find(int a) {
while (parent[a] != a) {
a = parent[a];
}
return a;
}
public WeightedUnionFindImpl(int n) {
parent = new int[n];
size = new int[n];
for (int i = 0; i < n; ++i) {
parent[i] = i;
size[i] = 1;
}
}
@Override
public void union(int a, int b) {
int rootA = find(a), rootB = find(b);
if (size[rootA] > size[rootB]) {
parent[rootB] = rootA;
size[rootA] += size[rootB];
} else {
parent[rootA] = rootB;
size[rootB] += size[rootA];
}
}
@Override
public boolean isConnected(int a, int b) {
return find(a) == find(b);
}
}
并查集的简单应用
贴两道OJ题目,都是简单练手的
并查集的简单变式
等价类的个数
也就是树的根节点的个数,一个节点a
是根节点当且仅当parent[a] == a
int nClass() {
int nClass = 0;
for (int i = 0; i < parent.length; ++i) {
if (parent[i] == i) {
nClass++;
}
}
return nClass;
}
可以维护的等价类性质
比如:
- 给定元素
a
,求a
所在等价类的元素个数 - 给定元素
a
,求a
所在等价类的最大值 - 给定元素
a
,求a
所在等价类的最小值
这类性质只需要新开一个数组,并在union
时进行维护就可以了:
void union(int a, int b) {
//....
parent[rootA] = rootB;
size[rootB] += size[rootA];
maximum[rootB] = Math.max(maximum[rootA], maximum[rootB]);
minimum[rootB] = Math.max(minimum[rootA], minimum[rootB]);
}