HomeArticlesAlgorithms

Union-Find: Merging Sets in Near-Constant Time

Two operations, and the two tricks - union by rank and path compression - that push disjoint-set queries to near-constant amortized time.

mysimulator teamUpdated June 2026≈ 7 min read▶ Open the simulation

Two operations, one forest

A disjoint-set (union-find) structure keeps a collection of elements partitioned into non-overlapping groups and answers two questions fast: find(x) - which group is x in? - and union(x, y) - merge x's group with y's group. Internally each set is a tree, and every element stores only a pointer to its parent. The root of a tree is the group's canonical representative: two elements sit in the same set exactly when their roots match.

live demo · nodes merging into disjoint-set trees● LIVE

Kruskal's minimum spanning tree algorithm is the classic use case: sort edges by weight, then walk them in increasing order and add an edge to the tree unless its two endpoints are already connected - a question union-find answers in the time it takes to walk two short chains to their roots. The same structure tracks connected components as edges stream in one at a time, detects cycles in a graph, and merges regions in image segmentation.

The naive version, and why it degrades

A first attempt makes find walk parent pointers to the root, and union attach one root under the other arbitrarily. That works, but nothing stops a tree from becoming a long chain - union elements in increasing order, always attaching the new root under the previous one, and you get a straight line of n nodes. Find then costs O(n) instead of the O(log n) or better you actually want.

function find(x) {
  while (parent[x] !== x) x = parent[x];
  return x;
}
function union(x, y) {
  const rx = find(x), ry = find(y);
  if (rx !== ry) parent[rx] = ry;   // arbitrary attachment - can chain badly
}

Union by rank

The fix is to always attach the shallower tree under the deeper one. Track either an exact size or an upper-bound rank (roughly, tree height) per root; when merging, the root with the smaller rank becomes a child of the root with the larger rank, and ties bump the winner's rank by one. This alone caps every tree's height at O(log n), because a tree of rank r can only be built by merging two trees of rank r minus 1, so it takes at least 2 to the power r elements to reach rank r.

Path compression: flattening as you go

Union by rank bounds the height, but you can do much better by rewriting history every time you walk to a root. Path compression makes every node visited during a find() point directly at the root once that root is known, so the next find from any of those nodes is O(1).

function find(x) {
  if (parent[x] !== x) parent[x] = find(parent[x]);   // point straight at the root
  return parent[x];
}

Tarjan and van Leeuwen proved that combining union by rank with path compression drives the amortized cost of any sequence of m operations on n elements down to O(m · alpha(n)), where alpha is the inverse of the fast-growing Ackermann function. For every practical n, alpha(n) is at most 4 or 5, so the structure behaves as if each operation were O(1) - a rare case where a genuinely non-constant bound is indistinguishable from constant time on real inputs.

Frequently asked questions

What is the difference between union-find and a graph traversal?

A BFS or DFS answers connectivity queries in O(V+E) each time you ask, and needs the whole graph in memory. Union-find processes edges one at a time as they arrive, answers same-set queries in near-constant amortized time, and never revisits an edge once it has been unioned - ideal for streaming graphs or for Kruskal's algorithm.

Why is the inverse-Ackermann function so slow-growing?

Because it inverts the Ackermann function, which grows faster than any tower of exponentials. Its inverse therefore grows unimaginably slowly - it stays below 5 for any n you could ever store on a real computer, which is why union-find with both optimizations is treated as effectively constant time in practice.

Do I need both union by rank and path compression?

Either optimization alone already gives a good bound, O(log n) per operation. Combining both drops the amortized cost to O(alpha(n)), the inverse-Ackermann function. Most implementations use both since each is nearly free to add on top of the other.

Try it live

Everything above runs in your browser - open Union-Find and change the parameters while it is running. Nothing is installed, nothing is uploaded, the whole model lives in one tab.

▶ Open Union-Find simulation

What did you find?

Add reproduction steps (optional)