You Can Recognize Fast-Slow Pointer Problems Before You Code

May 26, 20269 min read
dsaalgorithmsinterview-prepleetcode
You Can Recognize Fast-Slow Pointer Problems Before You Code
TL;DR
  • Fast-slow pointers cover five problem shapes: cycle detection, cycle start, middle-finding, kth-from-end, and functional graph cycles
  • Cycle detection uses while fast and fast.next; phase 2 resets one pointer to head, not both
  • Middle-finding initialization matters: both-at-head gives the second middle on even-length lists; fast = head.next gives the first
  • kth-from-end uses equal speed with a fixed gap of k+1 steps and a dummy node to handle head-removal safely
  • Functional graph insight: [1..n] value constraints let you treat array indices as linked list pointers (LeetCode 287, 202)
  • Loop guard is always while fast and fast.next; compare node identity, not values

When you see "detect a cycle in a linked list," you know exactly what to do. Two pointers, one moving twice as fast, they collide if there's a cycle. You've solved this a hundred times.

The trouble is fast-slow pointer problems show up in at least five distinct shapes, and cycle detection is the only one that announces itself. "Find the middle of a list in a single pass" looks like a counting problem. "Find the duplicate, values bounded by [1..n], no extra memory" looks like a hash set problem. Both have simpler-looking solutions. Neither is what the problem is testing.

Fast-slow pointers run wherever you can define a deterministic next step and the structure might loop, or you need to know when one pointer is twice as far as another. Once you know the signals, recognition is nearly automatic.

The ρ Shape and Two Different Templates

A cyclic linked list looks like the Greek letter ρ (rho). A tail of length F leads into a loop of length C.

head → [0] → [1] → [2] → [3] → [4]
                          ↑         ↓
                         [6] ← [5]

When fast moves at 2× the speed of slow, it laps slow somewhere inside the loop. That meeting point is not the cycle start. Finding the entry requires a second phase. Both are covered in full in Floyd's cycle detection algorithm and finding the cycle start.

What most tutorials blur is that middle-finding, kth-from-end, and non-linked-list variants use the same mechanical idea but a completely different template. There's no cycle in a "find the middle" problem. You're exploiting the speed difference to reach a position relative to the tail without knowing the length. Keep these two modes separate or you'll splice code that doesn't belong together.

The Five Signals in the Problem Statement

Read the constraints and the phrasing. These are the signals.

Signal 1: "detect a cycle" / "is there a loop"

The explicit one. The problem says it outright, or says "the list might contain a cycle."

def has_cycle(head): slow = fast = head while fast and fast.next: slow = slow.next fast = fast.next.next if slow is fast: return True return False

slow is fast checks identity, not equality. Two distinct nodes with value 3 do not trigger this. You're comparing memory addresses. In Java and C++ that's == on references, not .equals() or == on primitives.

Signal 2: "return the node where the cycle begins"

Detection first, then phase 2. Reset one pointer to head, move both one step at a time. The meeting point is the cycle entry. The proof: after detection, F = kC - M, so head-to-entry and meeting-point-to-entry are equal distances. See finding the cycle start for the full derivation.

Signal 3: "find the middle in one pass" / "O(1) space"

When fast walks off the end, slow is at the midpoint. No cycle involved. Deceptively simple, which is exactly why the initialization trap (see below) catches people off guard.

Signal 4: "remove the kth node from the end" / "find kth from last"

A fixed gap between pointers rather than a 2:1 speed ratio. Fast starts k steps ahead, then both advance together. When fast hits null, slow is k positions from the end.

Signal 5: "no extra memory" and "values are bounded by [1..n]" or "this sequence might repeat"

The least obvious one. Any sequence with a deterministic next function and a bounded state space must eventually cycle. Fast-slow works on it without a hash set. This is the signal that separates "I've seen this before" from "I actually understand what's happening."

Middle Finding: The Initialization Trap

The standard code initializes both at head and loops while fast and fast.next.

def find_middle(head): slow = fast = head while fast and fast.next: slow = slow.next fast = fast.next.next return slow

For an odd-length list, slow lands exactly in the center. For an even-length list, slow lands on the second of the two middle nodes.

If a problem needs the first middle, shift fast one step ahead at initialization: fast = head.next. This matters for LeetCode 234 (Palindrome Linked List) and 143 (Reorder List). Both split the list at the midpoint to reverse the second half. Land on the wrong middle and the merge produces garbage. Trace a four-node list by hand if you're unsure which one you need.

Kth From the End Is a Different Template

This one doesn't use a 2:1 speed ratio. Both pointers move one step per iteration. The trick is the fixed starting gap.

def remove_nth_from_end(head, k): dummy = ListNode(0, head) slow = fast = dummy for _ in range(k + 1): fast = fast.next # advance fast k+1 steps while fast: slow = slow.next fast = fast.next slow.next = slow.next.next # remove the target node return dummy.next

The dummy node is not optional. If k equals the list length, you're removing the head, and slow needs somewhere to point. Without dummy, slow is null at splice time and the operation crashes. Your test passes on [1,2,3] and fails on [1]. Classic. With dummy, slow points to dummy when fast runs off the end, and dummy.next = dummy.next.next correctly removes the old head.

Advance fast k+1 steps, not k. You want slow to land on the node before the target, not the target itself, so you can splice it out.

Danny DeVito holding two guns: "When you switch from C to Java... So anyway, I started pointing"

Fast-slow pointer problems, after you realize they're lurking in every "O(1) space" constraint.

The Array That's Actually a Linked List

LeetCode 287 looks nothing like a linked list problem. "Given an array of n+1 integers where each integer is between 1 and n inclusive, find the duplicate." The hash set solution is one line. The O(1) space solution requires a mental transformation.

The constraint that values are in [1..n] means every element is a valid array index. Treat nums[i] as the "next pointer" from index i. Start at index 0. Follow 0 → nums[0] → nums[nums[0]] → .... This chain must eventually revisit a value because there are n+1 starting points but only n valid destinations. The revisited value is the duplicate, and it's the entry point of a cycle.

def find_duplicate(nums): # Phase 1: meet inside the cycle slow = fast = nums[0] while True: slow = nums[slow] fast = nums[nums[fast]] if slow == fast: break # Phase 2: find cycle entry = the duplicate slow = nums[0] while slow != fast: slow = nums[slow] fast = nums[fast] return slow

The while True in phase 1 is safe. It looks wrong. It usually is wrong. Here, a duplicate is guaranteed by the problem, so a cycle is guaranteed. The loop will exit. The cycle entry is the duplicate because that's the first index with two predecessors in the functional graph.

The same reasoning extends to LeetCode 202 (Happy Number). The next function is "sum of squared digits." Either the sequence reaches 1 or it cycles. Fast-slow detects which case without storing every value you've seen.

Any time values live inside a closed, bounded universe and the problem forbids extra memory, ask whether you can define a next function and run Floyd's on it.

Three Loop Condition Bugs That Fail on Edge Cases

These three look similar. Only one is correct for acyclic traversal.

while fast.next and fast.next.next: # bug: crashes if fast is null while fast: # bug: NPE when accessing fast.next.next while fast and fast.next: # correct

The guard fast and fast.next handles both the empty list and the single-node list. If fast is null, the first condition short-circuits before touching fast.next. If fast.next is null, the second condition stops before you try fast.next.next.

Test on a two-node list. After one iteration, fast is at node 2 and fast.next is null. The loop exits. Slow is at node 1. Correct.

Two more bugs worth knowing. In phase 2 of cycle detection, you reset exactly one pointer to head. Reset both and you run detection again from scratch, not the entry-finding algorithm. And compare node identities during detection, not values. A list like 1 → 1 → 2 has two nodes with value 1. Fast-slow should not fire on that unless they are the same node in memory.

Code comment reading: "// I know that next node could possibly be null // but who cares about edge cases amirite?"

The comment that precedes every wrong loop guard in interview prep code.

Cold-Reading Fast-Slow Pointer Problems

You read a problem. Before touching the keyboard, you scan for these:

  • "O(1) extra space" or "in-place" on a linked list problem: fast-slow, not a set.
  • "single pass" on a linked list: probably middle or kth-from-end.
  • "values in [1..n]" or "each element is in range [0..n-1]": functional graph candidate.
  • "sequence that might cycle" or "detect a repeated state": Floyd's on the state function.
  • Explicit "cycle" or "loop": standard detection.

State the signal out loud before you write code. "The values are bounded by [1..n], so I can treat index i as a node whose next pointer is nums[i]. That makes this a cycle detection problem." That sentence tells the interviewer you recognized the pattern, not just memorized the template.

SpaceComplexity runs full DSA mock interviews over voice, exactly like a real interview. Practicing this narration out loud, not just writing code silently, is what trains the recognition reflex. Try it if your pattern identification is solid but your explanation still feels shaky.

Recap

  • Signal 1 and 2: "detect a cycle" or "find cycle start" means detection template plus optional phase 2 reset
  • Signal 3: "find the middle in one pass" means 2:1 speed, no phase 2, watch even-length initialization
  • Signal 4: "kth from the end in one pass" means equal speed, fixed gap of k+1, use a dummy node
  • Signal 5: "bounded values" or "repeating sequence" means define a next function, run Floyd's on it
  • Loop guard is always while fast and fast.next for acyclic problems
  • Compare node identity, not values
  • Dummy node prevents head-removal edge cases
  • Resetting both pointers in phase 2 is a bug; reset exactly one

Further Reading