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

Start a traversal from every node that has not been seen. Each fresh start discovers one entire component and marks all of it visited, so the number of starts equals the number of components. Isolated nodes are handled for free — they simply form components of size one.

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

Build an adjacency list first

The edge list is the wrong shape for traversal: finding a node's neighbours would mean scanning every edge. Convert to an adjacency list where entry i holds the nodes adjacent to i. Since the graph is undirected, each edge is added in both directions — omitting one direction turns the graph directed and silently splits components apart. Building the list is O(n + e).

2

Count the fresh starts

Loop over all nodes from 0 to n-1. If a node is unvisited, increment the component counter and run a DFS or BFS that marks everything reachable from it. Any node reached during that traversal belongs to the same component and will be skipped by the outer loop later. So the counter increments exactly once per component, and the outer loop guarantees no component is missed regardless of how the nodes are numbered.

3

Union-Find as the alternative

The same count comes from a disjoint-set structure: start with n components, and each edge that joins two different sets reduces the count by one. With path compression and union by rank each operation is effectively constant, giving O(n + e·α(n)). Traversal is simpler to write and equally fast here; Union-Find earns its place when edges arrive incrementally or when you must answer connectivity queries as you go.

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.