Algorithm GuideAI Generated16 min readDec 22, 2025

Master the Union Find Algorithm: Your Ultimate Guide for Coding Interviews

Unlock the power of the Union Find Algorithm (DSU) for graph and connectivity problems. This in-depth guide covers intuition, optimized implementation, and real interview examples to ace your next coding interview.

Introduction: Grouping Things Efficiently

Hey there, future algorithm master! Have you ever found yourself wrestling with problems where you need to manage groups of interconnected items? Maybe you're trying to figure out if two people are in the same social network, or if two cities are connected by roads, or perhaps you're building a maze generation algorithm. These types of problems, where you need to check connectivity and merge groups, pop up everywhere in computer science and, more importantly, in coding interviews.

That's where the Union Find Algorithm, also known as the Disjoint Set Union (DSU) data structure, comes to the rescue. It's an incredibly powerful and elegant algorithm designed to keep track of a set of elements partitioned into a number of disjoint (non-overlapping) subsets. While its name might sound a bit formal, its intuition is surprisingly simple, and its applications are vast.

In this guide, we're not just going to learn how to implement the Union Find Algorithm; we're going to dive deep into why it works, how to optimize it to achieve near-constant time operations, and when to unleash its power in real-world scenarios, especially during those tricky coding interviews. By the end, you'll not only understand Union Find but feel confident applying it to solve complex problems. Ready to turn this algorithm into your secret weapon? Let's go!

The Core Idea: What is Union-Find?

Imagine you're managing a sprawling collection of social circles. Each person belongs to exactly one circle. Your main tasks are:

  1. Checking if two people are in the same circle.* (e.g., Are Alice and Bob friends-of-friends?)
  2. Merging two circles when a connection is formed.* (e.g., Alice becomes friends with Carol, and their entire friend circles merge).

The Union Find Algorithm is built precisely for these two operations, find and union, on what we call disjoint sets.

Disjoint Sets Explained

Think of a disjoint set as a collection of elements, where each element belongs to exactly one 'set' or 'group', and these groups do not overlap. If an element x is in Set A, it cannot also be in Set B. Our goal is to efficiently perform two operations on these sets:

  • find(x): This operation returns a 'representative' (or 'root') of the set that x belongs to. If find(x) == find(y), then x and y are in the same set.
  • union(x, y): This operation merges the sets containing x and y into a single, larger set. If x and y are already in the same set, union does nothing.

Initially, every element is in its own individual set, acting as its own representative. As we perform union operations, these individual sets merge, forming larger and larger groups.

💡 Pro Tip: The representative isn't just an arbitrary element. It's often the 'root' of a tree-like structure we'll use to represent each set. All elements in a set will eventually lead back to the same root.

Building Our First Union-Find (The Basic Version)

How do we represent these sets in code? The simplest and most common way is to use an array (or hash map) called parent.

Let's say we have N elements, numbered 0 to N-1. The parent array will store, for each element i, which element is its parent. If an element i is its own parent (parent[i] == i), it means i is the representative (root) of its set.

Initialization

When we start, every element is its own set. So, we initialize parent[i] = i for all i from 0 to N-1.

class UnionFind:
 def __init__(self, n):
 # Initialize each element as its own parent
 # This means each element is initially in its own set
 self.parent = list(range(n))

`find(x)` Operation (Basic)

To find the representative of an element x, we simply traverse up the parent array until we reach a node that is its own parent. This is the root.

 def find(self, x):
 # If x is its own parent, it's the root of its set
 if self.parent[x] == x:
 return x
 # Otherwise, recursively find the parent of x
 return self.find(self.parent[x])

Time Complexity of Basic find: In the worst case, if our sets form a long 'chain' or a skewed tree, find(x) might have to traverse all the way up N elements. This would be O(N).

`union(x, y)` Operation (Basic)

To union two elements x and y, we first find the representatives of their respective sets. Let's call them rootX and rootY. If rootX and rootY are different, it means x and y are in different sets. To merge them, we simply make one root the parent of the other. For simplicity, let's say we always make rootY's parent rootX.

 def union(self, x, y):
 rootX = self.find(x)
 rootY = self.find(y)

 # If they are already in the same set, do nothing
 if rootX != rootY:
 # Make rootY's parent rootX (merging sets)
 self.parent[rootY] = rootX
 return True # Indicates a successful union
 return False # Indicates they were already connected

Time Complexity of Basic union: This operation depends on the find operations, so it also has a worst-case time complexity of O(N).

As you can see, a worst-case O(N) for each operation isn't great, especially for N up to 10^5 or 10^6. This is where our powerful optimizations come in!

Supercharging Union-Find: Path Compression & Union by Size

The basic Union Find Algorithm is functional, but its performance can degrade significantly. The key to making it incredibly fast lies in two brilliant optimizations:

1. Path Compression (for `find`)

The Problem: When you call find(x), you trace a path from x up to its root. In a naive implementation, this path might be long. The next time you call find on x (or any node on that path), you'd re-trace the exact same path.

The Solution: Why retrace? As you traverse the path from x to its root, make every node you visit point directly to the root. It's like flattening the tree as you climb it. This significantly shortens future find operations for all nodes on that path.

How it works in find:

 def find(self, x):
 if self.parent[x] == x:
 return x
 # Recursively find the root and then set current node's parent directly to it
 self.parent[x] = self.find(self.parent[x]) # This is the path compression step!
 return self.parent[x]

💡 Pro Tip: Path compression doesn't change the root of the tree, only the structure leading up to it. It's a fundamental optimization that makes find incredibly efficient after a few operations.

2. Union by Size (or Rank) (for `union`)

The Problem: In the basic union operation, when we merge rootY into rootX, we might inadvertently create a very deep, unbalanced tree. Imagine always attaching a large tree to a small tree's root – the depth grows rapidly.

The Solution: To keep our trees as flat as possible, we follow a simple rule: when merging two sets, always attach the root of the smaller tree to the root of the larger tree. This strategy minimizes the overall height of the trees, which in turn keeps find operations faster.

  • Union by Size: Maintain an array size (or count), where size[i] stores the number of elements in the set rooted at i (if i is a root). When merging, the set with fewer elements becomes a child of the set with more elements. Update the size of the new root.
  • Union by Rank (or Height): Similar to size, but we track the 'rank' (an approximation of height) of each tree. The tree with the smaller rank attaches to the one with the larger rank. If ranks are equal, pick one and increment its rank.

For most practical purposes and interview scenarios, Union by Size is often simpler to implement and just as effective.

How it works in union:

First, we need to initialize size array, where size[i] = 1 for all i initially.

class UnionFind:
 def __init__(self, n):
 self.parent = list(range(n))
 self.size = [1] * n # Initialize each set's size to 1

 # ... (find method with path compression as above) ...

 def union(self, x, y):
 rootX = self.find(x)
 rootY = self.find(y)

 if rootX != rootY:
 # Attach the smaller tree under the root of the larger tree
 if self.size[rootX] < self.size[rootY]:
 self.parent[rootX] = rootY
 self.size[rootY] += self.size[rootX]
 else:
 self.parent[rootY] = rootX
 self.size[rootX] += self.size[rootY]
 return True
 return False

⚠️ Common Pitfall: Forgetting to update the size array during union when using Union by Size. This will negate the benefit of the optimization and can lead to unbalanced trees.

Putting It All Together: Optimized Python Implementation

Here’s the complete optimized Union Find Algorithm class in Python, ready for action:

class UnionFind:
 def __init__(self, n):
 # parent[i] stores the parent of element i.
 # Initially, each element is its own parent, meaning it's the root of its own set.
 self.parent = list(range(n))
 
 # size[i] stores the number of elements in the set rooted at i.
 # This is used for the 'union by size' optimization.
 # Initially, each set contains only one element.
 self.size = [1] * n
 
 # Optional: Keep track of the number of disjoint sets
 self.num_components = n

 def find(self, x):
 # Path compression: if x is not the root of its set,
 # set its parent directly to the root.
 if self.parent[x] == x:
 return x
 self.parent[x] = self.find(self.parent[x])
 return self.parent[x]

 def union(self, x, y):
 # Find the roots of the sets containing x and y
 rootX = self.find(x)
 rootY = self.find(y)

 # If x and y are already in the same set, there's nothing to do.
 if rootX == rootY:
 return False # Indicates no union was performed

 # Union by size: attach the root of the smaller tree to the root of the larger tree.
 # This keeps the overall tree height minimal, improving future find operations.
 if self.size[rootX] < self.size[rootY]:
 self.parent[rootX] = rootY
 self.size[rootY] += self.size[rootX]
 else:
 self.parent[rootY] = rootX
 self.size[rootX] += self.size[rootY]
 
 self.num_components -= 1 # One less component after a successful union
 return True # Indicates a successful union

 def get_num_components(self):
 return self.num_components

 def are_connected(self, x, y):
 return self.find(x) == self.find(y)

# Example Usage:
# uf = UnionFind(5) # Elements 0, 1, 2, 3, 4
# uf.union(0, 1) # Merges set {0} and {1}
# uf.union(2, 3) # Merges set {2} and {3}
# uf.union(0, 2) # Merges set {0,1} and {2,3} -> {0,1,2,3}
# print(uf.are_connected(1, 3)) # True
# print(uf.are_connected(1, 4)) # False
# print(uf.get_num_components()) # 2 (sets are {0,1,2,3} and {4})

Demystifying Union-Find's Amortized Complexity

This is where the Union Find Algorithm truly shines! With both path compression and union by size (or rank) optimizations, the time complexity for both find and union operations becomes incredibly efficient.

Time Complexity: Amortized O(α(N))

  • α (alpha) is the inverse Ackermann function. While this sounds scary, the inverse Ackermann function grows extremely slowly. For any practically relevant number of elements N (even more than the number of atoms in the observable universe), α(N) is effectively a constant less than 5.
  • This means that, on average, a sequence of M operations (mix of find and union) on N elements will take approximately O(M * α(N)) time. We say

Real Interview Problem: Redundant Connection

Let's put our new Union Find skills to the test with a classic interview problem. This type of problem is incredibly common and a great way to demonstrate your understanding of graphs and data structures.

Problem: Redundant Connection (LeetCode 684)

We are given a graph that started as a tree with N nodes (labeled 1 to N) with N-1 edges. A new edge has been added to this tree, forming a cycle. The graph is now given as an array of edges where edges[i] = [u_i, v_i] represents an undirected edge between u_i and v_i. Return the edge that can be removed so that the resulting graph is a tree of N nodes. If there are multiple answers, return the answer that occurs last in the input edges array.

Example:

Input: edges = [[1,2],[1,3],[2,3]] Output: [2,3]

Explanation:

  • Initially, we have 1-2 and 1-3. This is a tree.
  • When we add [2,3], it creates a cycle 1-2-3-1. The edge [2,3] is the one that forms the cycle, and it's the last one in the input.

How Union-Find Helps

The core idea here is that a tree has N nodes and N-1 edges, and it contains no cycles. If we add an N-th edge to a tree, it must create a cycle. The problem asks us to find that edge.

We can iterate through the given edges one by one. For each edge (u, v):

  1. Check if u and v are already connected (i.e., find(u) == find(v)). If they are, adding the current edge (u, v) would create a cycle. Since we want the last such edge, this is our answer.
  2. If u and v are not connected, union(u, v) them. This means they now belong to the same component.

This approach works because when we encounter an edge that connects two nodes already in the same component, that edge must be the one completing a cycle. Since we iterate and return the first such edge we find (which by definition will be the last one in the input list that creates a cycle for the components built so far), we satisfy the problem's requirement.

Solution Walkthrough

Let's trace edges = [[1,2],[1,3],[2,3]] with our UnionFind.

  1. Initialize UnionFind for N nodes (let's assume 3 for this example, usually N is len(edges) or max(u,v)). Nodes are 1-indexed, so we might create a UF of size N+1 and ignore index 0.
  2. parent = [0,1,2,3], size = [0,1,1,1] (ignoring parent[0], size[0])

2. Edge [1,2]: - find(1) is 1. find(2) is 2. - They are different. union(1,2). Let's say we make 1 the root of 2's component. - parent = [0,1,1,3], size = [0,2,1,1] (set 1's size becomes 2)

3. Edge [1,3]: - find(1) is 1. find(3) is 3. - They are different. union(1,3). Let's say we make 1 the root of 3's component. - parent = [0,1,1,1], size = [0,3,1,1] (set 1's size becomes 3)

4. Edge [2,3]: - find(2) is 1 (after path compression find(2) -> find(parent[2]=1) -> 1). - find(3) is 1 (after path compression find(3) -> find(parent[3]=1) -> 1). - find(2) and find(3) are both 1. They are already connected! - This edge [2,3] is the redundant connection. Return [2,3].

Python Solution

class UnionFind:
 def __init__(self, n):
 self.parent = list(range(n + 1)) # +1 for 1-indexed nodes
 self.size = [1] * (n + 1)

 def find(self, x):
 if self.parent[x] == x:
 return x
 self.parent[x] = self.find(self.parent[x])
 return self.parent[x]

 def union(self, x, y):
 rootX = self.find(x)
 rootY = self.find(y)

 if rootX == rootY:
 return False # Already connected, means this edge forms a cycle

 if self.size[rootX] < self.size[rootY]:
 self.parent[rootX] = rootY
 self.size[rootY] += self.size[rootX]
 else:
 self.parent[rootY] = rootX
 self.size[rootX] += self.size[rootY]
 return True # Successfully united

def findRedundantConnection(edges):
 # Determine the maximum node label to initialize UnionFind correctly
 # The problem states N nodes labeled 1 to N.
 # N can be determined by the number of edges if it's a tree (N-1 edges).
 # Or simply find the max node value present in edges.
 max_node = 0
 for u, v in edges:
 max_node = max(max_node, u, v)
 
 uf = UnionFind(max_node)

 for u, v in edges:
 # If union returns False, it means u and v were already connected.
 # This current edge (u, v) is the one that forms the cycle.
 if not uf.union(u, v):
 return [u, v]

 return [] # Should not happen based on problem statement

# Test Cases:
# print(findRedundantConnection([[1,2],[1,3],[2,3]])) # Expected: [2,3]
# print(findRedundantConnection([[1,2],[2,3],[3,4],[1,4],[1,5]])) # Expected: [1,4]

Complexity Analysis for Redundant Connection Problem:

  • Time Complexity: O(N * α(N)) where N is the number of nodes (or edges, since it's roughly the same magnitude). We iterate through N edges, and each union operation (which internally calls find) takes amortized O(α(N)) time. This is practically O(N).
  • Space Complexity: O(N) for the parent and size arrays in our UnionFind data structure.

Common Pitfalls to Avoid

Even with a strong grasp of the Union Find Algorithm, it's easy to stumble on common mistakes. Here are some to watch out for:

  • Incorrect Initialization of parent and size arrays: Always ensure parent[i] = i and size[i] = 1 for all i (from 0 to N-1 or 1 to N, depending on indexing) at the start. Forgetting this means your initial sets are incorrect.
  • Forgetting Path Compression or Union by Size (or Rank): This is the biggest performance trap! Without these optimizations, your find and union operations can degrade to O(N), making a solution involving M operations O(M*N), which will TLE (Time Limit Exceed) in most competitive programming or interview settings for large inputs. Always include both!
  • Off-by-One Errors with Indexing: Many graph problems use 1-indexed nodes, while Python lists are 0-indexed. Be careful to adjust your UnionFind array sizes (e.g., n+1) and access patterns (parent[node]) accordingly. I've personally wasted hours debugging this simple mistake!
  • Not Understanding What find Returns: find(x) returns the root of the set containing x, not x itself, and not necessarily the immediate parent of x (especially with path compression). Always compare find(x) and find(y) to check if x and y are in the same set, never parent[x] and parent[y] directly.
  • Incorrectly Updating size (or rank) in union: When performing union by size, remember to add the size of the smaller tree to the size of the larger tree's root. Forgetting this means your size array will be out of sync, and the optimization becomes ineffective.
  • Returning the Wrong Value from union: Sometimes problems need to know if a union actually occurred (i.e., if two previously distinct sets were merged) or if the elements were already connected. Design your union method to return a boolean (True for successful merge, False for already connected) to make problem-solving easier.

By being mindful of these pitfalls, you'll save yourself a lot of debugging time and ensure your Union Find Algorithm performs as expected!

Sharpen Your Skills: Practice Problems

The best way to solidify your understanding of the Union Find Algorithm is through practice. Here are some excellent problems from LeetCode that you can tackle. They range in difficulty and will help you see the versatility of DSU.

  • 547. Number of Provinces (Medium): This is a direct application of finding connected components. If you can solve this, you understand the core Union Find Algorithm usage.
  • - [LeetCode Link](https://leetcode.com/problems/number-of-provinces/)
  • 684. Redundant Connection (Medium): The problem we just walked through! Try implementing it yourself without looking at the solution.
  • - [LeetCode Link](https://leetcode.com/problems/redundant-connection/)
  • 990. Satisfiability of Equality Equations (Medium): This problem introduces a twist with inequalities. You'll need to use Union Find for equalities and then check inequalities against the formed groups.
  • - [LeetCode Link](https://leetcode.com/problems/satisfiability-of-equality-equations/)
  • 323. Number of Connected Components in an Undirected Graph (Medium - LeetCode Premium, but very similar to 547): A classic problem. If you don't have premium, 547 is almost identical.
  • 1168. Optimize Water Distribution in a Village (Hard - LeetCode Premium): This one uses Union-Find within Kruskal's algorithm (for Minimum Spanning Tree), showing a more advanced application. A great challenge once you're comfortable with the basics.

Each problem will test a slightly different aspect of your Union-Find knowledge. Don't get discouraged if you struggle; that's part of the learning process! Try to solve them on your own first, and only look at solutions if you're truly stuck.

Conclusion: Your New Algorithm Superpower

Congratulations! You've just mastered one of the most powerful and efficient data structures for handling connectivity problems: the Union Find Algorithm. We've journeyed from its simple intuition of managing disjoint sets to its highly optimized implementation with path compression and union by size, achieving near-constant time operations. We've also applied it to a real-world coding interview problem, Redundant Connection, and explored various pitfalls to avoid.

The Union Find Algorithm is an indispensable tool in your algorithmic arsenal, especially for graph problems, network connectivity, and even some advanced data structure challenges. Its elegance lies in its simplicity and the sheer speed it delivers for dynamic connectivity queries.

Remember the key takeaways:

  • find(x): Get the representative (root) of x's set.
  • union(x, y): Merge the sets containing x and y.
  • Path Compression: Flatters the tree during find for future speed.
  • Union by Size/Rank: Keeps trees balanced during union to prevent deep trees.
  • Amortized O(α(N)): Practically constant time for operations.

Keep practicing with the suggested problems, and you'll find yourself confidently tackling graph problems that once seemed daunting. Go forth and connect those components, knowing you have a true algorithmic superpower at your command!

Recommended next

AI-Generated Content

This article was generated using AI (Google Gemini) and reviewed for accuracy. While we strive to provide helpful information, please verify technical details and test code examples before using them in production environments. This content is for educational purposes only.

💡 Ask me anything about coding!