Learn › DSA Patterns › Fast and Slow Pointers › Case Study
Linked List Cycle:
2 Bugs to a Clean Solution
Not a tutorial — a real debugging log. Three attempts, two distinct bugs, each one reproduced with a concrete failing input before it was fixed.
The Problem
LeetCode 141 — Linked List Cycle
Given the head of a linked list, determine
whether it contains a cycle — a node
reachable again by continuously following next.
Return true if a cycle exists, or
false otherwise.
Input
1→2→3→back to 1 Output
true Input
[1,2,3] (no cycle) Output
false Input
[1] (no cycle) Output
false The Journey
Attempt by Attempt
Every bug below was reproduced with a real failing input before being fixed — not just described.
function hasCycle(head: ListNode | null): boolean {
let slow = head, fast = head;
while (fast !== null && fast.next !== null) {
slow = head!.next;
fast = head!.next!.next;
if (slow!.value === fast!.value) return true;
}
return false;
} Two bugs surfaced together by hand-tracing a real cycle: slow/fast get rebuilt from head every iteration instead of advancing from their own position, and the comparison checks .value instead of node identity.
Floyd's algorithm needs slow/fast to move from where they currently are — one step and two steps respectively, each pass. Recomputing head.next / head.next.next every iteration freezes them at the same two nodes forever. Separately, slow.value === fast.value is meaningless for cycle detection: it's checking whether two different node objects happen to hold equal values, not whether it's the literal same node being revisited.
Proof — trace on 1→2→3→back to 1:
iter1: slow = head.next = Node(2), fast = head.next.next = Node(3). slow.value(2) === fast.value(3)? No.
iter2: slow = head.next = Node(2) ← same node as before, rebuilt from head again!
fast = head.next.next = Node(3) ← same node as before, too
Same comparison again: No.
...this repeats forever — slow and fast are frozen at the same two nodes every pass.
fast.next is never null either (it's the cycle target), so the loop condition never becomes false.
Infinite loop — the function never returns. Advance from the pointers' own previous position (slow = slow.next, fast = fast.next.next), and compare by reference (slow === fast) — identity, not value.
function hasCycle(head: ListNode | null): boolean {
let slow = head, fast = head;
while (fast !== null) {
slow = slow!.next;
fast = fast!.next!.next;
if (slow === fast) return true;
}
return false;
} Pointers now advance and compare correctly — but stress-testing the no-cycle case ([1,2,3]) and a tiny input ([1]) throws instead of returning false.
The loop only checks fast !== null, but the body immediately reads fast.next.next — two dereferences deep. If fast.next is null, fast.next.next throws before the loop can exit cleanly.
Proof — no-cycle and tiny-input crashes:
hasCycle([1,2,3]) (no cycle) iter1: fast(1)!==null → slow=2, fast=1.next.next=3. slow===fast? No. iter2: fast(3)!==null → slow=3, fast=3.next.next → 3.next=null → null.next → CRASH Expected: false. Got: TypeError. hasCycle([1]) (no cycle, single node) iter1: fast(1)!==null → slow=1.next=null, fast=1.next.next → 1.next=null → null.next → CRASH Expected: false. Got: TypeError.
Guard the loop on the exact thing about to be dereferenced: while (fast && fast.next) — not just while (fast).
// O(N) time, O(1) space — Floyd's Tortoise and Hare
function hasCycle(head: ListNode | null): boolean {
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow!.next; // 1 step
fast = fast.next.next; // 2 steps
if (slow === fast) return true; // same node object — cycle!
}
return false; // fast reached the end — no cycle
}
// 1→2→3→back to 1 (cycle)
console.log(hasCycle(cyclicList)); // → true
// [1,2,3] (no cycle)
console.log(hasCycle(linearList)); // → false Correct guard is in place — the loop only continues when both fast and fast.next exist, so fast.next.next never dereferences null. One more pass, not a bug fix: head was unused inside the loop now that slow/fast fully carry the traversal state, so it's pruned as a final polish step.
Proof — full trace on 1→2→3→back to 1:
Initial: slow = fast = N0 (val 1). iter1: slow→N1 (1 step). fast→N0→N1→N2 (2 steps) → N2. iter2: slow→N2 (1 step). fast→N2→N0→N1 (2 steps) → N1. iter3: slow→N0 (1 step). fast→N1→N2→N0 (2 steps) → N0. slow===fast===N0 → cycle detected! ✅ Also verified: [1,2,3] and [1] both return false with no crash.
Interactive
Cycle Visualizer
Watch slow and fast converge on 1→2→3→back to 1 step-by-step.
Initial: slow = fast = N0 (val 1).
Lessons Learned
Key Takeaways
Trace small examples by hand before trusting the code
Both a case that should work and an edge case that shouldn't. Hand-tracing 1→2→3→back to 1 is what surfaced the frozen pointers and the value-vs-identity bug in the first place.
Compare identity, not value, when 'same object' is what matters
slow === fast checks whether it's the literal same node being revisited. slow.value === fast.value only checks whether two possibly-different nodes hold equal data — meaningless for cycle detection.
Advance pointers from their own previous position
Don't recompute from a fixed origin every pass. slow = slow.next and fast = fast.next.next carry the traversal forward; slow = head.next resets it to square one every iteration.
Guard loop conditions on the exact thing about to be dereferenced
while (fast) isn't enough when the body reads fast.next.next. The guard needs to check fast && fast.next — the precise chain the body is about to walk.
Final Result
The Verified Solution
Floyd's Tortoise and Hare — one slow step, two fast steps, compared by reference.
// O(N) time, O(1) space — Floyd's Tortoise and Hare
function hasCycle(head: ListNode | null): boolean {
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow!.next; // 1 step
fast = fast.next.next; // 2 steps
if (slow === fast) return true; // same node object — cycle!
}
return false; // fast reached the end — no cycle
}
// 1→2→3→back to 1 (cycle)
console.log(hasCycle(cyclicList)); // → true
// [1,2,3] (no cycle)
console.log(hasCycle(linearList)); // → false | Solution | Time | Space | Verified |
|---|---|---|---|
| Floyd's Cycle Detection (Attempt 3) Verified ✓ | O(N) | O(1) | Cyclic, non-cyclic, and single-node cases |
Reuse This
Template: Floyd's Cycle Detection (Fast & Slow Pointers)
The generic shape behind this solution — adapt it to any "does this loop back on itself" question.
function hasCycle(head):
slow = head
fast = head
while fast is not null and fast.next is not null:
slow = slow.next // 1 step
fast = fast.next.next // 2 steps
if slow === fast: // reference equality — same node object
return true // cycle found
return false // fast reached the end — no cycle When to reach for this
Any "does this structure loop back on itself" question on a linked structure — O(1) space beats a hash set of visited nodes.
The trap this journey hit
Comparing by value instead of identity, and recomputing pointers from a fixed origin instead of advancing them — both defeat the whole point of the technique.
Test Yourself
Quiz: Check Your Understanding
Your score
0 / 12
Keep Learning
Bugs are part of the process 🎉
Two real bugs, two real fixes — that's what getting to a correct Floyd's Cycle Detection solution actually looks like.
Related: try LeetCode 142 — Linked List Cycle II (find where the cycle starts) or LeetCode 876 — Middle of the Linked List (same pattern, no cycle involved).