Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Run two pointers from the head: slow moves one node per step, fast moves two. If there's a cycle, fast laps slow and they land on the same node — return true. If fast (or fast.next) reaches null, the list ends, so there's no cycle. O(n) time, O(1) space — no hash set needed.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you know Floyd's fast/slow pointers, can argue why they must meet inside a cycle, and whether you null-guard fast.next before the double advance.
Where candidates slip
Advancing fast = fast.next.next without first checking fast and fast.next for null, causing a NullPointerException on lists with no cycle or an even number of nodes.
How to stand out
State the O(1)-space fast/slow solution, explain the 'fast gains one node per step on slow so it must close any gap inside the loop' argument, and mention the hash-set baseline plus the cycle-start follow-up (Floyd's phase 2).
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewDetect a cycle in a linked listO(1) space required?YesNoExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Hash Set — Remember Visited Nodes (Baseline)
1Hint 1Explore hintHide hint
How would you notice you've arrived at a node you've already stood on?
2Hint 2Explore hintHide hint
Store each node's identity as you walk. What does it mean if next leads to a node already stored?
3Hint 3Explore hintHide hint
If you reach null before re-seeing a node, the list is finite — no cycle.
A · Core idea
We walk the list and remember every node we've stood on; revisiting one means we've looped.
Why store nodes, not values. A cycle is a structural fact — some next points back to an earlier node. Storing values would misfire on lists with repeated numbers. The set holds node references (identity).
The two ways the walk ends. Either we step onto a node already in the set (we've come around a loop → return true), or we reach null (the list terminated → return false). Exactly one happens, so the algorithm always halts.
Why we improve on it. This is the most direct solution and easy to defend, but it spends O(n) memory. Floyd's pointers get the same answer in O(1) space, which is the usual target — so present this, then optimize.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
list = [1, 2, 3], 3.next → node 2Hash-map state
visit 1
n1Continue scanning
insert n1
trueC · Code with teaching comments
Reference implementation previewpublic class ListNode { int val; ListNode next; ListNode() {} ListNode(int val) { this.val = val; }Explore the full codeHide the full code
D · Why it works and complexityO(n) timeO(n) spaceOpen the invariant and proofExplore the proofHide the proof
Cycle detection becomes 'have I seen this exact node before?' — the same remember-and-probe membership idea as Two Sum, applied to references.
ComplexityO(n) time, O(n) space — Each node is visited at most once and does O(1) average set work → O(n) time. The set can hold up to n node references → O(n) space.
E · Edge cases and pitfalls5list = [] — head is null; loop never runs → false.Explore edge casesHide edge cases
Edge cases handled
list = []head is null; loop never runs → false.list = [1], no cycleAdd n1, advance to null → false.
Pitfalls
- Storing values instead of node references — false positives on repeated values.
- Adding cur after the check but forgetting to advance cur — infinite loop on acyclic lists.
- Treating O(n) space as acceptable when the interviewer wants O(1).
Approach 2 of 2 · Optimal
Floyd's Fast/Slow Pointers — Tortoise and Hare (Optimal)
1Hint 1Explore hintHide hint
Imagine two runners on a track, one twice as fast. On a circular track, what eventually happens to the gap between them?
2Hint 2Explore hintHide hint
Move slow one node and fast two nodes each step. Inside a cycle, fast closes the gap to slow by exactly one node per step — so it can't jump over without landing on it.
3Hint 3Explore hintHide hint
If there's no cycle, fast runs off the end. What two things must you null-check before doing fast = fast.next.next?
A · Core idea
We send a slow pointer (one step) and a fast pointer (two steps) from the head; a collision proves a cycle, and fast running off the end proves there isn't one.
Why they must meet inside a cycle. Once both pointers are inside the loop, consider the gap from fast to slow measured forward around the ring. Each step, slow advances 1 and fast advances 2, so fast gains exactly 1 on slow every step. A gap that shrinks by 1 each step inside a finite ring must hit 0 — and because it shrinks by exactly 1 (not 2 or more), fast can't leap over slow without landing on it. So they collide. This is why fast moves 2 and not 3: a larger stride could skip past slow on a small loop.
Why no cycle terminates safely. If the list ends, fast (the faster pointer) reaches null first. The loop condition fast != null && fast.next != null catches both the odd-length case (fast becomes null) and the even-length case (fast.next becomes null) before we ever dereference. Exit the loop → return false.
The null-guard trap. The single most common bug is fast = fast.next.next without first checking that fast and fast.next are non-null. On an acyclic list — or any list with an even node count — fast.next becomes null and fast.next.next throws. Both guards are mandatory, in that order (short-circuit).
Compare by identity, and move before comparing. The collision test is slow == fast on references. Also advance then compare: if you compare first, slow and fast both start at head and you'd wrongly report a cycle on step zero.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
list = [1, 2, 3, 4], 4.next → node 2 (cycle of length 3)Interactive diagram
Hare gains one node per step on the tortoise
initInside the loop the forward gap shrinks by exactly 1 each step, so the hare must land on the tortoise.
trueC · Code with teaching comments
D · Why it works and complexityO(n) timeO(1) spaceOpen the invariant and proofExplore the proofHide the proof
Different speeds turn 'is there a loop?' into 'do two walkers collide?'. Because fast gains exactly one node per step, no gap inside a cycle can survive — the collision is guaranteed, and the finite list lets fast escape to null. The named trap is the missing fast.next null-check before the double hop.
ComplexityO(n) time, O(1) space — Without a cycle, fast reaches the end in n/2 steps. With a cycle of length L, once slow enters the loop fast catches it within L steps. Total O(n). Only two pointers → O(1) space.
E · Edge cases and pitfalls7list = [] — fast is null; loop condition false immediately → false.Explore edge casesHide edge cases
Edge cases handled
list = []fast is null; loop condition false immediately → false.list = [1, 2] no cycleslow→2, fast→2.next.next: fast.next is null after one step → loop exits → false.list = [1], 1.next → 1 (self-loop)slow→1, fast→1 (1.next.next = 1) → slow is fast → true.
Pitfalls
- Writing fast = fast.next.next without guarding fast and fast.next — null dereference on acyclic or even-length lists.
- Comparing slow == fast before advancing — false 'cycle' on the very first check since both start at head.
- Comparing values instead of node identity.
- Moving fast by 3 (or more) instead of 2 — it can leap over slow on a small loop and miss the collision.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic boolean hasCycle(ListNode head) { ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { slow = slow.next;Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Skipping the fast.next null guard
Advancing fast two steps requires both fast and fast.next to exist. On an acyclic list (or any even-length one) fast.next becomes null, and fast.next.next throws a null dereference.
Explore the code comparisonHide the code comparison
Skipping the fast.next null guard
Advancing fast two steps requires both fast and fast.next to exist. On an acyclic list (or any even-length one) fast.next becomes null, and fast.next.next throws a null dereference.
Don't write this
while (fast != null) { // missing fast.next check
slow = slow.next;
fast = fast.next.next; // NPE when fast.next is null
}Write this instead
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) return true;
}2Comparing before moving
Both pointers start at head, so an equality check before the first move is trivially true and reports a cycle on a list that has none.
Explore the code comparisonHide the code comparison
Comparing before moving
Both pointers start at head, so an equality check before the first move is trivially true and reports a cycle on a list that has none.
Don't write this
while fast and fast.next:
if slow is fast: # true on entry — false positive
return True
slow = slow.next
fast = fast.next.nextWrite this instead
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
return TrueRevise
Carry the pattern into the interview
Pattern to remember
Floyd's tortoise and hare (fast/slow pointers)
Code pattern to memorize
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast: return True
return FalseRules to remember
- 1Guard both
fastandfast.nextbefore advancing fast two steps — skip either and you null-dereference (the named trap). - 2Move slow by 1 and fast by 2, then compare. Comparing before moving makes them 'equal' at the start and falsely reports a cycle.
- 3Compare by identity (
slow is fast), never by value — repeated values aren't a loop. - 4If the loop exits via the
whilecondition, fast hit the end → no cycle, return false. - 5Do not modify the list (no marking visited via mutation) — it must stay read-only.
Use this pattern when
- Decide whether a linked list loops back on itself
- O(1) extra space is required, ruling out a hash set
- Finding a cycle's start, its length, or the middle of a list (same fast/slow skeleton)
- Cycle detection in any 'functional graph' where each node has one successor
Choose another pattern when
- You also need every visited node recorded for other reasons → a hash set may be reused
- Nodes carry no stable identity (e.g. value-copied) → identity comparison won't work
- The structure has multiple out-edges per node → this is a general graph; use DFS/visited colors
- You may freely mutate and prefer the simplest code → marking visited can be acceptable
Interview takeaway
Two pointers at different speeds will always collide inside a loop and always exit a finite list — that single idea detects cycles in O(1) space. The same tortoise-and-hare skeleton finds the cycle's entry, the list's middle, and duplicates in Find the Duplicate Number.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternFloyd's tortoise-and-hare: two pointers at different speeds collide inside any cycle and escape any finite list, all in O(1) space. The same skeleton finds the cycle's entry, the list's middle, and duplicates in a functional graph.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Linked List Cycle IIAfter the collision, reset one pointer to head and advance both by one — they meet at the cycle's entry.
- Middle of the Linked ListSame fast/slow skeleton: when fast reaches the end, slow sits on the middle.
- Find the Duplicate NumberComing soonTreat the array as a functional graph and run Floyd's cycle detection to find the repeated value.