Cycle Detection: Practice Problems with Solutions
Welcome to the practice problems for cycle detection. If you need a refresher on the code, the code templates have Floyd’s algorithm, DFS state tracking, and Union-Find in all 6 languages. Each problem below includes a hint, a visual walkthrough, and the full solution.
Recommended Study Order
The problems are ordered by difficulty, but the progression matters as much as the individual solutions.
- Linked List Cycle teaches Floyd’s algorithm in its purest form. Master the two-pointer movement before adding the cycle-start math.
- Find the Duplicate Number applies Floyd’s to an array, showing that the algorithm works on any sequence with a deterministic next-element function, not just linked-list nodes.
- Linked List Cycle II extends Floyd’s with phase 2. Once you know a cycle exists, finding where it starts is the most common follow-up.
- Course Schedule introduces graph cycle detection with DFS state tracking and Kahn’s algorithm. This is the most frequent interview variant.
- Graph Valid Tree combines cycle detection with connectivity checking using Union-Find.
- Longest Cycle in a Graph is the hardest problem in the set. It requires running DFS from every unvisited node and tracking discovery times, entry times, and cycle lengths.
Easy Problems
1. Linked List Cycle
LeetCode 141 | Difficulty: Easy
Brief: Determine if a singly linked list has a cycle.
Why this pattern: This is Floyd’s tortoise and hare algorithm. Two pointers move at different speeds, and they meet if and only if a cycle exists.
Key Insight: The fast pointer gains one node on the slow pointer per iteration. If a cycle exists, the gap shrinks to zero and they meet. If the fast pointer reaches null, there is no cycle.
Visual:
graph TD
H["head"] --> N3["3"]
N3 --> N2["2"]
N2 --> N0["0"]
N0 --> N_4["-4"]
N_4 --> N2
style N2 fill:#f9f,stroke:#333,stroke-width:2px
Code:
var hasCycle = function(head) {
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true;
}
return false;
};The null check on fast && fast.next at the top of the loop prevents a null reference error when fast reaches the end. The slow pointer catches up to fast when a cycle exists because fast moves two steps for every one step of slow, reducing the distance between them by one each iteration.
2. Find the Duplicate Number
LeetCode 287 | Difficulty: Easy (Medium on LeetCode, but conceptually straightforward with cycle detection)
Brief: Given an array of n+1 integers where each integer is between 1 and n, find the single duplicate number without modifying the array and using only
Why this pattern: The array values act as pointers to indices. This forms an implicit linked list where each value tells you the next index to visit. Floyd’s algorithm detects the cycle and finds its entry, which is the duplicate number.
Key Insight: Treat nums[i] as the next index to visit, like ListNode.next. The duplicate number is the entry point of the cycle, just like in Linked List Cycle II.
Visual:
graph LR
I0["Index 0: value 1"] --> I1["Index 1: value 3"]
I1 --> I3["Index 3: value 2"]
I3 --> I2["Index 2: value 4"]
I2 --> I4["Index 4: value 2"]
I4 --> I2
style I2 fill:#f9f,stroke:#333,stroke-width:2px
Code:
var findDuplicate = function(nums) {
let slow = nums[0];
let fast = nums[0];
// Phase 1: detect the cycle
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while (slow !== fast);
// Phase 2: find the cycle entry (the duplicate)
slow = nums[0];
while (slow !== fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
};The key insight is that nums[0] is always part of the cycle-initiating chain because 0 is outside the value range (1 to n), so no node points back to index 0. This guarantees that the entry point of the cycle is the duplicate value. The do-while loop runs phase 1 at least once, which avoids the initial slow === fast condition.
Medium Problems
3. Linked List Cycle II
LeetCode 142 | Difficulty: Medium
Brief: Return the node where the cycle begins in a linked list, or null if there is no cycle.
Why this pattern: Extends Floyd’s algorithm with a second phase. Once the fast and slow pointers meet, resetting one to head and moving both one step at a time locates the cycle entry.
Key Insight: The distance from head to the cycle start equals the distance from the meeting point to the cycle start. This is a provable property of the pointer movements.
Visual:
graph LR
S["Start (head)"] --> A["A"]
A --> B["B"]
B --> C["C"]
C --> D["D"]
D --> E["E"]
E --> B
style B fill:#f9f,stroke:#333,stroke-width:2px
Code:
var detectCycle = function(head) {
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) {
slow = head;
while (slow !== fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
return null;
};Phase 2 works because of a property of Floyd’s algorithm. If the distance from head to cycle start is D and the distance from the meeting point to cycle start (going forward) is M, then D = M. Resetting one pointer to head and moving both one step guarantees they converge on the cycle entry.
4. Course Schedule
LeetCode 207 | Difficulty: Medium
Brief: Determine if you can finish all courses given an array of prerequisite pairs, where [a, b] means you must take course b before course a.
Why this pattern: The prerequisites form a directed graph. A cycle in this graph means a circular dependency that makes the schedule impossible.
Key Insight: Kahn’s algorithm (topological sort using indegrees) processes courses with no remaining prerequisites. If some courses never reach zero indegree, a cycle blocks them.
Visual:
graph TD
A["Course 0"] --> B["Course 1"]
B --> C["Course 2"]
C --> A
style A fill:#ff9999
style B fill:#ff9999
style C fill:#ff9999
Code:
var canFinish = function(numCourses, prerequisites) {
const graph = Array.from({length: numCourses}, () => []);
const indegree = new Array(numCourses).fill(0);
for (const [course, pre] of prerequisites) {
graph[pre].push(course);
indegree[course]++;
}
const queue = [];
for (let i = 0; i < numCourses; i++) {
if (indegree[i] === 0) queue.push(i);
}
let visited = 0;
while (queue.length) {
const node = queue.shift();
visited++;
for (const neighbor of graph[node]) {
indegree[neighbor]--;
if (indegree[neighbor] === 0) queue.push(neighbor);
}
}
return visited === numCourses;
};Kahn’s algorithm trades the DFS state tracking for an indegree-based approach. The graph does not need to be traversed recursively. Instead, nodes enter the queue only when all their prerequisites are satisfied. If the count of processed nodes equals the total number of nodes, the graph is a DAG. A cycle would leave some nodes with non-zero indegree and they would never enter the queue.
5. Graph Valid Tree
LeetCode 261 | Difficulty: Medium
Brief: Given n nodes labeled from 0 to n-1 and a list of undirected edges, determine if the edges form a valid tree.
Why this pattern: A tree must be fully connected and contain no cycles. Union-Find detects cycles in undirected graphs efficiently, and a connectedness check confirms the single-component requirement.
Key Insight: A valid tree must have exactly n-1 edges. If it has more, a cycle exists. If it has fewer, the graph is disconnected. Union-Find detects the cycle by checking if two nodes of an edge already share a root.
Visual:
graph TD
A["0"] --- B["1"]
A --- C["2"]
B --- D["3"]
D --- E["4"]
style A fill:#bbf
style E fill:#bbf
Code:
var validTree = function(n, edges) {
if (edges.length !== n - 1) return false;
const parent = Array.from({length: n}, (_, i) => i);
function find(x) {
if (parent[x] !== x) parent[x] = find(parent[x]);
return parent[x];
}
for (const [u, v] of edges) {
const rootU = find(u), rootV = find(v);
if (rootU === rootV) return false; // cycle detected
parent[rootU] = rootV;
}
return true;
};The edge count check edges.length !== n - 1 is a quick filter. A tree must have exactly n-1 edges. If it has more, a cycle is guaranteed. If it has fewer, the graph is disconnected. After that, Union-Find merges components as it processes edges. Finding two nodes that are already merged is the cycle signal.
Hard Problems
6. Longest Cycle in a Graph
LeetCode 2360 | Difficulty: Hard
Brief: You are given a directed graph of n nodes with each node having exactly one outgoing edge. Find the length of the longest cycle in the graph. If there is no cycle, return -1.
Why this pattern: Each node has exactly one outgoing edge, which means every node belongs to exactly one functional graph (a graph where each node has out-degree 1). This structure is composed of paths feeding into cycles. DFS with time tracking (entry times) finds cycle lengths.
Key Insight: When DFS encounters a node that has been visited in the current traversal, the difference between the current time and that node’s entry time is the cycle length.
Visual:
graph TD
N0["0"] --> N1["1"]
N1 --> N2["2"]
N2 --> N3["3"]
N3 --> N1
N4["4"] --> N5["5"]
N5 --> N4
style N1 fill:#f9f,stroke:#333,stroke-width:2px
style N2 fill:#f9f,stroke:#333,stroke-width:2px
style N3 fill:#f9f,stroke:#333,stroke-width:2px
style N4 fill:#bbf,stroke:#333,stroke-width:2px
style N5 fill:#bbf,stroke:#333,stroke-width:2px
Code:
var longestCycle = function(edges) {
const n = edges.length;
const visited = new Array(n).fill(false);
const entryTime = new Array(n).fill(0);
let maxLen = -1, time = 0;
for (let i = 0; i < n; i++) {
if (visited[i]) continue;
let node = i;
const startTime = time;
// Walk the path until we hit a visited node
while (node !== -1 && !visited[node]) {
visited[node] = true;
entryTime[node] = time++;
node = edges[node];
}
// If we hit a node we visited in THIS traversal, it is a cycle
if (node !== -1 && entryTime[node] >= startTime) {
maxLen = Math.max(maxLen, time - entryTime[node]);
}
}
return maxLen;
};This problem combines DFS traversal with a timestamp technique. The entry_time array records when each node was first visited. The start_time variable resets for each new DFS component. When the traversal hits a node that has an entry_time at or after start_time, that node is part of the current traversal, meaning a cycle was found. The length of the cycle is current_time - entry_time[node]. The -1 edges represent nodes with no outgoing edge, which terminate the path.
These six problems cover the full range of cycle detection techniques. Start with the simple two-pointer application in Linked List Cycle and Find the Duplicate Number. Work through the graph variants in Course Schedule and Graph Valid Tree. Finish with the timestamp-based cycle length computation in Longest Cycle in a Graph. By the end, you should be able to recognize when a cycle detection approach applies and reach for the right algorithm.