Union By Rank and Path Compression in Union-Find Algorithm
Last Updated : 22 Apr, 2025
In the previous post, we introduced the Union-Find algorithm. We employed the union() and find() operations to manage subsets.
Code Implementation:
C++ // Find the representative (root) of the // set that includes element i int Find(int i) { // If i itself is root or representative if (parent[i] == i) { return i; } // Else recursively find the representative // of the parent return Find(parent[i]); } // Unite (merge) the set that includes element // i and the set that includes element j void Union(int i, int j) { // Representative of set containing i int irep = Find(i); // Representative of set containing j int jrep = Find(j); // Make the representative of i's set be // the representative of j's set parent[irep] = jrep; }
Java // Find the representative (root) of the // set that includes element i int find(int i) { // if i itself is root or representative if (parent[i] == i) { return i; } // Else recursively find the representative // of the parent return find(parent[i]); } // Unite (merge) the set that includes element // i and the set that includes element j void union(int i, int j) { // Representative of set containing i int irep = find(i); // Representative of set containing j int jrep = find(j); // Make the representative of i's set be // the representative of j's set parent[irep] = jrep; }
Python def find(self, i): # If i itself is root or representative if self.parent[i] == i: return i # Else recursively find the representative # of the parent return self.find(self.parent[i]) def unite(self, i, j): # Representative of set containing i irep = self.find(i) # Representative of set containing j jrep = self.find(j) # Make the representative of i's set # be the representative of j's set self.parent[irep] = jrep
C# UnionFind(int size) { // Initialize the parent array with each // element as its own representative parent = new int[size]; for (int i = 0; i < size; i++) { parent[i] = i; } } // Find the representative (root) of the // set that includes element i int Find(int i) { // If i itself is root or representative if (parent[i] == i) { return i; } // Else recursively find the representative // of the parent return Find(parent[i]); }
JavaScript find(i) { // If i itself is root or representative if (this.parent[i] === i) { return i; } // Else recursively find the representative // of the parent return this.find(this.parent[i]); } unite(i, j) { // Representative of set containing i const irep = this.find(i); // Representative of set containing j const jrep = this.find(j); // Make the representative of i's set // be the representative of j's set this.parent[irep] = jrep; }
The above union() and find() operations are naive, and their worst-case time complexity is linear.This happens when the trees representing subsets become skewed, resembling a linked list in the worst case.
Below is an example worst case scenario:
Let there be 4 elements: 0, 1, 2, 3
Initially, all elements are in their own subsets: 0 1 2 3
Do Union(0, 1)
1 2 3
/
0
Do Union(1, 2)
2 3
/
1
/
0
Do Union(2, 3)
3
/
2
/
1
/
0
The above operations can be optimized to O(logn) in the worst case. The idea is to always attach a smaller depth tree under the root of the deeper tree. This technique is called union by rank. The term rank is preferred instead of height because if the path compression technique (we have discussed it below) is used, then the rank is not always equal to height. Also, the size (in place of height) of trees can also be used as rank. Using size as rank also yields worst-case time complexity as O(logn).
Let us see the above example with union by rank Initially, all elements are single element subsets: 0 1 2 3
Do Union(0, 1)
1 2 3
/
0
Do Union(1, 2)
1 3
/ \
0 2
Do Union(2, 3)
1
/ | \
0 2 3
The second optimization to naive method is Path Compression. The idea is to flatten the tree when find() is called. When find() is called for an element x, root of the tree is returned. The find() operation traverses up from x to find root. The idea of path compression is to make the found root as parent of x so that we don’t have to traverse all intermediate nodes again. If x is root of a subtree, then path (to root) from all nodes under x also compresses.
Let the subset {0, 1, .. 9} be represented as below and find() is called for element 3.
9
/ | \
4 5 6
/ / \
0 7 8
/
3
/ \
1 2
When find() is called for 3, we traverse up and find 9 as representative of this subset. With path compression, we also make 3 and 0 as the child of 9 so that when find() is called next time for 0, 1, 2 or 3, the path to root is reduced.
——–9——-
/ / / \ \
0 4 5 6 3
/ \ / \
7 8 1 2
By combining the two powerful techniques — Path Compression and Union by Rank/Size — the time complexity of Union-Find operations becomes almost constant in practice. In fact, the amortized time complexity per operation is O(α(n)), where α(n) is the inverse Ackermann function — a function that grows extremely slowly. So slowly, in fact, that α(n) < 5 for all practical input sizes (n < 10⁶⁰⁰).
Below is the implementation of Union-Find with Union by Rank and Path Compression:
C++ // C++ program for Union by Rank with Path Compression #include <iostream> #include <vector> using namespace std; class UnionFind { vector<int> Parent; vector<int> Rank; public: UnionFind(int n) { Parent.resize(n); for (int i = 0; i < n; i++) { Parent[i] = i; } // Initialize Rank array with 0s Rank.resize(n, 0); } // Function to find the representative (or the root // node) for the set that includes i int find(int i) { // If i is not the root, apply path compression if (Parent[i] != i) { Parent[i] = find(Parent[i]); } return Parent[i]; } // Unites the set that includes i and the set that // includes j by rank void unionByRank(int i, int j) { // Find the representatives (or the root nodes) int irep = find(i); int jrep = find(j); // Elements are in the same set, no need to unite if (irep == jrep) return; // Compare ranks to attach the smaller tree under the larger if (Rank[irep] < Rank[jrep]) { Parent[irep] = jrep; } else if (Rank[irep] > Rank[jrep]) { Parent[jrep] = irep; } else { // Same rank, make one root and increase its rank Parent[jrep] = irep; Rank[irep]++; } } }; int main() { int n = 5; UnionFind unionFind(n); unionFind.unionByRank(0, 1); unionFind.unionByRank(2, 3); unionFind.unionByRank(0, 4); for (int i = 0; i < n; i++) { cout << "Element " << i << ": Representative = " << unionFind.find(i) << endl; } return 0; }
Java // Java program for Union by Rank with Path Compression import java.util.Arrays; class UnionFind { private int[] Parent; private int[] Rank; public UnionFind(int n) { // Initialize Parent array Parent = new int[n]; for (int i = 0; i < n; i++) { Parent[i] = i; } // Initialize Rank array with 0s Rank = new int[n]; Arrays.fill(Rank, 0); } // Function to find the representative (or the root // node) for the set that includes i public int find(int i) { if (Parent[i] != i) { Parent[i] = find(Parent[i]); // Path compression } return Parent[i]; } // Unites the set that includes i and the set that // includes j by rank public void unionByRank(int i, int j) { int irep = find(i); int jrep = find(j); // If both elements are in same set, do nothing if (irep == jrep) return; // Attach smaller rank tree under root of higher rank tree if (Rank[irep] < Rank[jrep]) { Parent[irep] = jrep; } else if (Rank[irep] > Rank[jrep]) { Parent[jrep] = irep; } else { // If ranks are same, make one as root and // increase its rank Parent[jrep] = irep; Rank[irep]++; } } } public class GFG { public static void main(String[] args) { // Example usage int n = 5; UnionFind unionFind = new UnionFind(n); // Perform union operations unionFind.unionByRank(0, 1); unionFind.unionByRank(2, 3); unionFind.unionByRank(0, 4); // Print the representative of each element after // unions for (int i = 0; i < n; i++) { System.out.println("Element " + i + ": Representative = " + unionFind.find(i)); } } }
Python # Python program for Union by Rank with Path Compression class UnionFind: def __init__(self, n): self.Parent = list(range(n)) # Each node is its own parent self.Rank = [0] * n # All ranks initialized to 0 # Find with path compression def find(self, i): if self.Parent[i] != i: self.Parent[i] = self.find(self.Parent[i]) return self.Parent[i] # Union by rank def unionByRank(self, i, j): irep = self.find(i) jrep = self.find(j) if irep == jrep: return # Attach smaller rank tree under larger rank root if self.Rank[irep] < self.Rank[jrep]: self.Parent[irep] = jrep elif self.Rank[irep] > self.Rank[jrep]: self.Parent[jrep] = irep else: self.Parent[jrep] = irep self.Rank[irep] += 1 if __name__ == "__main__": n = 5 unionFind = UnionFind(n) unionFind.unionByRank(0, 1) unionFind.unionByRank(2, 3) unionFind.unionByRank(0, 4) # Print representatives for i in range(n): print(f'Element {i}: Representative = {unionFind.find(i)}')
C# // C# program for Union by Rank with Path Compression using System; using System.Linq; class UnionFind { private int[] Parent; private int[] Rank; public UnionFind(int n) { // Initialize Parent array Parent = new int[n]; for (int i = 0; i < n; i++) { Parent[i] = i; } // Initialize Rank array with 0s Rank = new int[n]; Array.Fill(Rank, 0); } // Function to find the representative (or // the root node) for the set that includes i public int Find(int i) { if (Parent[i] != i) { Parent[i] = Find(Parent[i]); } return Parent[i]; } // Unites the set that includes i and the set // that includes j by rank public void UnionByRank(int i, int j) { int irep = Find(i); int jrep = Find(j); if (irep == jrep) return; if (Rank[irep] < Rank[jrep]) { Parent[irep] = jrep; } else if (Rank[irep] > Rank[jrep]) { Parent[jrep] = irep; } else { Parent[jrep] = irep; Rank[irep]++; } } } class GFG { public static void Main(string[] args) { int n = 5; UnionFind unionFind = new UnionFind(n); unionFind.UnionByRank(0, 1); unionFind.UnionByRank(2, 3); unionFind.UnionByRank(0, 4); // Print the representative of each element after unions for (int i = 0; i < n; i++) { Console.WriteLine("Element " + i + ": Representative = " + unionFind.Find(i)); } } }
JavaScript // JavaScript program for Union by Rank with Path Compression class UnionFind { constructor(n) { this.Parent = Array.from({ length: n }, (_, i) => i); this.Rank = Array(n).fill(0); } // Function to find the representative (or the root node) find(i) { if (this.Parent[i] !== i) { this.Parent[i] = this.find(this.Parent[i]); } return this.Parent[i]; } // Unites the set that includes i and the set that includes j by rank unionByRank(i, j) { const irep = this.find(i); const jrep = this.find(j); if (irep === jrep) return; if (this.Rank[irep] < this.Rank[jrep]) { this.Parent[irep] = jrep; } else if (this.Rank[irep] > this.Rank[jrep]) { this.Parent[jrep] = irep; } else { this.Parent[jrep] = irep; this.Rank[irep]++; } } } const n = 5; const unionFind = new UnionFind(n); unionFind.unionByRank(0, 1); unionFind.unionByRank(2, 3); unionFind.unionByRank(0, 4); for (let i = 0; i < n; i++) { console.log(`Element ${i}: Representative = ${unionFind.find(i)}`); }
OutputElement 0: Representative = 0 Element 1: Representative = 0 Element 2: Representative = 2 Element 3: Representative = 2 Element 4: Representative = 0
Time Complexity: O(α(n)), Inverse Ackermann, nearly constant time, because of path compression and union by rank optimization.
Space Complexity: O(n), For parent and rank arrays as arrays store disjoint set info for n elements.
Related Articles :
Similar Reads
Depth First Search or DFS for a Graph
In Depth First Search (or DFS) for a graph, we traverse all adjacent vertices one by one. When we traverse an adjacent vertex, we completely finish the traversal of all vertices reachable through that adjacent vertex. This is similar to a tree, where we first completely traverse the left subtree and
13 min read
DFS in different language
Iterative Depth First Traversal of Graph
Given a directed Graph, the task is to perform Depth First Search of the given graph. Note: Start DFS from node 0, and traverse the nodes in the same order as adjacency list. Note : There can be multiple DFS traversals of a graph according to the order in which we pick adjacent vertices. Here we pic
10 min read
Applications, Advantages and Disadvantages of Depth First Search (DFS)
Depth First Search is a widely used algorithm for traversing a graph. Here we have discussed some applications, advantages, and disadvantages of the algorithm. Applications of Depth First Search:1. Detecting cycle in a graph: A graph has a cycle if and only if we see a back edge during DFS. So we ca
4 min read
Difference between BFS and DFS
Breadth-First Search (BFS) and Depth-First Search (DFS) are two fundamental algorithms used for traversing or searching graphs and trees. This article covers the basic difference between Breadth-First Search and Depth-First Search. ParametersBFSDFSStands forBFS stands for Breadth First Search.DFS st
2 min read
Depth First Search or DFS for disconnected Graph
Given a Disconnected Graph, the task is to implement DFS or Depth First Search Algorithm for this Disconnected Graph. Example: Input: Output: 0 1 2 3 Algorithm for DFS on Disconnected Graph:In the post for Depth First Search for Graph, only the vertices reachable from a given source vertex can be vi
7 min read
Printing pre and post visited times in DFS of a graph
Depth First Search (DFS) marks all the vertices of a graph as visited. So for making DFS useful, some additional information can also be stored. For instance, the order in which the vertices are visited while running DFS. Pre-visit and Post-visit numbers are the extra information that can be stored
8 min read
Tree, Back, Edge and Cross Edges in DFS of Graph
Given a directed graph, the task is to identify tree, forward, back and cross edges present in the graph. Note: There can be multiple answers. Example: Input: Graph Output:Tree Edges: 1->2, 2->4, 4->6, 1->3, 3->5, 5->7, 5->8 Forward Edges: 1->8 Back Edges: 6->2 Cross Edges
9 min read
Transitive Closure of a Graph using DFS
Given a directed graph, find out if a vertex v is reachable from another vertex u for all vertex pairs (u, v) in the given graph. Here reachable means that there is a path from vertex u to v. The reach-ability matrix is called transitive closure of a graph. For example, consider below graph: Transit
8 min read
Variations of DFS implementations