← SciSim / Computer Science

▲ Union-Find (Disjoint Set Union)

Path Compression + Union by Rank | Inverse Ackermann O(alpha(n)) | Kruskal/Connectivity

Standard Undergraduate

§1 — Interactive Simulation

Ready. Press Play or Next to begin.
Speed:
0
Operations
0
Comparisons
0/0
Step
n / size
FIND(x): // with path compression
  if parent[x] != x:
    parent[x] = FIND(parent[x]) // flatten path
  return parent[x]
UNION(x, y): // union by rank
  rx = FIND(x); ry = FIND(y)
  if rx == ry: return // same component
  if rank[rx] < rank[ry]: swap(rx, ry)
  parent[ry] = rx // merge smaller under larger
  if rank[rx] == rank[ry]: rank[rx]++

§2 — The Idea, Step by Step

Picture a party where everyone arrives in their own group of one. Each time two people realise they share a friend, their two groups merge into a single bigger group. At any moment you might want to do one of two things: merge these two groups, or ask which group is this person in? Union-Find is the data structure built to answer exactly those two questions — and to answer them almost instantly, no matter how many merges have already happened.

To make it concrete, give every element a parent. Follow the parent pointers upward and you always arrive at one special element whose parent is itself — the root, which acts as the name of the whole group. Two elements are in the same group exactly when they reach the same root. So $\text{FIND}(x)$ simply walks up to the root, and $x$ and $y$ are connected when $\text{FIND}(x) = \text{FIND}(y)$. To merge two groups, $\text{UNION}(x,y)$ finds both roots and points one at the other.

A worked merge. Start with five singletons $\{0\},\{1\},\{2\},\{3\},\{4\}$, so $\text{parent}[i]=i$. Call $\text{UNION}(0,1)$: the roots are $0$ and $1$, so point one root at the other, giving $\{0,1\}$. Then $\text{UNION}(2,3)$ gives $\{2,3\}$. Now $\text{UNION}(1,2)$: $\text{FIND}(1)=0$ and $\text{FIND}(2)=2$, so the two little trees join into $\{0,1,2,3\}$. Ask $\text{FIND}(3)$ and $\text{FIND}(0)$ — the same root — so $0$ and $3$ are now connected even though we never merged them directly.

Done naively, a long chain of merges can build a tall, skinny tree, and walking to the root grows slow — up to $O(n)$ steps. Two tricks fix that. Union by rank always hangs the shorter tree under the taller one, so trees stay bushy and shallow. Path compression happens during every $\text{FIND}$: after reaching the root, it re-points every node along the path straight to the root, so the next lookup is instant. Used together, the amortised cost per operation is $O(\alpha(n))$, where $\alpha$ is the inverse Ackermann function — a quantity so slow-growing that $\alpha(n) \le 4$ for any $n$ you could ever write down. In practice, each operation is effectively constant time.

In the controls above, the n slider sets how many items the step-by-step trace works through, and the Comparisons and Step counters tick up once per elementary operation — the same way each $\text{FIND}$ or $\text{UNION}$ costs you one unit of work in the analysis.

Try this in the sim above: (1) Press Next one click at a time and watch the Comparisons counter rise by exactly one each step — that is the “one unit per operation” cost you pay for every $\text{FIND}$. (2) Drag n from 4 up to 16 and press Reset; the total Step count grows much faster than $n$, a reminder of how the work can blow up without the rank-and-compression tricks. (3) Set the speed to 0.25x and play, so you can follow each individual step the way you would trace $\text{UNION}$ and $\text{FIND}$ by hand.

§3 — Algorithm Analysis & Derivation

SymbolMeaning
nInput size (vertices V, edges E, or array length)
T(n)Time complexity function
W(n)Worst-case complexity
A(n)Average-case complexity
hHeight or depth of structure
kNumber of distinct values / items
Step 1 -- Structure. Each set has a root (representative). parent[x] points to x's parent (root's parent is itself). rank[x] is upper bound on height of subtree rooted at x. Initially: parent[x]=x, rank[x]=0 for all x.
Step 2 -- FIND with Path Compression. Recursively find root. On the way back, set all nodes' parents directly to root (path compression). After FIND, the path is flattened to height 1. Amortized cost approaches O(1).
Step 3 -- UNION by Rank. Attach smaller-rank tree under larger-rank root. If ranks equal: attach either, increment winner's rank. This keeps tree height O(log n) without path compression, O(alpha(n)) with.
Step 4 -- Inverse Ackermann alpha(n). With both path compression AND union by rank: amortized O(alpha(n)) per operation. alpha(n) = inverse Ackermann function. alpha(n) <= 4 for any n that could ever be written down (alpha(10^80) = 4). Essentially O(1) in all practical cases.
Step 5 -- Proof Sketch. Tarjan (1975) proved the amortized bound. Simplified: path compression makes future FINDs faster (paying cost now saves later). Union by rank bounds tree height so each FIND traverses O(log n) initially. Together: amortized O(alpha(n)).
Step 6 -- Applications. Kruskal's MST (cycle detection). Connected components in dynamic graphs. Percolation theory. Image segmentation (pixel merging). Network connectivity queries. Offline LCA (Lowest Common Ancestor). Dynamic graph connectivity.

§4 — Frequently Asked Questions

What is path compression? concept+
Path compression sets every node on the FIND path directly to the root. Example: FIND(x) on path x->a->b->root sets parent[x]=root, parent[a]=root, parent[b]=root. Future FINDs from x, a, or b are O(1). Pays O(path length) now, saves on all future operations.
What is union by rank? concept+
Rank is an upper bound on tree height. When merging: attach root with smaller rank under root with larger rank. This prevents the tree from becoming a long chain. Without union by rank: repeated unions can create O(n) height trees and O(n) per FIND.
Why is the amortized complexity O(alpha(n)) and not O(1)? complex+
The Ackermann function A(n,n) grows incredibly fast. Its inverse alpha(n) grows incredibly slowly: alpha(n) <= 4 for n <= A(4,4) ~ 10^10000. The actual amortized cost is O(alpha(n)) per operation over m operations, total O(m*alpha(n)). Proved by Tarjan (1975) -- one of the most elegant analyses in CS.
Where is Union-Find used in Kruskal's algorithm? applied+
In Kruskal: for each edge (u,v), check if FIND(u)==FIND(v) (same component = would create cycle). If different components: add edge, call UNION(u,v). Union-Find makes each check/merge nearly O(1), giving Kruskal O(E log E + E*alpha(V)) = O(E log E).
What happens without union by rank? nonobv+
Without union by rank: in worst case, UNION always attaches first set under second regardless of size. After n-1 unions: a path of length n-1. FIND on deepest element: O(n). With only path compression (no rank): amortized O(log n). Both optimizations together: O(alpha(n)).
Is there a Union-Find with true O(1) operations? deep+
No deterministic algorithm is known with O(1) worst-case per operation. O(alpha(n)) amortized is optimal for the standard model (Fredman & Saks 1989 cell-probe lower bound). A randomized O(log* n) per operation exists. For offline queries (all known in advance), Tarjan's offline LCA gives O(alpha(n)) with Union-Find.

§5 — Common Misconceptions & Errors

Conceptual Misconceptions

✗ Union-Find gives O(1) per operation.

✓ O(alpha(n)) AMORTIZED with both optimizations. Without union by rank: O(log n). Without path compression: O(log n). Without both: O(n) worst case.

✗ Path compression alone is sufficient for O(alpha(n)).

✓ Both path compression AND union by rank are needed for O(alpha(n)). Path compression alone gives O(log n) amortized. Union by rank alone gives O(log n) per operation.

✗ UNION(x,y) immediately flattens the tree.

✓ UNION merges two trees but doesn't flatten them. Flattening (path compression) happens during FIND operations.

Implementation Errors

✗ Using union by size instead of union by rank gives different complexity.

✓ Union by size (attach smaller subtree under larger) also gives O(alpha(n)) amortized with path compression. It is equivalent in asymptotic complexity and often easier to implement correctly.

✗ Forgetting to call FIND before UNION -- using raw indices.

✓ Must do rx=FIND(x), ry=FIND(y) before merging. Directly merging parent[x]=y without FIND gives incorrect results when x and y are not roots.

✗ Assuming parent[x]==x means x is isolated (not in any set).

✓ parent[x]==x means x is the ROOT of its set -- it may represent a set with many elements. An isolated singleton also has parent[x]==x. Must track component sizes separately if needed.