Does an undirected graph contain a cycle?
A graph has n nodes numbered 0 to n − 1 and a list of undirected
edges. No edge joins a node to itself and no edge is repeated. Return true if the graph
contains a cycle, and false otherwise. The graph need not be connected.
Examples
| Input | Output | Why |
|---|---|---|
n = 6, edges = [[0,1],[1,2],[0,3],[3,4],[4,1],[2,5]] | true | 0–1–4–3–0 is a cycle. |
n = 5, edges = [[0,1],[1,2],[2,3],[3,4]] | false | A path. |
n = 6, edges = [[0,1],[2,3],[3,4],[4,2]] | true | The cycle is in the second piece; the first piece has none. |
Constraints
- 1 ≤ n ≤ 10⁵
- 0 ≤ edges.length ≤ 10⁵
- No self-loops and no repeated edges
Hints
Hint 1
Search from every unvisited node, because the cycle may be in any piece of the graph.
Hint 2
Remember, for each node, the node you reached it from.
Hint 3
From u, a neighbour that is already visited and is not the node you came from means a cycle.
Where people lose marks
- Treating the parent as a cycle: every undirected edge is seen from both ends.
- Searching only from node 0, which misses a cycle in another piece of the graph.
- Using the directed-graph colours here. They work, but the parent check is all an undirected graph needs.
Dry run on 6,[[0,1],[1,2],[0,3],[3,4],[4,1],[2,5]]
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 11current0parent—
Start at 0; it has no parent.
-
Step 2 / 11current1parent0
Enter 1 from 0. Its parent is 0.
-
Step 3 / 11current1parent0
0 is visited, but it is 1's parent: the edge 1–0 is the one just used, not a cycle.
-
Step 4 / 11current2parent1
Enter 2 from 1. Its parent is 1.
-
Step 5 / 11current2parent1
1 is visited, but it is 2's parent: the edge 2–1 is the one just used, not a cycle.
-
Step 6 / 11current5parent2
Enter 5 from 2. Its parent is 2.
-
Step 7 / 11current5parent2
2 is visited, but it is 5's parent: the edge 5–2 is the one just used, not a cycle.
-
Step 8 / 11current4parent1
Enter 4 from 1. Its parent is 1.
-
Step 9 / 11current4parent1
1 is visited, but it is 4's parent: the edge 4–1 is the one just used, not a cycle.
-
Step 10 / 11current3parent4
Enter 3 from 4. Its parent is 4.
-
Step 11 / 11current3parent4
0 is visited and is not 3's parent. There is a second way from 0 to 3: a cycle.
How to think about it
Search the graph and record the parent of every node. Each node is reached once, so the edges used form a
forest; an edge outside that forest connects two nodes that are already joined and so closes a cycle. From node
u, such an edge shows up as a neighbour that is already marked and is not u's parent.
The solutions use an explicit stack of (node, parent) pairs and mark each node as it is pushed, which keeps them safe on a path of 10⁵ nodes. A union-find over the edges is an equally good answer: an edge whose ends already share a root closes a cycle.
Complexity. Time O(n + m) · Space O(n + m)
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
def has_cycle(n, edges):
adj = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
seen = [False] * n
for s in range(n):
if seen[s]:
continue
seen[s] = True
stack = [(s, -1)] # (node, the node it was reached from)
while stack:
u, parent = stack.pop()
for v in adj[u]:
if v == parent:
continue # the edge we arrived on
if seen[v]:
return True # reached a second way: a cycle
seen[v] = True
stack.append((v, u))
return False#include <utility>
#include <vector>
bool solve(int n, const std::vector<std::vector<int>>& edges) {
std::vector<std::vector<int>> adj(n);
for (const auto& e : edges) {
adj[e[0]].push_back(e[1]);
adj[e[1]].push_back(e[0]);
}
std::vector<bool> seen(n, false);
for (int s = 0; s < n; ++s) {
if (seen[s]) continue;
seen[s] = true;
std::vector<std::pair<int, int>> stack{{s, -1}}; // (node, parent)
while (!stack.empty()) {
auto [u, parent] = stack.back();
stack.pop_back();
for (int v : adj[u]) {
if (v == parent) continue; // the edge we arrived on
if (seen[v]) return true; // reached a second way
seen[v] = true;
stack.push_back({v, u});
}
}
}
return false;
}import java.util.*;
class Solution {
public boolean hasCycle(int n, int[][] edges) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (int[] e : edges) {
adj.get(e[0]).add(e[1]);
adj.get(e[1]).add(e[0]);
}
boolean[] seen = new boolean[n];
ArrayDeque<int[]> stack = new ArrayDeque<>();
for (int s = 0; s < n; s++) {
if (seen[s]) continue;
seen[s] = true;
stack.push(new int[]{s, -1}); // {node, parent}
while (!stack.isEmpty()) {
int[] top = stack.pop();
int u = top[0], parent = top[1];
for (int v : adj.get(u)) {
if (v == parent) continue; // the edge we arrived on
if (seen[v]) return true; // reached a second way
seen[v] = true;
stack.push(new int[]{v, u});
}
}
}
return false;
}
}function hasCycle(n, edges) {
const adj = Array.from({ length: n }, () => []);
for (const [u, v] of edges) { adj[u].push(v); adj[v].push(u); }
const seen = new Array(n).fill(false);
for (let s = 0; s < n; s++) {
if (seen[s]) continue;
seen[s] = true;
const stack = [[s, -1]]; // [node, the node it was reached from]
while (stack.length) {
const [u, parent] = stack.pop();
for (const v of adj[u]) {
if (v === parent) continue; // the edge we arrived on
if (seen[v]) return true; // reached a second way: a cycle
seen[v] = true;
stack.push([v, u]);
}
}
}
return false;
}fn solve(n: i64, edges: &[[i64; 2]]) -> bool {
let n = n as usize;
let mut adj = vec![Vec::new(); n];
for e in edges {
let (u, v) = (e[0] as usize, e[1] as usize);
adj[u].push(v);
adj[v].push(u);
}
let mut seen = vec![false; n];
for s in 0..n {
if seen[s] {
continue;
}
seen[s] = true;
let mut stack = vec![(s, usize::MAX)]; // (node, parent)
while let Some((u, parent)) = stack.pop() {
for &v in &adj[u] {
if v == parent {
continue; // the edge we arrived on
}
if seen[v] {
return true; // reached a second way
}
seen[v] = true;
stack.push((v, u));
}
}
}
false
}import qualified Data.Map.Strict as M
import qualified Data.Set as S
solve :: Int -> [[Int]] -> Bool
solve n edges = go [0 .. n - 1] S.empty
where
adj = M.fromListWith (++) (concat [[(u, [v]), (v, [u])] | [u, v] <- edges])
nbrs u = M.findWithDefault [] u adj
-- start a search from every node not yet seen
go [] _ = False
go (s : rest) seen
| S.member s seen = go rest seen
| otherwise = case search [(s, -1)] (S.insert s seen) of
Nothing -> True
Just seen' -> go rest seen'
-- Nothing means a cycle was found; Just carries the visited set on
search [] seen = Just seen
search ((u, parent) : stack) seen = step (nbrs u) stack seen
where
step [] st sn = search st sn
step (v : vs) st sn
| v == parent = step vs st sn
| S.member v sn = Nothing
| otherwise = step vs ((v, u) : st) (S.insert v sn)
Variants to try
- Graph valid tree: return true if the edges form a single tree (connected, no cycle, exactly n − 1 edges).
- Return the nodes of one cycle, in order, by walking parent links from both ends of the closing edge.
- Edges may repeat. Two copies of the same edge form a cycle: skip the parent edge by its index instead of the parent node.
Next problems
Does a directed graph contain a cycle?
Return true if following the arrows can bring you back to where you started. A node still on the current path is the giveaway.
Open the problem → Easy12 minCount the islands in a grid
Every unvisited land cell starts a new island; flood it so it is never counted again. The first graph problem most interviews ask.
Open the problem → Medium15 minCan the graph be split in two?
Return true if the nodes can be coloured with two colours so that every edge joins different colours. Colour while you search.
Open the problem →Write a solution and run it against the real test table.
Keep going
Does a directed graph contain a cycle?
Return true if following the arrows can bring you back to where you started. A node still on the current path is the giveaway.
Open the problem → Easy12 minCount the islands in a grid
Every unvisited land cell starts a new island; flood it so it is never counted again. The first graph problem most interviews ask.
Open the problem → Medium15 minCan the graph be split in two?
Return true if the nodes can be coloured with two colours so that every edge joins different colours. Colour while you search.
Open the problem → GraphsThe pattern behind it
Grids, networks and dependencies are all graphs. Breadth-first search finds shortest paths, depth-first search and union-find find what is connected, Dijkstra handles weighted edges, and a queue of nodes with no remaining prerequisites gives an order.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.