Union-Find and disjoint sets
The parent array, union by rank and path compression, why the amortised cost is nearly constant, and the cycle detection and clustering patterns it powers.
A forest in an array
Union-Find tracks which elements belong to the same group. Each element points at a parent; the root of a tree is the group's representative. Two elements are in the same set when they share a root.
parent = list(range(n)) # every element starts as its own root
rank = [0] * n # upper bound on subtree height
def find(x):
root = x
while parent[root] != root: # walk to the root
root = parent[root]
while parent[x] != root: # path compression: flatten the chain
parent[x], x = root, parent[x]
return root
def union(a, b):
ra, rb = find(a), find(b)
if ra == rb:
return False # already together: cycle detected
if rank[ra] < rank[rb]:
ra, rb = rb, ra
parent[rb] = ra # attach the shorter tree under the taller
if rank[ra] == rank[rb]:
rank[ra] += 1
return TrueWhy it is effectively constant
| Optimisation | Union cost | Find cost | Note |
|---|---|---|---|
| None | O(1) | O(n) worst case | A long chain of parents |
| Union by rank only | O(log n) | O(log n) | Height stays logarithmic |
| Path compression only | O(log n) amortised | O(log n) amortised | Flattens on access |
| Both | O(alpha(n)) | O(alpha(n)) | Alpha is under 5 for any real n |
The inverse Ackermann function grows so slowly that for any input that fits in memory the cost is a small constant. Union-Find is the standard example of an amortised bound that is better than the naive worst case by a huge margin.
# counting groups after a series of unions
groups = len({find(i) for i in range(n)})
# largest component size
from collections import Counter
sizes = Counter(find(i) for i in range(n))
largest = max(sizes.values())What you build with it
- Kruskal's algorithm — sort edges by weight and union the endpoints; union returning false means the edge would close a cycle, so skip it.
- Cycle detection in an undirected graph, with no graph traversal at all.
- Connected components in a dynamic graph where edges are only added.
- Clustering and segmentation — merge pixels or records until a threshold stops the merging.
- Account or entity resolution — union records that share an email or phone number.
def kruskal(n, edges):
edges.sort(key=lambda e: e[2]) # by weight
return [e for e in edges if union(e[0], e[1])]💡
Union-Find only supports merging. There is no split operation, and no way to undo a union without rebuilding. If your problem needs deletion or rollback, you need a different structure such as a dynamic connectivity tree.
FAQ
Can I use Union-Find with string keys?
Map the strings to integer indices once, then work on the integers. The structure is array-based by design, and adding a dictionary lookup inside find would destroy its performance.
What is the difference between rank and size?
Both are valid heuristics. Union by size attaches the smaller set under the larger by element count; rank uses the height. Rank is slightly cheaper to maintain and gives the same bound.
Related
Graphs and their representations LRU caches and composite structures
Last refreshed 2026-09-18.