数据结构-并查集
并查集
- 一种特殊的树, 由子节点执行父节点
- 方便解决连接问题
主要操作
union(p,q)
用于合并p, q所在的集合
isConnected(p,q)
判断p,q是否相连
代码实现
首先先定义并查集的接口, 接口定义如下:
package tree.uf;
/**
* 并查集接口
* @author 七夜雪
*
*/
public interface UF {
/**
* 合并p,q两个节点
* @param p
* @param q
*/
public void union(int p, int q);
/**
* 判断p,q两个节点是否相连
* @param p
* @param q
* @return
*/
public boolean isConnected(int p, int q);
// 获取并查集中数据数量
public int getSize();
}
基于数组的并查集实现
-
使用一组数组存储并查集的数据
-
数组的索引表示数据的编号
-
数组的值表示数据所属的集合, 具有相同值的数据表示在同一个集合, 如下图所示:
代码实现如下 :
package tree.uf;
/**
* 第一版并查集, quick-sort方式实现
* 查询时间复杂度O(1)
* union时间复杂度O(n)
* 使用数组实现并查集:
* 数组下标表示并查集id
* 数组值表示并查集所属的集合
* @author 七夜雪
*
*/
public class UnionFind1 implements UF {
private int[] id;
public UnionFind1(int size) {
this.id = new int[size];
for (int i = 0; i < size; i++) {
id[i] = i;
}
}
/**
* 合并两个节点
* 合并两个节点之后, 表示这两个节点相连了
* 同样的, 这两个节点的所有其他元素也都相连了
* 所以可以认为两个节点合并之后, 就是把这两个节点所在的集合合并成一个集合
*/
@Override
public void union(int p, int q) {
int pid = find(p);
int qid = find(q);
if (pid == qid) {
return;
}
for (int i = 0; i < id.length; i++) {
if (find(i) != pid) {
id[i] = pid;
}
}
}
/**
* 判断节点p和节点q是否相连
* p,q属于一个集合时, 表示p,q相连
*/
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q) ;
}
/**
* 查找节点p所属的集合
* @param p
* @return
*/
private int find(int p) {
if (p < 0 || p >= id.length) {
throw new IllegalArgumentException("节点id越界");
}
return id[p];
}
@Override
public int getSize() {
return id.length ;
}
}
基于树的并查集实现
-
将每个元素, 看做一个节点
-
由子节点指向父节点, 根节点指向自身
-
具有相同根节点的两个节点之间是相连的
-
两个节点合并时将其中一个节点所在树的根节点指向另一个节点所在树的根节点即可
上图中567三个几点所在的集合与123三个节点所在的集合进行union操作时, 只需要将567所在的树的根节点5指向123所在的根节点2, 或者将2指向5即可
虽然是基于树实现并查集, 但是由于每个节点都只有一个父节点, 所以依然可以使用数组表示并查集中的数据, 表示方式如下 :
-
使用数组的下标表示数据的编号
-
数组的值表示该节点对应的父节点的数组下标值, 初始时将自己指向自己, 表示每个数据都是一个单独的集合,下图就是使用数组演示基于树实现并查集的union操作
-
进行查找时, 当数组的下标值等于数组的值时, 表示该节点为根节点, 如parent[8] == 8, 所以8是根节点
具体代码如下 :
package tree.uf;
/**
* 第二版并查集, quick-union方式实现
* 使用树来实现并查集
* 将数组组织成树的形式, 每个节点都指向一个父节点, 根节点指向自己
* 使用数组索引表示当前节点位置, 数组值表示父节点索引位置
*
* 查询和union操作时间复杂度都是O(h), h表示树高度
* @author 七夜雪
*
*/
public class UnionFind2 implements UF {
private int[] parent;
public UnionFind2(int size) {
this.parent = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
}
}
/**
* 合并两个节点
* 合并两个节点之后, 表示这两个节点相连了
* 同样的, 这两个节点的所有其他元素也都相连了
* 所以可以认为两个节点合并之后, 就是把这两个节点所在的集合合并成一个集合
* 合并方式:
* 找到节点p的根节点, 将p的根节点指向q的根节点
*
*/
@Override
public void union(int p, int q) {
// p的根节点
int pRoot = find(p);
// q的根节点
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
// 将p的根节点指向q的根节点
parent[pRoot] = qRoot;
}
/**
* 判断节点p和节点q是否相连
* p,q属于一个集合时, 表示p,q相连, 这里表示p,q有一个共同的根节点
*/
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q) ;
}
/**
* 查找节点p所属的跟节点
* @param p
* @return
*/
private int find(int p) {
if (p < 0 || p >= parent.length) {
throw new IllegalArgumentException("节点id越界");
}
while(p != parent[p]){
p = parent[p];
}
return p;
}
@Override
public int getSize() {
return parent.length ;
}
}
基于size的优化
针对上一个版本的并查集, 存在一个问题, 每次都是随机合并的, 会存在数据量大的集合向数据量小的集合进行合并, 会导致合并后的树高度比较高, 如果每次合并的时候, 都是数据量较小的集合往数据量较大的集合合并的话, 会使合并后的集合的树的高度没有那么高, 性能会有一定提高, 优化后代码如下 :
package tree.uf;
/**
* 第三版并查集, 记录每个根节点所在的树的节点数量, 合并时数量少的树合并到数量多的树上面
* 使用树来实现并查集
* 将数组组织成树的形式, 每个节点都指向一个父节点, 根节点指向自己
* 使用数组索引表示当前节点位置, 数组值表示父节点索引位置
*
* 查询和union操作时间复杂度都是O(h), h表示树高度
* @author 七夜雪
*
*/
public class UnionFind3 implements UF {
private int[] parent;
// 下标为对应根节点下标, 数组值为对应根节点对应树的节点数量
private int[] sz;
public UnionFind3(int size) {
this.parent = new int[size];
this.sz = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
sz[i] = 1;
}
}
/**
* 合并两个节点
* 合并两个节点之后, 表示这两个节点相连了
* 同样的, 这两个节点的所有其他元素也都相连了
* 所以可以认为两个节点合并之后, 就是把这两个节点所在的集合合并成一个集合
* 合并方式:
* 找到节点p的根节点, 将p的根节点指向q的根节点
*
*/
@Override
public void union(int p, int q) {
// p的根节点
int pRoot = find(p);
// q的根节点
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
if (sz[pRoot] < sz[qRoot]) {
// 将p的根节点指向q的根节点
parent[pRoot] = qRoot;
sz[qRoot]+= sz[pRoot];
} else {
// 将q的根节点指向p的根节点
parent[qRoot] = pRoot;
sz[pRoot]+= sz[qRoot];
}
}
/**
* 判断节点p和节点q是否相连
* p,q属于一个集合时, 表示p,q相连, 这里表示p,q有一个共同的根节点
*/
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q) ;
}
/**
* 查找节点p所属的跟节点
* @param p
* @return
*/
private int find(int p) {
if (p < 0 || p >= parent.length) {
throw new IllegalArgumentException("节点id越界");
}
while(p != parent[p]){
p = parent[p];
}
return p;
}
@Override
public int getSize() {
return parent.length ;
}
}
基于rank的优化
上面的优化其实并不是很精确, 仅仅考虑集合元素数量, 有时候树的元素数量,和高度并不一致, 如下图所示, 对4,2进行合并时, 按照上面的逻辑会将节点8指向节点7, 事实上节点7指向节点8的话, 效果会更好一点, 这个时候就可以根据树的高度进行合并操作, 而不是单单考虑集合元素数量.
代码实现如下:
package tree.uf;
/**
* 第四版并查集, 对比第三版, 将合并时以两个数节点数量为标准改为了以树高度为标准
*
* 查询和union操作时间复杂度都是O(h), h表示树高度
* @author 七夜雪
*
*/
public class UnionFind4 implements UF {
private int[] parent;
// 下标为对应根节点下标, 数组值为对应根节点对应树的高度
private int[] rank;
public UnionFind4(int size) {
this.parent = new int[size];
this.rank = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
rank[i] = 1;
}
}
/**
* 合并两个节点
* 合并两个节点之后, 表示这两个节点相连了
* 同样的, 这两个节点的所有其他元素也都相连了
* 所以可以认为两个节点合并之后, 就是把这两个节点所在的集合合并成一个集合
* 合并方式:
* 找到节点p的根节点, 将p的根节点指向q的根节点
*
*/
@Override
public void union(int p, int q) {
// p的根节点
int pRoot = find(p);
// q的根节点
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
if (rank[pRoot] < rank[qRoot]) {
// 将p的根节点指向q的根节点
parent[pRoot] = qRoot;
} else if(rank[pRoot] > rank[qRoot]) {
// 将q的根节点指向p的根节点
parent[qRoot] = pRoot;
} else {
parent[qRoot] = pRoot;
rank[pRoot] = rank[pRoot] + 1;
}
}
/**
* 判断节点p和节点q是否相连
* p,q属于一个集合时, 表示p,q相连, 这里表示p,q有一个共同的根节点
*/
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q) ;
}
/**
* 查找节点p所属的跟节点
* @param p
* @return
*/
private int find(int p) {
if (p < 0 || p >= parent.length) {
throw new IllegalArgumentException("节点id越界");
}
while(p != parent[p]){
p = parent[p];
}
return p;
}
@Override
public int getSize() {
return parent.length ;
}
}
路径压缩
经过上述优化之后, 虽然尽量避免了树的高度不平衡问题, 但是极端情况下, 仍然会出现树的高度较高的情况, 在这种情况下, 可以进行路径压缩操作, 即每次合并时, 对路径进行压缩, 如下图所示:
这里可以简单的使用parent[p] = parent[parent[p]], 即每次都将该节点指向其父节点的父节点的方式简单的进行压缩操作, 具体代码实现如下 :
package tree.uf;
/**
* 第五版并查集, 对比第四版, 在find时,添加了路径压缩
* 同时rank不在实际表示树的高度了,只是用来标识高度大小
*
* @author 七夜雪
*
*/
public class UnionFind5 implements UF {
private int[] parent;
// 下标为对应根节点下标, 数组值为对应根节点对应树的高度,
// 压缩之后不再表示树的高度了, 但是还可以用来标示树的高度大小, 所以这里用rank, 不是height
private int[] rank;
public UnionFind5(int size) {
this.parent = new int[size];
this.rank = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
rank[i] = 1;
}
}
/**
* 合并两个节点
* 合并两个节点之后, 表示这两个节点相连了
* 同样的, 这两个节点的所有其他元素也都相连了
* 所以可以认为两个节点合并之后, 就是把这两个节点所在的集合合并成一个集合
* 合并方式:
* 找到节点p的根节点, 将p的根节点指向q的根节点
*
*/
@Override
public void union(int p, int q) {
// p的根节点
int pRoot = find(p);
// q的根节点
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
if (rank[pRoot] < rank[qRoot]) {
// 将p的根节点指向q的根节点
parent[pRoot] = qRoot;
} else if(rank[pRoot] > rank[qRoot]) {
// 将q的根节点指向p的根节点
parent[qRoot] = pRoot;
} else {
parent[qRoot] = pRoot;
rank[pRoot] = rank[pRoot] + 1;
}
}
/**
* 判断节点p和节点q是否相连
* p,q属于一个集合时, 表示p,q相连, 这里表示p,q有一个共同的根节点
*/
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q) ;
}
/**
* 查找节点p所属的跟节点
* @param p
* @return
*/
private int find(int p) {
if (p < 0 || p >= parent.length) {
throw new IllegalArgumentException("节点id越界");
}
while(p != parent[p]){
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
@Override
public int getSize() {
return parent.length ;
}
}