Run two cursors over one sequence at different speeds — the hare two hops per beat, the tortoise one. Because the gap closes by exactly 1 each beat, the hare laps the tortoise inside any cycle and they collide (Floyd); when the hare reaches the end, the tortoise sits on the middle; and resetting one cursor to the head finds the cycle's entrance. One pass, O(1) space — and the “sequence” need not be a list: any implicit next function over indices or numbers works too.
Two runners on the same track, one twice as fast. On a straight track the fast one finishes first — and the slow one is exactly halfway. On a circular track the fast one can never escape, so it inevitably laps the slow one and they touch. That single picture answers middle, cycle?, and where does the loop start? — all in O(1) extra space.
Forget the two pointers for a second and watch the distance between them. Each beat the hare gains 2 and the tortoise gains 1, so the gap grows by 2 − 1 = 1 — or, viewed from inside a loop of length L, the hare creeps 1 closer to the tortoise modulo L every beat. A gap that changes by exactly 1 each step musteventually hit 0. That is the whole reason Floyd terminates: there is no way for the hare to “jump over” the tortoise.
slow +1 per beat, fast +2. The gap CLOSES by exactly 1 each beat.
head → 1 → 2 → 3 → 4 → 5
▲ │
└─── 7 ◄── 6 (tail 7 loops back to node 3)
beat 0: slow=1 fast=1 gap 0 (both on head)
beat 1: slow=2 fast=3
beat 2: slow=3 fast=5
beat 3: slow=4 fast=7
beat 4: slow=5 fast=4
beat 5: slow=6 fast=6 ◄── COLLISION → a cycle exists
No loop? fast hits null and you stop — return false / no cycle.On an acyclic list the hare runs out of track. Since it covered twice the ground, the tortoise has covered exactly half — it is standing on the middle:
fast goes twice as fast, so when fast finishes, slow is halfway.
ODD : 1 → 2 → 3 → 4 → 5
s s s slow ends on 3 (the exact middle)
f f f(null) fast ends past 5
EVEN : 1 → 2 → 3 → 4
s s slow ends on 3 (SECOND middle)
f f(null) fast ends on nullOn a cyclic list the hare cannot fall off, so it keeps circling until it rear-ends the tortoise. The collision node is notthe loop's entrance, but a tiny bit of algebra turns it into one:
WHY resetting to head finds the entrance:
head ──a──► [E]ntrance ──b──► [M]eeting
▲ │
└──────c──────────┘ (loop length = b + c)
slow walked a + b to reach M
fast walked 2(a + b) and is also at M, so it did k extra loops:
2(a + b) = a + b + k(b + c) ⇒ a = (k-1)(b+c) + c
So a (head→entry) equals c plus whole loops (meeting→entry).
Reset one pointer to head, step BOTH +1 → they meet at the Entrance.Set — all point straight at two speeds.The one-line mantra that prevents every crash and every infinite loop: “while fast and fast.next— slow +1, fast +2.” The guard order is not optional: you must know fast is non-null before you read fast.next, and fast.next non-null before you take fast.next.next.
slow === fast during the loop, there's a cycle.” if — when the loop ends, slowis the middle.”slow = head, then step bothby 1 until they meet.”The technique never actually needed nodes and arrows — it only needed a deterministic successor for each position. Anything that gives you a unique next is a linked list in disguise:
[1..n], treat i → nums[i] as the arrow. Two indices sharing a value create a cycle whose entrance is the duplicate.next(x)and the state space is finite, Floyd applies — that's how a “Linked List” trick solves Array and Math problems with O(1) space.Every variant is the same two-speed loop; only the ending differs. Read it as a sentence, then pick the tail you need:
# one sequence, two speeds — the universal shape
slow = fast = head # (or nums[0] for the array variant)
while fast and fast.next: # GUARD fast first, THEN fast.next
slow = slow.next # +1 (tortoise)
fast = fast.next.next # +2 (hare)
if slow is fast: # collision → there IS a cycle
break
# variant ENDINGS:
# detect cycle → return (slow is fast)
# find middle → drop the 'if'; when loop ends, slow is the middle
# cycle entrance → after collision: reset slow=head; step BOTH +1 until equal
# find duplicate → same as entrance, over nums[i] as the 'next' functionInternalize this and the technique collapses to one decision: “do I want the meeting (cycle?), the halfway point (middle), or the entrance (reset + walk)?”
Place two cursors at the start of a sequence and advance them at different rates — classically fast += 2 and slow += 1. The “sequence” can be the .next arrows of a linked list, or an implicit successor function like i → nums[i] over array indices or x → digitSquareSum(x) over numbers.
Three questions fall out of this one setup, each with no auxiliary memory: does the sequence cycle, what is its middle, and where does a cycle begin.
Set of visited nodes — costs O(n) memory. Two speeds get the same answer with two integer/pointer variables.The fast cursor gains on the slow one by 2 − 1 = 1 position every beat. On a straight run this means the fast one finishes in half the steps, leaving the slow one at the midpoint. Inside a loop, the relative gap shrinks by 1 (mod the loop length) until it is 0 — a guaranteed collision. The fast cursor can never “leap over” the slow one, because their separation only ever changes by a single step.
The tortoise visits each node at most a constant number of times before the pointers meet or the hare exits, so the work is O(n). Only two pointers (plus, for phase 2, one reset cursor) are stored, so the extra space is O(1) regardless of input size.
The same loop, three endings:
slow === fast ever holds during the loop. If the hare exits through null, there is no cycle.slow sits on the middle node.3. The hare will move two nodes per beat; the tortoise one. Each step the gap between them shrinks by exactly 1.Reach for fast & slow when a problem walks a sequence and asks a distance or cyclicity question — and especially when it forbids extra memory. The sequence may be an explicit ListNode chain or an implicit next function over indices or numbers.
| "detect a cycle" | Floyd's fast & slow: collision ⇒ cycle, fast hits null ⇒ none |
| "find the middle in one pass" | fast +2, slow +1; when fast ends, slow is the middle |
| "find the start of the loop" | Floyd phase 2: after collision reset one pointer to head, step both +1 |
| "duplicate number with array-as-linked-list" | treat i → nums[i] as next; the duplicate is the cycle entrance |
| "O(1) space required" (no Set/visited) | two cursors replace a visited-set; same answer, constant memory |
| "sequence of numbers eventually repeats" (e.g. happy number) | cycle detection on next(x) = digit-square-sum; reaches 1 or loops |
nextper state. If a node can branch (a tree or graph with out-degree > 1), use DFS/BFS with a visited set instead.When → Any “does it loop?” question on a list — or on any single-successor sequence — where you want a yes/no in one pass and O(1) space.
// Shared node shape (used by the list templates below)
interface ListNode {
val: number;
next: ListNode | null;
}
// Floyd cycle DETECTION — O(n) time, O(1) space
// slow +1, fast +2. If they ever land on the same node, a cycle exists;
// if fast walks off the end (null), the list is acyclic.
function hasCycle(head: ListNode | null): boolean {
let slow = head;
let fast = head;
while (fast !== null && fast.next !== null) { // guard fast FIRST, then fast.next
slow = slow!.next; // tortoise: 1 hop
fast = fast.next.next; // hare: 2 hops
if (slow === fast) return true; // collision → cycle
}
return false; // fell off the end → no cycle
}fast !== null before fast.next !== null. Reversing them dereferences null on even-length lists.When → You need the midpointin a single pass — as an answer itself, or as the split point for “reverse the second half” problems (reorder list, palindrome).
// Shared node shape (used by the list templates below)
interface ListNode {
val: number;
next: ListNode | null;
}
// Find the MIDDLE in one pass — O(n) time, O(1) space
// When fast reaches the end, slow has gone exactly half the distance.
function middleNode(head: ListNode | null): ListNode | null {
let slow = head;
let fast = head;
while (fast !== null && fast.next !== null) {
slow = slow!.next; // +1
fast = fast.next.next; // +2
}
// ODD length → slow is the exact middle.
// EVEN length → slow is the SECOND of the two middles.
// (Want the first middle instead? start fast = head.next.)
return slow;
}head, slow lands on the second middle. Start fast = head.next to get the first middle instead.When → You need where the loop begins, not just whether one exists. After detecting the collision, a reset-and-walk finds the entry node.
// Shared node shape (used by the list templates below)
interface ListNode {
val: number;
next: ListNode | null;
}
// Find the cycle ENTRANCE — Floyd phase 2, O(n) time, O(1) space
// Math: distance(head → entry) === distance(meeting → entry) around the loop.
function detectCycle(head: ListNode | null): ListNode | null {
let slow = head;
let fast = head;
while (fast !== null && fast.next !== null) {
slow = slow!.next;
fast = fast.next.next;
if (slow === fast) {
// Phase 2: reset ONE pointer to head, then advance BOTH by 1.
// They re-meet exactly at the loop's entrance node.
let entry = head;
while (entry !== slow) {
entry = entry!.next;
slow = slow!.next;
}
return entry;
}
}
return null; // no cycle
}head, then advance both by 1; they meet at the entrance because head→entry equals meeting→entry (mod the loop length).When → An array of n + 1 values in [1..n] with exactly one repeat, and you may not modify the array or use extra space. Treat i → nums[i] as a next pointer.
// Array as an implicit LINKED LIST — find the duplicate, O(n) time, O(1) space
// nums[i] is the "next" pointer of node i. With n+1 values in [1..n], two
// indices point at the same value → that value is a cycle ENTRANCE.
function findDuplicate(nums: number[]): number {
let slow = nums[0];
let fast = nums[0];
// Phase 1: find a meeting point inside the cycle.
do {
slow = nums[slow]; // +1 hop: i → nums[i]
fast = nums[nums[fast]]; // +2 hops
} while (slow !== fast);
// Phase 2: walk one pointer from the start; they meet at the duplicate.
slow = nums[0];
while (slow !== fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow; // the repeated value (the cycle entrance)
}The hare advances two steps, so before fast.next.next you must confirm fast !== null and then fast.next !== null — in that order. Checking only fast crashes on even-length lists (the hare lands on the last node); checking only fast.next crashes on an empty list. This is the single most common runtime error in the pattern.
With both pointers starting at head, odd-length lists leave slow on the exact middle, but even-length lists leave it on the second of the two middles. If the problem wants the first middle (e.g. to make the first half the longer one when splitting), start fast = head.next. Decide deliberately — don't discover it from a failing test.
A frequent bug is returning the collision node as the loop start. It almost never is. Phase 2 is mandatory: set one pointer back to head, keep the other at the meeting point, then advance both by 1 until they are equal. Only then are you standing on the entrance.
For the array variant, advance before the first comparison — a do/while loop — or both pointers start equal at nums[0] and you exit instantly with a false positive. Likewise, never test slow === fast on the very first frame of detection: they begin together by construction. Step first, compare second.