Detect whether a linked list contains a cycle using Floyd’s tortoise and hare two-pointer algorithm.
Problem Restatement
We are given the head of a linked list.
Return True if the linked list contains a cycle.
Otherwise, return False.
A cycle exists if some node can be reached again by continuously following next pointers.
LeetCode internally represents the cycle using an index pos, where the tail node points back to the node at position pos. This value is not passed to the function. (leetcode.com)
Examples
Example 1:
3 -> 2 -> 0 -> -4
^ |
|_________|The tail points back to the node with value 2.
So the list contains a cycle.
Output:
TrueExample 2:
1 -> 2
^ |
|____|The second node points back to the first node.
Output:
TrueExample 3:
1 -> NoneThere is no cycle.
Output:
FalseInput and Output
| Item | Meaning |
|---|---|
| Input | head: Optional[ListNode] |
| Output | bool |
| Goal | Detect whether a cycle exists |
| Important detail | The list structure must not be modified |
Function shape:
class Solution:
def hasCycle(self, head: Optional[ListNode]) -> bool:
...First Thought: Store Visited Nodes
One simple idea is:
- Walk through the linked list.
- Store every visited node in a set.
- If we ever visit the same node again, a cycle exists.
Example:
A -> B -> C -> D
^ |
|_________|Traversal order:
A, B, C, D, BWhen we see B again, we know there is a cycle.
This works, but it uses extra memory:
O(n)We can do better.
Key Insight
Use two pointers moving at different speeds.
| Pointer | Speed |
|---|---|
slow | One step at a time |
fast | Two steps at a time |
This is Floyd’s cycle detection algorithm, also called the tortoise and hare algorithm.
There are two possibilities:
- No cycle:
fasteventually reachesNone
- Cycle exists:
fasteventually catchesslow
Why Fast Eventually Catches Slow
Suppose the list contains a cycle.
Inside the cycle:
slowmoves one step per iterationfastmoves two steps per iteration
So relative to slow, the fast pointer gains:
2 - 1 = 1step per iteration.
That means the distance between them inside the cycle shrinks modulo the cycle length.
Eventually, fast lands on the same node as slow.
So if the pointers ever meet, a cycle must exist.
Example Walkthrough
Consider:
1 -> 2 -> 3 -> 4 -> 5
^ |
|_________|Initial:
| Step | slow | fast |
|---|---|---|
0 | 1 | 1 |
Move:
| Step | slow | fast |
|---|---|---|
1 | 2 | 3 |
2 | 3 | 5 |
3 | 4 | 4 |
The pointers meet at node 4.
So a cycle exists.
Algorithm
Initialize:
slow = head
fast = headThen repeatedly:
slow = slow.next
fast = fast.next.nextBefore moving fast, check that:
fast and fast.nextexist.
If at any moment:
slow == fastreturn True.
If the loop finishes because fast reaches None, return False.
Correctness
If the list has no cycle, the fast pointer moves toward the end of the list twice as quickly as slow. Since the list is finite and acyclic, fast eventually becomes None or fast.next becomes None. The algorithm then returns False.
Now suppose the list contains a cycle.
Once both pointers enter the cycle, the fast pointer gains one node on the slow pointer during each iteration because it moves two steps while slow moves one. Since the cycle length is finite, this relative distance eventually becomes zero modulo the cycle length, meaning both pointers point to the same node.
Therefore, if a cycle exists, the pointers eventually meet and the algorithm returns True.
Thus, the algorithm correctly detects whether the linked list contains a cycle.
Complexity
Let n be the number of nodes.
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | Each pointer traverses at most a linear number of nodes |
| Space | O(1) | Only two pointers are used |
Implementation
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def hasCycle(self, head: Optional[ListNode]) -> bool:
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return FalseCode Explanation
Both pointers start at the head:
slow = head
fast = headThe loop condition:
while fast and fast.next:ensures that moving fast by two steps is safe.
Inside the loop:
slow = slow.nextmoves one step.
fast = fast.next.nextmoves two steps.
If they point to the same node:
if slow == fast:a cycle exists.
If the loop ends naturally:
return Falsethen fast reached the end of the list, so no cycle exists.
Why Compare Nodes, Not Values
Suppose two different nodes both contain:
5That does not mean a cycle exists.
We must compare node identity:
slow == fastnot:
slow.val == fast.valThe algorithm checks whether the pointers reference the same node object.
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.hasCycle(a) is True
a = ListNode(1)
b = ListNode(2)
a.next = b
b.next = a
assert s.hasCycle(a) is True
a = ListNode(1)
assert s.hasCycle(a) is False
a = ListNode(1)
b = ListNode(2)
c = ListNode(3)
a.next = b
b.next = c
assert s.hasCycle(a) is False
a = ListNode(1)
a.next = a
assert s.hasCycle(a) is True
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
| Standard cycle example | Normal cycle detection |
| Two-node cycle | Small cycle |
| Single node without cycle | Minimum acyclic case |
| Multi-node acyclic list | Normal no-cycle case |
| Self-loop | Node points to itself |