Chapter 6
20 min read
Section 35 of 261

Fast-Slow Pointer Technique

Linked Lists

The Idea: Two Walkers, Different Speeds

A linked list hides a brutal asymmetry: you can walk forward in O(1)O(1) per step, but you cannot index, you cannot look back, and you cannot tell from a node alone whether it sits on a list, on a loop, or near the end. The fast-slow pointer technique — also called the two-pointer trick or Floyd's tortoise and hare — is the most elegant antidote in our toolbox. It says: walk the same list with two pointers at two different speeds, and their relative positions become a measuring device for structure you cannot see directly.

The canonical pair is “slow advances by 1, fast advances by 2.” Concretely, in Python:

🐍python
1slow = head
2fast = head
3while fast is not None and fast.next is not None:
4    slow = slow.next            # +1 step
5    fast = fast.next.next       # +2 steps

That tiny loop is the basis for at least five separate algorithms. Each one reads off a different invariant of the same simulation:

QuestionWhat to read off the simulation
Where is the middle node?When fast falls off the end, slow is at floor(n/2).
Is there a cycle?If fast ever equals slow inside the loop, yes; if fast hits None, no.
Where does the cycle start?After they meet, reset slow to head; advance both by 1; they meet at the entrance.
What is the k-th from the end?Run the lead pointer k steps first, then walk both at speed 1.
Does this number-theoretic sequence loop?Apply the same idea to f(n) = sum of squared digits.

Why is this loop so cheap?

Both pointers traverse every node at most a constant number of times, so the wall-clock work is O(n)O(n). Memory is O(1)O(1) — only two pointers, regardless of how long or tangled the list is. That last fact is what makes fast-slow the only reasonable cycle detector in memory-constrained settings (kernels, GC, embedded firmware).

Application 1 — Find the Middle of a List

Given a singly linked list of nn nodes, return the middle node in one pass without computing the length first. The fast-slow recipe makes this almost trivial.

The Invariant

Whenever fast has advanced 2k2k steps from the head, slow has advanced exactly kk. So when fast falls off the end after nn total steps, slow has taken n/2\lfloor n/2 \rfloor steps and is therefore at the middle (rounded down).

Two flavours appear in interview questions, depending on which middle you want for an even-length list:

Loop guardn=6 → returnsn=7 → returnsUsed in
while fast and fast.next:node 3 (the second middle)node 3 (the unique middle)Reorder list, palindrome check (right half)
while fast.next and fast.next.next:node 2 (the first middle)node 3 (the unique middle)Splitting for merge sort (left half stays one bigger or equal)

Pick a flavour and stick with it

Mixing the two guards across helper functions (one for “split list,” another for “reverse right half”) is one of the top-five linked-list bug families. Document the convention in the function docstring. The first flavour above is more common in modern texts.

Application 2 — Detect a Cycle (Floyd's Algorithm)

Robert W. Floyd popularised this algorithm in the late 1960s; it has been rediscovered countless times because the proof is genuinely surprising the first time you see it.

Setup. A list of nn nodes either ends in None (no cycle) or loops back to some earlier node (a cycle). Let

  • μ\mu = distance from the head to the cycle entrance (so μ=0\mu = 0 if the head itself is on the cycle), and
  • λ\lambda = length of the cycle.

Claim. If a cycle exists, slow and fast meet inside it within μ+λ\mu + \lambda total slow-steps. If no cycle exists, fast hits None in n/2\lfloor n/2 \rfloor slow-steps.

The Catch-Up Argument

Once both pointers are inside the cycle, fast gains exactly one node per step on slow (fast moves 2, slow moves 1, net +1+1). Modulo λ\lambda, the gap between them decreases by 1 each step. A non-negative integer that strictly decreases must hit 0, and once it does, slow == fast: collision detected. So they collide within at most λ\lambda steps after both are inside.

Both being inside takes at most μ\mu slow-steps (slow first reaches the entrance after exactly μ\mu steps; fast is already on the cycle by then because it moves twice as fast). Total work: O(μ+λ)=O(n)O(\mu + \lambda) = O(n).

The litmus test

Imagine the cycle as a circular running track. Slow is jogging clockwise; fast is running clockwise twice as fast. Will fast catch slow? Yes — because fast laps slow, and lapping on a finite track is inevitable. That is the entire correctness argument, dressed up.

Application 3 — Find the Cycle Entrance

Floyd's real wizardry isn't cycle detection; it's the second phase. Once slow and fast meet, you can locate the exact node where the cycle begins, in O(n)O(n) time and O(1)O(1) memory, by walking two pointers at speed 1.

The Algorithm

  1. Run phase 1 until slow == fast (the meeting point). Call it M.
  2. Reset slow to head; leave fast at M.
  3. Advance both by exactly 1 step at a time.
  4. The first node where they coincide is the cycle entrance. (If the head itself was on the cycle, they coincide immediately at head.)

Why It Works — A Real Proof

Pick the meeting point M. Let mm be the distance from the cycle entrance to M measured along the direction of travel (so 0m<λ0 \le m < \lambda).

At the moment of collision, slow has taken μ+m\mu + m steps. Fast has taken twice as many, 2(μ+m)2(\mu + m), but fast also went around the cycle some integer number k1k \ge 1 of extra times. So fast's actual path length is μ+m+kλ\mu + m + k\lambda. Setting both expressions for fast's path equal:

2(μ+m)=μ+m+kλ    μ+m=kλ    μ=kλm.2(\mu + m) = \mu + m + k\lambda \;\Longrightarrow\; \mu + m = k\lambda \;\Longrightarrow\; \mu = k\lambda - m.

Read this in words: the distance from head to the entrance equals the distance from M to the entrance, modulo λ\lambda. So if you start one walker at head and another at M and step both forward by 1, they will both reach the entrance simultaneously after μ\mu steps — the head walker by definition, and the M walker because it covers μm(modλ)\mu \equiv -m \pmod{\lambda} steps within the cycle, landing at the entrance.

Geometric reading

The cycle is a clock face with λ\lambda tick marks. The meeting point sits mm ticks past the entrance. The head sits μ\mu ticks behind the entrance along the stem. The arithmetic identity μ+m0(modλ)\mu + m \equiv 0 \pmod{\lambda} says these two distances are perfectly complementary — their sum is a whole number of laps, so a walker from each side meets at the entrance.

Phase 2 must use speed 1, not speed 2

A common confusion: students reuse the fast.next.next trick in phase 2. Don't. The proof depends on both walkers covering the same number of edges. Speed 2 in phase 2 would land on the wrong node.

Application 4 — k-th Node from the End

Same machinery, different speeds. To find the kk-th node from the tail in one pass, use a fixed-gap two-pointer walk:

  1. Place lead and trail both at head.
  2. Advance lead by exactly kk steps. (If it walks off the list, the input has fewer than kk nodes — raise.)
  3. Now walk both pointers at speed 1 until lead is null. At that moment trail is the kk-th from the end.

The invariant: lead is always exactly kk steps ahead of trail. So when lead reaches null, trail is kk nodes behind — which is precisely the definition of “k-th from end.”

This is structurally the same algorithm as middle-finding, with two changes: the gap is kk instead of dynamic, and we walk both pointers at speed 1 instead of mismatched speeds. That common skeleton is what makes “two pointers” a single technique rather than five different ones.


Application 5 — Happy Numbers and Functional Iteration

Here is a stunning use of fast-slow that has nothing to do with linked lists. A positive integer is happy if iterating “replace it with the sum of squares of its digits” eventually reaches 1; it is unhappy if the sequence enters a cycle that does not contain 1.

Example for n=19n = 19: 198268100119 \to 82 \to 68 \to 100 \to 1. Happy.

Example for n=4n = 4: 416375889145422044 \to 16 \to 37 \to 58 \to 89 \to 145 \to 42 \to 20 \to 4. Cycle. Unhappy.

The trick: define f(x)f(x) = sum of squared digits of xx. The sequence x,f(x),f(f(x)),x, f(x), f(f(x)), \ldots is a linked list of integers where node.next is f(node.value)f(\text{node.value}). Floyd's algorithm tells us whether this list cycles, and the only fixed point is f(1)=1f(1) = 1, so “reaches 1” is equivalent to “does not enter a non-1 cycle.”

The general principle

For any deterministic function f:SSf : S \to S on a finite set, Floyd's algorithm finds whether x,f(x),f(f(x)),x, f(x), f(f(x)), \ldots ever cycles, in O(1)O(1) memory. This is the foundation of Pollard's rho integer factorization (use f(x)=x2+1(modN)f(x) = x^2 + 1 \pmod{N}), pseudorandom generator period detection, and a host of cryptographic attacks. The expected cycle length is O(S)O(\sqrt{|S|}) by the birthday paradox — which makes Pollard's rho run in O(N1/4)O(N^{1/4}) for finding a factor of NN.

Interactive: Tortoise & Hare Lab

Pick a topology, set the cycle position with the slider, and step the simulation by hand. Watch slow (green S) and fast (red F) collide inside the loop; then press start phase 2 to launch the head walker (blue H) and see the proof come alive: head and slow each walk one step, and they meet exactly at the cycle entrance.

Tortoise & Hare Lab — slow walks 1, fast walks 2

steps = 0phase: walk
0123entrance4567SF
legend
slow fast head
walking… slow at node 0, fast at node 0

Try a few experiments to internalise what the variables mean:

  • Set μ=0\mu = 0 (cycle start = 0). Slow and fast meet, and phase 2 immediately reports the entrance as the head — no extra walking needed.
  • Make the stem long and the cycle short (e.g. n=12n = 12, cycle start = 9). Phase 1 spends most of its time in the stem; phase 2 walks the long way.
  • Switch to the straight topology and watch fast outrun slow. When fast hits None, slow is at n/2\lfloor n/2 \rfloor — that is the middle-finding algorithm.

Reference Implementation in Python

These four routines together cover every flavour of fast-slow you will meet in interviews and production code. Read them as variations on the same loop.

Find the Middle

Middle of a singly linked list — single-pass, O(1) extra memory
🐍fast_slow.py
1Function signature

Takes the head pointer of a singly linked list. Works for empty (None), single-node, and arbitrary lengths. No length parameter — fast-slow discovers it implicitly.

2Docstring choice

We commit to a flavour: even-length n=6 returns the SECOND middle. Document this loudly because callers will assume one or the other.

3slow := head

Both pointers start at head, not at head.next. Starting offset matters: starting both at head gives the floor(n/2)-th node when fast falls off; starting fast at head.next gives ceil(n/2).

EXAMPLE
For head = 1→2→3→4→5, slow ends at node 3 (the unique middle).
4fast := head

Same starting point. Slow and fast are 'in sync'; the difference will be entirely in their per-iteration speed.

5Loop guard — both nullity checks are essential

We must check fast (the empty-list case, head=None) AND fast.next (so that fast.next.next on line 7 is safe). Forgetting fast.next is the #1 NoneType crash for this routine.

EXAMPLE
On a 4-node list 1→2→3→4: iter 1 fast=node3 (fast.next=node4 OK); iter 2 fast=None — loop exits.
6slow advances by 1

Standard one-step walk. After k iterations of the loop body, slow has moved k nodes from head.

7fast advances by 2

Two-step walk in a single statement — safe because the loop guard verified fast and fast.next are non-None. After k iterations, fast has moved 2k nodes.

EXAMPLE
After 2 iterations on 1→2→3→4→5→6: slow at node 3 (1+2), fast at node 5 (1+4). After 3 iterations: slow=node 4, fast=None — loop exits.
8Return slow

When the loop exits, fast has fallen off (or is None). Slow has taken floor(n/2) steps from head, which is exactly the middle node by our convention.

1def find_middle(head):
2    """Return the middle node. For even n, returns the second of the two middles."""
3    slow = head
4    fast = head
5    while fast is not None and fast.next is not None:
6        slow = slow.next
7        fast = fast.next.next
8    return slow

Detect a Cycle

Floyd's tortoise-and-hare cycle detection — O(n) time, O(1) memory
🐍fast_slow.py
1Signature

head is a node or None. We return a bool. We MUST handle head=None (empty list, no cycle) gracefully.

3Initialise slow at head

Critical: do NOT start fast at head.next. If you do, the equality check on line 8 needs an extra special case for length-2 cycles. Starting both at head and checking AFTER moving keeps the logic uniform.

4Initialise fast at head

Same starting point as slow. Their starting gap is 0; the asymmetric speed grows it.

5Loop guard — same as middle-finding

The condition does double duty: in a cycle, fast.next is never None so we never exit; in a non-cycle, fast eventually falls off and we return False on line 10.

6slow += 1

After the move, slow has covered one more edge. In the no-cycle case slow's total step count after k iterations is k.

7fast += 2

Fast covers two edges per iteration. The asymmetric speed is what causes the lap-collision in the cycle.

8Identity check, not value equality

Use 'is' (pointer identity), NOT '==', because the same value can appear in multiple distinct nodes. A list 1→2→3→2→3→… has the value 2 appearing repeatedly but only one node IS the cycle's '2'.

EXAMPLE
head=A→B→C→B (C points back to B). Iter 1: slow=B, fast=C. Iter 2: slow=C, fast=C — slow IS fast — return True.
9Return True on collision

Because fast cannot 'jump over' slow inside the cycle (fast gains exactly 1 per step modulo λ), an identity match is the unambiguous signature of a cycle.

10Exit means fast hit None

If we ever leave the while loop, fast became None or fast.next became None, which means the list terminates — no cycle. Return False.

1 lines without explanation
1def has_cycle(head):
2    """Return True if the list contains a cycle, False otherwise."""
3    slow = head
4    fast = head
5    while fast is not None and fast.next is not None:
6        slow = slow.next
7        fast = fast.next.next
8        if slow is fast:
9            return True
10    return False

Find the Cycle Entrance

Cycle start node — proven by μ + m ≡ 0 (mod λ)
🐍fast_slow.py
1Signature

Returns the cycle entrance node, or None if the list is acyclic. Strictly more informative than has_cycle.

3Phase 1 setup — slow at head

Same starting position as has_cycle. We will reuse this variable for phase 2.

4Phase 1 setup — fast at head

Same starting position as slow.

5Phase 1 — collide-or-fall-off loop

Identical to has_cycle except for what we do on collision: we 'break' instead of 'return True', because we still need fast's exact position M for phase 2.

6slow += 1

Standard one-edge walk for the slow pointer.

7fast += 2

Standard two-edge walk for the fast pointer.

8Collision check

Same identity check used by has_cycle.

9break — not return

When this fires, fast holds the meeting point M — exactly m nodes past the entrance, where μ + m = kλ. We need this M; do not overwrite fast.

10Python while-else: the elegant 'no cycle' path

while-else runs the else block only if the while condition went false naturally (no break). That means fast hit None — no cycle — return None. This avoids a separate boolean flag.

EXAMPLE
For head=1→2→3→None: loop exits on fast.next being None; else fires; we return None.
13Phase 2 setup: slow := head

Reset ONLY slow. Fast stays at M. Now slow is μ steps from the entrance and fast is m steps past it; the proof says these two distances meet after μ more steps each.

14Walk-while-not-equal

If μ = 0 (head is on the cycle), this guard is false on entry (slow == head and fast might or might not equal head). When it does equal, we return immediately — the head is the entrance.

15slow.next — speed 1

CRITICAL: speed 1 for BOTH. If you accidentally write fast = fast.next.next, the math collapses and you'll land on a wrong node. The proof requires symmetric speed.

16fast.next — speed 1

Fast advances around the cycle. Because (μ + m) ≡ 0 (mod λ), after μ steps fast wraps to land exactly at the entrance.

17Return slow (== fast) — the entrance

The first pointer-equality during phase 2 is the cycle entrance. If the head was on the cycle, the loop body executed 0 times and we returned head — handled correctly by the while-condition check.

3 lines without explanation
1def cycle_entrance(head):
2    """Return the node where the cycle begins, or None if there is no cycle."""
3    slow = head
4    fast = head
5    while fast is not None and fast.next is not None:
6        slow = slow.next
7        fast = fast.next.next
8        if slow is fast:
9            break
10    else:
11        return None  # while-else: ran to completion without break => no cycle.
12
13    slow = head
14    while slow is not fast:
15        slow = slow.next
16        fast = fast.next
17    return slow

k-th Node from the End

k-th-from-end — fixed-gap two-pointer walk
🐍fast_slow.py
1Signature

head is the list's head; k is 1-indexed (k=1 means the tail itself). We raise on bad input rather than returning None to keep the contract loud.

3Validate k

k=0 is meaningless ('the 0-th from the end'). Negative k is a caller bug. Always validate at the boundary, never let it crash deep inside.

4raise ValueError

Use the exception type that matches the invariant (ValueError for value-domain bugs, TypeError for wrong type).

6lead := head

lead will sprint k steps ahead and then walk in lockstep with trail. Both start at head so the gap is initially 0.

7Sprint loop — gap-builder

Move lead exactly k times. After this, lead is k nodes past head; the lead-trail gap is now k, which we will preserve for the rest of the walk.

8Bounds check during the sprint

If lead becomes None before we've taken k steps, the list has fewer than k nodes. Raise — never silently return the head, that's a classic interview trap.

9Raise on short list

Same exception type as the line-3 check; consistent error API.

10lead = lead.next

Advance one edge per sprint iteration. After the loop, lead is at index k (zero-indexed from head). For k=1 on a 5-node list, lead = node 1 (second node, since k=1 just means we want the tail).

12trail := head

Trail starts at head; the gap (lead − trail) is now exactly k.

13Walk in lockstep until lead falls off

Each iteration preserves the invariant 'lead is exactly k nodes ahead of trail'. When lead becomes None (one step past the tail), trail is exactly k nodes back from None — the k-th from the tail.

EXAMPLE
k=2, list 1→2→3→4→5. After sprint: lead=node 3, trail=node 1. Iter 1: lead=4, trail=2. Iter 2: lead=5, trail=3. Iter 3: lead=None — stop. Return node 4 (the 2nd from tail).
14lead.next

Lead advances first; we don't care if it just became None — we'll catch that on the next while-condition check.

15trail.next

Trail advances exactly the same number of times as lead's post-sprint walk. Symmetric speed = invariant preserved.

16Return trail

Trail is the answer. Time: O(n) — at most n+k steps across both pointers; n+k ≤ 2n. Memory: O(1).

3 lines without explanation
1def kth_from_end(head, k):
2    """Return the k-th node from the tail (1-indexed). Raises if k is out of range."""
3    if k < 1:
4        raise ValueError("k must be >= 1")
5
6    lead = head
7    for _ in range(k):
8        if lead is None:
9            raise ValueError("list has fewer than k nodes")
10        lead = lead.next
11
12    trail = head
13    while lead is not None:
14        lead = lead.next
15        trail = trail.next
16    return trail

Same Routines in TypeScript

Here is the same suite ported to TypeScript. The translation is mechanical, but two language-specific details deserve attention: nullability is in the type (we get compile-time help we did not have in Python), and identity vs. equality is unambiguous because === on objects compares references.

TypeScript port — strict null checks make the contract explicit
📘fastSlow.ts
1LNode<T> definition

A linked-list node carries a value of generic type T and a next that is either another LNode<T> or null. The union with null is what makes strict-null TypeScript catch the bugs Python lets you hit at runtime.

3findMiddle generic over T

The traversal does not care what T is, so we keep it generic. Returning LNode<T> | null lets us handle empty input cleanly: head=null returns null.

4let slow = head

Same starting offset as Python. 'let' (not 'const') because we will reassign.

5let fast = head

Same starting offset as slow. The asymmetric speed grows the gap.

6Loop guard with strict equality

Compare to null with !== (not !=). Strict prevents the 'undefined vs null' confusion that bites generics in TypeScript.

7slow! non-null assertion

TypeScript doesn't know that slow tracks fast at half speed and is therefore non-null whenever fast.next is non-null. The ! tells the compiler 'trust me, I checked it via fast.' If you dislike the assertion, refactor to track length explicitly — but this is a standard idiom for two-pointer walks.

8fast.next.next — no assertion needed

The loop guard verified fast !== null AND fast.next !== null, so TypeScript can narrow fast.next.next to LNode<T> | null automatically.

13hasCycle — same skeleton

Identical to findMiddle except for the inner identity check. Notice how rare it is to have to think when porting between languages once you've internalised the pattern.

18slow === fast (reference equality)

=== on object references compares identity, not value. This is exactly what we want: two distinct nodes carrying the value 7 are NOT the same node.

24cycleEntrance — explicit met flag

TypeScript has no while-else, so we use a bool. Cleaner alternative: extract phase 1 into a helper returning LNode<T>|null. We chose the inline form for parity with the Python reference.

30met = true; break

Set the flag and exit. fast still holds the meeting point M, which we need for phase 2.

32Early-return if no cycle

If we exited the while loop without setting met, fast hit null — no cycle.

34Phase 2 reset — slow = head

Reset ONLY slow. Fast stays at the meeting point. The non-null assertions on slow!.next and fast!.next are safe because the loop terminates only when both are non-null and equal (so neither is null in the body).

35while (slow !== fast)

Identity check again. If the head IS the cycle entrance, the body never runs.

42kthFromEnd — RangeError, not TypeError

RangeError is the canonical exception type for out-of-range indices in JavaScript. Throw it, don't return null silently — callers get a stack trace pointing to the bug.

44for-loop sprint

Identical semantics to Python's for _ in range(k). The bounds check inside the loop catches the 'list shorter than k' case before we'd dereference null.

49Lockstep walk

After the sprint, lead is k ahead. Walking both at speed 1 preserves that gap, so when lead becomes null, trail is exactly k from the tail.

39 lines without explanation
1type LNode<T> = { value: T; next: LNode<T> | null };
2
3function findMiddle<T>(head: LNode<T> | null): LNode<T> | null {
4  let slow = head;
5  let fast = head;
6  while (fast !== null && fast.next !== null) {
7    slow = slow!.next;
8    fast = fast.next.next;
9  }
10  return slow;
11}
12
13function hasCycle<T>(head: LNode<T> | null): boolean {
14  let slow = head;
15  let fast = head;
16  while (fast !== null && fast.next !== null) {
17    slow = slow!.next;
18    fast = fast.next.next;
19    if (slow === fast) return true;
20  }
21  return false;
22}
23
24function cycleEntrance<T>(head: LNode<T> | null): LNode<T> | null {
25  let slow = head;
26  let fast = head;
27  let met = false;
28  while (fast !== null && fast.next !== null) {
29    slow = slow!.next;
30    fast = fast.next.next;
31    if (slow === fast) { met = true; break; }
32  }
33  if (!met) return null;
34
35  slow = head;
36  while (slow !== fast) {
37    slow = slow!.next;
38    fast = fast!.next;
39  }
40  return slow;
41}
42
43function kthFromEnd<T>(head: LNode<T> | null, k: number): LNode<T> | null {
44  if (k < 1) throw new RangeError("k must be >= 1");
45  let lead: LNode<T> | null = head;
46  for (let i = 0; i < k; i++) {
47    if (lead === null) throw new RangeError("list has fewer than k nodes");
48    lead = lead.next;
49  }
50  let trail = head;
51  while (lead !== null) {
52    lead = lead.next;
53    trail = trail!.next;
54  }
55  return trail;
56}

Brent's Algorithm — A Faster Cycle Detector

Richard P. Brent (1980) gave a different cycle detection scheme that is asymptotically the same as Floyd's but with smaller constants in practice. The idea: instead of walking both pointers every step, keep slow stationary for 2k2^k fast-steps, then teleport slow to fast and double the budget.

🐍python
1def has_cycle_brent(head):
2    if head is None or head.next is None:
3        return False
4    slow = head
5    fast = head.next
6    power = 1
7    length = 1
8    while slow is not fast:
9        if fast is None or fast.next is None:
10            return False
11        if length == power:
12            slow = fast
13            power *= 2
14            length = 0
15        fast = fast.next
16        length += 1
17    return True

Why is this faster in practice? Floyd does two pointer dereferences per step (slow + fast); Brent does one (only fast moves). When pointer chasing is the bottleneck (almost always, on real hardware where each node.next is a cache miss), halving the dereferences halves the wall-clock time. Brent also discovers λ\lambda, the cycle length, as a free side effect — useful for Pollard's rho.

When to use which

Floyd's is shorter to write, easier to remember in interviews, and extends naturally to entrance-finding. Brent's is faster in production cycle-detection code (reaching pseudo-random functions, cryptographic period detection). Most textbooks teach Floyd's; most factorization libraries use Brent's.

Where Fast-Slow Pointers Run in Production

Operating Systems and Memory

  • Garbage collectors use cycle detection variants when freeing reference-counted objects: a cycle of references would otherwise leak forever. CPython's gc module is the canonical example, though it uses a different (graph-based) algorithm; embedded reference-counting allocators in C++ smart-pointer libraries use Floyd-style detection on suspicious sub-graphs.
  • Memory leak detectors in Linux kernel modules walk allocator linked lists and apply Floyd to detect circular corruption from buggy drivers.
  • Filesystem fsck tools use cycle detection when verifying that the directory tree is actually a tree (no cycles via misplaced hard links on systems where directory hard-linking is permitted).

Compilers and Language Runtimes

  • Module loaders (Python's import system, Node's require, Java's class loaders) detect circular imports during dependency resolution. The classical approach uses an explicit visited set, but in low-memory bootloaders the fast-slow trick gets used on the “currently resolving” stack.
  • Type-inference engines for languages with structural types must detect cycles in type-alias chains: type A = B; type B = C; type C = A; — a cycle Floyd's detects in O(1)O(1) memory.

Networking and Distributed Systems

  • Routing protocols use TTL fields as a poor-man's cycle bound, but on the control plane, link-state databases run cycle detection over forwarding tables to catch routing loops.
  • Distributed-deadlock detection: the wait-for graph among lock holders forms a directed structure where a cycle = deadlock. Banker's and Chandy-Misra-Haas algorithms use cycle-detection primitives related to fast-slow.

Cryptography and Number Theory

  • Pollard's rho factorization — the most famous application. To factor an integer NN, iterate f(x)=x2+1modNf(x) = x^2 + 1 \bmod N and run Floyd's on the sequence. When you find a collision, gcd(xy,N)\gcd(|x - y|, N) often gives a non-trivial factor. Expected time O(N1/4)O(N^{1/4}) by the birthday paradox.
  • Pollard's rho for discrete logarithms uses the same skeleton in cryptanalysis of weak elliptic-curve parameters.
  • Pseudorandom-number-generator validation: any deterministic PRNG over a finite state space must eventually cycle; Floyd or Brent detects the period. Engineers running statistical simulations check this to ensure their generator's period exceeds the simulation length.

Algorithms Interviews and Competitive Programming

  • Find the Duplicate Number (Leetcode 287). Given an array of n+1 integers in [1, n] with one duplicate, use values as pointers (i → nums[i]); the duplicate is the entrance of a cycle. O(n)O(n) time, O(1)O(1) memory — a result that astonished interviewers when first popularised.
  • Linked List Reorder, Palindrome Check, Rotate by k — all rely on middle-finding via fast-slow as a subroutine.
  • Intersection of Two Linked Lists uses a related two-pointer trick: walk both, switch heads when one runs out, and you are guaranteed to meet at the intersection (or both at None) after at most m+nm + n steps.

Robotics, AI, and Everyday Software

  • State-machine cycle detection in autonomous-driving planners: a planner exploring possible state sequences must avoid revisiting equivalent states; Floyd's runs on the canonical-state hash chain.
  • Spreadsheet circular-reference detection: every time you type =A1 into cell A1, Excel runs a cycle check on the dependency graph — a problem with elegant fast-slow flavour when the graph is sparse.
  • Database query planners: view-resolution traversal must detect circular view definitions (view A references view B references view A), often via Floyd-style detection during the planner's dependency walk.

Pitfalls and How to Avoid Them

  1. NullPointer on fast.next.next. Always guard with both fast is not None AND fast.next is not None. Dropping the second check kills the program on any odd-length list at the last iteration.
  2. Off-by-one in middle-finding. while fast and fast.next vs. while fast.next and fast.next.next give different middles for even nn. Pick one and document it.
  3. Using == instead of is (or === in TS). Identity, not equality, is what cycle detection requires. Two distinct nodes can carry equal values.
  4. Phase 2 with speed 2. The cycle-entrance proof requires both pointers walking by 1 in phase 2. Speed 2 lands on the wrong node.
  5. Resetting the wrong pointer. In phase 2, reset slow to head; do not reset fast. Fast must stay at the meeting point M for the math to work.
  6. Forgetting μ=0\mu = 0 case. If the head itself is on the cycle, phase 2 must return head immediately. Both reference implementations above handle this because the while slow is not fast guard checks before stepping.
  7. k-th-from-end with k = list length. The sprint loop walks lead off the tail and into None exactly when k equals the length; the lockstep loop then runs zero times and returns head — correct. Test this boundary explicitly.
  8. Mutating during traversal. If you delete or splice nodes while a cycle-detection pass is in flight, all bets are off. Either snapshot a list of (prev, curr) pairs and operate on it afterwards, or hold a lock.

Quick Checks

Quick Check

On a 6-node list 1→2→3→4→5→6→None with the standard `while fast and fast.next:` loop guard, where is slow when fast first becomes None?

Quick Check

A list has a cycle that begins at the head itself (μ = 0, λ = 5). After phase 1 of Floyd's algorithm, slow and fast meet. What does phase 2 do before returning the cycle entrance?


Fast-slow pointers are the kind of trick you only need to understand once. Internalise the catch-up argument and the μ=kλm\mu = k\lambda - m identity, and a whole family of problems — middle, cycle, entrance, k-from-end, happy numbers, Pollard's rho — collapses into one short loop. In the next section we will look at “When to Use Linked Lists vs Arrays” and revisit why the techniques that make linked lists tolerable do not change the underlying cache-locality verdict.

Loading comments...