資料結構-並查集
並查集
- 一種特殊的樹, 由子節點執行父節點
- 方便解決連線問題
主要操作
union(p,q)
用於合併p, q所在的集合
isConnected(p,q)
判斷p,q是否相連
程式碼實現
首先先定義並查集的介面, 介面定義如下:
package tree.uf;
/**
* 並查集介面
* @author 七夜雪
*
*/
public interface UF {
/**
* 合併p,q兩個節點
* @param p
* @param q
*/
public void union(int p, int q);
/**
* 判斷p,q兩個節點是否相連
* @param p
* @param q
* @return
*/
public boolean isConnected(int p, int q);
// 獲取並查集中資料數量
public int getSize();
}
基於陣列的並查集實現
-
使用一組陣列儲存並查集的資料
-
陣列的索引表示資料的編號
-
陣列的值表示資料所屬的集合, 具有相同值的資料表示在同一個集合, 如下圖所示: 程式碼實現如下 :
package tree.uf;
/**
* 第一版並查集, quick-sort方式實現
* 查詢時間複雜度O(1)
* union時間複雜度O(n)
* 使用陣列實現並查集:
* 陣列下標表示並查集id
* 陣列值表示並查集所屬的集合
* @author 七夜雪
*
*/
public class UnionFind1 implements UF {
private int[] id;
public UnionFind1(int size) {
this.id = new int[size];
for (int i = 0; i < size; i++) {
id[i] = i;
}
}
/**
* 合併兩個節點
* 合併兩個節點之後, 表示這兩個節點相連了
* 同樣的, 這兩個節點的所有其他元素也都相連了
* 所以可以認為兩個節點合併之後, 就是把這兩個節點所在的集合合併成一個集合
*/
@Override
public void union(int p, int q) {
int pid = find(p);
int qid = find(q);
if (pid == qid) {
return;
}
for (int i = 0; i < id.length; i++) {
if (find(i) != pid) {
id[i] = pid;
}
}
}
/**
* 判斷節點p和節點q是否相連
* p,q屬於一個集合時, 表示p,q相連
*/
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q) ;
}
/**
* 查詢節點p所屬的集合
* @param p
* @return
*/
private int find(int p) {
if (p < 0 || p >= id.length) {
throw new IllegalArgumentException("節點id越界");
}
return id[p];
}
@Override
public int getSize() {
return id.length ;
}
}
基於樹的並查集實現
-
將每個元素, 看做一個節點
-
由子節點指向父節點, 根節點指向自身
-
具有相同根節點的兩個節點之間是相連的
-
兩個節點合併時將其中一個節點所在樹的根節點指向另一個節點所在樹的根節點即可
上圖中567三個幾點所在的集合與123三個節點所在的集合進行union操作時, 只需要將567所在的樹的根節點5指向123所在的根節點2, 或者將2指向5即可
雖然是基於樹實現並查集, 但是由於每個節點都只有一個父節點, 所以依然可以使用陣列表示並查集中的資料, 表示方式如下 :
-
使用陣列的下標表示資料的編號
-
陣列的值表示該節點對應的父節點的陣列下標值, 初始時將自己指向自己, 表示每個資料都是一個單獨的集合,下圖就是使用陣列演示基於樹實現並查集的union操作
-
進行查詢時, 當陣列的下標值等於陣列的值時, 表示該節點為根節點, 如parent[8] == 8, 所以8是根節點
具體程式碼如下 :
package tree.uf;
/**
* 第二版並查集, quick-union方式實現
* 使用樹來實現並查集
* 將陣列組織成樹的形式, 每個節點都指向一個父節點, 根節點指向自己
* 使用陣列索引表示當前節點位置, 陣列值表示父節點索引位置
*
* 查詢和union操作時間複雜度都是O(h), h表示樹高度
* @author 七夜雪
*
*/
public class UnionFind2 implements UF {
private int[] parent;
public UnionFind2(int size) {
this.parent = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
}
}
/**
* 合併兩個節點
* 合併兩個節點之後, 表示這兩個節點相連了
* 同樣的, 這兩個節點的所有其他元素也都相連了
* 所以可以認為兩個節點合併之後, 就是把這兩個節點所在的集合合併成一個集合
* 合併方式:
* 找到節點p的根節點, 將p的根節點指向q的根節點
*
*/
@Override
public void union(int p, int q) {
// p的根節點
int pRoot = find(p);
// q的根節點
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
// 將p的根節點指向q的根節點
parent[pRoot] = qRoot;
}
/**
* 判斷節點p和節點q是否相連
* p,q屬於一個集合時, 表示p,q相連, 這裡表示p,q有一個共同的根節點
*/
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q) ;
}
/**
* 查詢節點p所屬的跟節點
* @param p
* @return
*/
private int find(int p) {
if (p < 0 || p >= parent.length) {
throw new IllegalArgumentException("節點id越界");
}
while(p != parent[p]){
p = parent[p];
}
return p;
}
@Override
public int getSize() {
return parent.length ;
}
}
基於size的優化
針對上一個版本的並查集, 存在一個問題, 每次都是隨機合併的, 會存在資料量大的集合向資料量小的集合進行合併, 會導致合併後的樹高度比較高, 如果每次合併的時候, 都是資料量較小的集合往資料量較大的集合合併的話, 會使合併後的集合的樹的高度沒有那麼高, 效能會有一定提高, 優化後代碼如下 :
package tree.uf;
/**
* 第三版並查集, 記錄每個根節點所在的樹的節點數量, 合併時數量少的樹合併到數量多的樹上面
* 使用樹來實現並查集
* 將陣列組織成樹的形式, 每個節點都指向一個父節點, 根節點指向自己
* 使用陣列索引表示當前節點位置, 陣列值表示父節點索引位置
*
* 查詢和union操作時間複雜度都是O(h), h表示樹高度
* @author 七夜雪
*
*/
public class UnionFind3 implements UF {
private int[] parent;
// 下標為對應根節點下標, 陣列值為對應根節點對應樹的節點數量
private int[] sz;
public UnionFind3(int size) {
this.parent = new int[size];
this.sz = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
sz[i] = 1;
}
}
/**
* 合併兩個節點
* 合併兩個節點之後, 表示這兩個節點相連了
* 同樣的, 這兩個節點的所有其他元素也都相連了
* 所以可以認為兩個節點合併之後, 就是把這兩個節點所在的集合合併成一個集合
* 合併方式:
* 找到節點p的根節點, 將p的根節點指向q的根節點
*
*/
@Override
public void union(int p, int q) {
// p的根節點
int pRoot = find(p);
// q的根節點
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
if (sz[pRoot] < sz[qRoot]) {
// 將p的根節點指向q的根節點
parent[pRoot] = qRoot;
sz[qRoot]+= sz[pRoot];
} else {
// 將q的根節點指向p的根節點
parent[qRoot] = pRoot;
sz[pRoot]+= sz[qRoot];
}
}
/**
* 判斷節點p和節點q是否相連
* p,q屬於一個集合時, 表示p,q相連, 這裡表示p,q有一個共同的根節點
*/
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q) ;
}
/**
* 查詢節點p所屬的跟節點
* @param p
* @return
*/
private int find(int p) {
if (p < 0 || p >= parent.length) {
throw new IllegalArgumentException("節點id越界");
}
while(p != parent[p]){
p = parent[p];
}
return p;
}
@Override
public int getSize() {
return parent.length ;
}
}
基於rank的優化
上面的優化其實並不是很精確, 僅僅考慮集合元素數量, 有時候樹的元素數量,和高度並不一致, 如下圖所示, 對4,2進行合併時, 按照上面的邏輯會將節點8指向節點7, 事實上節點7指向節點8的話, 效果會更好一點, 這個時候就可以根據樹的高度進行合併操作, 而不是單單考慮集合元素數量.
程式碼實現如下:
package tree.uf;
/**
* 第四版並查集, 對比第三版, 將合併時以兩個數節點數量為標準改為了以樹高度為標準
*
* 查詢和union操作時間複雜度都是O(h), h表示樹高度
* @author 七夜雪
*
*/
public class UnionFind4 implements UF {
private int[] parent;
// 下標為對應根節點下標, 陣列值為對應根節點對應樹的高度
private int[] rank;
public UnionFind4(int size) {
this.parent = new int[size];
this.rank = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
rank[i] = 1;
}
}
/**
* 合併兩個節點
* 合併兩個節點之後, 表示這兩個節點相連了
* 同樣的, 這兩個節點的所有其他元素也都相連了
* 所以可以認為兩個節點合併之後, 就是把這兩個節點所在的集合合併成一個集合
* 合併方式:
* 找到節點p的根節點, 將p的根節點指向q的根節點
*
*/
@Override
public void union(int p, int q) {
// p的根節點
int pRoot = find(p);
// q的根節點
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
if (rank[pRoot] < rank[qRoot]) {
// 將p的根節點指向q的根節點
parent[pRoot] = qRoot;
} else if(rank[pRoot] > rank[qRoot]) {
// 將q的根節點指向p的根節點
parent[qRoot] = pRoot;
} else {
parent[qRoot] = pRoot;
rank[pRoot] = rank[pRoot] + 1;
}
}
/**
* 判斷節點p和節點q是否相連
* p,q屬於一個集合時, 表示p,q相連, 這裡表示p,q有一個共同的根節點
*/
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q) ;
}
/**
* 查詢節點p所屬的跟節點
* @param p
* @return
*/
private int find(int p) {
if (p < 0 || p >= parent.length) {
throw new IllegalArgumentException("節點id越界");
}
while(p != parent[p]){
p = parent[p];
}
return p;
}
@Override
public int getSize() {
return parent.length ;
}
}
路徑壓縮
經過上述優化之後, 雖然儘量避免了樹的高度不平衡問題, 但是極端情況下, 仍然會出現樹的高度較高的情況, 在這種情況下, 可以進行路徑壓縮操作, 即每次合併時, 對路徑進行壓縮, 如下圖所示:
這裡可以簡單的使用parent[p] = parent[parent[p]], 即每次都將該節點指向其父節點的父節點的方式簡單的進行壓縮操作, 具體程式碼實現如下 :
package tree.uf;
/**
* 第五版並查集, 對比第四版, 在find時,添加了路徑壓縮
* 同時rank不在實際表示樹的高度了,只是用來標識高度大小
*
* @author 七夜雪
*
*/
public class UnionFind5 implements UF {
private int[] parent;
// 下標為對應根節點下標, 陣列值為對應根節點對應樹的高度,
// 壓縮之後不再表示樹的高度了, 但是還可以用來標示樹的高度大小, 所以這裡用rank, 不是height
private int[] rank;
public UnionFind5(int size) {
this.parent = new int[size];
this.rank = new int[size];
for (int i = 0; i < size; i++) {
parent[i] = i;
rank[i] = 1;
}
}
/**
* 合併兩個節點
* 合併兩個節點之後, 表示這兩個節點相連了
* 同樣的, 這兩個節點的所有其他元素也都相連了
* 所以可以認為兩個節點合併之後, 就是把這兩個節點所在的集合合併成一個集合
* 合併方式:
* 找到節點p的根節點, 將p的根節點指向q的根節點
*
*/
@Override
public void union(int p, int q) {
// p的根節點
int pRoot = find(p);
// q的根節點
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
if (rank[pRoot] < rank[qRoot]) {
// 將p的根節點指向q的根節點
parent[pRoot] = qRoot;
} else if(rank[pRoot] > rank[qRoot]) {
// 將q的根節點指向p的根節點
parent[qRoot] = pRoot;
} else {
parent[qRoot] = pRoot;
rank[pRoot] = rank[pRoot] + 1;
}
}
/**
* 判斷節點p和節點q是否相連
* p,q屬於一個集合時, 表示p,q相連, 這裡表示p,q有一個共同的根節點
*/
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q) ;
}
/**
* 查詢節點p所屬的跟節點
* @param p
* @return
*/
private int find(int p) {
if (p < 0 || p >= parent.length) {
throw new IllegalArgumentException("節點id越界");
}
while(p != parent[p]){
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
@Override
public int getSize() {
return parent.length ;
}
}