1,需求分析
- 假设有n个村庄,有些村庄之间有连接的路,有些村庄之间并没有连接的路
- 设计一个数据结构,能够快速的执行2个操作
1,查询2个村庄之间是否有连接的路
2,连接2个村庄
并查集 非常适合解决这类的连接问题
2,并查集
- 并查集也叫作不相交的集合
- 并查集有2个核心操作
查找:查找元素所在的集合
合并:将两个元素所在的集合合并为一个集合
有2种常见的实现思路
1,快速查找
查找(find)的时间复杂度为:O(1)
合并(union)的时间复杂度:O(n)
2,快速合并
查找的时间复杂度:O(logn)
合并的时间复杂度:O(logn)
2.1,Quick Find - 快速查找方案实现
1,假设并查集处理的数据都是整型,可以用整型数组来存储
实现思路:
1,方法union(v1,v2): 让v1所在集合的所有元素都指向v2的根节点
实现代码:
//查找,父节点就是根节点
public int find(int v) {
//检查是否超过范围
rangeCheck(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);
}
//
protected void rangeCheck(int v) { if (v < 0 || v >= parents.length) { throw new IllegalArgumentException("v is out of bounds"); }
}
2.2,快速合并的实现
1,快速合并的方法union(v1,v2): 让v1的根节点指向v2的根节点
实现代码:
//查找
public int find(int v) {
rangeCheck(v);
while (v != parents[v]) {
v = parents[v];
}
}
//合并
public void union(int v1,int v2) {
int p1 = find(v1);
int p2 = find(v2);
if (p1 == p2) return;
parents[p1] = p2;
}
3,快速查找的并查集-优化
- 在union的过程中,可能会出现树不平衡的情况,甚至退化成链表
- 有两种常见的优化方案
基于size的优化:元素少的树 嫁接到 元素多的树
基于rank的优化:矮的树 嫁接到 高的树
3.1,基于size的优化
只是在合并的时候,将元素少的树,嫁接到元素多的树
代码实现:
//初始化size,默认存储元素的个数为1
sizes = new int[capacity];
for (int i = 0; i < sizes.length; i++){
sizes[i] = 1;
}
//合并
public void union(int v1,int v2){
int p1 = find(v1);
int p2 = find(v2);
if (p1 == p2) return;
if (sizes[p1] < sizes[p2]) {
//将p1根节点嫁接到p2
parents[p1] = p2;
//p2的size要加上p1的size大小
sizes[p2] += sizes[p1];
}
}
3.2,基于rank的优化
ranks = new int[capacity];
for (int i = 0; i < ranks.length; i++){
ranks[i] = 1;
}
//
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 {//只有当相等的时候,rank才会加1
parents[p1] = p2;
ranks[p2]++;
}
}
4,路径压缩
- 虽然有了基于rank的优化,树会相对平衡一点
- 但是随着union次数的增多,树的高度依然会越来越高,导致find操作变慢,尤其是底层节点
路径压缩:在find时使路径上的所有节点都指向根节点,从而降低树的高度
代码实现
public int find(int v){
if (parents[v] != v){
//找到路径上的所有节点,都指向根节点
parents[v] = find(parents[v]);
}
return parents[v];
}
还有2种更优的做法,不但能降低树高,实现成本也比路径压缩底
路径分裂 和 路径减半
4.1,路径分裂
路径分裂:使路径上每隔一个节点就指向其祖父节点
//在查找时修改节点的指向
public int find(int v) {
while (v != parents[v]) {
int parent = parents[v];
parents[v] = parents[parent];
v = parent;
}
return v;
}
4.2,路径减半
路径减半:使路径上每隔一个节点就指向其祖父节点
代码实现:
//
public int find(int v){
while (v != parents[v]){
parents[v] = parents[parents[v]];
v = parents[v];
}
return v;
}