Fast and Slow Pointers: Cycle Detection & Invariants
The Fast and Slow Pointers pattern (often called Floyd's Cycle-Finding Algorithm or the Tortoise and Hare algorithm) is an algorithmic pointer technique where two references traverse a data structure or sequence at different velocities—typically 1 step vs 2 steps per tick.
While a naive approach to detecting cycles or finding middle nodes relies on hash sets or multiple traversal passes that waste O(N) memory, the fast-and-slow strategy delivers O(N) time with strict O(1) auxiliary space.
The Mathematical Invariant: Why Do They Always Meet?
Consider a linked list containing a cycle of length C. Once both pointers enter the loop, every iteration reduces the distance between the fast pointer and the slow pointer by exactly 1 node:
flowchart LR
Head([Head]) --> N1["Node 1"]
N1 --> Entry["Cycle Entry (Node 2)"]
Entry --> Loop1["Node 3 (Slow: +1)"]
Loop1 --> Loop2["Node 4"]
Loop2 --> Loop3["Node 5 (Fast: +2)"]
Loop3 --> Entry
Because the gap shrinks by 1 on every step, the fast pointer cannot skip over the slow pointer; it is guaranteed to collide within at most C iterations once the slow pointer enters the cycle.
Phase 1 & Phase 2: Finding the Exact Cycle Entry Point
Detecting that a loop exists is only half the battle. Finding the start of the cycle requires understanding Floyd's mathematical offset:
- Let
Fbe the distance from the head to the cycle entry point. - Let
abe the distance from the cycle entry to the collision point. - Let
Cbe the total cycle length.
When collision occurs:
- Distance traveled by slow pointer:
d(slow) = F + a - Distance traveled by fast pointer:
d(fast) = F + kC + a(wherekis integer loops) - Since fast moves at 2x speed:
2(F + a) = F + kC + a ⇒ F = kC - a
The takeaway: If you reset one pointer back to the head and keep the other at the collision node, moving both at 1 step per iteration guarantees they will collide exactly at the cycle entrance after F steps.
flowchart TD
A["slow = head, fast = head"] --> B["slow = slow.next\nfast = fast.next.next"]
B --> C{"slow == fast ?"}
C -->|No| D{"fast == null || fast.next == null ?"}
D -->|Yes| E(["No Cycle Detected (Return null)"])
D -->|No| B
C -->|Yes: Phase 1 Complete| F["Reset slow = head\nKeep fast at collision"]
F --> G["slow = slow.next\nfast = fast.next (Step 1-by-1)"]
G --> H{"slow == fast ?"}
H -->|No| G
H -->|Yes| I(["Cycle Entry Point Located!"])
Pattern 1: Finding the Middle of a Linked List
To split a list (such as during Merge Sort) in a single pass without computing its length beforehand:
Java Implementation
public class LinkedListMiddle {
static class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
}
/**
* Returns the middle node (second middle for even-length lists).
*/
public static ListNode findMiddle(ListNode head) {
if (head == null) return null;
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow; // Points directly to the middle node
}
}
Pattern 2: Detecting Cycle & Finding Entry Point
Python Implementation
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def detect_cycle_entry(head: ListNode | None) -> ListNode | None:
"""Returns the node where the cycle begins, or None if no cycle exists."""
if not head or not head.next:
return None
slow = head
fast = head
# Phase 1: Determine if a cycle exists
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
break
else:
return None # Reached end of list without cycle
# Phase 2: Locate cycle start node
slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow
Pattern 3: Implicit Graph Cycles (Happy Number Problem)
The Fast and Slow pointer pattern is not restricted to linked lists. It applies to any state-transition graph where each state has a deterministic next state.
For example, in the Happy Number problem, a number is replaced by the sum of the squares of its digits. If the sequence reaches 1, it stays at 1. If it gets trapped in an infinite loop (e.g., 4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4), fast and slow pointers detect the loop without needing a hash set.
TypeScript Implementation
function getSumOfSquares(n: number): number {
let sum = 0;
while (n > 0) {
const digit = n % 10;
sum += digit * digit;
n = Math.floor(n / 10);
}
return sum;
}
export function isHappyNumber(n: number): boolean {
let slow = n;
let fast = getSumOfSquares(n);
while (fast !== 1 && slow !== fast) {
slow = getSumOfSquares(slow);
fast = getSumOfSquares(getSumOfSquares(fast));
}
return fast === 1;
}
Core Pitfalls & Invariant Checklist
- Null Pointer Guard: Always guard
fast != null && fast.next != nullin the while loop condition. Evaluatingfast.next.nextbefore verifyingfast.nexttriggers runtime exceptions. - Even vs. Odd Middle: If you need the first middle node of an even-length list (for splitting lists before reversal), stop when
fast.next == null || fast.next.next == null. - Cycle Length: After detecting collision in Phase 1, you can measure cycle length
Cby keepingfastfixed and counting how many single stepsslowtakes to return tofast.
Complexity Comparison
| Approach | Time Complexity | Space Complexity | Downsides |
|---|---|---|---|
| Hash Table / Visited Set | O(N) |
O(N) |
Heap memory overhead, hash collisions, dynamic allocation |
| Two Passes (Count & Halve) | O(N) |
O(1) |
Cannot detect infinite loops/cycles; requires 2x traversal |
| Fast & Slow Pointers | O(N) |
O(1) |
Optimal: Single pass, zero allocations, handles cycles |
- Opposite and same direction arrays: read Two Pointers Technique.
- Linked list pointer mutation: read In-Place Reversal of Linked List.
- Fundamental node data structure: read Linked List Architecture.