HARD Lab · Graphs · 47

323. Number of Connected Components in an Undirected Graph

Given n nodes and a list of undirected edges, count how many connected components exist. Union-Find solves this in near-linear time: start with n components and decrement each time a successful merge joins two previously separate groups.

MediumUnion-FindTypeScript

PROBLEM What we're solving

You have n nodes labeled 0 to n-1 and a list of undirected edges. Return the number of connected components — groups of nodes that can reach each other. For n=5 and edges [[0,1],[1,2],[3,4]], nodes 0–1–2 form one component and 3–4 form another, so the answer is 2.

KEY IDEA Track "how many trees remain" as edges arrive

Insight → treat every node as its own tree (one component each). For every edge, check if the two endpoints already share a root. If they do, the edge adds nothing. If they don't, merge their trees — and decrement the component count by one. After all edges the counter holds the answer, with no BFS/DFS required.

RECIPE Initialize, union, decrement

  • 0 · Initialize. parent[i] = i for every node; set components = n. Each node is its own root.
  • 1 · Find (with path compression). Walk up parent[] until you reach a self-loop. Path halving flattens the tree as you go — nearly O(1) amortized.
  • 2 · Union each edge. Call find on both endpoints. If roots differ, set parent[rootB] = rootA — merging the two trees — then components--.
  • 3 · Return. After all edges, return components.
Classic confusion → decrement only on a successfulmerge (when the two roots differ). If you decrement unconditionally — even when an edge is redundant — you'll undercount. Always guard with if (ra !== rb) before touching the counter.

COST Complexity & alternatives

BFS/DFS over adjacency list
O(V + E)
Clean, but allocates a visited set and a queue/stack.
Union-Find (path halving)
O(E · α(n))
α is inverse-Ackermann — effectively O(1). O(n) space.

Both are optimal in big-O terms, but Union-Find is incremental: you can add edges one at a time and the component count stays correct — BFS must restart from scratch for each batch.

Pattern transfer → the same Union-Find skeleton solves Graph Valid Tree (check components === 1 and no cycle), Redundant Connection (first edge whose two endpoints share a root is the cycle edge), Accounts Merge, and Smallest String With Swaps.

RUN IT Union-Find each edge, count remaining roots

step 0 / 7
STARTStart with 5 nodes, each its own component. parent[i] = i for all i. Components = 5.
edges0-11-23-4
State
0
p[0]
1
p[1]
2
p[2]
3
p[3]
4
p[4]
5
components
merged (different roots)already connected (same root)evaluating
slowfast

TYPESCRIPT The solution, annotated

countComponents.ts
function countComponents(n: number, edges: number[][]): number {
  const parent = Array.from({ length: n }, (_, i) => i);

  function find(x: number): number {
    while (parent[x] !== x) {
      parent[x] = parent[parent[x]]; // path compression (halving)
      x = parent[x];
    }
    return x;
  }

  function union(a: number, b: number): boolean {
    const ra = find(a);
    const rb = find(b);
    if (ra === rb) return false; // already connected
    parent[rb] = ra;             // merge rb's tree under ra
    return true;
  }

  let components = n;
  for (const [a, b] of edges) {
    if (union(a, b)) components--; // successful merge = one fewer component
  }
  return components;
}

Reading it block by block

Line 2 — initialize the forest. Every node starts as its own parent — n singleton trees, n components. Array.from with the index initializer is the idiomatic one-liner.
Lines 4–9 — find with path halving. Walking parent[x] until a self-loop gives the root. The line parent[x] = parent[parent[x]] skips one level each visit — no extra storage, nearly flat tree after a few operations.
Lines 11–17 — union. Find both roots. Equal roots mean already connected — return false so the caller knows not to decrement. Otherwise link rb's tree under ra and return true.
Lines 19–23 — count down and return. Start at n. Each successful union reduces the count by one. After all edges the counter is the answer.
Complexity → O(E · α(n)) time — α is the inverse-Ackermann function, bounded by 4 for any input that fits in the observable universe. O(n) space for the parent array. BFS/DFS would also be O(V + E) but Union-Find supports online edge insertion with no restart.

INTERVIEWFollow-ups they'll ask

  • "What if you need the actual groups, not just the count?" After all unions, group nodes by their root: find(i) as the bucket key in a Map.
  • "Graph Valid Tree variant?" A graph on n nodes is a valid tree iff it has exactly n-1 edges, no cycle, and is fully connected — equivalently, Union-Find reports exactly 1 component and every edge is a successful merge.
  • "Redundant Connection?" Process edges in order; the first edge where find(a) === find(b) before the union is the redundant one (it closes a cycle).
  • "Union by rank vs path compression?" Adding union-by-rank (always merge the shorter tree under the taller) gives the full O(α(n)) guarantee; path halving alone is O(log* n) — still practically constant and simpler to code under pressure.
  • "What if edges arrive as a stream?" Union-Find is the only classic structure that handles fully-online incremental connectivity; BFS/DFS must re-traverse the whole graph after each new edge.

MNEMONIC The one-liner

"Start with n islands. Each bridge that links two separate islands sinks one — count the islands left."

TRIGGERS When you see ___ → reach for ___

"number of connected components"Union-Find, start at n and decrement
same-root check on edgefind(a) === find(b) → skip; else union + --
"graph valid tree / no cycle"Union-Find: every edge must be a successful merge
edges arrive online / incrementallyUnion-Find (BFS must restart)

SKELETON The reusable shape

skeleton.ts
const parent = Array.from({ length: n }, (_, i) => i);

function find(x: number): number {
  while (parent[x] !== x) {
    parent[x] = parent[parent[x]]; // path halving
    x = parent[x];
  }
  return x;
}

let components = n;
for (const [a, b] of edges) {
  const ra = find(a), rb = find(b);
  if (ra !== rb) {
    parent[rb] = ra;
    components--;
  }
}
return components;

FLASHCARDS Tap to flip

What does parent[i] = i mean in Union-Find?
Node i is its own root — it belongs to a singleton component.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
n=5, edges [[0,1],[1,2],[3,4]]. How many connected components?
QUESTION 02
In Union-Find, why must you check find(a) !== find(b) before decrementing the component count?
QUESTION 03
What does path halving do inside find()?
QUESTION 04
What is the time complexity of this Union-Find approach for E edges on n nodes?
QUESTION 05
If you initialize components = n and process 5 edges where 3 are successful unions and 2 are redundant, what is the final component count?
QUESTION 06
Which problem is most directly solved by the same Union-Find skeleton?
QUESTION 07
You process edge [2, 2] (a self-loop). What happens?