Disjoint Sets (Union-Find)
The disjoint-set data structure tracks a partition of elements into groups, answering "are these two in the same group? " and merging groups on demand.
╌╌╌╌
Some problems keep a collection of items partitioned into disjoint groups that only ever merge, never split, and repeatedly ask whether two items currently share a group. Are these two cities on the same electrical grid? Do these two pixels belong to the same connected region? Does adding this edge to a graph create a cycle? The disjoint-set (or union-find) data structure answers exactly these questions, and does so in near-constant amortized time per operation1, slow-growing enough that for any realistic input it is effectively .
There is a recurring lesson in the design of efficient algorithms: the right data structure is what makes an algorithm fast. Dijkstra's and Prim's shortest- path and MST algorithms are correct with any priority queue, but their speed hinges on the queue: a binary heap gives , while a Fibonacci heap (with amortized ) shaves it toward . Kruskal's MST illustrates the same point with a different structure. The algorithm is one line of logic, and every bit of its efficiency comes from the disjoint-set structure beneath it. We will build that structure from the ground up and improve it from per query down to inverse Ackermann.
The disjoint-set ADT
We maintain a collection of disjoint sets that together partition a universe of elements. Each set is named by a representative, some fixed member of the set chosen by the structure. The ADT has three operations:
- creates a new set whose only member is (so is its own representative). must not already be in any set.
- returns the representative of the set containing . Two elements are in the same set iff they return the same representative.
- merges the sets containing and into one, picking a representative for the combined set. The two old sets are destroyed.
The query are and together?
is just the test
. After operations
there can be at most Union operations, since each union reduces the
number of sets by one.
The forest representation
The fast implementation represents each set as a rooted tree, and the whole collection as a forest.2 Every element points only to its parent; the root of each tree is the set's representative and points to itself. There are no child pointers and no key ordering. This is not a search tree, just a tangle of upward pointers whose only job is to lead to a root.
Two sets: with representative , and with representative . Each node's single arrow points at its parent; the roots loop to themselves. follows parent pointers up to the root; Union makes one tree's root a child of the other's.
- 1Make-Set(x):
- 2
- 3Find-Set(x):
- 4while do
- 5walk to the root
- 6return
- 7Union(x, y):
- 8
So far this is correct but not fast: a careless sequence of unions can build a tall, skinny tree, a path of nodes, making cost . Two heuristics, used together, flatten the forest and make the structure fast.
A warm-up: labels, and always relabel the smaller side
Before the forest, consider the most naive possible implementation, along with the one idea that already makes it efficient. Keep an array that stores, for each element, a label naming its current set. Then is just , a single array lookup, and the same-set test is instant. The whole cost is in : merging two sets means walking through one of them and rewriting every member's label to match the other.
The question is which set to rewrite. If we are careless and always relabel, say, the set containing , an adversary can force work on every union. The fix is a single disciplined rule:
To do this efficiently, alongside keep a list of the elements currently carrying label , plus each set's size; the union then splices the smaller list into the larger and relabels only the short side.
Why does this help so much?
This small example already shows the idea: a structurally trivial rule (relabel the
smaller side) plus an amortized doubling
argument turns a quadratic-looking
cost into . The forest representation below keeps exactly this
intuition, the smaller thing yields to the larger, but replaces the explicit
relabeling with a single pointer move, so a union becomes instead of
.
Everything now hinges on which root we hang beneath the other. Get it wrong and the forest degenerates into exactly the chain from before; get it right and the trees stay flat. The figure contrasts the two outcomes of the same four merges.
Heuristic 1: union by rank
The trouble is unions that make a tall tree a child of a short one, deepening it. Union by rank prevents this. Each root carries a rank, an upper bound on the height of its tree. When uniting two trees, we attach the root of smaller rank beneath the root of larger rank, so the taller tree's height never grows. Only when the two ranks are equal does the height increase, and then by exactly one (and we bump the surviving root's rank).
This single rule already guarantees a logarithmic height bound:
Notice this is the same doubling argument from the warm-up, read from the other direction: there, a set doubled each time an element was relabeled; here, a root's rank rises only when two equal-rank trees merge, which doubles the node count. Either way, is the ceiling, because nothing can double more than that many times.
The figure shows a under this rule. The left tree has rank , the
right rank ; since their ranks differ, the smaller-rank root is hung
beneath the larger-rank root and no rank changes. The result still has
rank , exactly as the warm-up's smaller side yields to the larger
demands,
but now it costs a single pointer move rather than relabeling every member.
Heuristic 2: path compression
Path compression attacks the cost from the other side. Each time walks up to the root, it makes a second pass and points every node it visited directly at the root. The path is paid for once; every future on those nodes is then a single hop.
Before, sits at the bottom of a chain ; after , the nodes , , all point straight at . The that pays for the walk leaves the tree much flatter for every later operation.
- 1if then
- 2callpoint x at the root
- 3return
The recursion bottoms out at the root, and as it unwinds it reassigns every node's parent to that root. With path compression in use, the rank of a root is only an upper bound on its height (compression can make the tree shorter than its rank suggests), which is why the heuristic is called union by rank rather than by height.
The near-constant amortized bound
Used together, union by rank and path compression drive the disjoint-set structure's cost to near-constant.
The function grows so slowly it is practically constant: for every up to roughly , a number far larger than the count of atoms in the universe. So for any conceivable input, each operation costs amortized .3
The two heuristics achieve this by attacking complementary failure modes: union
by rank keeps trees from getting tall in the first place (height ),
while path compression ensures that any depth a tree does accumulate gets
paid down and reused, so the expensive walks cannot recur. Neither alone gives
(union by rank alone is amortized, path compression
alone is amortized), but their combination collapses to inverse
Ackermann. The full proof uses a subtle potential-function (amortized) argument,
charging each node's cost against the steady growth of the ranks above it. The
intuition to keep is that a node can be lifted closer to the root
only so many
times before it is the root's child, and ranks climb too slowly for that to
happen often.
A worked trace: both heuristics together
For a trace of the two heuristics together, start with seven singletons , each its own tree of rank , and process this sequence of operations:
Union by rank governs each merge, breaking ties by keeping the first-named root.
- . Both rank ; equal ranks, so hang under and bump to rank . Tree: .
- . Both rank ; hang under , becomes rank . Tree: .
- . Both roots have rank ; equal ranks, so hang under and bump to rank . Now has children and , and still has child : sits at depth .
- . Both rank ; hang under , to rank .
- . has rank , has rank ; ranks differ, so hang the smaller-rank root under and no rank changes ( stays rank ). The tree rooted at now holds all seven elements, with and at depth .
- . Walk to reach root , then compress: point (and , already a child of ) straight at . The next is a single hop.
Two details from this run are worth stating precisely. First, only step 1, 2, 3, 4
ever raised a rank, and each raise required merging two trees of equal rank,
exactly the doubling that caps rank at . Step 5 merged unequal ranks
and left every rank untouched, which is the common case in practice. Second,
notice that after compression 's rank-based ancestor is still recorded as
rank even though is now a leaf, this is why ranks are only an upper
bound on height, and why the heuristic keeps the name rank
rather than
height.
Application: connectivity and minimum spanning trees
Two applications make the structure indispensable.
Connectivity. Given a graph, call on every vertex, then for every edge . Afterward, holds iff and lie in the same connected component. The structure also processes online edge insertions: each new edge is one Union, and connectivity queries between insertions are each one pair of calls, both amortized .
Kruskal's minimum spanning tree. Kruskal's algorithm builds a minimum spanning tree by scanning edges in increasing weight order and adding each edge unless it would form a cycle. An edge forms a cycle exactly when and are already connected (i.e. already in the same set), which is the disjoint-set query verbatim.4
- 1
- 2foreach vertex in do
- 3call
- 4sort the edges of into nondecreasing order by weight
- 5foreach edge in that order do
- 6if call call Find-Set then
- 7joins two components
- 8call
- 9return
Now read off the running time. Sorting the edges costs , and since a simple graph has edges we have , so the sort is , and this dominates. The disjoint-set work spans tests and s; even with only the warm-up's relabel-the-smaller scheme this is , already cheaper than the sort, and with the rank/compression forest it is , effectively linear. Either way:
The lesson here is worth stating plainly: Kruskal's logic is one acyclicity test per edge, but its efficiency is entirely a property of the structure answering that test. The cycle test is the same-set query, and a good disjoint-set structure is what makes Kruskal both simple and fast.
Lower bounds, and where union-find runs
Two threads extend the textbook treatment, one theoretical and one practical.
The bound is tight, and is unavoidable. It is natural to suspect the inverse-Ackermann factor is an artifact of a loose analysis, that a cleverer argument would prove . It cannot. Fredman and Saks (1989) proved a matching lower bound: in the cell-probe model, any data structure for the disjoint-set problem must spend time on some sequence of operations. So Tarjan's analysis is not merely the best known, it is the best possible, and is an intrinsic feature of the problem, not of the algorithm. The function itself is the inverse of the fast-growing Ackermann function , whose rows climb from addition () to multiplication, exponentiation, towers of exponents, and beyond; asks how many rows up you must go before the values exceed , and the answer is at most for any that could be written down.
Beyond the merge-only model. Plain union-find handles only incremental
connectivity, edges arrive and components merge, never split. Two extensions
answer harder queries. A union-find with rollback (used inside offline
dynamic-connectivity algorithms) forgoes path compression, so that unions can be
undone in a stack discipline; it keeps per operation but supports a
decremental or fully offline stream of edge insertions and deletions. And the
Euler-tour / link-cut structures solve fully dynamic connectivity, edges
inserted and deleted online, in amortized time, well outside what
parent pointers can do. In practice, the merge-only structure is enough for the
dominant applications: Kruskal's MST, connected-component labeling in image
segmentation (the Felzenszwalb–Huttenlocher segmenter is union-find on a pixel
graph, edges scanned by weight exactly as in Kruskal), percolation simulations,
and the type-inference union
of equivalence classes in compilers.5
Takeaways
- The disjoint-set ADT — , , Union — maintains a
partition under merges and answers
same group?
by comparing representatives. - A labels + relabel-the-smaller warm-up already costs only total: each element is relabeled times because its set doubles whenever it moves. This doubling argument is the seed of union by rank.
- The forest representation stores each set as a tree of parent pointers whose root is the representative; walks to the root, Union links two roots, turning the warm-up's relabel into one pointer move.
- Union by rank keeps trees short (attach shorter under taller), and path compression flattens each path to point straight at the root.
- Together they give amortized time per operation — inverse Ackermann, so for any realistic , i.e. effectively constant.
- It powers connectivity queries and Kruskal's MST, where the same-set test doubles as the cycle test. Kruskal runs in , with the sort, not the union-find, as the bottleneck.
- The unifying theme: clever data structures are what make algorithms fast. Kruskal's logic is one line; all of its speed comes from the disjoint-set structure underneath.
Footnotes
- CLRS, Ch. 21 — Data Structures for Disjoint Sets (§21.1): the Make-Set/Find-Set/Union ADT and its near-constant amortized cost. ↩
- Erickson, Ch. — Disjoint Sets: the parent-pointer forest representation of a partition. ↩
- CLRS, Ch. 21 — Data Structures for Disjoint Sets (§21.4): Tarjan's inverse-Ackermann amortized bound. ↩
- Skiena, §6.1 — Union-Find: the cycle test in Kruskal's MST coincides with the same-set query. ↩
- Fredman & Saks,
The cell probe complexity of dynamic data structures
(1989), for the lower bound; Felzenszwalb & Huttenlocher,Efficient graph-based image segmentation
(2004), for union-find segmentation; Holm, de Lichtenberg & Thorup,Poly-logarithmic deterministic fully-dynamic algorithms for connectivity
(2001), for fully dynamic connectivity. ↩
╌╌ END ╌╌