
什么是并查集
并查集(Union-Find / Disjoint Set Union)维护不相交集合的合并与查询,只有两个操作:
- Find(x):x 属于哪个集合(返回集合代表元/根);
- Union(x, y):合并 x、y 所在的两个集合。
典型应用:图连通分量、Kruskal 最小生成树、网络连通性、社交网络”朋友圈”、动态连通性判断。
基本实现
用数组 parent 表示森林:parent[i] 是 i 的父节点,根的父节点指向自己。
type UnionFind struct {
parent []int
count int // 连通分量个数
}
func NewUnionFind(n int) *UnionFind {
uf := &UnionFind{parent: make([]int, n), count: n}
for i := 0; i < n; i++ {
uf.parent[i] = i
}
return uf
}
// Find 找到 x 所在集合的根
func (uf *UnionFind) Find(x int) int {
for uf.parent[x] != x {
x = uf.parent[x]
}
return x
}
// Union 合并 x、y 所在集合
func (uf *UnionFind) Union(x, y int) {
rx, ry := uf.Find(x), uf.Find(y)
if rx == ry {
return
}
uf.parent[rx] = ry
uf.count--
}
裸实现的问题:Union 顺序不当时树退化成链,Find 最坏 O(n)。两个优化解决它:路径压缩 + 按秩合并。
优化一:路径压缩
Find 沿途把路径上所有节点直接挂到根下面,后续查找几乎 O(1)。递归写法一行搞定:
func (uf *UnionFind) Find(x int) int {
if uf.parent[x] != x {
uf.parent[x] = uf.Find(uf.parent[x])
}
return uf.parent[x]
}

注意:单次 Find 仍可能 O(log n),但摊还下来接近常数——这正是并查集快的来源。
优化二:按秩合并
用 rank(近似树高)记录每棵树的高度,矮树挂到高树下,防止树长高:
type UnionFind struct {
parent []int
rank []int
count int
}
func NewUnionFind(n int) *UnionFind {
uf := &UnionFind{
parent: make([]int, n),
rank: make([]int, n),
count: n,
}
for i := 0; i < n; i++ {
uf.parent[i] = i
uf.rank[i] = 1
}
return uf
}
func (uf *UnionFind) Union(x, y int) {
rx, ry := uf.Find(x), uf.Find(y)
if rx == ry {
return
}
if uf.rank[rx] > uf.rank[ry] {
uf.parent[ry] = rx
} else if uf.rank[rx] < uf.rank[ry] {
uf.parent[rx] = ry
} else {
uf.parent[ry] = rx
uf.rank[rx]++
}
uf.count--
}

铁律:合并前先 Find 再比较根,把根挂到根上。直接挂普通节点会破坏森林结构,产生假根。
复杂度分析
- 只做路径压缩:均摊 O(log n);
- 只做按秩合并:最坏 O(log n);
- 两者都用:均摊 O(α(n)),α 是反阿克曼函数,增长极慢——n 取宇宙原子数量级也不超过 5,工程上视为常数。
空间复杂度 O(n)。
实战应用
1. 连通分量个数
func countComponents(n int, edges [][]int) int {
uf := NewUnionFind(n)
for _, e := range edges {
uf.Union(e[0], e[1])
}
return uf.count
}
2. 判断图中是否有环
func hasCycle(n int, edges [][]int) bool {
uf := NewUnionFind(n)
for _, e := range edges {
if uf.Find(e[0]) == uf.Find(e[1]) {
return true // 两端点已连通,再加这条边就成环
}
uf.Union(e[0], e[1])
}
return false
}
3. 朋友圈问题
func findCircleNum(isConnected [][]int) int {
n := len(isConnected)
uf := NewUnionFind(n)
for i := 0; i < n; i++ {
for j := i + 1; j < n; j++ {
if isConnected[i][j] == 1 {
uf.Union(i, j)
}
}
}
return uf.count
}
开源实现参考
- github.com/yourbasic/uf:简洁高效的 Go 实现,路径压缩,支持动态扩容,接口清晰(New/Union/Find/Count);
- github.com/kelvinlau/go-unionfind:按大小合并(Union by Size),提供 Connected 方法,支持重置;
- github.com/wangzheng0822/algo:路径压缩 + 按秩合并同时使用,文档详细。
总结
并查集的全部精华就两句话:Find 时路径压缩,Union 时按秩合并。两者都做,操作均摊常数时间。代码量极小、边界清晰,是动态连通性问题的默认首选。
