Intuition for Floyd's Cycle Detection
This article explains my intuition behind Floyd’s Cycle Detection algorithm. You can find two related LeetCode problems here:
Linked List Cycle is about detecting whether a cycle exists in a linked list. You’ll want to understand this first before moving on to Linked List Cycle II, which builds on it to implement Floyd’s full cycle detection algorithm.
Cycle Detection
Reference: Linked List Cycle I
To determine whether a linked list contains a cycle, we could use a hash map to record the addresses of all visited nodes. However, this approach is not memory-efficient for very long linked lists.
Instead, we can use a two-pointer approach: initialize two pointers, fast and slow, that traverse the linked list. The fast pointer moves twice as fast as the slow pointer. If a cycle exists, fast will eventually catch up to slow. If no cycle exists, fast will reach the end of the list.
Why does this work? Let’s prove this by contradiction. Assume there is a cycle in the linked list, but fast and slow never meet. We know that within the cycle, fast will eventually lap slow. Consider the moment just before fast overtakes slow for the first time:
- Situation 1:
fastis one node behindslow. After the next step (wherefastmoves two nodes andslowmoves one node), they meet atslow.next. - Situation 2:
fastandsloware at the same node, meaning they’ve already met.
Therefore, we can use the condition fast == slow to detect a cycle.
Finding the Start of the Cycle
Case 1: Large Cycle, Short Linear Path
Let’s start by assuming the cycle is large and the linear portion of the linked list (before the cycle begins) is relatively short.
When fast and slow first meet at a point we’ll call meet, we can establish an relationship: the number of nodes from the start (head) to meet equals the number of nodes from meet back to meet when traversing the cycle.
Why? Since fast moves twice as fast as slow, when they meet, slow has traversed half the distance that fast has traversed (see the note at the end for a more precise explanation).
- Slow’s path:
head→ … →meet - Fast’s path:
head→ … →meet, thenmeet→ around the cycle →meetThis means the path fromheadtomeetequals the path frommeetaround the cycle back tomeet.
Now, let’s introduce two new pointers, slow1 and slow2. Place slow1 at head and slow2 at meet. Move both pointers at the same speed:
slow1moves fromheadtowardmeetslow2moves frommeetaround the cycle
Since they move at the same speed and traverse the same number of nodes, and they share a common path segment (from the cycle start to meet), they will meet at the cycle’s starting point.
Case 2: Small Cycle, Long Linear Path
Our previous analysis assumed a large cycle and short linear path. Now let’s consider the opposite: a small cycle with a long linear path. In this scenario, fast may complete several full cycles before meeting slow.
This slightly modifies our relationship: the distance from head to meet equals the distance from meet back to meet plus several complete cycles. However, this doesn’t change our technique for finding the cycle’s start point, because moving around complete cycles brings us back to the same position.
Important Note
The statement that “fast walks twice the path of slow when they meet” requires clarification.
While fast moves twice as fast as slow, the “double path” refers to edge path, not node count. For example, in the figure above:
- Fast visits: 13 nodes
- Slow visits: 7 nodes
But when measuring distances:
- Fast travels: 12 edge paths
- Slow travels: 6 edge paths
This is why we start slow2 at the meet point itself (not meet.next) — we want slow1 and slow2 to traverse the same number of nodes (not edge distances) when they eventually meet.
Implementation
The golang implementation is as follows:
func detectCycle(head *ListNode) *ListNode {
if head == nil {
return nil
}
slow, fast := head, head
// Part 1: Detect if a cycle exists (Linked List Cycle I)
for {
if fast.Next == nil || fast.Next.Next == nil {
return nil // No cycle
}
fast = fast.Next.Next
slow = slow.Next
if slow == fast { // Cycle detected
break
}
}
// Part 2: Find the cycle's starting point
// Reset slow to head
// fast remains at the meet point
// Here, slow is slow1 and fast is slow2 from our explanation
slow = head
for {
if slow == fast {
break // Found the cycle start
}
slow = slow.Next
fast = fast.Next
}
return slow
}