Detect Cycle in a Directed Graph
Given a directed graph, check if it contains at least one cycle. A directed cycle is a sequence of directed edges where a vertex is reachable from itself.
Why the Undirected Logic Fails
In a directed graph, simply checking if an already-visited neighbor is not the parent leads to false positives.
Consider this Directed Acyclic Graph (DAG):
If we start DFS from 0:
- Visit
0$\rightarrow$ Visit1. - Backtrack to
0$\rightarrow$ Visit2. - From
2, we look at its neighbor1.1is already visited, and the parent of2is0(not1). - If we used the undirected logic, we would incorrectly flag this as a cycle. However, this is not a cycle; it is just two paths converging on
1(a cross-edge).
The Logic (DFS with Recursion Stack Tracker)
To detect a cycle, we must check for back-edges—edges that point back to an ancestor in the active DFS path. We do this by keeping track of the vertices currently active in the recursion stack (recStack).
- Maintain a
visitedset to avoid repeating work. - Maintain a
recStackset to track nodes in the current DFS path. - For each vertex:
- If the vertex is already in
recStack, we have looped back $\rightarrow$ Cycle detected. - If the vertex is already in
visited(but notrecStack), skip it (it's already been fully explored and is cycle-free).
- If the vertex is already in
- Recursively visit all neighbors.
- After exploring all paths from the current node, remove it from
recStack(backtrack).
Visual Example
Let's look at this cyclic directed graph:
- Start DFS at
0.visited = {0}recStack = {0}
- Visit neighbor
1of0.visited = {0, 1}recStack = {0, 1}
- Visit neighbor
2of1.visited = {0, 1, 2}recStack = {0, 1, 2}
- Neighbor of
2is0.0is already inrecStack! - Cycle detected!
JavaScript Code Implementation
class Graph {
constructor() {
this.list = {};
}
addVertex(vertex) {
if (!this.list[vertex]) this.list[vertex] = [];
}
addEdge(v1, v2) {
this.addVertex(v1);
this.addVertex(v2);
this.list[v1].push(v2); // Directed edge v1 -> v2
}
// Main check
isCyclic() {
const visited = {};
const recStack = {};
// Initialize trackers
Object.keys(this.list).forEach(vertex => {
visited[vertex] = false;
recStack[vertex] = false;
});
// Run DFS helper for all unvisited nodes
for (let vertex of Object.keys(this.list)) {
if (!visited[vertex]) {
if (this._isCyclicUtil(vertex, visited, recStack)) {
return true;
}
}
}
return false;
}
// DFS Utility
_isCyclicUtil(v, visited, recStack) {
// If node is already in recursion stack, we found a loop
if (recStack[v]) return true;
// If node is already fully explored, no need to check again
if (visited[v]) return false;
// Add node to visited and active recursion path
visited[v] = true;
recStack[v] = true;
const neighbors = this.list[v] || [];
for (let neighbor of neighbors) {
if (this._isCyclicUtil(neighbor, visited, recStack)) {
return true;
}
}
// Backtrack: Remove node from active path stack
recStack[v] = false;
return false;
}
}
// Driver code
const g = new Graph();
g.addEdge("0", "1");
g.addEdge("0", "2");
g.addEdge("1", "2");
g.addEdge("2", "0");
g.addEdge("3", "3");
console.log("Directed Graph contains cycle:", g.isCyclic()); // Output: trueComplexity Analysis
- Time Complexity: $O(V + E)$
- Standard DFS traversal covering all reachable vertices and edges.
- Space Complexity: $O(V)$
- Recursive call stack and tracking structures.