並查集
把東西分成不相交的群組,隨時問「這兩個是不是同一群」。路徑壓縮加上按大小合併,讓每次操作幾乎都是常數時間。
版本
a
b
箭頭從子節點指向父節點;最上面一排是各群的根。
正在往上走根指標被改寫
每個元素指向自己的父節點,指向自己的就是一群的代表。選兩個元素合併,或查某個元素屬於哪一群;切換 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 O | n = 1,000 | n = 2,000 | n = 4,000 | n = 8,000 | 成長倍數:實測(理論) | |
|---|---|---|---|---|---|---|
| naive | O(n) | 500 | 1,000 | 2,000 | 4,000 | ×8.0 (×8.0) |
| 按大小合併+路徑壓縮 | O(1) | 1.0 | 1.0 | 1.0 | 1.0 | ×1.0 (×1.0) |
同一串合併:naive 版長成一條鏈,查找平均要走半條鏈;最佳化的版本每個元素都直接接在根底下,不管 n 多大都只要一步。
和其他做法比
| 依序合併後:最深幾步 | 依序合併後:查找平均幾步 | 隨機合併後:查找平均幾步 | |
|---|---|---|---|
| naive | 999 | 499.50 | 61.41 |
| 按大小合併+路徑壓縮 | 1 | 1.00 | 1.13 |
1,000 個元素,兩個版本跑同樣的合併:「依序」是 (0,1)、(1,2)…;「隨機」是 1,000 次隨機挑兩個合併。合併完再把每個元素都查一次,數走過幾個父指標。
真實世界裡的它
- 影像處理標記連通區域、遊戲判斷地圖上兩格是否相通。
- 編譯器的型別推論(unification)和等價類別合併。
- 網格模擬的滲流問題:上下兩邊什麼時候第一次接通。
取捨與陷阱
- 只做一半的最佳化差很多:沒有按大小合併,依序合併就會長成一條鏈(試試 naive 版的「依序串起 0–9」)。
- 遞迴寫的路徑壓縮在一條很長的鏈上會爆堆疊;這裡用兩趟迴圈,不會有這個問題。
- 它只回答「是不是同一群」,不會告訴你兩者之間的路徑;要路徑得用 BFS(Breadth-First Search,廣度優先搜尋)。