Part V · Advanced Data Structures Chapter 19 Extra depth

Data Structures for Disjoint Sets

Two heuristics, three lines each, that take a structure from linear to effectively constant — and produce the strangest running time in the book.

A disjoint-set structure maintains a collection of non-overlapping sets under two operations: merge two sets, and ask which set an element belongs to. It is a small interface with an outsized payoff, because the analysis is the most surprising in CLRS. A naive implementation is O(n) per operation. Add union by rank and path compression — six lines of code between them — and the amortized cost falls to O(α(n)), where α is the inverse Ackermann function and is at most 4 for any n that could be written down in this universe.

4th edition note. Numbering shifted from 21 to 19. Content is unchanged. Note that the 3rd edition’s neighbouring chapters on Fibonacci heaps and van Emde Boas trees were removed from the printed 4th edition, which is why Part V is now only three chapters.

Contents

  1. The three operations
  2. The application: connected components
  3. Linked-list representation
  4. Disjoint-set forests
  5. Union by rank
  6. Path compression
  7. The inverse Ackermann function
  8. Recap

The three operations

A disjoint-set data structure maintains a collection S = {S₁, S₂, …, Sₖ} of disjoint dynamic sets. Each set is identified by a representative, some member of it. Which member is the representative does not matter, only that it stays the same as long as the set is unchanged.
OperationMeaning
MAKE-SET(x)Create a new set whose only member, and representative, is x. Requires x not already in another set.
UNION(x, y)Merge the two sets containing x and y into one, and destroy the originals.
FIND-SET(x)Return a pointer to the representative of the set containing x.

Throughout, n is the number of MAKE-SET operations and m is the total number of operations. Since the sets are disjoint, each UNION reduces the set count by one, so there are at most n - 1 unions.

The application: connected components

The motivating use is deciding, for an undirected graph, whether two vertices are connected.

CONNECTED-COMPONENTS(G) 1 for each vertex v ∈ G.V 2 MAKE-SET(v) 3 for each edge (u, v) ∈ G.E 4 if FIND-SET(u) ≠ FIND-SET(v) 5 UNION(u, v) SAME-COMPONENT(u, v) 1 return FIND-SET(u) == FIND-SET(v)

Each vertex starts alone; each edge merges the two endpoints’ components. After processing every edge, two vertices are connected exactly when they share a representative.

Depth-first search from Chapter 20 also finds connected components, in O(V + E). Disjoint sets win when the graph is dynamic — edges arriving over time with connectivity queries interleaved — because DFS would have to rerun from scratch after every edge. This is also the exact structure Kruskal’s algorithm in Chapter 21 uses to detect whether an edge would form a cycle.

Linked-list representation

The first attempt: each set is a linked list. The list head is the representative, and every object points back to it.

The quadratic worst case. Perform n MAKE-SETs, then union the sets one at a time always appending the larger list onto the smaller. The ith union updates i pointers, so n-1 unions cost Θ(n²) — amortized Θ(n) per operation.
The weighted-union heuristic: always append the shorter list onto the longer, keeping a length field in each head. Now an object’s pointer is updated only when it is in the smaller set, which means the set it belongs to at least doubles in size each time. So each object is updated at most lg n times, and a sequence of m operations costs O(m + n lg n).

The doubling argument here is worth internalising on its own — the same “merge the smaller into the larger” trick gives good bounds in many other settings.

Disjoint-set forests

The faster representation abandons lists for trees. Each set is a rooted tree, each node points only to its parent, and the root is the representative and points to itself.

MAKE-SET(x) FIND-SET(x) UNION(x, y) 1 x.p = x 1 if x ≠ x.p 1 LINK(FIND-SET(x), 2 x.rank = 0 2 return FIND-SET(x.p) FIND-SET(y)) 3 return x.p
By itself this is no better. A tree can degenerate into a single path of n nodes, making FIND-SET cost Θ(n). The representation only pays off once both heuristics are applied.

Union by rank

Union by rank: make the root of the tree with smaller rank point to the root with larger rank. rank is an upper bound on the node’s height. On a tie, pick either and increment the winner’s rank by one.
LINK(x, y) 1 if x.rank > y.rank 2 y.p = x 3 else 4 x.p = y 5 if x.rank == y.rank 6 y.rank = y.rank + 1 // only case where a rank grows

This is the tree analogue of the weighted-union heuristic: hang the shallower tree under the deeper one so the result does not get taller. Union by rank alone gives O(m lg n).

Path compression

Path compression: during FIND-SET, after finding the root, make every node on the path point directly to the root. It costs nothing extra asymptotically — the path was walked anyway — and it flattens the tree permanently for all future queries.
FIND-SET(x) // with path compression 1 if x ≠ x.p 2 x.p = FIND-SET(x.p) // two-pass: recurse, then re-point 3 return x.p

Three lines. The recursion walks up to the root; the assignment on the way back down re-parents every node visited.

before FIND-SET(e) a b c e depth 3 FIND-SET(e) re-point on the way back after a b c e every node now one hop from the root
Figure 19.1 — Path compression. The work was already being done to reach the root; the only addition is writing the pointers back.
Ranks become upper bounds, not heights. Path compression shortens paths but does not update ranks — doing so would be expensive. So after compression a node’s rank may exceed its actual height. That is fine: the analysis only ever needs rank as an upper bound, and the two heuristics remain compatible.

The inverse Ackermann function

Theorem 19.14. A sequence of m MAKE-SET, UNION, and FIND-SET operations, n of which are MAKE-SET, can be performed on a disjoint-set forest with union by rank and path compression in worst-case time O(m · α(n)).
Heuristics usedTotal for m operations
NeitherO(m n)
Union by rank onlyO(m lg n)
Path compression onlyΘ(n + f·(1 + log2+f/n n))
BothO(m · α(n))

α(n) is the inverse of the Ackermann function, which grows so explosively that its inverse grows more slowly than any function you are likely to name — slower than lg n, slower than lg* n, slower than lg lg lg n.

n in rangeα(n)
0 to 20
31
4 to 72
8 to 20473
2048 to A₄(1), a number far exceeding the atoms in the observable universe4
So for every practical purpose α(n) ≤ 4 and the operations are constant time. CLRS is careful to say the bound is not constant — α(n) does grow without bound, just unimaginably slowly — but no input you will ever run on can push it past 4. Tarjan later proved Θ(m α(n)) is also a lower bound for this problem, so the analysis is tight and cannot be improved.
Where the two heuristics each contribute. Union by rank keeps trees shallow as they are built. Path compression flattens them as they are queried. Either alone gives a logarithmic bound; only together do they reach α(n), and proving that is one of the hardest analyses in the book — CLRS devotes a full section to it using a potential function in the style of Chapter 16.

Recap

The seven things to carry forward

Where this goes next

Part V is complete, and with it every data structure in the book. Part VI turns to graphs. Chapter 20 covers the representations — adjacency lists and matrices — and the two traversals, breadth-first and depth-first search, that every later graph algorithm is built from, along with topological sort and strongly connected components.


Ch 18 — B-Trees Ch 20 — Elementary Graph Algorithms