Detect Cycle in Graphs
Algorithm · Core
Step through it: Fast & Slow Pointers pattern
both start at node 1
Step 1 of 6
Question: does this chain of nodes loop back on itself? Walking it one node at a time never answers that, because inside a loop you just keep going. Instead send two walkers from the start: a slow one moving 1 node per step and a fast one moving 2.
Step 2 of 6
After one step the slow walker is on node 2 and the fast one on node 3.
Step 3 of 6
Again: slow on 3, fast on 5, the last node.
Step 4 of 6
The last node points back to 3, so fast carries on 5 → 3 → 4 while slow steps to 4. They land on the same node.
Step 5 of 6
Why they must meet: inside a loop the fast walker gains one node on the slow one every step, so it catches up. Without a loop, fast would simply fall off the end (null) first. Two walkers at different speeds is called fast and slow pointers.
Step 6 of 6
Your turn.
In graph theory, a cycle is a path of edges and vertices wherein a vertex is reachable from itself. There are several different types of cycles, principally a closed walk and a simple cycle.
Definitions
A closed walk consists of a sequence of vertices starting and ending at the same vertex, with each two consecutive vertices in the sequence adjacent to each other in the graph. In a directed graph, each edge must be traversed by the walk consistently with its direction: the edge must be oriented from the earlier of two consecutive vertices to the later of the two vertices in the sequence. The choice of starting vertex is not important: traversing the same cyclic sequence of edges from different starting vertices produces the same closed walk.
A simple cycle may be defined either as a closed walk with no repetitions of vertices and edges allowed, other than the repetition of the starting and ending vertex, or as the set of edges in such a walk. The two definitions are equivalent in directed graphs, where simple cycles are also called directed cycles: the cyclic sequence of vertices and edges in a walk is completely determined by the set of edges that it uses. In undirected graphs the set of edges of a cycle can be traversed by a walk in either of two directions, giving two possible directed cycles for every undirected cycle. A circuit can be a closed walk allowing repetitions of vertices but not edges; however, it can also be a simple cycle, so explicit definition is recommended when it is used.
Example
A graph with edges colored to illustrate path H-A-B (green), closed path or
walk with a repeated vertex B-D-E-F-D-C-B (blue) and a cycle with no repeated edge or
vertex H-D-G-H (red)
Cycle in undirected graph
Cycle in directed graph
Code
import depthFirstSearch from '../depth-first-search/depthFirstSearch';
/**
* Detect cycle in directed graph using Depth First Search.
*
* @param {Graph} graph
*/
export default function detectDirectedCycle(graph) {
let cycle = null;
// Will store parents (previous vertices) for all visited nodes.
// This will be needed in order to specify what path exactly is a cycle.
const dfsParentMap = {};
// White set (UNVISITED) contains all the vertices that haven't been visited at all.
const whiteSet = {};
// Gray set (VISITING) contains all the vertices that are being visited right now
// (in current path).
const graySet = {};
// Black set (VISITED) contains all the vertices that has been fully visited.
// Meaning that all children of the vertex has been visited.
const blackSet = {};
// If we encounter vertex in gray set it means that we've found a cycle.
// Because when vertex in gray set it means that its neighbors or its neighbors
// neighbors are still being explored.
// Init white set and add all vertices to it.
/** @param {GraphVertex} vertex */
graph.getAllVertices().forEach((vertex) => {
whiteSet[vertex.getKey()] = vertex;
});
// Describe BFS callbacks.
const callbacks = {
enterVertex: ({ currentVertex, previousVertex }) => {
if (graySet[currentVertex.getKey()]) {
// If current vertex already in grey set it means that cycle is detected.
// Let's detect cycle path.
cycle = {};
let currentCycleVertex = currentVertex;
let previousCycleVertex = previousVertex;
while (previousCycleVertex.getKey() !== currentVertex.getKey()) {
cycle[currentCycleVertex.getKey()] = previousCycleVertex;
currentCycleVertex = previousCycleVertex;
previousCycleVertex = dfsParentMap[previousCycleVertex.getKey()];
}
cycle[currentCycleVertex.getKey()] = previousCycleVertex;
} else {
// Otherwise let's add current vertex to gray set and remove it from white set.
graySet[currentVertex.getKey()] = currentVertex;
delete whiteSet[currentVertex.getKey()];
// Update DFS parents list.
dfsParentMap[currentVertex.getKey()] = previousVertex;
}
},
leaveVertex: ({ currentVertex }) => {
// If all node's children has been visited let's remove it from gray set
// and move it to the black set meaning that all its neighbors are visited.
blackSet[currentVertex.getKey()] = currentVertex;
delete graySet[currentVertex.getKey()];
},
allowTraversal: ({ nextVertex }) => {
// If cycle was detected we must forbid all further traversing since it will
// cause infinite traversal loop.
if (cycle) {
return false;
}
// Allow traversal only for the vertices that are not in black set
// since all black set vertices have been already visited.
return !blackSet[nextVertex.getKey()];
},
};
// Start exploring vertices.
while (Object.keys(whiteSet).length) {
// Pick fist vertex to start BFS from.
const firstWhiteKey = Object.keys(whiteSet)[0];
const startVertex = whiteSet[firstWhiteKey];
// Do Depth First Search.
depthFirstSearch(graph, startVertex, callbacks);
}
return cycle;
}import depthFirstSearch from '../depth-first-search/depthFirstSearch';
/**
* Detect cycle in undirected graph using Depth First Search.
*
* @param {Graph} graph
*/
export default function detectUndirectedCycle(graph) {
let cycle = null;
// List of vertices that we have visited.
const visitedVertices = {};
// List of parents vertices for every visited vertex.
const parents = {};
// Callbacks for DFS traversing.
const callbacks = {
allowTraversal: ({ currentVertex, nextVertex }) => {
// Don't allow further traversal in case if cycle has been detected.
if (cycle) {
return false;
}
// Don't allow traversal from child back to its parent.
const currentVertexParent = parents[currentVertex.getKey()];
const currentVertexParentKey = currentVertexParent ? currentVertexParent.getKey() : null;
return currentVertexParentKey !== nextVertex.getKey();
},
enterVertex: ({ currentVertex, previousVertex }) => {
if (visitedVertices[currentVertex.getKey()]) {
// Compile cycle path based on parents of previous vertices.
cycle = {};
let currentCycleVertex = currentVertex;
let previousCycleVertex = previousVertex;
while (previousCycleVertex.getKey() !== currentVertex.getKey()) {
cycle[currentCycleVertex.getKey()] = previousCycleVertex;
currentCycleVertex = previousCycleVertex;
previousCycleVertex = parents[previousCycleVertex.getKey()];
}
cycle[currentCycleVertex.getKey()] = previousCycleVertex;
} else {
// Add next vertex to visited set.
visitedVertices[currentVertex.getKey()] = currentVertex;
parents[currentVertex.getKey()] = previousVertex;
}
},
};
// Start DFS traversing.
const startVertex = graph.getAllVertices()[0];
depthFirstSearch(graph, startVertex, callbacks);
return cycle;
}import DisjointSet from '../../../data-structures/disjoint-set/DisjointSet';
/**
* Detect cycle in undirected graph using disjoint sets.
*
* @param {Graph} graph
*/
export default function detectUndirectedCycleUsingDisjointSet(graph) {
// Create initial singleton disjoint sets for each graph vertex.
/** @param {GraphVertex} graphVertex */
const keyExtractor = (graphVertex) => graphVertex.getKey();
const disjointSet = new DisjointSet(keyExtractor);
graph.getAllVertices().forEach((graphVertex) => disjointSet.makeSet(graphVertex));
// Go trough all graph edges one by one and check if edge vertices are from the
// different sets. In this case joint those sets together. Do this until you find
// an edge where to edge vertices are already in one set. This means that current
// edge will create a cycle.
let cycleFound = false;
/** @param {GraphEdge} graphEdge */
graph.getAllEdges().forEach((graphEdge) => {
if (disjointSet.inSameSet(graphEdge.startVertex, graphEdge.endVertex)) {
// Cycle found.
cycleFound = true;
} else {
disjointSet.union(graphEdge.startVertex, graphEdge.endVertex);
}
});
return cycleFound;
}References
General information:
Cycles in undirected graphs:
- Detect Cycle in Undirected Graph on GeeksForGeeks
- Detect Cycle in Undirected Graph Algorithm on YouTube
Cycles in directed graphs:
Practice
Fast & Slow Pointers
- Linked List CycleEasy
- Middle of the Linked ListEasy
- Palindrome Linked ListEasy
- Happy NumberEasy
- Linked List Cycle IIMedium
- Find the Duplicate NumberMedium
- Remove Nth Node From End of ListMedium
- Reorder ListMedium
- Sort ListMedium
- Circular Array LoopMedium
- Delete the Middle Node of a Linked ListMedium
- Rotate ListMedium
- Intersection of Two Linked ListsMedium
Spotted a mistake or something unclear? Report it on GitHub.