Intuition
Race a slow pointer (one step) against a fast pointer (two steps). With no cycle, fast runs off the end. With a cycle, fast can never escape and steadily laps slow until they collide.
Floyd's tortoise and hare. If a cycle exists the fast pointer laps the slow one and they must meet; if not, fast simply falls off the end. The reason they can't skip past each other is that the gap shrinks by exactly one each step — that argument is what makes O(1) space possible where a hash set would need O(n).
Approach
The hash-set answer costs memory
You can store every node you visit in a set and declare a cycle the moment you revisit one. It's simple and O(n) time, but it's O(n) space. The elegant solution gets the same answer with two pointers and no extra memory — Floyd's tortoise and hare.
A fast runner must lap a slow one inside a loop
Run slow one step at a time and fast two steps at a time. If the list ends, fast (or fast.next) hits null and there's clearly no cycle. But if there is a cycle, fast can never escape it; once both pointers are inside the loop, fast gains exactly one node on slow every step, so the gap shrinks to zero and they must collide. A collision is therefore proof of a cycle.
Step, then compare
While fast and fast.next exist, advance slow by 1 and fast by 2, then check whether they point to the same node (identity, not value). If so, return true; if fast runs off the end, return false. O(n) time, O(1) space.
Solution & live demo
Common pitfalls
Comparing values instead of identity
if slow.val == fast.val: return True
if slow is fast: return True
Two distinct nodes can easily hold the same value in a perfectly acyclic list, producing a false positive. A cycle means the pointers reach the same node, which is an identity question.
Advancing both by one
slow = slow.next fast = fast.next
slow = slow.next fast = fast.next.next
Equal speeds keep the gap constant forever, so they never meet inside a cycle and the loop runs indefinitely. The difference in speed is the entire mechanism.
Checking for a meeting before moving
while fast and fast.next:
if slow is fast: return True
slow = slow.next
fast = fast.next.next slow = slow.next
fast = fast.next.next
if slow is fast: return TrueBoth start at head, so testing before the first move reports a cycle on every non-empty list. The comparison belongs after the advance.
Edge cases
fast or fast.next becomes null and the loop exits with false.
The pointers still meet inside the loop; identity (is) compares nodes, not values.
The loop condition is false immediately, returning false.