跳到主要內容

資料結構

資料怎麼排,決定了哪些操作便宜

主題 · 並查集

並查集

把東西分成不相交的群組,隨時問「這兩個是不是同一群」。路徑壓縮加上按大小合併,讓每次操作幾乎都是常數時間。

版本
a
b
0123456789

箭頭從子節點指向父節點;最上面一排是各群的根。

正在往上走根指標被改寫

每個元素指向自己的父節點,指向自己的就是一群的代表。選兩個元素合併,或查某個元素屬於哪一群;切換 naive 版看看差別。

幾群
10
最深的元素(步)
0
這次走了幾個指標
–
亮起來的是這一步執行的程式碼
class NaiveUnionFind {
parent: number[];
constructor(n: number) {
this.parent = Array.from({ length: n }, (_, i) => i);
}
find(x: number): number {
while (this.parent[x] !== x) x = this.parent[x];
return x;
}
union(a: number, b: number): void {
const ra = this.find(a), rb = this.find(b);
if (ra === rb) return;
this.parent[ra] = rb;
}
}
class UnionFind {
parent: number[];
size: number[];
constructor(n: number) {
this.parent = Array.from({ length: n }, (_, i) => i);
this.size = new Array(n).fill(1);
}
find(x: number): number {
let root = x;
while (this.parent[root] !== root) {
root = this.parent[root];
}
while (this.parent[x] !== root) {
const next = this.parent[x];
this.parent[x] = root;
x = next;
}
return root;
}
union(a: number, b: number): void {
let ra = this.find(a), rb = this.find(b);
if (ra === rb) return;
if (this.size[ra] < this.size[rb]) [ra, rb] = [rb, ra];
this.parent[rb] = ra;
this.size[ra] += this.size[rb];
}
}

模型假設與範圍

  • Big O 與實測步數以頁面列出的基本操作為單位;視覺化的快照、畫圖、程式高亮和輸出成本另計,不是執行時間 benchmark。

什麼時候用

  • 東西會一直被合併成群,而且要隨時問「這兩個是不是同一群」:連通分量、朋友圈、網路裡哪些機器互通。
  • Kruskal 最小生成樹:一條邊的兩端已經在同一群,加進來就會成環。
  • 只會合併、不會拆開的情況;要能拆開,並查集就不適合。

和其他主題的關係

由這些組成
動態陣列
延伸閱讀
圖的表示法

時間與空間複雜度(Big O)

操作平均最差
查找、合併(naive)
平均要看合併的順序;最差就是依序合併成一條鏈
—O(n)
只用按大小合併
小的接到大的底下,樹高不會超過 log₂ n
O(log n)O(log n)
按大小合併+路徑壓縮
平均是均攤值;α 是反阿克曼函數,對任何實際的 n 都不超過 4
O(α(n))O(log n)

空間:O(n),每個元素一個父指標和一個大小

Big O 實測:n 變大時步數怎麼長

數的是:依序合併後,每次查找平均走過的父指標數

Big On = 1,000n = 2,000n = 4,000n = 8,000成長倍數:實測(理論)
naiveO(n)5001,0002,0004,000×8.0 (×8.0)
按大小合併+路徑壓縮O(1)1.01.01.01.0×1.0 (×1.0)

同一串合併:naive 版長成一條鏈,查找平均要走半條鏈;最佳化的版本每個元素都直接接在根底下,不管 n 多大都只要一步。

和其他做法比

依序合併後:最深幾步依序合併後:查找平均幾步隨機合併後:查找平均幾步
naive999499.5061.41
按大小合併+路徑壓縮11.001.13

1,000 個元素,兩個版本跑同樣的合併:「依序」是 (0,1)、(1,2)…;「隨機」是 1,000 次隨機挑兩個合併。合併完再把每個元素都查一次,數走過幾個父指標。

真實世界裡的它

  • 影像處理標記連通區域、遊戲判斷地圖上兩格是否相通。
  • 編譯器的型別推論(unification)和等價類別合併。
  • 網格模擬的滲流問題:上下兩邊什麼時候第一次接通。

取捨與陷阱

  • 只做一半的最佳化差很多:沒有按大小合併,依序合併就會長成一條鏈(試試 naive 版的「依序串起 0–9」)。
  • 遞迴寫的路徑壓縮在一條很長的鏈上會爆堆疊;這裡用兩趟迴圈,不會有這個問題。
  • 它只回答「是不是同一群」,不會告訴你兩者之間的路徑;要路徑得用 BFS(Breadth-First Search,廣度優先搜尋)。

LeetCode 練習