1.第一种实现方式:quick_find
查询时间复杂度:O(1)
合并时间复杂度:O(n)
实现方式:采用数组,维护的数组实际上是一颗高度最大为2的树
//quick_find实现
class Disjoint_Set {
private int[] parents;
public Disjoint_Set(int n) {
parents = new int[n];
for (int i = 0; i < n; i++) {
parents[i] = i;
}
}
// 查找x所属集合的根节点
public int find(int x) {
return parents[x];
}
// 返回x,y是否同属一个集合
public boolean isSame(int x, int y) {
return find(x) == find(y);
}
// 合并操作:将与x所属在同一集合的所有节点,连接在y的根节点上
public void union(int x, int y) {
int source_root = find(x);
int target_root = find(y);
if (source_root == target_root)
return;
for (int i = 0; i < parents.length; i++) {
if (parents[i] == source_root)
parents[i] = target_root;
}
return;
}
@Override
public String toString() {
return "Disjoint_Set [parents=" + Arrays.toString(parents) + "]";
}
}
2.第二种实现方式:quick_union
查询平均时间复杂度:O(logn)
合并平均时间复杂度:O(logn)
实现方式:find用递归实现,可能出现单叉树的情况,退化为链表,时间复杂度最坏为O(n)
public class Main {
public static void main(String[] args) {
Disjoint_Set Disjoint_Set = new Disjoint_Set(5);
System.out.println(Disjoint_Set);
Disjoint_Set.union(1, 0);
System.out.println(Disjoint_Set);
Disjoint_Set.union(1, 2);
System.out.println(Disjoint_Set);
Disjoint_Set.union(4, 1);
System.out.println(Disjoint_Set);
Disjoint_Set.union(0, 3);
System.out.println(Disjoint_Set);
}
}
//quick_union实现
class Disjoint_Set {
private int[] parents;
public Disjoint_Set(int n) {
parents = new int[n];
for (int i = 0; i < n; i++) {
parents[i] = i;
}
}
// 查找x所属集合的根节点
public int find(int x) {
if(parents[x] == x)
return x;
return find(parents[x]);
}
// 返回x,y是否同属一个集合
public boolean isSame(int x, int y) {
return find(x) == find(y);
}
// 合并操作:将与x的根节点连接在y的根节点上
public void union(int x, int y) {
int source_root = find(x);
int target_root = find(y);
parents[source_root] = target_root;
return;
}
@Override
public String toString() {
return "Disjoint_Set [parents=" + Arrays.toString(parents) + "]";
}
}
/*结果:
Disjoint_Set [parents=[0, 1, 2, 3, 4]]
Disjoint_Set [parents=[0, 0, 2, 3, 4]]
Disjoint_Set [parents=[2, 0, 2, 3, 4]]
Disjoint_Set [parents=[2, 0, 2, 3, 2]]
Disjoint_Set [parents=[2, 0, 3, 3, 2]]*/
3.基于size对quick_union的优化
虽然上述优化平均时间复杂度已经为O(logn),但是它的最坏时间复杂度仍然为O(n),这是由于合并操作的时候,建立起来的树(这里是通过数组模拟的)可能严重失去平衡,退化成单叉树(形如链表),通过叶子节点找它的祖先节点的时候,就要遍历所有的节点,时间复杂度为O(logn)。
- 优化:设一个size数组,用于表示每个节点所属集合的大小(所属集合节点的个数)。合并操作时,将树小的节点与树大的节点合并(结合代码来看,说的不是很清楚)。
public class Main {
public static void main(String[] args) {
Disjoint_Set Disjoint_Set = new Disjoint_Set(5);
System.out.println(Disjoint_Set);
Disjoint_Set.union(1, 0);
System.out.println(Disjoint_Set);
Disjoint_Set.union(1, 2);
System.out.println(Disjoint_Set);
Disjoint_Set.union(4, 1);
System.out.println(Disjoint_Set);
Disjoint_Set.union(0, 3);
System.out.println(Disjoint_Set);
}
}
class Disjoint_Set {
private int[] parents;
private int[] size;
public Disjoint_Set(int n) {
parents = new int[n];
size = new int[n];
for (int i = 0; i < n; i++) {
parents[i] = i;
size[i] = 1; //初始每个集合大小为1
}
}
public int find(int x) {
if(parents[x] == x)
return x;
return find(parents[x]);
}
public boolean isSame(int x, int y) {
return find(x) == find(y);
}
public void union(int x, int y) {
int p1 = find(x);
int p2 = find(y);
if(p1 == p2) //所属同一集合,直接返回
return ;
if(size[p1] < size[p2]) { //小树连接到大树上,大树的集合大小要改变
parents[p1] = p2;
size[p2] += size[p1];
}else { //反之同理,如果大小相同,谁连接谁都可以
parents[p2] = p1;
size[p1] += size[p2];
}
return;
}
@Override
public String toString() {
return "Disjoint_Set [parents=" + Arrays.toString(parents) + "]";
}
}
/*
Disjoint_Set [parents=[0, 1, 2, 3, 4]]
Disjoint_Set [parents=[1, 1, 2, 3, 4]]
Disjoint_Set [parents=[1, 1, 1, 3, 4]]
Disjoint_Set [parents=[1, 1, 1, 3, 1]]
Disjoint_Set [parents=[1, 1, 1, 1, 1]]
*/
4.基于rank对quick_union的优化
size维护的是根节点集合的大小,实际上集合大并不意味着树一定高,我们希望的是树高越小越好,所以维护一个rank树组,用于表示节点所属集合的树高。
改动不大,部分省略:
class Disjoint_Set {
private int[] parents;
private int[] rank;
public Disjoint_Set(int n) {
parents = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) {
parents[i] = i;
rank[i] = 1; //初始每个集合大小为1
}
}
public int find(int x) {
if(parents[x] == x)
return x;
return find(parents[x]);
}
public boolean isSame(int x, int y) {
return find(x) == find(y);
}
public void union(int x, int y) {
int p1 = find(x);
int p2 = find(y);
if(p1 == p2) //所属同一集合,直接返回
return ;
if(rank[p1] < rank[p2]) { //树矮的连接到树高的,rank不变
parents[p1] = p2;
}else if (rank[p1] > rank[p2]){
parents[p2] = p1;
}else { //只有树高相同时,连接的时候,连接的根节点rank+1
parents[p2] = p1;
rank[p1]++ ;
}
return;
}
}
5.采取路径压缩(Path Compression)的quick_union
虽然树高有所减小,但是随这节点的增多,树的合并,树高仍然会越来越大。如果建立一个n叉树,那么查找操作的时间复杂度不就是O(1)了吗?
我们采取这样的策略:在合并两颗树的同时,对于两个节点分别找它们的祖先节点,在寻找的过程中,沿着轨迹不断将途中的节点连接到祖先节点上(即将指针指向祖先节点),经过适当的操作,树高就会减小到2。
主要重写find方法,递归思想很重要,尤其是最后返回的可不是x,而是parent[x],函数功能是返回x的根节点,在find函数中递归的使用自己要时刻明白返回值是什么。
class Disjoint_Set {
private int[] parents;
public Disjoint_Set(int n) {
parents = new int[n];
for (int i = 0; i < n; i++) {
parents[i] = i;
}
}
//返回x的根节点
public int find(int x) {
if(parents[x] != x) {
parents[x] = find(parents[x]);//将途中节点连接到根节点上
}
return parents[x];
}
public boolean isSame(int x, int y) {
return find(x) == find(y);
}
public void union(int x, int y) {
int p1 = find(x);
int p2 = find(y);
if(p1 == p2) //所属同一集合,直接返回
return ;
parents[p1] = p2;
return;
}
}
- 分析:这种方法并不是所有时候都会效率高,比如两个集合合并完了以后,并没有再进行查询操作,那这些连接操作不是白做了吗,实现成本大于效益,所以有以下的优化。
6.采取路径分裂(Path Spliting)的quick_union
路径分裂:使路径上的每个节点都指向其祖父节点
//返回x的根节点
public int find(int x) {
while(x != parents[x]) {
int temp = parents[x]; //保存x的父节点
parents[x] = parents[parents[x]]; //将x的父节点指向祖父节点
x = temp; //对x的父节点做同样的操作,所以更新x
}
return x;
}
7.采取路径减半(Path Halving)的quick_union
路径减半:使路径上每隔一个节点就指向它的祖父节点
//返回x的根节点
public int find(int x) {
while(x != parents[x]) {
parents[x] = parents[parents[x]]; //将x的父节点指向祖父节点
x = parents[x];
}
return x;
}
路径分裂和路径减半的优化在于对工程角度的考虑,因为对每一个节点都指向它的根节点开销太大,那么我们不妨折中以下,指向里他们较近的节点,这样树高不会太大以至于查找操作的时间复杂度太差。
时间复杂度的分析:
大概翻译以下,就是使用路径压缩、路径分裂、路径减半这三种之一,结合rank或者size的优化可以保证每种操作的均摊时间达到
O
(
α
(
n
)
)
O(\alpha(n))
O(α(n)),这样是最理想的,这里的
α
(
n
)
\alpha(n)
α(n)是Ackermann函数的反函数。这个值小于5,所以并查集操作本质上需要花费常熟级的时间。
工地英语请见谅,深入研究还请看其他文献资料。
维基百科:并查集