LeetCode 142 - Linked List Cycle II
This problem asks us to determine whether a singly linked list contains a cycle and, if it does, return the exact node where that cycle begins. A linked list is normally a sequence of nodes where each node points to the next one, eventually ending with null.
Difficulty: 🟡 Medium
Topics: Hash Table, Linked List, Two Pointers
Solution
Problem Understanding
This problem asks us to determine whether a singly linked list contains a cycle and, if it does, return the exact node where that cycle begins.
A linked list is normally a sequence of nodes where each node points to the next one, eventually ending with null. However, in this problem, the next pointer of some node may point back to an earlier node instead of ending. When this happens, traversing the list repeatedly would cause us to revisit nodes forever, creating a cycle.
The input consists of the head node of a singly linked list. Internally, LeetCode provides a variable called pos, which indicates where the tail connects back into the list. However, this value is only used for constructing test cases and is not available to our algorithm. We must determine the cycle purely by traversing the linked list structure.
The expected output is the node object itself where the cycle begins. If there is no cycle, we return null (or None in Python). This distinction is important because we are not returning an index or node value, but the actual linked list node reference.
The constraints indicate that the linked list contains at most 10^4 nodes. This is small enough for an O(n) traversal to be efficient. The follow-up question asks for O(1) extra memory, which strongly suggests that solutions using auxiliary storage, such as a hash set, are not optimal.
Several edge cases are important to consider upfront. The linked list may be empty (head = null), meaning there cannot be a cycle. The list may contain only one node, either pointing to itself or ending normally. The cycle could begin at the head itself, which means returning the first node. The cycle might also begin deep in the list after several non-cyclic nodes. A naive implementation that only checks for repeated values instead of repeated node references would fail because multiple nodes can have identical values.
Approaches
Brute Force Approach, Hash Set
A straightforward way to solve this problem is to traverse the linked list while keeping track of every node we have already visited using a hash set.
As we move through the list, we check whether the current node has already been seen. If it has, then we know we have looped back into a cycle, and the current node must be the cycle entry point. If we eventually reach null, then no cycle exists.
This approach works because each node in a valid non-cyclic traversal should be visited exactly once. Revisiting a node means we have encountered a loop.
Although correct, this solution uses extra memory proportional to the number of nodes visited. Since the follow-up explicitly asks for constant extra memory, we can do better.
Optimal Approach, Floyd's Cycle Detection Algorithm
The key insight is that we can exploit pointer movement speeds to detect cycles without extra memory.
We use two pointers:
- A slow pointer that moves one step at a time.
- A fast pointer that moves two steps at a time.
If no cycle exists, the fast pointer will eventually reach null. If a cycle exists, the fast pointer will eventually "lap" the slow pointer inside the cycle, causing them to meet.
The deeper insight is how to find the entry point of the cycle after this meeting occurs.
Suppose:
Lis the distance from the head to the cycle entry.Cis the cycle length.Xis the distance from the cycle entry to the meeting point.
At the meeting point:
- Slow pointer distance =
L + X - Fast pointer distance =
L + X + kC
Since the fast pointer moves twice as fast:
$$2(L + X) = L + X + kC$$
Rearranging:
$$L + X = kC$$
Which implies:
$$L = kC - X$$
This means the distance from the head to the cycle entry equals the distance from the meeting point to the cycle entry.
Therefore, if we place one pointer back at the head and keep another at the meeting point, moving both one step at a time, they will meet exactly at the cycle entry node.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n) | O(n) | Uses a hash set to track visited nodes |
| Optimal | O(n) | O(1) | Uses Floyd's two-pointer technique |
Algorithm Walkthrough
- Initialize two pointers,
slowandfast, both starting athead. The slow pointer will move one node at a time, while the fast pointer moves two nodes at a time. - Traverse the linked list while ensuring the fast pointer and
fast.nextare notnull. This check prevents invalid pointer access and also tells us whether the list ends normally. - Move the pointers:
slow = slow.nextfast = fast.next.next
If there is no cycle, the fast pointer eventually reaches null.
4. After each movement, check whether slow == fast. If they meet, a cycle exists. This meeting point is guaranteed by Floyd's Cycle Detection Algorithm.
5. Once the pointers meet, initialize another pointer, cycle_start, at head.
6. Move both slow and cycle_start one step at a time:
slow = slow.nextcycle_start = cycle_start.next
- The node where these two pointers meet is the start of the cycle. Return that node.
- If the loop ends without the pointers meeting, return
nullbecause no cycle exists.
Why it works
The correctness comes from the mathematical relationship between the meeting point and the cycle entry. When the slow and fast pointers meet, the slow pointer has traveled a distance that aligns perfectly with the remaining distance to the cycle start. Resetting one pointer to the head ensures both pointers move the same remaining distance, causing them to meet exactly at the cycle entry node.
Python Solution
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
from typing import Optional
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
slow = head
fast = head
# Step 1: Detect if a cycle exists
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
# Step 2: Find the cycle entry point
cycle_start = head
while cycle_start != slow:
cycle_start = cycle_start.next
slow = slow.next
return cycle_start
return None
The implementation begins by initializing both slow and fast pointers at the head of the linked list. The while fast and fast.next condition ensures safe pointer movement because the fast pointer advances two steps at a time.
Inside the loop, the slow pointer advances one node while the fast pointer advances two. If the list contains a cycle, the fast pointer eventually catches the slow pointer, proving a loop exists.
After detecting a meeting point, the algorithm creates a new pointer called cycle_start positioned at the head. Both cycle_start and slow then move one step at a time. Their meeting point is mathematically guaranteed to be the cycle entry node, which we return immediately.
If the loop exits naturally, the fast pointer reached the end of the list, meaning there is no cycle, so the function returns None.
Go Solution
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func detectCycle(head *ListNode) *ListNode {
slow := head
fast := head
// Step 1: Detect cycle
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
if slow == fast {
// Step 2: Find cycle entry
cycleStart := head
for cycleStart != slow {
cycleStart = cycleStart.Next
slow = slow.Next
}
return cycleStart
}
}
return nil
}
The Go implementation follows the same logic as the Python version. The main difference is handling nil explicitly instead of None. Since Go pointers are strongly typed, we check fast != nil && fast.Next != nil before moving the fast pointer.
Go compares linked list nodes using pointer equality, which is exactly what we want because the problem requires identifying the same node object, not merely matching values.
There are no concerns about integer overflow because we are only moving pointers and not performing arithmetic on node values.
Worked Examples
Example 1
Input: head = [3,2,0,-4], pos = 1
The structure is:
3 → 2 → 0 → -4
↑ ↓
└───────┘
Cycle Detection Phase
| Iteration | Slow | Fast |
|---|---|---|
| Start | 3 | 3 |
| 1 | 2 | 0 |
| 2 | 0 | 2 |
| 3 | -4 | -4 |
The pointers meet at node -4.
Find Cycle Start
Reset cycle_start = head.
| Step | cycle_start | slow |
|---|---|---|
| Start | 3 | -4 |
| 1 | 2 | 2 |
They meet at node 2, which is the cycle entry.
Example 2
Input: head = [1,2], pos = 0
Structure:
1 → 2
↑ ↓
└───┘
Cycle Detection Phase
| Iteration | Slow | Fast |
|---|---|---|
| Start | 1 | 1 |
| 1 | 2 | 1 |
| 2 | 1 | 1 |
Pointers meet at node 1.
Find Cycle Start
| Step | cycle_start | slow |
|---|---|---|
| Start | 1 | 1 |
They already match, so node 1 is returned.
Example 3
Input: head = [1], pos = -1
Structure:
1 → null
Cycle Detection Phase
| Iteration | Slow | Fast |
|---|---|---|
| Start | 1 | 1 |
Since fast.next is null, the loop ends immediately.
Result: return null.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each pointer traverses the list at most a constant number of times |
| Space | O(1) | Only a few pointer variables are used |
The time complexity is O(n) because, even though there are two phases, each pointer moves through the linked list a bounded number of times. In the cycle detection phase, the pointers move until they meet or the list ends. In the second phase, both pointers move toward the cycle entry. Altogether, this remains linear in the number of nodes.
The space complexity is O(1) because we only store a constant number of pointer references regardless of input size.
Test Cases
# Definition for testing
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def detectCycle(self, head):
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
cycle_start = head
while cycle_start != slow:
cycle_start = cycle_start.next
slow = slow.next
return cycle_start
return None
def build_cycle_list(values, pos):
if not values:
return None
nodes = [ListNode(v) for v in values]
for i in range(len(nodes) - 1):
nodes[i].next = nodes[i + 1]
if pos != -1:
nodes[-1].next = nodes[pos]
return nodes[0], nodes
solution = Solution()
# Example 1
head, nodes = build_cycle_list([3, 2, 0, -4], 1)
assert solution.detectCycle(head) == nodes[1] # cycle starts at node value 2
# Example 2
head, nodes = build_cycle_list([1, 2], 0)
assert solution.detectCycle(head) == nodes[0] # cycle starts at head
# Example 3
head, _ = build_cycle_list([1], -1)
assert solution.detectCycle(head) is None # no cycle
# Empty list
assert solution.detectCycle(None) is None # empty linked list
# Single node with self-cycle
head, nodes = build_cycle_list([1], 0)
assert solution.detectCycle(head) == nodes[0] # node points to itself
# Long list with cycle in middle
head, nodes = build_cycle_list([1, 2, 3, 4, 5, 6], 2)
assert solution.detectCycle(head) == nodes[2] # cycle starts at node value 3
# Two-node list without cycle
head, _ = build_cycle_list([1, 2], -1)
assert solution.detectCycle(head) is None # normal termination
# Cycle at last node to itself
head, nodes = build_cycle_list([1, 2, 3], 2)
assert solution.detectCycle(head) == nodes[2] # self-loop at tail
| Test | Why |
|---|---|
[3,2,0,-4], pos=1 |
Validates a standard cycle beginning in the middle |
[1,2], pos=0 |
Verifies cycle begins at head |
[1], pos=-1 |
Ensures no cycle is detected |
| Empty list | Confirms null input handling |
| Single node self-cycle | Tests smallest possible cycle |
| Long list with middle cycle | Validates cycle detection after long traversal |
| Two-node no cycle | Ensures correct termination |
| Tail self-loop | Confirms cycle entry can be the final node |
Edge Cases
Empty Linked List
The input may be head = null. This is an easy case to overlook because dereferencing fast.next would immediately cause an error if not handled carefully. The implementation safely handles this through the loop condition while fast and fast.next, which fails immediately and returns None.
Single Node With Self-Cycle
A linked list with one node pointing to itself is a special but valid cycle. Some implementations mistakenly assume at least two nodes are needed for cycle detection. In this implementation, both pointers eventually meet on the same node, and resetting the second pointer correctly identifies that node as the cycle entry.
Cycle Starts at the Head
The cycle entry may be the very first node. This can break implementations that incorrectly assume there is always a non-cyclic prefix before the cycle. Floyd's algorithm naturally handles this case because, after the meeting point, the reset pointer and slow pointer will meet directly at the head.
No Cycle Exists
The linked list may terminate normally. A buggy implementation might loop forever if it assumes a cycle exists. Here, the fast pointer eventually becomes null, causing the loop to stop safely and return None.
Duplicate Node Values
Multiple nodes may contain identical values. A flawed approach might compare node values instead of node references, producing false positives. This implementation compares node objects directly, ensuring correctness even when values repeat.