Fanpilot

ProblemsLinked lists

Linked List Cycle

EasyLeetCode 1415 min read

You'll find a cycle with fast and slow pointers, using no extra memory. You need linked list nodes and Python's is. Related chapter: linked lists.

1. The problem

You get the head of a linked list. Return whether following next from the head ever reaches the same node twice. Repeated values don't make a cycle; reaching the same node again does. You get only the head, not where the cycle starts. This is LeetCode 141.

Structure Answer Why
A → B → C → B True B is revisited
Two different nodes both holding 7, then None False Equal values, different nodes
Empty list False There is nothing to revisit

2. A correct baseline

class ListNode:
    def __init__(self, val=0, next=None):
        self.val, self.next = val, next

def baseline(head):
    seen = set()
    while head is not None:
        if id(head) in seen:
            return True
        seen.add(id(head))
        head = head.next
    return False

def example():
    a, b, c = ListNode(1), ListNode(2), ListNode(3)
    a.next, b.next, c.next = b, c, b
    return a

assert baseline(example())
assert not baseline(ListNode(7, ListNode(7)))
assert not baseline(None)

This doesn't change the list. For n nodes, it takes O(n) time and O(n) extra memory. On a million nodes, the set holds a million entries.

The bottleneck. The time is already as good as it gets. The waste is memory: it stores every node just to notice a repeat.

3. The observation

Picture two runners on a track. If the track is a straight line, the fast runner reaches the end. If it loops, the fast runner laps the slow one and they meet.

Do the same with two pointers. slow moves one node per step, and fast moves two. Without a cycle, fast reaches None. In a cycle, fast gains one node per step on slow, so the gap shrinks by one each step until they meet. Two pointers replace the whole set.

4. The solution and a trace

class ListNode:
    def __init__(self, val=0, next=None):
        self.val, self.next = val, next

class Solution:
    def hasCycle(self, head) -> bool:
        """Detect a cycle without changing a singly linked list."""
        slow = fast = head  # Reachable finite nodes; each has one next link.
        while fast is not None and fast.next is not None:
            slow = slow.next
            fast = fast.next.next
            # Compare node identity after moving, not their stored values.
            if slow is fast:
                return True
        return False

solve = Solution().hasCycle
assert solve(example())
assert not solve(ListNode(7, ListNode(7)))
assert not solve(None)

Trace A → B → C → B:

Round Slow Fast Result
Start A A Don't compare before moving
1 B C Different objects
2 C C Cycle found

5. Why it works

No cycle means False. After t steps, slow is t nodes in and fast is 2t nodes in. Without a cycle, those are different nodes for any t > 0, so they never meet. fast reaches None and the loop ends.

A cycle means True. Say the cycle has L nodes. Once both pointers are inside it, fast gains one node per step. So the gap between them shrinks by one each step, and they meet within L steps.

A finite list either ends or loops, so these two cases cover everything.

6. Cost, edge cases, and failure

Cost. O(n) time, growing in step with the list, even when a straight part leads into the cycle. O(1) extra space.

Edge cases. A node that points to itself is a cycle. Compare only after moving: both pointers start at the head, so comparing first would call every list a cycle.

Where it fails. Comparing values instead of nodes gives wrong answers:

head = ListNode(7, ListNode(7, ListNode(7)))
assert head.next.val == head.next.next.val
assert head.next is not head.next.next
assert not solve(head)
one = ListNode(9)
one.next = one
assert solve(one)
Going further: random checks, related problems, and a reconstruction exercise

Check it against the baseline

import random
rng = random.Random(141)
for _ in range(1000):
    nodes = [ListNode(rng.randrange(3)) for _ in range(rng.randrange(40))]
    for a, b in zip(nodes, nodes[1:]):
        a.next = b
    if nodes and rng.randrange(2):
        nodes[-1].next = rng.choice(nodes)
    head = nodes[0] if nodes else None
    links = [node.next for node in nodes]
    assert solve(head) == baseline(head)
    assert all(node.next is link for node, link in zip(nodes, links))

Related problems

Problem What changes What follows
Linked List Cycle II Return the node where the cycle starts After they meet, move one pointer back to the head. Step both one node at a time; they meet at the cycle's start.
Course Schedule Each node can point to many others Looks similar, but two pointers on one path can't find every cycle in a graph. Use topological sort. See the lesson.

Reconstruct it

Explain the check before fast moves two nodes. Explain why gaining one node per step means fast catches slow, and why you compare nodes, not values.