Foundation · Module 10 · Lesson 4
Fast & slow pointers
Middle node, cycles, and cycle entry.
Lesson 56 of 123
1d streakTortoise and Hare Mechanics: Midpoint & Boundary Parity
The two-pointer technique known as the Tortoise and Hare advances two reference cursors through a list at different velocities: slow steps 1 node per iteration while fast steps 2 nodes per iteration (fast = fast.next.next). This 2:1 velocity differential allows single-pass inspection of structural properties without pre-computing list length.
Architectural Boundary · Do Not Confuse
Right-Biased Midpoint: while (fast != null && fast.next != null)
Left-Biased Midpoint: while (fast.next != null && fast.next.next != null)
For an even list [1, 2, 3, 4], the right-biased condition terminates with slow at node 3 (second middle). The left-biased condition terminates with slow at node 2 (first middle), which is essential when splitting a list into two independent halves for Merge Sort.
| List Length | List Values | Loop Condition | Final slow Position | Final fast Position |
|---|---|---|---|---|
| Odd (5) | [1, 2, 3, 4, 5] | fast != null && fast.next != null | Node(3) (Exact Middle) | Node(5) (fast.next == null) |
| Even (4) | [1, 2, 3, 4] | fast != null && fast.next != null | Node(3) (Second Middle) | null (fast == null) |
| Even (4) | [1, 2, 3, 4] | fast.next != null && fast.next.next != null | Node(2) (First Middle) | Node(3) (fast.next.next == null) |
// Finding Left Middle for Clean Bisecting
public ListNode getLeftMiddle(ListNode head) {
if (head == null) return null;
ListNode slow = head;
ListNode fast = head;
// Terminates with slow at first middle for even lengths
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}Active Recall Checkpoint
Why does dividing a linked list for Merge Sort strictly require the left-biased middle instead of the right-biased middle?
Show answer & explanation
If an even list of 2 nodes [1, 2] uses the right-biased middle, slow lands on node 2. Splitting at slow.next creates halves of length 2 and 0, resulting in infinite recursive splitting and a StackOverflowError.
Floyd’s Cycle Detection: Mathematical Proof of Convergence
Floyd’s Cycle-Finding Algorithm determines whether a linked list contains a cycle using strictly O(1) auxiliary memory. Rather than maintaining a hash set of visited memory addresses (which consumes O(n) heap memory), the 2:1 runner speed difference mathematically guarantees detection.
- 01Phase 1: Both pointers enter the cycle. Let the cycle have length L.
- 02Phase 2: Once slow enters the cycle, fast is already somewhere inside the cycle.
- 03Phase 3: Consider the relative distance d between fast and slow measured along the direction of traversal (0 <= d < L).
- 04Phase 4: In every discrete time step, slow moves +1 and fast moves +2. The relative distance decreases by (2 - 1) = 1 step per tick modulo L.
- 05Phase 5: Because the gap closes by strictly 1 unit each step, the distance must hit exactly 0 mod L in at most L iterations. Fast can never hop over slow without colliding.
Maximum Iterations Bound
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) return false;
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next; // 1 step
fast = fast.next.next; // 2 steps
if (slow == fast) {
return true; // Collision confirms cycle existence
}
}
return false; // Fast reached terminal null: linear chain
}Active Recall Checkpoint
If fast moved 3 steps per tick instead of 2, would cycle collision still be guaranteed?
Show answer & explanation
Not unconditionally in a single cycle revolution. With speed 3, the relative distance closes by (3 - 1) = 2 steps per tick. In an odd-length cycle, if the initial gap is odd, fast could skip over slow. While it would eventually collide after multiple loop circuits if gcd(2, L) divides the gap, speed 2 is optimal because step difference 1 guarantees collision within at most L steps without modular skipping.
Finding the Cycle Start Node: Mathematical Derivation
Once Floyd’s algorithm detects a collision between slow and fast, we can locate the exact starting node of the cycle in O(n) time and O(1) space using an elegant mathematical symmetry.
| Segment Variable | Definition / Geometric Meaning |
|---|---|
| a | Distance from list head to the cycle entrance node |
| b | Distance from cycle entrance node to collision meeting point |
| c | Distance from collision meeting point back to cycle entrance node |
| L = b + c | Total perimeter length of the cycle |
- 01At collision: distance(slow) = a + b.
- 02At collision: distance(fast) = a + b + n * L (where n >= 1 is the number of full cycle loops completed by fast).
- 03Since fast moves at twice the speed of slow: distance(fast) = 2 * distance(slow).
- 04Substitute: a + b + n * L = 2 * (a + b) => a + b + n * L = 2a + 2b.
- 05Simplify: a = n * L - b.
- 06Decompose n * L: Since L = b + c, we rewrite n * L - b = (n - 1) * L + (L - b) = (n - 1) * L + c.
- 07Conclusion: a = (n - 1) * L + c. The distance from the list head to the cycle entrance (a) equals the distance from the collision point to the cycle entrance (c), plus (n - 1) full cycle revolutions!
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) return null;
ListNode slow = head;
ListNode fast = head;
// 1. Detect collision
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
// 2. Derive cycle start: reset ptr1 to head, keep ptr2 at collision
ListNode ptr1 = head;
ListNode ptr2 = slow;
// Both step at 1 node per iteration until meeting at entrance
while (ptr1 != ptr2) {
ptr1 = ptr1.next;
ptr2 = ptr2.next;
}
return ptr1; // Cycle entrance node
}
}
return null; // No cycle
}Use it when
Use Floyd’s cycle detection and entry derivation when analyzing pointer graphs with O(1) space constraints, detecting infinite redirects in state transitions, or solving array duplicate problems (e.g., LeetCode 287 Find the Duplicate Number).
Reconsider when
Use a HashSet of visited identifiers if nodes lack distinct references or if modifying memory during detection is acceptable.
Cost Model
Complexity & Trade-offs
When to use it
- Frequent local insertions, deletions, chain rewiring, and cycle analysis.
- Problems where contiguous storage or random access is unnecessary.
- In-place sequence transformations.
Common mistakes
- Losing the unread suffix by overwriting next too early.
- Failing to update head or tail after a boundary mutation.
- Dereferencing null or looping forever on a cycle.
Practice prompts
- Reproduce the worked Fast & slow pointers example without looking at the trace.
- Change one input condition and explain whether the Fast & slow pointers invariant still holds.
- Implement the approach, derive its cost, and compare it with a straightforward baseline.
Mastery check