LeetCode #323 Medium

Number of Connected Components in an Undirected Graph

Number of Connected Components in an Undirected Graph: given n nodes and a list of undirected edges, count how many connected components the graph has.

Constraints
  • 1 <= n <= 2000
  • 1 <= edges.length <= 5000
  • edges[i].length == 2
  • There are no repeated edges and no self-loops.
depth-first-searchbreadth-first-searchunion-findgraph
Open on LeetCode ↗
02

Intuition

Number of connected components in an undirected graph counts the separate groups of nodes in a graph given as an edge list. Two nodes belong to the same component when a path connects them. Two approaches solve it, and they think about the problem differently. The traversal approach starts a DFS or BFS from every unvisited node. Each launch reaches exactly one full component and marks it, so: - The number of times a fresh traversal has to be started is precisely the number of components. That requires building an adjacency list first, with both directions per edge — omitting the reverse entry splits components that are genuinely connected. The Union-Find approach never builds a graph. Start with n components, one per node, and union the endpoints of each edge. A union that actually merges two distinct sets decrements the count; a union of nodes already together changes nothing. That conditional decrement is where implementations go wrong. Decrementing on every edge ignores redundant edges within a component and undercounts badly on dense graphs. Isolated nodes with no edges are handled correctly by both — the traversal finds them as single-node components, and Union-Find counts them from the initial n. The traversal is O(V + E) with O(V + E) space for the adjacency list; Union-Find is near-linear at O(E · α(V)) with only O(V) space, and needs no adjacency structure at all. Union-Find suits this particular problem well, since the input is already an edge list and only a count is required — not the components themselves.

How to spot this pattern

Counting connected components is the archetypal 'traverse from every unvisited node' problem, and the same outer-loop-plus-flood-fill shape solves Number of Islands, Number of Provinces, and Max Area of Island. The tell is a question about groups rather than paths.

03

Approach

Try it first

Before reading on: if you begin a traversal at an unvisited node, what exactly have you discovered by the time it finishes? How does that let you count components without any extra bookkeeping? Aim for O(n + e).

1

Count traversal launches

Start a DFS or BFS from each unvisited node. Each launch covers exactly one component, so the number of launches is the answer.

2

Build both edge directions

For the traversal approach, add each edge to both adjacency lists. Omitting the reverse entry splits components that are genuinely connected.

3

Or start Union-Find at n

Begin with n components, one per node. Each node is initially its own set, which correctly counts isolated nodes with no edges.

4

Decrement only on a real merge

Reduce the count only when a union joins two distinct sets. Decrementing on every edge undercounts badly, since redundant edges within a component merge nothing.

5

Skip the adjacency list entirely

Union-Find works straight from the edge list, needing no graph structure — a real advantage when only the count is required.

6

Cost of each approach

Traversal is O(V + E) time with O(V + E) space. Union-Find is O(E · α(V)) — effectively linear — with only O(V) space.

04

Solution & live demo

▶1class Solution:
▶2 def countComponents(self, n, edges):
▶3 adjacency = [[] for _ in range(n)]
▶4 for a, b in edges:
▶5 adjacency[a].append(b)
▶6 adjacency[b].append(a)
▶7 visited = [False] * n
▶8 components = 0
▶9 for start in range(n):
▶10 if visited[start]:
▶11 continue
▶12 components += 1
▶13 stack = [start]
▶14 visited[start] = True
▶15 while stack:
▶16 node = stack.pop()
▶17 for neighbour in adjacency[node]:
▶18 if not visited[neighbour]:
▶19 visited[neighbour] = True
▶20 stack.append(neighbour)
▶21 return components
05

Common pitfalls

Adding edges in only one direction

✗ Wrong
adjacency[a].append(b)
✓ Right
adjacency[a].append(b)
adjacency[b].append(a)

The graph is undirected, so both endpoints must know about the edge. With one direction the traversal cannot walk backwards and one component gets counted as several.

Counting nodes instead of traversal starts

✗ Wrong
components = len(visited)
✓ Right
components += 1  # once per fresh start

The number of visited nodes is n once everything is explored. It is the number of times a traversal had to be started that equals the component count.

Only traversing from node 0

✗ Wrong
dfs(0)
return 1
✓ Right
for start in range(n):
    if not visited[start]: ...

A single traversal reaches only the component containing node 0. Every node must be tried as a potential start, or disconnected components are never discovered.

06

Edge cases

No edges

Every node is isolated, so the answer is n.

All nodes connected

One traversal covers everything and the answer is 1.

Isolated node alongside a component

The outer loop reaches it, starts a traversal that visits only it, and counts it as its own component.

Duplicate edges

The visited check makes a repeated edge a no-op.

Self-loop

The node is already visited when the loop is followed, so nothing changes.

07

Complexity

Time
O(n + e)
Space
O(n + e)
Each node and edge is processed once. Union-Find gives O(n + e·α(n)), effectively the same.