Union Find Disjoint Set Data Structure in DSA Python - Time & Space Complexity
We want to understand how fast the Union Find data structure works when we join groups or check if items belong to the same group.
How does the time to do these operations grow as we add more items?
Analyze the time complexity of the following code snippet.
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
if self.parent[x] != 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:
if self.rank[rootX] < self.rank[rootY]:
self.parent[rootX] = rootY
elif self.rank[rootX] > self.rank[rootY]:
self.parent[rootY] = rootX
else:
self.parent[rootY] = rootX
self.rank[rootX] += 1
This code creates groups of items and efficiently finds which group an item belongs to, using path compression and union by rank.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: The
findfunction uses recursion to find the root parent of an item, compressing paths along the way. - How many times: Each
unioncallsfindtwice, andfindmay recurse up the tree, but path compression keeps this short.
As we add more items, the time to find or union grows very slowly because the tree stays almost flat.
| Input Size (n) | Approx. Operations per find/union |
|---|---|
| 10 | About 2-3 steps |
| 100 | About 4-5 steps |
| 1000 | About 5-6 steps |
Pattern observation: The number of steps grows very slowly, almost like a flat line, even as input grows large.
Time Complexity: O(α(n)) per operation
This means each find or union operation takes almost constant time, growing extremely slowly with input size.
[X] Wrong: "Each find operation takes linear time because it may traverse many parents."
[OK] Correct: Path compression flattens the tree during finds, so future finds are much faster, making the average time almost constant.
Understanding Union Find's time complexity shows you how clever data structures keep operations fast even with many items, a skill valuable in many coding challenges.
"What if we removed path compression from the find method? How would the time complexity change?"
Practice
Union Find Disjoint Set data structure?Solution
Step 1: Understand the purpose of Union Find
The Union Find data structure is designed to keep track of which elements belong to which groups or sets, especially when these groups merge.Step 2: Compare with other data structures
Sorting, key-value storage, and queue operations are unrelated to Union Find's main goal.Final Answer:
To efficiently track and merge groups of connected elements -> Option AQuick Check:
Union Find = track and merge groups [OK]
- Confusing Union Find with sorting algorithms
- Thinking it stores key-value pairs
- Mixing it up with queue or stack data structures
Solution
Step 1: Understand initialization of Union Find
Each element starts as its own parent, so the parent array should have each index equal to itself.Step 2: Check each option
parent = [0, 1, 2, 3, 4] correctly sets parent[i] = i for i in 0 to 4. Others do not represent this.Final Answer:
parent = [0, 1, 2, 3, 4] -> Option DQuick Check:
Parent array init = self indices [OK]
- Setting all parents to 1 or a fixed number
- Using None which breaks find operations
- Confusing size array with parent array
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, a, b):
rootA = self.find(a)
rootB = self.find(b)
if rootA != rootB:
self.parent[rootB] = rootA
uf = UnionFind(4)
uf.union(0, 1)
uf.union(2, 3)
uf.union(1, 3)
print(uf.parent)Solution
Step 1: Trace unions step-by-step
Init: [0,1,2,3]
Union(0,1): find(0)=0, find(1)=1 -> parent[1]=0 -> [0,0,2,3]
Union(2,3): find(2)=2, find(3)=3 -> parent[3]=2 -> [0,0,2,2]
Union(1,3): find(1)=0, find(3)=2 (sets parent[3]=2) -> parent[2]=0 -> [0,0,0,2]Step 2: Verify print output
No additional find calls before print. parent[3] points to 2 (now root 0), but not fully compressed to 0. Output: [0, 0, 0, 2]Final Answer:
[0, 0, 0, 2] -> Option AQuick Check:
parent = [0,0,0,2] after partial compression [OK]
- Forgetting last union sets parent[2]=0 -> [0,0,2,2]
- Over-applying path compression -> [0,0,0,0]
- Ignoring unions entirely
union method:def union(self, a, b):
rootA = self.find(a)
rootB = self.find(b)
if rootA != rootB:
self.parent[rootA] = rootBSolution
Step 1: Understand union direction
Union should attach one root to the other to merge sets. Usually, parent of rootB is set to rootA or vice versa consistently.Step 2: Check given code
Code sets parent[rootA] = rootB, which reverses the usual convention and can cause incorrect tree structure or confusion.Final Answer:
It should assign parent of rootB to rootA, not the other way -> Option BQuick Check:
Union sets parent[rootB] = rootA [OK]
- Swapping roots incorrectly in union
- Not calling find before union
- Confusing equality check in if condition
[(0,1), (1,2), (3,4)], how many connected components does the Union Find structure report after union operations on these edges?Solution
Step 1: Initialize 6 separate nodes
Initially, each node is its own component: 6 components total.Step 2: Perform unions for edges
Union(0,1) merges nodes 0 and 1 -> components reduce to 5.
Union(1,2) merges node 2 with 0-1 group -> components reduce to 4.
Union(3,4) merges nodes 3 and 4 -> components reduce to 3.Step 3: Count remaining components
Node 5 remains alone -> total components = 3.Final Answer:
3 -> Option CQuick Check:
6 nodes - 3 unions = 3 components [OK]
- Counting edges instead of components
- Forgetting isolated nodes count as components
- Miscounting after unions
Ready to prove it?
Pass the test and this pattern moves unknown
