How to detect cycle in a graph

Three ways to detect cycle in graph, with DFS walkthroughs for directed and undirected variants plus Union Find.

10 minutes
Intermediate
What you will learn

Why cycle detection differs between directed and undirected graphs

How DFS parent tracking catches undirected cycles without false positives

How the three-color algorithm identifies back edges in directed graphs

When Union Find is a better choice than DFS for cycle detection

How do you detect cycle in graph traversal? The answer depends on whether the graph is directed or undirected, and most explanations gloss over why that distinction matters. It's not a technicality. Use the wrong method on the wrong graph type and you'll get false positives or miss real cycles entirely.

TL;DR
Undirected graphs need parent tracking during DFS (or Union Find). Directed graphs need three-color state tracking. The approaches aren't interchangeable, and the reason comes down to what a "back edge" means in each context.

What cycle detection actually solves

A cycle is a path that starts and ends at the same node. In undirected graphs, DFS flags one when it reaches an already-visited node that isn't the direct parent. In directed graphs, DFS uses three colors to track node states, and a back edge to a grey node confirms a cycle.

That definition sounds clean, but things get messier in practice. "Already visited" means different things depending on graph type.

In an undirected graph, every edge goes both ways. If node A connects to node B, then B also connects to A. So when you run DFS from A and visit B, B's neighbor list includes A. If you just check "have I visited this neighbor before?" you'll immediately flag A as a cycle. But you literally just walked across that edge. That's not a cycle.

In a directed graph, edges have direction. A to B doesn't imply B to A, so the problem flips. You can visit a node, finish exploring all its descendants, move on to a different branch, and then hit that node again through a completely separate path. That's also not a cycle, just two paths that happen to converge on the same node.

These two situations need different detection methods. Mixing them up is the single most common mistake in cycle detection code.

Important
Cycle detection is a prerequisite for topological sort. If a directed graph has a cycle, no valid topological ordering exists. Detecting the cycle first saves you from running an algorithm that can't produce a correct result.

The practical applications are concrete. Build systems catch circular imports. Operating systems catch circular waits that cause deadlock. Course prerequisite validators catch circular requirements. In every case, a cycle means the process is stuck and can't make forward progress.

DFS cycle detection in undirected graphs

The core idea is simple: run DFS, and for every neighbor you're about to visit, check two things. Is it already in the visited set, and is it not the node you just came from?

That parent check is the whole game. Without it, every single edge in an undirected graph looks like a cycle because adjacency is bidirectional.

Consider five nodes connected as A-B-C-D-B (forming a cycle through B-C-D) with A-E as a separate branch that has no cycle.

  1. Python

Walk through this on the example. You start DFS from A with parent=None and move to B (parent=A). From B, neighbors are A, C, D. You skip A because it's the parent, then go to C (parent=B). C's neighbors are B and D, so you skip B as the parent and move to D (parent=C). D's neighbors are B and C. You skip C as the parent and check B, which is already visited but isn't the parent. That confirms a cycle.

The parent parameter does all the heavy lifting. Remove it and the algorithm reports a cycle in every connected graph with more than one edge.

“The parent check is what makes undirected cycle detection correct. Without it, bidirectional edges masquerade as cycles.”
The fundamental invariant

One thing worth noting: this handles disconnected graphs correctly only if you iterate over all nodes and start a new DFS from any unvisited node. A single DFS call from one starting node only covers its connected component.

DFS cycle detection in directed graphs

Parent tracking doesn't work here. In a directed graph, there's no "parent" in the undirected sense. Edge direction means you can arrive at a node through multiple independent paths, and that's perfectly normal. The real question isn't "did I visit this node before?" but "is this node currently in my call stack?"

That distinction requires three states instead of two. The standard algorithm assigns each node a color.

  • White: Means not yet visited
  • Grey: Means currently in the DFS call stack (being processed)
  • Black: Means fully processed, all descendants explored

A cycle exists when DFS encounters a grey node. Grey means "I started exploring this node but haven't finished yet," so reaching it again means there's a path from this node back to itself.

A black node is totally fine. You've already explored everything reachable from it and you're reaching it through a different path, so there's no cycle.

  1. Python

Trace this on a directed graph with edges A to B, B to C, C to D, D to B, A to E.

You start from A (color A grey), then go to B (grey), then C goes grey, then D goes grey. D's neighbor is B, and B's color is grey, which means you've found a back edge confirming the cycle B to C to D to B.

Now consider a non-cyclic case to see why two colors aren't enough. The edges are A to B, A to C, B to D, C to D. You start from A, go to B, then go to D. You mark D black, then mark B black. Back to A, you go to C, and C's neighbor D is already black, so there's no cycle. With only visited/unvisited, D would look like a cycle when reached from C because it was "already visited." Three colors correctly distinguish "currently in progress" from "finished."

Practicing directed and undirected detection in alternation, rather than learning one completely before starting the other, builds stronger discrimination between when each method applies. The research on interleaving backs this up pretty consistently.

Union Find for undirected cycle detection

DFS isn't the only option for undirected graphs. Union Find (Disjoint Set Union) works differently, and it's especially useful when your graph arrives as a list of edges rather than an adjacency list.

You process edges one at a time. For each edge (u, v), you check if u and v already belong to the same connected component. If they do, this edge creates a cycle. If they don't, merge their components.

  1. Python

When does Union Find beat DFS? There are two main scenarios. First, when the graph is given as an edge list and you'd need to build an adjacency list just to run DFS, Union Find processes edges directly. Second, when you're building the graph incrementally and need cycle checks as you add edges. Each union call is nearly O(1) with path compression and union by rank, so cycle detection is essentially constant-time per edge.

The trade-off matters though. Union Find only works for undirected graphs. It can't detect cycles in directed graphs because it has no concept of edge direction. It treats the graph as a collection of connected components, and that's an inherently undirected abstraction.

DFS cycle detection
Union Find cycle detection
Requires adjacency list or matrix representation
Works directly on edge lists
Explores the full graph in one pass
Processes edges incrementally
Works for both directed and undirected graphs
Only works for undirected graphs
O(V + E) time, O(V) space
O(E * α(V)) time, nearly O(E), O(V) space

The connection to topological sort

Topological sort produces a linear ordering of nodes where every directed edge points "forward." This only works if the graph has no cycles. A cycle means node A must come before B, B before C, and C before A. No linear ordering can satisfy that contradiction.

The connection runs both ways. You can detect cycles in a directed graph by attempting a topological sort. If the sort produces fewer nodes than the graph contains, there's a cycle.

That's where Kahn's algorithm comes in.

Kahn's algorithm (BFS-based topological sort) starts with all nodes that have zero in-degree, removes them, updates in-degrees, and repeats. If a cycle exists, the nodes in the cycle never reach in-degree zero. They're stuck waiting on each other.

💡 Tip
If your problem already requires topological sort (dependency resolution, course scheduling), you don't need a separate cycle detection pass. Run Kahn's algorithm and check if the output contains all nodes. If it doesn't, the remaining nodes form cycles.

For a deeper look at how BFS and DFS compare across graph problems, including when each traversal strategy is the right fit, see the BFS vs DFS comparison. The broader graph algorithms landscape, from shortest paths to connected components, is covered in the complete graph algorithms guide.

Which algorithm for which graph

Here's a quick decision framework you can use.

  • Undirected graph (adjacency list available): DFS with parent tracking
  • Undirected graph (edge list input): Union Find
  • Directed graph (any representation): DFS with three colors
  • Directed graph (need topological sort anyway): Kahn's algorithm (cycle detection comes free)

The three methods are distinct, but the underlying question is the same. Can I reach a node that I'm currently in the process of exploring? For undirected graphs, "in the process of exploring" means "visited but not my direct parent." For directed graphs, it means "grey in the three-color system." For Union Find, it means "already in the same component."

Six months ago, you'd open a cycle detection problem and try to remember which version you'd last memorized. The directed vs undirected distinction felt like a footnote. Now you recognize it is the algorithm. You see an undirected graph and your first instinct is parent tracking, because you understand why bidirectional edges produce false positives. You see a directed graph and you reach for three colors, because you know a finished node and a node in progress are different states that need different treatment.

Codeintuition's Graph course covers cycle detection in both directed and undirected graphs from first principles. The undirected detection lesson and directed detection lesson each build the reasoning before the implementation. Union Find, topological sort, and all five graph patterns follow the same construction-first model across 40 lessons. The free tier covers complete courses on Arrays and Singly Linked Lists, where the same first-principles approach applies to foundational patterns like two pointers and fast-slow pointers.

The first time you pick the right cycle detection method without checking which one a problem expects, you'll know the understanding is there.

Do you want to master data structures?

Try our data structures learning path made of highly visual and interactive courses. Get hands on experience by solving real problems in a structured manner. All resources you would ever need in one place for FREE

For directed graphs, BFS-based cycle detection works through Kahn's algorithm (topological sort). You process nodes with zero in-degree, and if nodes remain unprocessed at the end, those nodes form cycles. For undirected graphs, BFS with parent tracking works similarly to the DFS version. DFS is more common because the recursive structure maps naturally to back-edge detection, but BFS works in both cases.
Union Find tracks connected components, which is an undirected concept. It answers "are these two nodes connected?" but not "is there a directed path from A to B?" In a directed graph, A to B and A to C to B can both exist without forming a cycle. Union Find would merge A, B, and C into one component, losing the directional information needed to tell convergence apart from cycles.
Both the undirected and directed DFS approaches run in O(V + E) time, where V is vertices and E is edges. Each node gets visited once, and each edge gets examined once (twice for undirected graphs since edges appear in both adjacency lists). Space is O(V) for the visited set or color array, plus the recursion stack.
They're tightly connected. Topological sort only works on directed acyclic graphs (DAGs). Interview problems that ask "is this ordering possible?" (course schedules, build dependencies, task ordering) are really asking "does this directed graph have a cycle?" You can solve both in a single pass with Kahn's algorithm. If the output contains all nodes, the graph is acyclic and the ordering works. If nodes remain, those form cycles and the ordering is impossible.
After. Cycle detection builds directly on DFS traversal mechanics. You need to understand how DFS explores nodes, maintains a call stack, and processes neighbors before you add parent-tracking or three-color logic on top. Most structured courses teach traversal first, then cycle detection as the first real application. Trying cycle detection without solid DFS foundations means you'll memorize the code without grasping why it works.
Was this helpful?