101 Logo
onenoughtone

Cycle Detection Pattern

What is Cycle Detection?

Cycle detection is a pattern that uses DFS to find cycles (loops) in a graph. A cycle is a path that starts and ends at the same vertex, with at least one edge.

This pattern is useful in many applications, such as detecting deadlocks in systems, finding circular dependencies, and checking if a graph is a tree or DAG.

Directed vs. Undirected Graphs

The approach to cycle detection differs slightly between directed and undirected graphs:

  • Directed Graphs: Use a recursion stack to keep track of vertices in the current DFS path
  • Undirected Graphs: Keep track of the parent vertex to avoid false cycles due to bidirectional edges

Cycle Detection in Directed Graphs

Here's an implementation of cycle detection in a directed graph:

function hasCycle(graph) { // Set to keep track of visited vertices const visited = new Set(); // Set to keep track of vertices in the current recursion stack const recursionStack = new Set(); // Check each vertex as a possible starting point for (const vertex in graph) { if (dfsHasCycle(graph, vertex, visited, recursionStack)) { return true; } } // No cycle found return false; } function dfsHasCycle(graph, vertex, visited, recursionStack) { // If the vertex is not visited yet if (!visited.has(vertex)) { // Mark the vertex as visited visited.add(vertex); // Add the vertex to the recursion stack recursionStack.add(vertex); // Visit all the neighbors for (const neighbor of graph[vertex]) { // If the neighbor is not visited and a cycle is found in the subtree if (!visited.has(neighbor) && dfsHasCycle(graph, neighbor, visited, recursionStack)) { return true; } // If the neighbor is in the recursion stack, a cycle is found else if (recursionStack.has(neighbor)) { return true; } } } // Remove the vertex from the recursion stack recursionStack.delete(vertex); // No cycle found in this subtree return false; } // Example usage: const directedGraph = { 'A': ['B', 'C'], 'B': ['D'], 'C': ['D'], 'D': ['E'], 'E': ['A'] // This edge creates a cycle: A -> B -> D -> E -> A }; console.log(hasCycle(directedGraph)); // Output: true

Cycle Detection in Undirected Graphs

Here's an implementation of cycle detection in an undirected graph:

function hasCycleUndirected(graph) { // Set to keep track of visited vertices const visited = new Set(); // Check each vertex as a possible starting point for (const vertex in graph) { if (!visited.has(vertex) && dfsHasCycleUndirected(graph, vertex, visited, null)) { return true; } } // No cycle found return false; } function dfsHasCycleUndirected(graph, vertex, visited, parent) { // Mark the vertex as visited visited.add(vertex); // Visit all the neighbors for (const neighbor of graph[vertex]) { // If the neighbor is not visited if (!visited.has(neighbor)) { // Recursively check if there's a cycle in the subtree if (dfsHasCycleUndirected(graph, neighbor, visited, vertex)) { return true; } } // If the neighbor is visited and not the parent, a cycle is found else if (neighbor !== parent) { return true; } } // No cycle found in this subtree return false; } // Example usage: const undirectedGraph = { 'A': ['B', 'C'], 'B': ['A', 'D', 'E'], 'C': ['A', 'F'], 'D': ['B', 'E'], // This edge creates a cycle: B -> D -> E -> B 'E': ['B', 'D'], 'F': ['C'] }; console.log(hasCycleUndirected(undirectedGraph)); // Output: true

Time and Space Complexity

Time Complexity:

O(V + E), where V is the number of vertices and E is the number of edges in the graph.

Space Complexity:

O(V), where V is the number of vertices in the graph, for storing the visited set and recursion stack.

Application: Course Schedule

A common application of cycle detection is the Course Schedule problem, where we need to determine if it's possible to finish all courses given their prerequisites.

function canFinish(numCourses, prerequisites) { // Build the adjacency list representation of the graph const graph = Array(numCourses).fill().map(() => []); for (const [course, prereq] of prerequisites) { graph[prereq].push(course); } // 0 = not visited, 1 = visiting (in recursion stack), 2 = visited const visited = Array(numCourses).fill(0); // Check for cycles using DFS for (let i = 0; i < numCourses; i++) { if (visited[i] === 0 && hasCycle(graph, i, visited)) { return false; } } // No cycle found, all courses can be finished return true; } function hasCycle(graph, vertex, visited) { // Mark the vertex as visiting visited[vertex] = 1; // Visit all neighbors for (const neighbor of graph[vertex]) { // If the neighbor is not visited, check if there's a cycle in the subtree if (visited[neighbor] === 0) { if (hasCycle(graph, neighbor, visited)) { return true; } } // If the neighbor is in the recursion stack, a cycle is found else if (visited[neighbor] === 1) { return true; } } // Mark the vertex as visited visited[vertex] = 2; // No cycle found in this subtree return false; } // Example usage: const numCourses = 4; const prerequisites = [[1, 0], [2, 1], [3, 2], [0, 3]]; // This creates a cycle: 0 -> 1 -> 2 -> 3 -> 0 console.log(canFinish(numCourses, prerequisites)); // Output: false
IntroVisualizePatternsPractice
101 Logo
onenoughtone