Union-find path compression and weighting: which implementation the α(n) bound belongs to
How size weighting, path compression and path halving change a union-find parent array, and which bound, worst-case Θ(log n) or O(m α(n)) per sequence, algs4 gives each implementation.
Union-find (disjoint sets) is often summed up as “practically O(1)” or “O(α(n)) per operation”. That claim comes with conditions. The implementation has to combine weighting (by size or rank) with path compression, and the bound covers a whole sequence of operations starting from an empty structure, not any single operation. The implementation docs on the official site for Sedgewick and Wayne’s Algorithms, 4th Edition (algs4) say that even with both techniques, a single union or find takes Θ(log n) time in the worst case.
Below, using four algs4 implementations, I separate what each technique does to the parent array and which complexity statement belongs to which implementation.
The parent array and find
Quick-union style implementations store, for each element, another element of the same set (or itself) in parent[i]. An element with parent[i] == i is a root, and find follows links up to the root. Two elements are in the same set if they reach the same root. Union finds both roots and links one under the other.
So the cost of find depends on how deep the starting node is. Section 1.5 of algs4 counts cost in array accesses and states as a proposition that find in quick-union uses 1 plus twice the depth of the node. Every technique below is a way of keeping that depth down.
Size weighting: smaller tree under the larger
Plain quick-union doesn’t choose which root goes under which. Call union(i, i+1) for i = 0, 1, … on n elements, and algs4’s QuickUnionPathCompressionUF (id[rootP] = rootQ) builds a chain 0 → 1 → … → n−1. Each find in that sequence starts at a root, so there is no path to compress, and element 0 ends up at depth n−1.
WeightedQuickUnionUF records each tree’s element count in size[] and links the root of the smaller tree under the root of the larger one.
if (size[rootP] < size[rootQ]) { parent[rootP] = rootQ; size[rootQ] += size[rootP]; }
else { parent[rootQ] = rootP; size[rootP] += size[rootQ]; }
On a tie the else branch runs and q’s root goes under p’s root. Feed this implementation the same union(i, i+1) sequence and every element lands directly under 0, for a maximum depth of 1.
For weighted quick-union, algs4 gives a proposition that no node in a forest built from n sites is deeper than lg n, and a corollary that the worst-case order of growth of find, connected and union is log n. Both statements are about the implementation without path compression, and both are worst-case bounds on each individual operation.
Here it is with 8 elements, 0 to 7.
| Call | parent[0..7] | size change |
|---|---|---|
| start | 0 1 2 3 4 5 6 7 | all 1 |
| union(0,1) | 0 0 2 3 4 5 6 7 | size[0]=2 |
| union(2,3) | 0 0 2 2 4 5 6 7 | size[2]=2 |
| union(0,2) | 0 0 0 2 4 5 6 7 | size[0]=4 |
| union(4,5) | 0 0 0 2 4 4 6 7 | size[4]=2 |
| union(6,7) | 0 0 0 2 4 4 6 6 | size[6]=2 |
| union(4,6) | 0 0 0 2 4 4 4 6 | size[4]=4 |
| union(0,4) | 0 0 0 2 0 4 4 6 | size[0]=8 |
The final size array is 8 1 2 1 4 1 2 1. From 7 to the root is 7 → 6 → 4 → 0, three links, exactly lg 8. Every union merged two trees of equal size, which is why this example hits the bound. Each find inside those seven unions started at a root, so an implementation with path compression ends up with exactly the same array.
Path compression: link the visited nodes straight to the root
Section 1.5 notes that ideally every node would link directly to its root, but we don’t want to pay for changing a large number of links. The approach it describes is to link only the nodes find actually examines. Find in WeightedQuickUnionPathCompressionUF makes two passes: one to find the root, one to relink each node on the path to it.
int root = p;
while (root != parent[root]) root = parent[root];
while (p != root) { int newp = parent[p]; parent[p] = root; p = newp; }
return root;
Calling find(7) on the state above follows three links to reach root 0 in the first loop, and the second loop links 7, 6 and 4 directly to 0.
| parent[0..7] | |
|---|---|
| before find(7) | 0 0 0 2 0 4 4 6 |
| after find(7), full compression | 0 0 0 2 0 4 0 0 |
A few things to notice.
- This find still followed three links. Compression makes the next find cheaper. It doesn’t shorten the current one. That is why the algs4 docs still list union and find as Θ(log n) in the worst case for this implementation.
- The root doesn’t change. The algs4 docs say a set’s canonical element can change only when the set itself changes during union, never during find. Compression only reshapes the tree.
size[]isn’t updated. After compression only 5 is left under 4, yetsize[4]is still 4. The source comment says size is not necessarily correct when i isn’t a root. Union only comparessize[rootP]andsize[rootQ], the values at roots, so nothing breaks. Compression never moves an element into another tree, so a root’s size is still the number of elements in its set.
Path halving: one pass, link to the grandparent
UF, the class whose main() Section 1.5 uses to solve its dynamic connectivity example, uses a different pair: weighting by rank instead of size, and path compression by halving.
while (p != parent[p]) {
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
In one pass, each node it visits is linked to its grandparent, and the walk then jumps to that grandparent. Only every other node on the path gets changed. Run the same seven unions through UF and, since every union in this example joins two trees of equal rank, the parent array matches the size-weighted one; the ranks end up as 3 0 1 0 2 0 1 0. Now find(7) gives a different result.
| parent[0..7] | |
|---|---|
| before find(7) | 0 0 0 2 0 4 4 6 |
| after find(7), path halving | 0 0 0 2 0 4 4 4 |
7 moves up to its grandparent 4. Node 4’s grandparent is already 0, so it stays put, and 6 is skipped over and left alone. The tree doesn’t flatten in one go the way full compression does; call find(7) again and 7 moves to 0. In exchange it needs only the one loop that finds the root, and a web exercise in Section 1.5 introduces path halving as a simpler strategy.
Which statement goes with which implementation
The table restates what the algs4 class docs and the Section 1.5 text say.
| Implementation | Weighting | Path compression | Single operation (worst case) | Whole sequence |
|---|---|---|---|---|
WeightedQuickUnionUF |
size | none | union, find Θ(log n); depth ≤ lg n is a proposition in the text | not stated |
QuickUnionPathCompressionUF |
none | full | not stated | union, find, connected in logarithmic amortized time |
WeightedQuickUnionPathCompressionUF |
size | full | union, find Θ(log n) | O(m α(n)) for m operations |
UF |
rank | halving | union, find Θ(log n) | O(m α(n)) for m operations |
The exact condition for O(m α(n)) is this: starting from an empty structure with n sites, any intermixed sequence of m union and find operations takes O(m α(n)) time in total, where α(n) is the inverse of Ackermann’s function. “α(n) per operation” is that total divided by m, an amortized cost. No assumption about random input goes into it. If amortized analysis is new to you, why appending to a dynamic array is O(1) walks through the same kind of argument.
What the sources actually establish
It’s worth separating the forms these figures take in the algs4 material.
- Depth ≤ lg n and the log n worst case are a proposition and corollary in the Section 1.5 text.
- That path compression without weighting has logarithmic amortized cost, and that weighting plus path compression has amortized cost bounded by the inverse Ackermann function, appear only as notes to Exercises 1.5.12 and 1.5.13, phrased as “known”. The note on 1.5.13 adds that the value is less than 5 for any conceivable n that arises in practice.
- O(m α(n)) is a sentence in the class docs of
WeightedQuickUnionPathCompressionUFandUF.
None of the material I opened proves the α(n) bound, and I didn’t check the original paper. So this post doesn’t compare rank against size, or halving against full compression, on speed. The table only goes as far as the algs4 docs go for each implementation.
Sources
All checked on 2026-10-18.