Problem Walkthrough

Connected Components LeetCode 323 Guide

Solve LeetCode 323 Number of Connected Components in an Undirected Graph using Union-Find with path compression and rank, or DFS/BFS traversal — both approaches count the number of maximal connectivity groups in O(V+E) time, with Union-Find offering amortized near-O(1) per edge operation for dynamic connectivity scenarios.

8 min read|

Connected Components

LeetCode 323

Problem Overview

LeetCode 323 — Number of Connected Components in an Undirected Graph — gives you an integer n representing nodes labeled 0 through n-1, and a list of undirected edges where each edge [u, v] connects node u and node v. Your task is to return the number of connected components in the graph. A connected component is a maximal set of nodes such that there exists a path between every pair of nodes in the set.

The problem is a foundational graph connectivity question. Understanding it unlocks a whole family of related problems: Number of Islands (LeetCode 200) is essentially the same problem on an implicit grid graph, Accounts Merge (LeetCode 721) unions emails under a shared account, and Graph Valid Tree (LeetCode 261) checks whether the graph has exactly one component with no cycles. Mastering LeetCode 323 directly transfers to all of these.

Two classical algorithms solve this problem: DFS/BFS traversal and Union-Find (Disjoint Set Union). Both run in O(V+E) time for this problem. The choice between them depends on the context: DFS/BFS is more familiar to most engineers and works naturally on an adjacency list, while Union-Find is more concise, handles dynamic edge additions gracefully, and becomes the preferred tool in problems where edges arrive one at a time.

  • Constraints: 1 <= n <= 2000; 0 <= edges.length <= 5000
  • Each edge [u, v] is undirected — u connects to v and v connects to u
  • No self-loops and no duplicate edges in the input
  • Nodes are labeled 0 through n-1
  • Return the number of connected components as an integer

DFS/BFS Approach

The DFS/BFS approach builds an adjacency list from the edge list, then iterates through all n nodes. For each unvisited node, it launches a DFS or BFS traversal that marks every reachable node as visited. Each new traversal launch corresponds to discovering a new connected component, so we increment the component counter each time we start a traversal from an unvisited node.

DFS can be implemented recursively (using the call stack) or iteratively (using an explicit stack). BFS uses a queue. Both visit each node exactly once and traverse each edge twice (once from each endpoint in an undirected graph), giving O(V+E) time and O(V+E) space — O(V) for the visited set and O(E) for the adjacency list.

The DFS approach is the most natural first solution and is very readable. The adjacency list can be built with a defaultdict(list) in Python or a HashMap of lists in Java. The visited set is typically a boolean array of size n. After the loop, the component count holds the answer.

💡

This Is the Foundational Graph Connectivity Problem

LeetCode 323 Number of Connected Components is the purest form of the graph connectivity question. Mastering its DFS/BFS and Union-Find solutions directly unlocks Number of Islands (200), Accounts Merge (721), Number of Provinces (547), Graph Valid Tree (261), and Redundant Connection (684). Every one of those problems reduces to counting or verifying connected components — the same core logic you write here.

Union-Find Approach

Union-Find (Disjoint Set Union) solves connected components by maintaining a parent array of size n where parent[i] initially equals i — every node is its own component. We also initialize a component count to n. For each edge [u, v], we call find(u) and find(v) to get the roots of their respective components. If the roots differ, the two nodes are in different components, so we union them and decrement the component count by 1.

After processing all edges, the component count holds the answer — it started at n (each node isolated) and was decremented exactly once for each edge that merged two previously separate components. This directly counts the number of distinct connected components without ever building an adjacency list or running a traversal.

The basic Union-Find without optimizations runs in O(V + E * V) worst case due to potentially tall trees. Adding path compression and union by rank brings this to O(V + E * alpha(V)) — effectively O(V+E) — making it faster than DFS/BFS for large inputs with many edges.

  1. 1Initialize parent = [0, 1, 2, ..., n-1] (each node is its own root)
  2. 2Initialize count = n (n isolated components)
  3. 3For each edge [u, v]:
  4. 4 rootU = find(u), rootV = find(v)
  5. 5 If rootU != rootV: union(rootU, rootV); count -= 1
  6. 6Return count after all edges are processed

Union-Find with Path Compression and Rank

Path compression optimizes the find(x) operation: after finding the root of x by walking up the parent chain, we set parent[i] = root for every node i visited along the way. This flattens the tree so that future find calls on those nodes return the root in O(1). Without path compression, a chain of n unions can create a tree of height n, making each find O(n).

Union by rank keeps trees balanced. Each node starts with rank 0. When unioning two trees of equal rank, attach one root under the other and increment the new root's rank by 1. When ranks differ, always attach the lower-rank root under the higher-rank root without changing any rank. This ensures tree height stays O(log n) in the worst case even without path compression.

Together, path compression and union by rank achieve an amortized time complexity of O(alpha(n)) per find and union operation, where alpha is the inverse Ackermann function — a function that grows so slowly it is at most 4 for any practical input size. This means Union-Find processes all edges in effectively O(E) time after the O(n) initialization.

ℹ️

With Both Optimizations, Union-Find Is Practically O(E)

With path compression and union by rank, Union-Find processes all E edges in O(E * alpha(V)) time. Since alpha(V) <= 4 for all practical values of V (alpha grows unimaginably slowly — the inverse of the Ackermann function), this is effectively O(E). For LeetCode 323 with up to 5000 edges and 2000 nodes, Union-Find with both optimizations runs in essentially constant time per edge — far faster than any DFS/BFS traversal in practice.

DFS vs Union-Find Trade-offs

DFS/BFS requires building an adjacency list from the edge list (O(E) preprocessing) and maintaining an explicit visited set of size V. It is straightforward to implement, easy to reason about correctness, and works naturally with the graph's structure. It is the right choice when you also need to inspect or process nodes and edges during the traversal — for example, to detect cycles, compute distances, or collect all nodes in each component.

Union-Find only needs a parent array (and optionally a rank array) of size V — no adjacency list, no visited set. The code is more compact: initialize the arrays, then loop over edges calling find and union. It is the natural choice when edges arrive one at a time (online/dynamic connectivity) or when you only need to know whether two nodes are in the same component, not the full structure of each component.

For LeetCode 323 specifically, both are correct and efficient. Union-Find with optimizations is slightly faster in practice and uses less memory. DFS is more readable to engineers unfamiliar with Union-Find. In interviews, demonstrating both approaches and articulating the trade-offs shows deep understanding of graph algorithms.

  1. 1DFS pros: familiar, works naturally with adjacency lists, easy to inspect component membership
  2. 2DFS cons: requires O(V+E) adjacency list, explicit visited set, recursive stack depth O(V)
  3. 3Union-Find pros: O(V) space only (no adjacency list), simpler code, handles dynamic edges
  4. 4Union-Find cons: path compression and rank logic requires careful implementation
  5. 5Use DFS when: you need to explore component structure or detect cycles during traversal
  6. 6Use Union-Find when: you only need component count or need to merge components dynamically

Code Walkthrough: Python and Java

Python DFS solution: build adjacency list with defaultdict(list), maintain visited set; for each unvisited node run DFS marking all reachable nodes, increment count each launch. Python Union-Find: initialize parent list and count; find(x) with recursive path compression; union(x, y) calls find on both, returns early if same root, otherwise attaches by rank and decrements count. Both are O(V+E) time.

Java DFS solution: build Map<Integer, List<Integer>> adjacency list; boolean[] visited array; for each unvisited node launch iterative DFS with a Deque stack, marking nodes visited. Java Union-Find: int[] parent and rank arrays; find with iterative path compression (parent[x] = parent[parent[x]] halving); union attaches by rank and decrements count. Both O(V+E) time and O(V+E) or O(V) space respectively.

The Union-Find implementation is shorter in both languages: roughly 20 lines versus 30 lines for DFS. Path compression in Python can be written as a one-liner: def find(x): parent[x] = find(parent[x]) if parent[x] != x else x; return parent[x]. In Java, iterative path compression with path halving (parent[x] = parent[parent[x]]) is preferred to avoid stack overflow on large inputs.

⚠️

Initialize Union-Find Count to n, Not 0

A common Union-Find bug is initializing the component count to 0 instead of n. The correct initialization is count = n because at the start, with no edges processed, every node is its own isolated component — there are n components. Each successful union (where the two nodes were in different components) decrements the count by exactly 1. After all edges, count equals the number of remaining components. Starting at 0 produces an answer that is off by n minus the number of union operations.

Ready to master algorithm patterns?

YeetCode flashcards help you build pattern recognition through active recall and spaced repetition.

Start practicing now