Skip to content
Part IA Lent Term

Forest Representation: Union by Rank and Path Compression

Deep Forest Representation

Union-find forest: before and after path compression flattens the tree structure

Each set is represented as a rooted tree where each node points to its parent. The root’s parent pointer is NIL (or points to itself). The root serves as the representative of the set.

MakeSet

MakeSet(x):
    x.parent = x    // or NIL, depending on convention
    x.rank = 0

Creates a singleton tree. Θ(1)\Theta(1).

Find (Chase)

Follow parent pointers from x up to the root:

Find(x):
    while x.parent != NIL:    // or != x
        x = x.parent
    return x

Without optimisations, worst-case Θ(n)\Theta(n) if trees become long chains.

Union by Rank

When merging two trees, make the root with the smaller rank point to the root with the larger rank. If ranks are equal, choose one as the new root and increment its rank by 1.

Rank is an estimate (upper bound) on the height of the tree. It is NOT the same as height after path compression is applied; ranks never decrease.

Union(x, y):
    rx = Find(x)
    ry = Find(y)
    if rx == ry: return
    if rx.rank > ry.rank:
        ry.parent = rx
    else if rx.rank < ry.rank:
        rx.parent = ry
    else:                     // equal ranks
        ry.parent = rx
        rx.rank = rx.rank + 1

Union by rank guarantees that a tree of rank rr contains at least 2r2^r nodes (provable by induction: each rank increase to r+1r+1 merges two trees of rank rr, each having 2r\ge 2^r nodes, yielding 2r+1\ge 2^{r+1}). Therefore, rank is at most log2n\lfloor \log_2 n \rfloor, and tree height (without compression) is O(logn)O(\log n).

Path Compression

During Find(x), after locating the root, revisit all nodes on the path and set their parent pointer directly to the root. This flattens the search path:

Find(x):
    if x.parent != x:
        x.parent = Find(x.parent)    // recursive compression
    return x.parent

// Iterative version:
Find(x):
    root = x
    while root.parent != root:
        root = root.parent
    // Second pass: compress
    while x != root:
        next = x.parent
        x.parent = root
        x = next
    return root

Path compression makes future Find operations on the same or descendant nodes much faster. The cost of the first traversal is amortised over future operations.

Worked Example

Start with 7 singletons, all rank 0:

MakeSet(1..7): each node parent = self, rank = 0
OperationActionResulting Trees
Union(1,2)ranks equal, 2→1, rank(1)=11(rank1)←2
Union(3,4)ranks equal, 4→3, rank(3)=13(rank1)←4
Union(1,3)rank(1)=1 > rank(3)=1? No, equal. 3→1, rank(1)=21(rank2)←2, 1←3(rank1)←4
Union(5,6)equal, 6→5, rank(5)=15(rank1)←6
Union(5,7)rank(5)=1 > rank(7)=0, 7→55(rank1)←6, 5←7
Union(1,5)rank(1)=2 > rank(5)=1, 5→11(rank2)←2,1←3←4,1←5←6,1←5←7
Find(4)path 4→3→1, compress: 4→1, 3→14 and 3 now point directly to 1

After Find(4) with compression:

      1(rank2)
    / | \  \
   2 3  4  5
          / \
         6   7

Combined Complexity

Using both union by rank and path compression, the amortised time per operation is O(α(n))O(\alpha(n)), where α\alpha is the inverse Ackermann function. For any practical nn (less than 222655362^{2^{2^{65536}}}), α(n)4\alpha(n) \le 4. Effectively constant time.

The proof of this bound is complex and uses a potential function based on partitioning ranks into levels; the full proof is beyond the Tripos scope but the result and the two heuristics are examinable.

Kruskal Complexity with Forest DS

With forest + union by rank + path compression:

  • MakeSet: VV calls, O(V)O(V)
  • Find: 2E2E calls, O(Eα(V))O(E \cdot \alpha(V))
  • Union: V1V-1 calls, O(Vα(V))O(V \cdot \alpha(V))
  • Sorting: O(ElogV)O(E \log V)

Total: O(ElogV+Eα(V))=O(ElogV)O(E \log V + E \cdot \alpha(V)) = O(E \log V) since sorting dominates.

Summary

HeuristicGuaranteeAloneCombined
NoneO(n)O(n) per Find worst
Union by rankTree height log2n\le \lfloor \log_2 n \rfloorO(logn)O(\log n) per FindO(α(n))O(\alpha(n))
Path compressionFlattens search pathO(logn)O(\log n) amortisedO(α(n))O(\alpha(n))
BothInverse AckermannEssentially O(1)O(1)

Key exam technique: When tracing Union/Find with compression, show the tree before and after each operation, and update ranks only on equal-rank Unions.