并查集(Union Find)
并查集可以很高效的解决连接问题 (Connectivity Problem)
网络中节点间的连接状态
- 网络是个抽象的概念: 用户之间形成的网络
数学中的集合
1. 实现方式一
首先设置每个元素在id数组中的值就是自己,find操作就是直接返回当前元素所对应的id数组中的值,如果在同一个集合的话,那么他们的值应该相同的。合并操作就是首先找到两个元素的id值,如果相同则表明他们属于同一个集合,直接返回,否者遍历所有的元素,将值等于p的id的元素全部改为q的id即可。上面这种操作实现简单,但效率较低。
package com.liuyao.union;
/**
* Created By liuyao on 2018/4/29 17:08.
*/
public class UnionFind1 {
private int[] id; // 我们的第一版Union-Find本质就是一个数组
private int count; // 数据个数
public UnionFind1(int n) {
count = n;
id = new int[n];
// 初始化, 每一个id[i]指向自己, 没有合并的元素
for (int i = 0; i < n; i++) {
id[i] = i;
}
}
private int find(int p) {
assert p >= 0 && p < count;
return id[p];
}
// 查看元素p和元素q是否所属一个集合
// O(1)复杂度
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(n) 复杂度
public void unionElements(int p, int q) {
int pID = find(p);
int qID = find(q);
if (pID == qID) {
return;
}
// 合并过程需要遍历一遍所有元素, 将两个元素的所属集合编号合并
for (int i = 0; i < count; i++) {
if (id[i] == pID) {
id[i] = qID;
}
}
}
public static void main(String[] args) {
int n = 100000;
UnionFindTestHelper.testUF1(n);
}
}
2. 实现方式二
最开始初始化的时候,每个元素都将父节点指向自己,两个元素union时,就将这个元素的parent指向一个元素的parent即可。unionElements时先判断两个节点的最前面的parent是否相等,相等则返回,不等则将前面的这个元素的最前面的parent指向后面的这个元素的最前面的parent。
package com.liuyao.union;
/**
* Created By liuyao on 2018/4/29 17:43.
*/
public class UnionFind2 {
private int[] parent; //parentb[i]表示第一个元素所指向的父节点
private int count; //数据个数
public UnionFind2(int count) {
parent = new int[count];
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for (int i = 0; i < count; i++) {
parent[i] = i;
}
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p) {
assert (p >= 0 && p < count);
// 不断去查询自己的父亲节点, 直到到达根节点
// 根节点的特点: parent[p] == p
while (p != parent[p]) {
p = parent[p];
}
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
parent[pRoot] = qRoot;
}
public static void main(String[] args) {
int n = 100000;
UnionFindTestHelper.testUF2(n);
}
}
3. 实现方式三——size优化
基于size的优化,每次unionElements的时候先判断两个元素的size的大小,每次将小的个数的元素链接到大的,这样可以避免每次都是左边链接到右边而形成了链表的情况发生。
package com.liuyao.union;
/**
* Created By liuyao on 2018/4/29 17:43.
*/
public class UnionFind3 {
private int[] parent; //parentb[i]表示第一个元素所指向的父节点
private int count; //数据个数
private int[] sz; //当前根下的元素个数
public UnionFind3(int count) {
parent = new int[count];
sz = new int[count];
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for (int i = 0; i < count; i++) {
parent[i] = i;
sz[i] = 1; //初始的时候每个集合里面只有自己一个元素
}
}
//其他操作类似
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
// 根据两个元素所在树的元素个数不同判断合并方向
// 将元素个数少的集合合并到元素个数多的集合上
if (sz[pRoot] < sz[qRoot]) {
parent[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
} else {
parent[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}
}
}
4. 实现方式四——rank优化
此时根据每个集合对应高度来比较,然后选择高度小的做为根,不更新rank值,如果两个高度相同,则任意选择,但此时两个parent相连,高度将会加一。
package com.liuyao.union;
/**
* Created By liuyao on 2018/4/29 17:43.
*/
public class UnionFind4 {
private int[] parent; //parentb[i]表示第一个元素所指向的父节点
private int count; //数据个数
private int[] rank; //当前根下的元素个数
public UnionFind4(int count) {
parent = new int[count];
rank = new int[count];
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for (int i = 0; i < count; i++) {
parent[i] = i;
rank[i] = 1;
}
}
//其他操作类似
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
// 根据两个元素所在树的元素个数不同判断合并方向
// 将元素个数少的集合合并到元素个数多的集合上
if (rank[pRoot] < rank[qRoot]) {
parent[pRoot] = qRoot;
} else if (rank[pRoot] > rank[qRoot]) {
parent[qRoot] = pRoot;
} else {
parent[pRoot] = qRoot;
rank[qRoot] += 1; //qRoot作为根,高度加一
}
}
}
5. 实现方式五——路径压缩
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p) {
assert (p >= 0 && p < count);
// 不断去查询自己的父亲节点, 直到到达根节点
// 根节点的特点: parent[p] == p
while (p != parent[p]) {
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
通过递归的方式找到最后的根节点并返回。再将其他元素的parent设置为它。
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p) {
assert (p >= 0 && p < count);
// 不断去查询自己的父亲节点, 直到到达根节点
// 根节点的特点: parent[p] == p
//递归算法
if (p != parent[p]) {
parent[p] = find(parent[p]); //递归查找并做该结合中的元素都的根节点都变成第一个元素
}
return parent[p];
}