一.定义
简而言之就是有“合并集合”和“查找集合中的元素”两种操作的关于数据结构的一种算法。
- 连接两个对象
- 判断是否这两个对象是连接的
上例中,0,7是没有路径的;8,9之间有一条可达路径,因此就算是连接的。 (学过图论的人应该不难理解)
二.数学模型
- 等价性: p连接到p,每个对象都能连接到自己
- 对称性: p连接到q;等价于q连接到p
- 传递性: 如果p连接到q,q连接到r,那么,p连接到r。
三.快速查找
1.数据结构
这里用到的数据结构是数组。
- 对象索引整数数组,用索引来表示N个对象,0表示第一个对象,以此类推。
- 如果p和q是连接的,那么它们数组的值相同。
2.查找
只需要检查p和q是否有相同的值。
3.合并
将它们的值设为相同的。假如需要合并两个分别包含p和q的子集,将所有值等于id[p]的对象的值改为id[q],即改为q的值。
4.实现
初始化、判断连接以及合并
public class QuickFindUF {
private int [] id;
public QuickFindUF(int N){
id = new int[N];
for (int i = 0; i < N; i++) {
id[i] = i; //初始化
}
}
/**
* 判断p和q是否是连接的
* @param p
* @param q
* @return
*/
public boolean connected(int p,int q){
return id[p] == id[q];
}
/**
* 合并p的子集和q的子集
* 将所有值等于id[p]的对象的值改为id[q]
* @param p
* @param q
*/
public void union(int p,int q){
int pid = id[p];
int qid = id[q];
for (int i = 0; i < id.length; i++) {
if(id[i] == pid){
id[i] = qid;
}
}
}
}
5.时间复杂度分析
算法 | 初始化 | 合并 | 查询 |
quick-find | O(N) | O(N) | O(1) |
在这个实现中,可以看到合并操作的代价很高。如果需要在N个对象上进行N次合并,那就是O(n*n)。效率不高。
四.快速合并
1.数据结构
- 对象索引整数数组,用索引来表示N个对象,0表示第一个对象,以此类推。
- id[i]存放的值代表 i 的父节点索引
- 若id[i]==i,则说明i是根节点
上图中,0,1,9,6 ,7,8 下标对应的值都是下标本身的值,说明这几个节点是根节点。
2 号下标对应的值是 9 ,根据上述的定义,节点 9 就是节点 2 的父节点,以此类推3,4,5节点的状态。
2.查找
检查p和q根节点是否相同。
3.合并
合并操作就非常简单了。
要合并p,q两个对象的分量(合并树),将p的根节点设为q的根节点(p=>q p的根节点指向q)。
上面只需要将9的值改为6即可完成合并。
4.实现
public class QuickUnionUF {
private int [] id;
public QuickUnionUF(int N){
id = new int[N];
for (int i = 0; i < N; i++) {
id[i] = i;
}
}
/**
* 找到i的根节点
* @param i
* @return
*/
private int root(int i){
while(i != id[i]){
i = id[i];//i和id[i] 一起更新
}
return i;
}
/**
* 判断p和q是否是连接的
* @param p
* @param q
* @return
*/
public boolean connected(int p,int q){
return root(p) == root(q);
}
/**
* 合并p和q
* 将p的根节点设为q的根节点(p=>q p的根节点指向q)。
* @param p
* @param q
*/
public void union(int p,int q){
int qroot = root(q);//找到q的根节点
int proot = root(p);
id[proot] = qroot;
}
}
5.时间复杂度分析
算法 | 初始化 | 合并 | 查询 |
quick-find | O(N) | O(N) | O(1) |
quick-union | O(N) | O(N) | O(N) |
最坏的情况下,会生成一颗瘦长树。
五.加权快速合并
1.数据结构
与快速合并类似,但是维护了一个额外的数组sz[i],它包含了以i为根节点的树的节点数量。
2.查找
与快速合并完全相同
3.合并
- 将小树指向大树的根节点
- 更新sz[]数组
4.实现
package com.algorithms.UnionFind;
public class WeightedQuickUnionUF {
private int [] id;
private int [] sz;//size of component for roots
private int count;//number of components
public WeightedQuickUnionUF(int N){
count = N;
id = new int[N];
for (int i = 0; i < N; i++) id[i] = i;
sz = new int[N];
for (int i = 0; i < N; i++) sz[i] = 1;
}
public int count(){
return count;
}
/**
* 找到i的根节点
* @param i
* @return
*/
private int root(int i){
while(i != id[i]){
i = id[i];//i和id[i] 一起更新
}
return i;
}
/**
* 判断p和q是否是连接的
* @param p
* @param q
* @return
*/
public boolean connected(int p,int q){
return root(p) == root(q);
}
/**
* 合并p和q
* 将p的根节点设为q的根节点(p=>q p的根节点指向q)。
* @param p
* @param q
*/
public void union(int p,int q){
int i = root(q);
int j = root(p);
if( i == j) return;
//Make smaller root point to larger one.
if(sz[i] <sz[j]){
id[i] = j;
sz[j] += sz[i];
}else{
id[j] = i;
sz[i] += sz[j];
}
count --;
}
}
算法 | 初始化 | 合并 | 查询 |
quick-find | O(N) | O(N) | O(1) |
quick-union | O(N) | O(N) | O(N) |
Weighted quick-union | O(N) | O(lgN) | O(lgN) |
其实还能改进。
六.路径压缩
在计算p节点的根后,将每个经过的节点都指向根
上图中,从p找到根0,需要经过9(当然要包括自己),6,3,1(已经指向根节点了)这些节点。让它们都指向根:
如果要实现完全展平,会很复杂。我们可以选择一种折中的方案,将节点p指向节点p的祖父(父节点的父节点)节点。
神奇的是,代码只需要在加权快速合并的基础上,进行一些小的改动:
将路径上的每个节点指向它的祖父节点:
private int root(int i){
while(i != id[i]){
id[i] = id[id[i]];//(父节点的父节点)
i = id[i];//i和id[i] 一起更新
}
return i;
}
增加了id[i] = id[id[i]]
id[i]代表i的父节点,id[父节点] 就是祖父节点。
虽然这不如完全展平那么好(如上图所示的那样),但是,在实际应用中,两者的效果差不多。