Algorithm

Linked List Cycle

Two Pointer Pattern

Linked List Cycle

Given the 'head' of a linked list, determine if the list contains a cycle. A cycle occurs if there is some node in the list that can be reached again by continuously following the 'next' pointer.

CONSTRAINTS
  • The number of the nodes in the list is in the range [0, 10,000]
  • -100,000 <= Node.val <= 100,000
  • Can you solve it using O(1) memory?
EXAMPLE 1
Input: head = [3,2,0,-4], pos = 1
Output: true
There is a cycle in the linked list, where the tail connects to the 1st node (0-indexed).
EXAMPLE 2
Input: head = [1,2], pos = 0
Output: true
There is a cycle in the linked list, where the tail connects to the 0th node.
EXAMPLE 3
Input: head = [1], pos = -1
Output: false
There is no cycle in the linked list.
Do I return true/false, or the looping node itself?
Just a boolean — whether a cycle exists. Returning the node where the loop starts is a harder follow-up, Linked List Cycle II.
Should I compare nodes by value or by reference?
By reference — the node objects themselves. Two different nodes can hold the same value; that is not a cycle. A cycle means `next` leads back to a node that is physically already in the chain.
Am I allowed to modify the list to mark visited nodes?
Assume not, unless the interviewer says otherwise. Marking nodes destroys the input; the two-pointer method needs no marking and no extra memory at all.

We are handed a singly linked list, and we need to determine if it contains a cycle—a loop where a node points back to a previously visited node, causing any traversal to get trapped in an infinite loop. In an ordinary list, we eventually fall off the end when we reach a null pointer. But in a cyclic list, we could walk forever without ever finding an end. How can we detect if a list is an endless loop without getting stuck in it ourselves?

The Memory Breadcrumbs

The most intuitive way to spot a loop is to keep track of where we have already been. As we travel down the list, we drop a memory breadcrumb by storing the unique address of each visited node in a hash set. At each new node, we look down at our records. If we ever step onto a node that is already in our set, we have found the entrance to the loop and can confidently confirm that a cycle exists.

While this works, it requires O(N) memory to store every visited node. If the list is massive, tracking thousands of breadcrumbs is a high-cost memory tax.

python
# Save visited node addresses to a set
seen = set()
curr = head

while curr:
    if curr in seen:
        return True  # Already been here! Loop detected.
    seen.add(curr)
    curr = curr.next

return False  # Fell off the end, no cycle.
The Pointer State: The Jogger & The Sprinter

To eliminate the memory tax entirely, we can detect cycles using two coordinated pointers moving at different speeds down the track. Each pointer has a strict contract:
- The Slow Pointer (slow): Advances steadily, taking exactly one node per step.
- The Fast Pointer (fast): Sprints ahead, taking exactly two nodes per step.

Both start together at the head of the list.

The Track Collision Symmetry

Think of this like two runners on a track. If the list is a straight line, the sprinter (fast) will reach the end of the track and stop, while the jogger (slow) is still halfway.

However, if the track has a circular loop, the sprinter is trapped in the circle and will keep running forever. Because the sprinter moves twice as fast, it will gradually catch up to the jogger from behind. At every step, the distance between them shrinks by exactly one node. No matter how large the loop is, the sprinter is mathematically guaranteed to "lap" the jogger, colliding on the exact same node.

Synchronized Loops

We advance both pointers in synchronized strides. If the fast pointer or its next node becomes null, we know the list has a definite end and contains no cycle. But if the pointers ever meet at the exact same physical node address, we have detected a collision and proven a cycle exists.

python
slow = head
fast = head

# Traverse until the sprinter runs out of track
while fast and fast.next:
    slow = slow.next      # Jogger moves 1 step
    fast = fast.next.next # Sprinter moves 2 steps
    
    # If they occupy the exact same physical address, a cycle exists
    if slow == fast:
        return True

return False # Sprinter safely reached the end of the line
Empty Tracks and Solitary Nodes

This zero-memory approach handles small lists cleanly without crashing:
- Empty List (head is null): The while loop condition checks fail immediately. We exit and return false.
- Single-Node List (no cycle): fast.next is null from the start. We exit immediately and return false.
- Self-Loop (single node pointing to itself): slow steps to Node 1, and fast steps twice (which, due to the self-loop, lands back on Node 1). They collide immediately on the first step.

Worked Example:Trapping the Loop
3
slowfast
2
0
-4
⟲ back to idx 1
We start with both 'slow' and 'fast' pointers at the head node containing 3, with a cycle looping back from the tail to node 2.
3
2
slow
0
fast
-4
⟲ back to idx 1
We advance: 'slow' moves one step to 2, and 'fast' moves two steps to 0.
3
2
fast
0
slow
-4
⟲ back to idx 1
We advance again: 'slow' moves one step to 0, and 'fast' moves two steps (through -4, looping back to 2).
3
2
0
-4
slowfast
⟲ back to idx 1
Both pointers advance: 'slow' moves to -4, and 'fast' moves two steps (from 2 to 0 to -4). They collide on the same node, proving a cycle exists.
Interactive Strategy Visualization
DETECTION ENGINE

Floyd's Tortoise & Hare Animation

SlowFast

Current Phase

Wait, is this a loop?

Imagine walking down a path. Usually, you eventually hit a dead end (NULL). But what if the path loops back? If you keep walking and never hit a dead end, how can you prove you are in a cycle without a map?

Strategy: Constant Space Detection

While a Hash Set works in O(N) space, Floyd's Algorithm solves this in O(1) extra space.

O(N) Time · O(N) Space Hash Set
O(N) Time · O(1) Space Fast/Slow