LearnDSA PatternsFast and Slow PointersCase Study

LeetCode 141 · Easy · Floyd's Cycle Detection · TypeScript

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.

Scroll to follow the journey

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.

1 Value Equality, and Pointers That Never Actually Move 🐛 Buggy
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.
Fix →

Advance from the pointers' own previous position (slow = slow.next, fast = fast.next.next), and compare by reference (slow === fast) — identity, not value.

2 Fixed the Progression — Now It Crashes on Non-Cyclic Input 🐛 Buggy
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.
Fix →

Guard the loop on the exact thing about to be dereferenced: while (fast && fast.next) — not just while (fast).

3 Verified, and Simplified ✅ ✅ Correct
// 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.

cycle 1 2 3 🐢 slow 🐇 fast

Initial: slow = fast = N0 (val 1).

slow fast meeting point

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.

Final — Attempt 3
// 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.

Pseudocode
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

Question 1 of 12 Score: 0
In a linked list that contains a cycle, why are the slow and fast pointers guaranteed to eventually land on the exact same node? basic

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