本文首發於我的Github博客
本文是數據結構預算法複習系列的第一篇博文,會介紹寫作該系列博文的原因
本文複習了並查集的概念,基礎的API,良好的實現(路徑壓縮與權重),簡單的應用和變式
寫作本篇博文的原因
- 生活所迫
- 懂的都懂,IT行業從業者(或者是考研保研),跑不了的
- 數據結構與算法是大學生活裏花費的時間很多的部分,複習一下,也算是對自己的大學生涯有個交代
- 三次元的生活真的無聊,找點代碼寫,嘿嘿(
難道不能寫工程項目嗎?質問!)
並查集的概念與基礎API
並查集的概念
假設擁有N個元素1…N,我們需要對其做兩種基礎的操作:
- 宣告某兩個元素a, b處在同一個等價類裏
- 判斷某兩個元素a, b是否在一個等價類裏
用於處理這種問題,提供這兩種操作接口的數據結構,可以被稱爲並查集
並查集基礎的API
interface UnionFind {
void union(int a, int b);
boolean isConnected(int a, int b);
}
良好的實現
良好的實現往往將一個等價類以樹的形式進行組織,使用一個數組int[] parent
,parent[i]
記錄了元素i
的父親節點
如下圖
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-tY25IREj-1592185859107)(union-find-tree.png)]
isConnected(a, b)
接口即判斷a
和b
是否在同一棵樹內(是否有同樣的根)union(a, b)
接口即需要將a
所屬的樹歸併到b
所屬的樹下,或者將b
所屬的樹歸併到a
所屬的樹下- 提高執行速度,就是想辦法降低樹的高度
路徑壓縮
路徑壓縮基本上能夠完全解決並查集的接口執行速度問題
其基本思想是:
在沿着某條路徑查詢到
a
元素的根之後,將這條路徑上所有的元素父親,都直接設爲根節點
這樣,在以後查詢的時候,查找到這條路徑上的元素,只需要一次向上查詢,就可以找到根節點了
路徑壓縮實現
public class PathCompressUnionFindImpl implements UnionFind {
int[] parent;
public PathCompressUnionFindImpl(int n) {
parent = new int[n];
for (int i = 0; i < parent.length; ++i) {
parent[i] = i;
}
}
private int find(int a) {
if (parent[a] == a) {
return a;
}
return parent[a] = find(parent[a]);
}
@Override
public void union(int a, int b) {
int rootA = find(a), rootB = find(b);
parent[rootA] = rootB;
}
@Override
public boolean isConnected(int a, int b) {
return find(a) == find(b);
}
}
加權並查集
雖然路徑壓縮基本能夠解決並查集的執行速度問題,但是還有一個解決方案值得我們思考
加權並查集,其實是基於這樣的觀察:
在代表一個等價類的樹的高度快速增長時,往往是由於將高的樹合併成爲了矮的樹的子樹
如果每次合併,都將矮的樹合併成爲高的樹的子樹,那麼樹的高度增長就會減緩
從數學的角度來看,減緩的原因是
每次當樹的高度增加1,就意味着樹的節點總數至少翻倍了
那麼一共有N個節點,樹的高度最多也就是lg N
加權並查集實現
public class WeightedUnionFindImpl implements UnionFind {
int[] parent, size;
private int find(int a) {
while (parent[a] != a) {
a = parent[a];
}
return a;
}
public WeightedUnionFindImpl(int n) {
parent = new int[n];
size = new int[n];
for (int i = 0; i < n; ++i) {
parent[i] = i;
size[i] = 1;
}
}
@Override
public void union(int a, int b) {
int rootA = find(a), rootB = find(b);
if (size[rootA] > size[rootB]) {
parent[rootB] = rootA;
size[rootA] += size[rootB];
} else {
parent[rootA] = rootB;
size[rootB] += size[rootA];
}
}
@Override
public boolean isConnected(int a, int b) {
return find(a) == find(b);
}
}
並查集的簡單應用
貼兩道OJ題目,都是簡單練手的
並查集的簡單變式
等價類的個數
也就是樹的根節點的個數,一個節點a
是根節點當且僅當parent[a] == a
int nClass() {
int nClass = 0;
for (int i = 0; i < parent.length; ++i) {
if (parent[i] == i) {
nClass++;
}
}
return nClass;
}
可以維護的等價類性質
比如:
- 給定元素
a
,求a
所在等價類的元素個數 - 給定元素
a
,求a
所在等價類的最大值 - 給定元素
a
,求a
所在等價類的最小值
這類性質只需要新開一個數組,並在union
時進行維護就可以了:
void union(int a, int b) {
//....
parent[rootA] = rootB;
size[rootB] += size[rootA];
maximum[rootB] = Math.max(maximum[rootA], maximum[rootB]);
minimum[rootB] = Math.max(minimum[rootA], minimum[rootB]);
}