Cycle Detection: Code Templates in 6 Languages
If you have not read the concept guide yet, start there for the intuition and complexity analysis. This page gives you the code you can memorize and adapt during an interview.
Main Template: Floyd’s Cycle Detection (Linked List)
The tortoise and hare algorithm is the standard for detecting cycles in linked lists. It uses
Use this for Linked List Cycle and Linked List Cycle II .
function hasCycle(head) {
// Null or single-node lists cannot have a cycle
if (!head || !head.next) return false;
let slow = head;
let fast = head;
// The fast pointer moves twice as fast. If a cycle exists,
// fast will lap slow before it ever reaches null.
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true;
}
return false;
}
function detectCycle(head) {
if (!head || !head.next) return null;
let slow = head;
let fast = head;
// Phase 1: detect the cycle
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) break;
}
if (!fast || !fast.next) return null;
// Phase 2: find where the cycle begins.
// The distance from head to cycle entry equals the distance
// from the meeting point to cycle entry.
slow = head;
while (slow !== fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}Code Breakdown
Key Variables
slow: moves one step per iteration. In the detection phase it probes each node in order. In the start-finding phase it resets to head.fast: moves two steps per iteration. It is the probe that either reaches the end (no cycle) or laps the slow pointer (cycle found).head: the starting reference. Resettingslowtoheadafter detection is the key to finding the cycle entry.
Visual Mechanism
stateDiagram-v2
[*] --> Init: slow=head, fast=head
Init --> Probe: fast != null && fast.next != null
Probe --> Advance: slow=slow.next, fast=fast.next.next
Advance --> Check: slow == fast?
Check --> Found: Yes -> Cycle detected
Check --> Probe: No -> Continue
Probe --> NoCycle: fast or fast.next == null
Found --> FindStart: slow=head
FindStart --> Locate: slow != fast
Locate --> AdvanceBoth: slow=slow.next, fast=fast.next
AdvanceBoth --> Locate: Still not equal
Locate --> Entry: slow == fast -> Cycle start
NoCycle --> [*]
Entry --> [*]
Critical Sections
The null check at the start handles empty and single-node lists. Without it, accessing head.next on a null reference throws an error.
The while loop condition fast && fast.next ensures the fast pointer never accesses null. If fast can make two moves, the list is long enough to check one more step.
Phase 2 relies on a mathematical property: the distance from the head to the cycle start equals the distance from the meeting point to the cycle start. Resetting one pointer to head and moving both one step at a time guarantees they converge at the entry node.
2. DFS Cycle Detection (Directed and Undirected)
For graph cycle detection, DFS with state tracking is the standard approach.
- Directed graphs: track each node as unvisited, visiting (in the current recursion stack), or visited (fully processed). A back edge to a visiting node means a cycle.
- Undirected graphs: track visited nodes and the parent of each node. A back edge to a visited node that is not the parent means a cycle.
Use this for Course Schedule and general graph cycle detection.
// Directed Graph
function hasCycleDirected(graph) {
const visiting = new Set();
const visited = new Set();
function dfs(node) {
if (visiting.has(node)) return true; // back edge to active stack
if (visited.has(node)) return false; // already fully processed
visiting.add(node);
for (const neighbor of graph[node]) {
if (dfs(neighbor)) return true;
}
visiting.delete(node);
visited.add(node);
return false;
}
for (let i = 0; i < graph.length; i++) {
if (dfs(i)) return true;
}
return false;
}
// Undirected Graph
function hasCycleUndirected(graph) {
const visited = new Set();
function dfs(node, parent) {
visited.add(node);
for (const neighbor of graph[node]) {
if (neighbor === parent) continue; // skip the edge we came from
if (visited.has(neighbor)) return true; // back edge = cycle
if (dfs(neighbor, node)) return true;
}
return false;
}
for (let i = 0; i < graph.length; i++) {
if (!visited.has(i) && dfs(i, -1)) return true;
}
return false;
}3. Union-Find Cycle Detection (Undirected Graphs)
For undirected graphs, Union-Find detects cycles without a DFS traversal. If two nodes of an edge already belong to the same set, adding that edge creates a cycle.
Use this for Graph Valid Tree .
function hasCycleUnionFind(edges, n) {
const parent = Array.from({length: n}, (_, i) => i);
function find(x) {
// Path compression flattens the tree so future
// lookups are nearly constant time
if (parent[x] !== x) parent[x] = find(parent[x]);
return parent[x];
}
for (const [u, v] of edges) {
const rootU = find(u);
const rootV = find(v);
if (rootU === rootV) return true; // already connected = cycle
parent[rootU] = rootV;
}
return false;
}Variations
1. Topological Sort with Kahn’s Algorithm (Directed Graph)
Instead of DFS, Kahn’s algorithm uses indegrees to detect cycles. Nodes with indegree zero have no dependencies and can be processed first. If the graph has a cycle, some nodes will never reach indegree zero.
from collections import deque
def has_cycle_kahn(num_nodes, edges):
graph = [[] for _ in range(num_nodes)]
indegree = [0] * num_nodes
for u, v in edges:
graph[u].append(v)
indegree[v] += 1
queue = deque([i for i in range(num_nodes) if indegree[i] == 0])
processed = 0
while queue:
node = queue.popleft()
processed += 1
for neighbor in graph[node]:
indegree[neighbor] -= 1
if indegree[neighbor] == 0:
queue.append(neighbor)
# If we could not process all nodes, a cycle exists
return processed != num_nodes2. Collecting Cycle Nodes
If you need the actual nodes in the cycle (not just a boolean), collect them after detection.
// After slow === fast in Floyd's algorithm
if (slow === fast) {
const cycle = [];
let temp = slow;
do {
cycle.push(temp.val);
temp = temp.next;
} while (temp !== slow);
return cycle;
}3. Happy Number (Floyd’s on Digits)
Floyd’s algorithm works on sequences where each element is derived from the previous one by a deterministic function, even without explicit linked-list nodes.
def is_happy(n: int) -> bool:
def next_num(x):
return sum(int(d) ** 2 for d in str(x))
slow = n
fast = next_num(n)
while fast != 1 and slow != fast:
slow = next_num(slow)
fast = next_num(next_num(fast))
return fast == 1Now head to the practice problems to apply these templates to real interview questions.