Graphs · Graph Traversal Fundamentals

Graph Cycle Detection

Detecting a cycle by tracking, in an undirected graph, whether a visited neighbor is anything other than the edge just arrived from (since a simple back-edge to the parent is not a cycle); or in a directed graph, whether a visited neighbor is still on the current recursion path (a back edge to an ancestor still being explored, not merely one already fully finished).

  • 6 min read
  • 8 practice questions
  • 3 graded problems
On this page
  1. Description
  2. The approach
  3. A second flavor: count what Kahn's algorithm can't take
  4. Where you'll see it
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

Does the graph loop back on itself? For a build system, a cycle means modules that can't be compiled in any order. For a network, it's a physical loop that circulates broadcasts forever. A traversal answers the question, but "I've been here before" means different things in undirected and directed graphs. This skill covers both checks, plus Kahn's algorithm, which finds directed cycles without recursion.

We'll go from simple to more technical:

  1. The pattern: the parent check (undirected) and three-state DFS (directed).
  2. A second flavor: Kahn's algorithm, where the nodes it can't take reveal a cycle.
  3. Where cycle checks run in production (builds, deadlocks, spreadsheets, networks).
  4. What breaks it: the wrong meaning of "visited", resetting finished nodes, and missing components.

The approach

Java

// Undirected: a visited neighbor that isn't the node we came from closes a cycle
boolean bfsHasCycle(int start, List<List<Integer>> adj, boolean[] visited) {
    Deque<int[]> queue = new ArrayDeque<>();          // {node, parent}
    queue.add(new int[]{start, -1});
    visited[start] = true;
    while (!queue.isEmpty()) {
        int[] cur = queue.poll();
        int node = cur[0], parent = cur[1];
        for (int next : adj.get(node)) {
            if (!visited[next]) { visited[next] = true; queue.add(new int[]{next, node}); }
            else if (next != parent) return true;     // reached again by a different route
        }
    }
    return false;
}

// Directed: only an edge back to a node still on the current path closes a cycle
boolean dfs(int node, List<List<Integer>> adj, int[] state) {   // 0 = unvisited, 1 = in progress, 2 = done
    state[node] = 1;
    for (int next : adj.get(node)) {
        if (state[next] == 1) return true;                        // back edge: next is an ancestor on this path
        if (state[next] == 0 && dfs(next, adj, state)) return true;
    }
    state[node] = 2;                                              // finished - safe to reach again later
    return false;
}

// Either check covers one component; loop over every unvisited node to cover the graph
for (int v = 0; v < n; v++) if (state[v] == 0 && dfs(v, adj, state)) return true;

Python

from collections import deque


# Undirected: a visited neighbor that isn't the node we came from closes a cycle
def bfs_has_cycle(start: int, adj: list[list[int]], visited: list[bool]) -> bool:
    queue = deque([(start, -1)])                 # (node, parent)
    visited[start] = True
    while queue:
        node, parent = queue.popleft()
        for nxt in adj[node]:
            if not visited[nxt]:
                visited[nxt] = True
                queue.append((nxt, node))
            elif nxt != parent:
                return True                      # reached again by a different route
    return False


# Directed: only an edge back to a node still on the current path closes a cycle
def dfs(node: int, adj: list[list[int]], state: list[int]) -> bool:   # 0 unvisited, 1 in progress, 2 done
    state[node] = 1
    for nxt in adj[node]:
        if state[nxt] == 1:
            return True                          # back edge: nxt is an ancestor on this path
        if state[nxt] == 0 and dfs(nxt, adj, state):
            return True
    state[node] = 2                              # finished - safe to reach again later
    return False

# Either check covers one component; loop over every unvisited node to cover the graph:
# any(state[v] == 0 and dfs(v, adj, state) for v in range(n))

Undirected adjacency lists store every edge both ways, so the node you just came from is always "visited" - that's the edge you walked, not a cycle. Directed graphs have the opposite problem: two paths can legitimately converge on the same node (A->B->D and A->C->D), so "visited" isn't a cycle either. What closes a directed cycle is an edge back to a node whose DFS hasn't finished yet - an ancestor on the current path - which is exactly what the "in progress" state marks.

A second flavor: count what Kahn's algorithm can't take

A directed graph is acyclic exactly when all its nodes can be put in a topological order. Kahn's algorithm (repeatedly take a node with no remaining incoming edges) finds such an order - and it gets stuck on cycles, because every node on a cycle keeps an incoming edge from the cycle itself:

Java

int[] indegree = new int[n];
for (List<Integer> next : adj) for (int v : next) indegree[v]++;
Deque<Integer> ready = new ArrayDeque<>();
for (int v = 0; v < n; v++) if (indegree[v] == 0) ready.add(v);
int taken = 0;
while (!ready.isEmpty()) {
    int u = ready.poll();
    taken++;
    for (int v : adj.get(u)) if (--indegree[v] == 0) ready.add(v);
}
boolean hasCycle = taken < n;    // the nodes left over are on a cycle or depend on one

Python

from collections import deque

indegree = [0] * n
for targets in adj:
    for v in targets:
        indegree[v] += 1
ready = deque(v for v in range(n) if indegree[v] == 0)
taken = 0
while ready:
    u = ready.popleft()
    taken += 1
    for v in adj[u]:
        indegree[v] -= 1
        if indegree[v] == 0:
            ready.append(v)
has_cycle = taken < n    # the nodes left over are on a cycle or depend on one

No recursion, so no stack-depth limit. On 0->1, 1->2, 2->1, 2->3 it takes only node 0: nodes 1 and 2 wait on each other forever, and 3 waits on 2.

Where you'll see it

Build tools and package managers reject circular dependencies before building anything; databases detect deadlocks by finding a cycle in the "transaction waits for transaction" graph; spreadsheet engines flag circular references; network switches run spanning-tree protocols precisely because an undirected loop would circulate broadcasts forever.

Which tool for which job?

You needUse
A cycle in an undirected graphBFS or DFS with a parent check
A cycle in a directed graphthree-state DFS: a back edge to an in-progress node
Directed, no recursion, plus an orderKahn's algorithm: fewer than n taken means a cycle
The cycle's nodesDFS, walking parents back from the back edge
Undirected edges arriving one at a timeunion-find: an edge inside one set closes a cycle

Common mistakes

MistakeWhat happensDo this instead
Undirected, plain "visited"every edge comes back as a false cycleskip the parent
Directed, plain "visited"a diamond looks like a cyclethree states: in progress vs done
Resetting finished nodes to unvisitedexponential re-exploration (1,048,575 calls vs 39)mark them done
Checking from node 0 onlya cycle in another component is missedloop over every unvisited node
Treating Kahn's leftovers as the cycledependents of the cycle are includedfind the cycle with DFS

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. In an undirected graph, why isn't "I reached a visited node" enough to report a cycle?

Show answer

Every undirected edge is stored in both endpoints' lists, so right after walking from A to B, B's list shows A as visited - that's the same edge, not a cycle. Only a visited neighbor other than the parent means the node was reached by a second route.

2. Why does a directed check need three states instead of visited/unvisited?

Show answer

A visited node might be finished (reached before along another branch - fine, as in a diamond) or still on the current path (an ancestor - a cycle). "In progress" marks the current path; "done" marks finished nodes that can be reached again safely.

3. Kahn's algorithm takes only 1 of 4 nodes. What does that tell you?

Show answer

The graph has a cycle. Nodes on a cycle always keep an incoming edge from the cycle, so their in-degree never reaches zero; nodes that depend on them get stuck too. Any graph where Kahn's takes all n nodes is acyclic.

Key takeaways

  • Undirected: a visited neighbor other than the parent closes a cycle - the parent is just the edge you walked.
  • Directed: only an edge to a node still in progress (on the current path) closes a cycle; three states tell "ancestor" from "finished".
  • Mark finished nodes "done", not "unvisited", or they're re-explored exponentially often.
  • Kahn's algorithm detects directed cycles without recursion: fewer than n nodes taken means a cycle.
  • Loop over every unvisited node - a cycle can hide in any component.

Quick revise

The whole skill on one page, for the night before an interview.

This page puts the lesson and the deep dive together. Skim it before practice or an interview, and go back to the full lesson for anything you don't recognize.

Pick the tool

You needUse
Undirectedparent check
Directedthree-state DFS
Directed, no recursionKahn's (taken < n)
The cycle itselfDFS plus parents
Edges arriving one at a time, undirectedunion-find

Snippets worth knowing by heart

Java

boolean dfs(int u) {
    state[u] = 1;                                   // in progress
    for (int v : adj.get(u)) {
        if (state[v] == 1) return true;             // back edge
        if (state[v] == 0 && dfs(v)) return true;
    }
    state[u] = 2;                                   // done
    return false;
}

Python

def union(a, b):                         # returns True if edge a-b closes a cycle
    ra, rb = find(a), find(b)
    if ra == rb:
        return True
    parent[ra] = rb
    return False

The traps

  • Plain "visited": undirected edges look like cycles back to the parent, and directed diamonds look cyclic. Skip the parent; use three states.
  • Resetting finished nodes to unvisited: exploration turns exponential. Mark them done.
  • No outer loop over components: a cycle in another component is missed. Start from every unvisited node.
  • Kahn's leftovers aren't the cycle: they include nodes that depend on it. Find the cycle itself with DFS.

Staff-level points, one line each

  • A directed graph has a cycle if and only if DFS finds a back edge (an edge to a gray node).
  • An iterative three-state DFS agreed with Kahn's on 300 random digraphs, and every cycle it reported was real.
  • Kahn's left [1, 2, 3], but the cycle was only 2 -> 1 -> 2.
  • Union-find flagged edge #452 as the first to close a cycle; a full check agreed.
  • findDeadlockedThreads() found 2 threads waiting on each other, which is a cycle in the wait-for graph.

Say it in an interview

  • "Undirected, I skip the parent. Directed, I look for an edge to a node still in progress."
  • "Finished nodes stay done, so each node is explored once: O(V + E)."
  • "Kahn's taking fewer than n nodes proves a cycle, and DFS shows me which one."
  • "For edges arriving online, I'd use union-find."

Practice

This skill comes with 8 multiple-choice questions that tell you which misconception a wrong answer reveals and 3 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Graph Cycle Detection free

Detect Cycle in an Undirected Graph

MediumProCoding problemJava · Python

Given an undirected graph with n nodes (labeled 0 to n-1) and a list of edges, return whether the graph contains a cycle.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Detect Cycle in a Directed Graph

MediumProCoding problemJava · Python

Given a directed graph with n nodes (labeled 0 to n-1) and a list of directed edges, return whether the graph contains a cycle.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Find Eventual Safe States

MediumProCoding problemJava · Python

In a directed graph with nodes 0 to n-1, call a node terminal if no edges leave it, and safe if every walk that starts from it ends at a terminal node, so none of them can get caught in a cycle. List the safe nodes in ascending order.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Graph Cycle Detection free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime