Find the node where a linked list cycle begins using Floyd’s tortoise and hare algorithm with cycle entry mathematics.
Problem Restatement
We are given the head of a linked list.
If the linked list contains a cycle, return the node where the cycle begins.
If there is no cycle, return None.
LeetCode internally represents the cycle using an index pos, where the tail points back to the node at index pos. This value is not passed into the function. (leetcode.com)
Examples
Example 1:
3 -> 2 -> 0 -> -4
^ |
|_________|The cycle begins at the node with value 2.
Output:
node with value 2Example 2:
1 -> 2
^ |
|____|The cycle begins at the node with value 1.
Example 3:
1 -> NoneThere is no cycle.
Output:
NoneInput and Output
| Item | Meaning |
|---|---|
| Input | head: Optional[ListNode] |
| Output | Node where the cycle starts, or None |
| Important detail | Return the node itself, not the value |
| Constraint | Do not modify the linked list |
Function shape:
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
...Relation to LeetCode 141
LeetCode 141 only asks:
Does a cycle exist?LeetCode 142 asks a harder question:
Where does the cycle begin?We still use Floyd’s tortoise and hare algorithm.
But after detecting the cycle, we perform one more step to locate the entry node.
First Step: Detect Whether a Cycle Exists
Use two pointers:
| Pointer | Speed |
|---|---|
slow | One step |
fast | Two steps |
If no cycle exists:
fasteventually reaches None.
If a cycle exists:
slow == fastat some node inside the cycle.
This part is identical to LeetCode 141.
Key Insight
After the two pointers meet:
- Move one pointer back to the head.
- Keep the other pointer at the meeting point.
- Move both pointers one step at a time.
- The node where they meet again is the cycle entry.
This looks surprising at first.
The reason comes from the distance relationships inside the cycle.
Mathematical Proof
Define:
| Symbol | Meaning |
|---|---|
a | Distance from head to cycle entry |
b | Distance from cycle entry to meeting point |
c | Remaining cycle length |
So the total cycle length is:
b + cWhen the pointers meet:
slowtraveled:
a + bfasttraveled:
2(a + b)But fast also traveled exactly one extra full cycle more than slow.
So:
2(a + b) = a + b + k(b + c)for some integer k >= 1.
Simplify:
a + b = k(b + c)Rearrange:
a = k(b + c) - bFor the first meeting, think of one cycle difference:
a = cmodulo the cycle length.
That means:
- starting from the head and walking
asteps reaches the cycle entry - starting from the meeting point and walking
csteps also reaches the cycle entry
So if:
- one pointer starts at the head
- one pointer starts at the meeting point
- both move one step at a time
they meet exactly at the cycle entry.
Example Walkthrough
Consider:
1 -> 2 -> 3 -> 4 -> 5
^ |
|_________|The cycle entry is node 3.
Suppose the pointers meet at node 5.
Now:
- move one pointer to the head
- keep one pointer at
5
Move both one step:
| Step | Pointer A | Pointer B |
|---|---|---|
0 | 1 | 5 |
1 | 2 | 3 |
2 | 3 | 4 |
3 | 4 | 5 |
4 | 5 | 3 |
Depending on cycle length and meeting location, they eventually synchronize at the cycle entry.
The formal proof guarantees this.
Algorithm
Phase 1:
- Use slow and fast pointers.
- Detect whether a cycle exists.
Phase 2:
If a meeting occurs:
- Set:
ptr1 = head
ptr2 = meeting_node- Move both one step at a time.
- When they meet, return that node.
If no cycle exists, return None.
Correctness
If the linked list has no cycle, the fast pointer eventually reaches None, so the algorithm correctly returns None.
Now suppose the list contains a cycle.
By Floyd’s cycle detection argument, the slow and fast pointers eventually meet inside the cycle.
Let:
| Symbol | Meaning |
|---|---|
a | Distance from head to cycle entry |
b | Distance from cycle entry to meeting point |
L | Cycle length |
At the meeting point, the fast pointer has traveled one whole cycle more than the slow pointer, so:
2(a + b) = a + b + kLwhich simplifies to:
a + b = kLTherefore:
a = kL - bThis means the distance from the meeting point to the cycle entry equals the distance from the head to the cycle entry modulo the cycle length.
So if one pointer starts at the head and another starts at the meeting point, and both move one step at a time, they meet exactly at the cycle entry.
Therefore, the algorithm correctly returns the node where the cycle begins.
Complexity
Let n be the number of nodes.
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | Linear traversal |
| Space | O(1) | Only pointer variables are used |
Implementation
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
ptr1 = head
ptr2 = slow
while ptr1 != ptr2:
ptr1 = ptr1.next
ptr2 = ptr2.next
return ptr1
return NoneCode Explanation
Initialize two pointers:
slow = head
fast = headMove them at different speeds:
slow = slow.next
fast = fast.next.nextIf they meet:
if slow == fast:a cycle exists.
Now:
ptr1 = head
ptr2 = slowOne pointer starts from the head.
The other starts from the meeting point.
Move both one step:
while ptr1 != ptr2:
ptr1 = ptr1.next
ptr2 = ptr2.nextWhen they meet again:
return ptr1that node is the cycle entry.
If no meeting ever occurs:
return Nonethe list has no cycle.
Why Returning slow Immediately Is Wrong
The first meeting point is not necessarily the cycle entry.
Example:
1 -> 2 -> 3 -> 4 -> 5
^ |
|_________|The cycle entry is 3.
But the first meeting may happen at 4 or 5.
So we need the second phase to locate the true entry node.
Testing
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
def run_tests():
s = Solution()
a = ListNode(3)
b = ListNode(2)
c = ListNode(0)
d = ListNode(-4)
a.next = b
b.next = c
c.next = d
d.next = b
assert s.detectCycle(a) == b
a = ListNode(1)
b = ListNode(2)
a.next = b
b.next = a
assert s.detectCycle(a) == a
a = ListNode(1)
assert s.detectCycle(a) is None
a = ListNode(1)
b = ListNode(2)
c = ListNode(3)
a.next = b
b.next = c
assert s.detectCycle(a) is None
a = ListNode(1)
a.next = a
assert s.detectCycle(a) == a
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
| Standard cycle | Cycle entry in the middle |
| Two-node cycle | Entry at head |
| Single node without cycle | Smallest acyclic case |
| Multi-node acyclic list | No cycle |
| Self-loop | Node points to itself |