Cycle Detection in Graphs — DFS, Union-Find, and Floyd's Algorithm
- Back edges in DFS indicate cycles in directed graphs; gray nodes in the current path are back-edge targets.
- Union-Find detects cycles in undirected graphs by checking if edge endpoints are already in the same component.
- Floyd's tortoise-and-hare uses O(1) space for linked list cycle detection.
For undirected graphs: DFS marks nodes as visited; a cycle exists if you encounter an already-visited node that is not the parent. For directed graphs: track nodes in the current DFS path (recursion stack); a cycle exists if you encounter a node already in the recursion stack. Union-Find is an efficient alternative for undirected graphs.
What is Cycle Detection? — Plain English
A cycle in a graph is a path that starts and ends at the same node. Cycle detection is important in many algorithms: topological sort requires an acyclic graph (DAG), dependency resolution must detect circular dependencies, and deadlock detection in operating systems finds cycles in resource-allocation graphs. For directed graphs, we use DFS with three-color marking (white=unvisited, gray=in current path, black=done). For undirected graphs, Union-Find is simpler. For linked lists, Floyd's cycle detection (tortoise and hare) uses O(1) space.
How Cycle Detection Works — Step by Step
Directed graph cycle detection using DFS coloring: 1. Initialize all nodes as WHITE (unvisited). 2. For each WHITE node, start a DFS. 3. Mark node as GRAY (currently being explored) before visiting neighbors. 4. If we reach a GRAY neighbor, a back edge exists → cycle detected → return True. 5. After exploring all neighbors, mark node as BLACK (fully explored). 6. If DFS completes without finding a gray neighbor, no cycle through this path.
Undirected graph cycle detection using Union-Find: 1. For each edge (u,v), check if u and v are already in the same component (find(u) == find(v)). 2. If yes, adding this edge creates a cycle → return True. 3. If no, union the components and continue. 4. If all edges are processed without triggering step 2, no cycle exists.
Linked list cycle detection (Floyd's): 1. slow = fast = head. 2. Each step: slow moves 1, fast moves 2. 3. If fast or fast.next is None → no cycle. 4. If slow == fast → cycle exists.
Worked Example — Tracing Directed Graph Cycle Detection
Directed graph: 0→1→2→3→1 (cycle: 1→2→3→1). Edges: (0,1),(1,2),(2,3),(3,1).
DFS from 0: 1. Visit 0: color[0]=GRAY. Explore neighbor 1. 2. Visit 1: color[1]=GRAY. Explore neighbor 2. 3. Visit 2: color[2]=GRAY. Explore neighbor 3. 4. Visit 3: color[3]=GRAY. Explore neighbor 1. 5. Node 1 is GRAY → BACK EDGE found! Cycle detected. Return True.
Undirected example: edges (0,1),(1,2),(2,0). Union-Find: process (0,1): find(0)=0, find(1)=1, different → union. Process (1,2): different → union. Process (2,0): find(2)=0, find(0)=0, SAME → cycle detected!
Implementation
For directed graphs, maintain a color array (0=white,1=gray,2=black). DFS sets color to gray on entry and black on exit; finding a gray neighbor is a cycle. For undirected graphs, Union-Find checks connectivity before adding each edge. For linked lists, two pointers advance at different speeds — if they meet, a cycle exists. The DFS approach is O(V+E) time and O(V) space; Union-Find is O(E * alpha(V)) ≈ O(E) with path compression.
# Directed graph cycle detection (DFS coloring) def has_cycle_directed(num_v, adj): WHITE, GRAY, BLACK = 0, 1, 2 color = [WHITE] * num_v def dfs(v): color[v] = GRAY for nb in adj.get(v, []): if color[nb] == GRAY: # back edge → cycle return True if color[nb] == WHITE and dfs(nb): return True color[v] = BLACK return False return any(dfs(v) for v in range(num_v) if color[v] == WHITE) # Test directed adj_d = {0:[1], 1:[2], 2:[3], 3:[1]} # cycle 1->2->3->1 print(has_cycle_directed(4, adj_d)) # True # Undirected graph cycle detection (Union-Find) def has_cycle_undirected(num_v, edges): parent = list(range(num_v)) def find(x): while parent[x] != x: parent[x] = parent[parent[x]] x = parent[x] return x def union(a, b): ra, rb = find(a), find(b) if ra == rb: return True # already connected → cycle parent[ra] = rb return False return any(union(u, v) for u, v in edges) print(has_cycle_undirected(3, [(0,1),(1,2),(2,0)])) # True print(has_cycle_undirected(3, [(0,1),(1,2)])) # False
True
False
🎯 Key Takeaways
- Back edges in DFS indicate cycles in directed graphs; gray nodes in the current path are back-edge targets.
- Union-Find detects cycles in undirected graphs by checking if edge endpoints are already in the same component.
- Floyd's tortoise-and-hare uses O(1) space for linked list cycle detection.
Interview Questions on This Topic
- QHow do you detect a cycle in a directed graph?
- QWhat is the difference between a back edge and a cross edge?
- QHow does Union-Find detect cycles?
Frequently Asked Questions
Why use three colors (white/gray/black) instead of just visited/unvisited for directed graphs?
Two colors cannot distinguish between a neighbor on the current DFS path (which indicates a cycle) and a neighbor that was visited via a different DFS path (which is fine). A back edge (gray→gray) is a cycle; a cross edge (gray→black) is not. Three colors are required to make this distinction.
Why doesn't the simple visited set work for undirected graphs in cycle detection?
For undirected graphs, each edge appears twice (u→v and v→u). When DFS is at node u having come from v, v is already visited. This is a 'backward edge' in undirected DFS but not a cycle. The fix: track the parent and skip the parent when checking neighbors.
How does Floyd's algorithm find the cycle start after detecting a cycle?
After slow meets fast inside the cycle, reset one pointer to the head. Advance both pointers one step at a time. The meeting point is the cycle start. Mathematical proof: if the distance from head to cycle start is F, and the cycle length is C, then slow has traveled F+k*C+m steps for some k. The reset pointer meets slow at the cycle start after F more steps.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.