Fast & Slow Pointers

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.

Technique5 problems
The unlock

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.

MENTAL MODEL Relative speed closes the gap by 1 every step

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.
The unlock → a +2 / +1 pair can never skip past each other inside a cycle, because their separation only ever changes by 1. Collision is guaranteed.

SEE IT Half-speed finds the middle; full-loop finds the meeting

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 null

On 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.
The smell test →“middle in one pass,” “does it loop,” or “where does the loop start” — and you're forbidden from using a Set — all point straight at two speeds.

SAY IT Guard fast first, then fast.next, then take two hops

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.

  • Detect: “if slow === fast during the loop, there's a cycle.”
  • Middle: “no if — when the loop ends, slowis the middle.”
  • Entrance: “after collision, reset slow = head, then step bothby 1 until they meet.”

GENERALIZE The "list" can be implicit — any next function works

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:

  • Find the Duplicate Number: with values in [1..n], treat i → nums[i] as the arrow. Two indices sharing a value create a cycle whose entrance is the duplicate.
  • Happy Number: the successor of a number is the sum of the squares of its digits. The sequence either reaches 1 or falls into a cycle — pure cycle detection on a number sequence, no list at all.
The reach → if you can write 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.

SHAPE One skeleton, four endings

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' function

Internalize this and the technique collapses to one decision: “do I want the meeting (cycle?), the halfway point (middle), or the entrance (reset + walk)?”

MNEMONIC Hare laps the tortoise.

Hare laps the tortoise. Two cursors on one sequence: the hare takes two hops per beat, the tortoise one. Inside any loop the hare cannot escape, so it laps the tortoise and they collide. Step it in the Visualize tab — watch the gap close by exactly 1 each beat.

PATTERN Two speeds over one sequence

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.

Why O(1) space → the alternative — a Set of visited nodes — costs O(n) memory. Two speeds get the same answer with two integer/pointer variables.

KEY IDEA The gap closes by 1 each step

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.

Termination guarantee →a separation that changes by exactly 1 per step must reach 0. That is the entire correctness argument for Floyd's algorithm.

COST O(n) time, O(1) space, one pass

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.

Visited-set approach
O(n) space
Store every node seen; simple, but uses memory proportional to n.
Fast & slow
O(1) space
Two cursors, one pass. The interview-preferred answer.

VARIANTS Detect · middle · entrance

The same loop, three endings:

  • Cycle detection. Return whether slow === fast ever holds during the loop. If the hare exits through null, there is no cycle.
  • Find the middle. Drop the equality check entirely; when the loop ends (hare at the end), slow sits on the middle node.
  • Cycle entrance (phase 2). After the collision, reset one pointer to the head and advance both by 1 — they re-meet exactly at the node where the loop begins. The same math powers find the duplicate number.

RUN IT Tortoise & hare: the hare laps the tortoise inside the loop

step 0 / 3
STARTBoth pointers start on the head, node 3. The hare will move two nodes per beat; the tortoise one. Each step the gap between them shrinks by exactly 1.
320-4slowfast
slow (tortoise, +1)fast (hare, +2)
slowfast

TRIGGERS When you see ___ → reach for ___

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

RED FLAGSWhen it's NOT this pattern

  • You need to count nodes or address them by position.If the task is really “the k-th element” or “how many,” a plain array index or a single length pass is simpler than juggling two speeds.
  • The successor isn't deterministic / single-valued. Floyd needs exactly one nextper state. If a node can branch (a tree or graph with out-degree > 1), use DFS/BFS with a visited set instead.
  • You also need O(1) lookup, not just traversal.Problems like LRU Cache pair a list with a hash map — that's a combined structure, not a two-speed walk.

TEMPLATE Cycle detection (Floyd)

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.

cycle-detection-floyd-.ts
// 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
}
Guard order matters → check fast !== null before fast.next !== null. Reversing them dereferences null on even-length lists.

TEMPLATE Find the middle

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).

find-the-middle.ts
// 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;
}
Even-length choice → with both pointers starting at head, slow lands on the second middle. Start fast = head.next to get the first middle instead.

TEMPLATE Cycle entrance (Floyd phase 2)

When → You need where the loop begins, not just whether one exists. After detecting the collision, a reset-and-walk finds the entry node.

cycle-entrance-floyd-phase-2-.ts
// 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
}
The reset is the trick → the collision node is not the entrance. Move one pointer back to head, then advance both by 1; they meet at the entrance because head→entry equals meeting→entry (mod the loop length).

TEMPLATE Array as a linked list (find the duplicate)

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-a-linked-list-find-the-duplicate-.ts
// 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)
}
Why a cycle exists →two different indices hold the same value, so two arrows point at one node — and that shared target is precisely the cycle's entrance, i.e. the duplicate.

PITFALL Missing or mis-ordered null checks on fast && fast.next

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.

PITFALL Even vs odd length: which node is the "middle"

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.

PITFALL Forgetting the phase-2 reset for the cycle entrance

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.

PITFALL Off-by-one on where (and whether) they meet

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.