12-並查集 UnionFind
阿新 • • 發佈:2020-07-03
目錄
1、簡介
並查集是一種樹型的資料結構,用於處理一些不相交集合(Disjoint Sets)的合併及查詢問題
- union(p,q),合併兩個元素所在的集合
- isConnected(p,q),查詢兩個元素是否屬於一個集合
2、實現
2.1、實現思路
- 將每一個元素看作是一個結點
- 獨立的結點指向自己本身
- 合併兩個結點所屬集合,只需要讓兩個結點的根結點連線即可
- 判斷兩個結點是否屬於一個集合,只需要檢視它們的根結點是否為同一個結點即可
2.2、介面
public interface unionFind {
//p,q不是具體的元素,而是像元素在陣列的索引
int getSize();
// 檢視元素p和元素q是否屬於一個集合
boolean isConnected(int p, int q);
// 合併元素p和元素q所屬的集合
void unionElement(int p, int q);
}
2.3、初始狀態
每一個結點指向自己,這種狀態可以使用陣列進行表示:每一個結點對應陣列中的相應索引處的值。
2.4、union操作
- union 4,3
- union 3,8
- union 9,4
- union 2,1
- union 5,0
- union 7,2
- union 6,2
2.5、程式碼
public class UnionFind_2 implements unionFind { private int[] parent; // 每一個元素單獨屬於一個集合 public UnionFind_2(int size) { parent = new int[size]; for(int i = 0; i < size; i++){ parent[i] = i; } } @Override public int getSize() { return parent.length; } @Override public boolean isConnected(int p, int q) { return find(p) == find(q); } @Override public void unionElement(int p, int q) { int pRoot = find(p); int qRoot = find(q); if(pRoot == qRoot){ return; } parent[pRoot] = qRoot; } // 查詢元素p對應的集合編號 private int find(int p){ while (p != parent[p]) p = parent[p]; return p; } }
3、程式碼優化
3.1、基於size的優化
union 8,9
顯然,這樣的合併操作有可能會不斷增加樹的高度,進而導致isConnected
操作的複雜度過大。
解決方法:讓結點數少的樹的根結點指向結點數多的樹的根結點。
public class UnionFind_3 implements unionFind {
private int[] parent;
private int[] size; //size[i] 表示以i為根的集合中元素的個數
public UnionFind_3(int size) {
parent = new int[size];
this.size = new int[size];
for(int i = 0; i < size; i++){
parent[i] = i;
this.size[i] = 1;
}
}
@Override
public int getSize() {
return parent.length;
}
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
@Override
public void unionElement(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot){
return;
}
if(size[pRoot] < size[qRoot]){
parent[pRoot] = qRoot;
size[qRoot] += size[qRoot];
}
else{
parent[qRoot] = pRoot;
size[pRoot] += size[qRoot];
}
}
private int find(int p){
while (p != parent[p])
p = parent[p];
return p;
}
}
3.2、基於rank的優化
union 7,8
很明顯,按照之前的優化方式,樹的高度將增大。
解決方法:讓高度小的樹的根結點指向高度大的的樹的根結點。
與前面基於size的優化相比,基於rank的優化可以減少樹的高度增長的概率。
public class UnionFind_4 implements unionFind {
private int[] parent;
private int[] rank; // rank[i] 表示以i為根的集合所表示的樹的高度
public UnionFind_4(int size) {
parent = new int[size];
this.rank = new int[size];
for(int i = 0; i < size; i++){
parent[i] = i;
rank[i] = 1;
}
}
@Override
public int getSize() {
return parent.length;
}
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
@Override
public void unionElement(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[qRoot] = pRoot;
rank[pRoot] += 1;
}
}
private int find(int p){
while (p != parent[p])
p = parent[p];
return p;
}
}
4、路徑壓縮
理想情況下,一棵樹的形狀應該是③ :樹的高度儘可能的小。
為此我們需要進行路徑壓縮,減小樹的高度,此過程設計在find
方法中,因為在合併兩個結點前要進行find操作。
4.1、壓縮方式一
private int find(int p){
while (p != parent[p]){
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
4.2、壓縮方式二
// 遞迴實現路徑壓縮
private int find(int p){
if(p != parent[p]){
parent[p] = find(parent[p]);
}
return parent[p];
}
5、測試
private static long getRunTimeNano(UnionFind unionFind, int times){
int size = unionFind.getSize();
Random random = new Random();
long startTime = System.nanoTime();
for(int i=0; i<times; i++){
int a = random.nextInt(size);
int b = random.nextInt(size);
unionFind.unionElement(a, b);
}
for(int i=0; i<times; i++){
int a = random.nextInt(size);
int b = random.nextInt(size);
unionFind.isConnected(a, b);
}
long endTime = System.nanoTime();
return endTime - startTime;
}
public static void main(String[] args) {
int size = 100000;
int times = 100000;
UnionFind_6 unionFind_6 = new UnionFind_6(size);
long runTime = getRunTimeMills(unionFind_6, times);
System.out.println(runTime);
}