《数据结构与算法》
本文来源于liuyubobobo的“算法与数据结构--综合提升篇”视频教程
并查集是一种树形的数据结构,可用于判断数据是否相连接。
例如在下图中任意取两个点,怎么判断这两个点是否连接在一起呢?
上图中的这个问题太复杂,举个简单的例子来练手。
使用quick find算法实现并查集
public class QuickFind {
private int[] id;
/**
* 初始化数据,数据是下标,元素值也是下标,表示初始化时没有相连接的数据
*/
public QuickFind(int size){
id = new int[size];
for (int i=0; i<size; i++){
id[i] = i;
}
}
public int getSize(){
return id.length;
}
private int find(int p){
if (p<0 || p>=id.length){
throw new RuntimeException("越界");
}
return id[p];
}
// 判断元素id[p]是否混合id[q]想连接
public boolean isConnected(int p, int q){
return find(p) == find(q);
}
// 连接接id[p]、id[q]
public void unionElements(int p, int q){
int pID = find(p);
int qID = find(q);
if (pID == qID){
return;
}
// 将id[p]的值改成id[q]的值,则id[p]、id[q]就可以表示为相连接了
for (int i=0; i<id.length; i++){
if (id[i]==pID){
id[i] = qID;
}
}
}
public static void main(String[] args) {
QuickFind uf1 = new QuickFind(10);
System.out.println(uf1.find(8));
System.out.println(uf1.find(1));
System.out.println(uf1.isConnected(1, 8));
uf1.unionElements(1, 8);
System.out.println(uf1.find(8));
System.out.println(uf1.find(1));
uf1.isConnected(1, 8);
System.out.println(uf1.isConnected(1, 8));
}
}
quick find算法的特点是查找快、合并慢。
unionElements(int p, int q)方法的时间复杂度是O(n)级别的,find(int p)、isConnected(int p, int q)方法的时间复杂度都是O(1)级别的。
为了平衡查找、合并的效率,实现并查集更加常用的算法是quick union。
下面用一个数组讲解quick union合并流程
public class QuickUnion {
// 指向的父亲节点数组
private int[] parent;
// sz[i]表示以i为根的集合元素个数
private int[] sz;
public QuickUnion(int size){
parent = new int[size];
sz = new int[size];
for (int i=0; i<size; i++){
parent[i] = i;
sz[i] = 1;
}
}
public int getSize(){
return parent.length;
}
// 返回的是根节点
private int find(int p){
if (p<0 || p>=parent.length){
throw new RuntimeException("越界");
}
/**
* 如果p的父亲节点不是自己,则继续寻找上一级节点,直至节点的父亲节点也是自己
* 即返回根节点
*/
while (p != parent[p]){
p=parent[p];
}
return p;
}
public boolean isConnected(int p, int q){
return find(p) == find(q);
}
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot){
return;
}
if (sz[pRoot] < sz[qRoot]){
// sz[pRoot]个数少
// pRoot作为子节点指向qRoot
parent[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
} else {
// sz[qRoot] <= sz[pRoot]
// qRoot作为子节点指向pRoot
parent[qRoot] = pRoot;
sz[pRoot] += sz[pRoot];
}
}
public static void main(String[] args) {
QuickUnion uf1 = new QuickUnion(10);
System.out.println(uf1.find(8));
System.out.println(uf1.find(1));
System.out.println(uf1.isConnected(1, 8));
uf1.unionElements(1, 8);
System.out.println(uf1.find(8));
System.out.println(uf1.find(1));
uf1.isConnected(1, 8);
System.out.println(uf1.isConnected(1, 8));
}
}
上面的代码中sz记录的是相连接节点集合的个数,这种方式称为基于size的优化。
基于size的优化在某些情况下存在bug,比方说下面的情况。
基于rank优化的代码
public static class QuickUnionRank {
// 指向的父亲节点数组
private int[] parent;
// rank[i]表示以i为根的树高度
private int[] rank;
public QuickUnionRank(int size){
parent = new int[size];
rank = new int[size];
for (int i=0; i<size; i++){
parent[i] = i;
rank[i] = 1;
}
}
public int getSize(){
return parent.length;
}
// 返回的是根节点
private int find(int p){
if (p<0 || p>=parent.length){
throw new RuntimeException("越界");
}
/**
* 如果p的父亲节点不是自己,则继续寻找上一级节点,直至节点的父亲节点也是自己
* 即返回根节点
*/
while (p != parent[p]){
p=parent[p];
}
return p;
}
public boolean isConnected(int p, int q){
return find(p) == find(q);
}
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot){
return;
}
if (rank[pRoot] < rank[qRoot]){
// pRoot高度 小于 qRoot高度
// pRoot指向qRoot
parent[pRoot] = qRoot;
} else if (rank[qRoot] < rank[pRoot]){
// qRoot高度 小于 pRoot高度
// qRoot指向pRoot
parent[qRoot] = pRoot;
} else {
parent[pRoot] = qRoot;
}
}
public static void main(String[] args) {
QuickUnionRank uf1 = new QuickUnionRank(10);
System.out.println(uf1.find(8));
System.out.println(uf1.find(1));
System.out.println(uf1.isConnected(1, 8));
uf1.unionElements(1, 8);
System.out.println(uf1.find(8));
System.out.println(uf1.find(1));
uf1.isConnected(1, 8);
System.out.println(uf1.isConnected(1, 8));
}
}