DSA sheet · Linked List · Fast & slow pointer pattern

Linked List Cycle

This is the second problem of the fast and slow pointer pattern (the first one was the middle of the list). The question is: does the list loop back on itself somewhere? The teacher first solves it the natural way, by remembering every node we have seen in a hash set. That is already O(n) time, but it costs O(n) extra space. Then she removes the set completely with two runners going at different speeds on the same track. This "two runners on a circle" picture is the base of the next problem too (finding where the cycle starts), so learn it well here.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

What is a linked list?

A linked list is a chain of small boxes called nodes. Each node holds two things: a value (val) and a pointer to the next node (next). The last node's next is None, which means "the chain ends here".

given by LeetCode, don't write this in the solution
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val       # the number stored in this box
        self.next = next     # the next box, or None at the end
[1] → [2] → [3] → [4] → None
 ↑
head

What is a cycle?

Normally the last node points to None. In a list with a cycle, some node's next points back to an earlier node. Then there is no end: if you keep walking you go round and round forever.

[1] → [2] → [3] → [4] → [5] → [6]
       ↑                         │
       └─────────────────────────┘     (6.next is node 2, not None)

The fast and slow pointer tool

Two pointers start at the head. slow moves 1 node per step (slow = slow.next). fast moves 2 nodes per step (fast = fast.next.next). Because fast goes at double speed, at any moment slow has covered half the distance fast has covered. That is why, in the previous problem, slow ends at the middle when fast reaches the end.

Part A · Brute force: remember visited nodes in a hash set

LeetCode 141

1The question in simple words

You get the head of a linked list. Return True if the list has a cycle (some node can be reached again by following next), and False if the list ends at None.

cycle → True
[1] → [2] → [3] → [4] → [5] → [6]
       ↑                         │
       └─────────────────────────┘
no cycle → False
[1] → [2] → [3] → [4] → None

On LeetCode you also see a number pos in the examples. It only tells the judge where the last node links back to. Your function never receives it; you only get head.

2What the constraints tell us

3Intuition: tick off every node you visit

Imagine walking through the list and putting a tick mark on every node you pass: 1 ✓, 2 ✓, 3 ✓, 4 ✓, 5 ✓, 6 ✓… and then you arrive at 2 again. It already has a tick! The only way to reach a ticked node again is to have gone round a loop. So "I've been here before" means there is a cycle.

If there is no loop, you never meet a ticked node. You simply fall off the end into None.

4Building the logic from examples

Where do we keep the ticks? A hash set

We need to ask "have I seen this before?" quickly. A Python set answers that in O(1) on average. So we keep a set called seen.

Store the node, not the value

The teacher asks: do we put the values in the set, or the nodes? Look at this list:

[1] → [2] → [2] → None        (two different nodes that both hold 2)
Doubt 1: how can Python put a node in a set?
→ A plain Python object is hashed by its identity (where it lives in memory), not by its value. So node in seen asks "is this exact box in the set?", which is exactly the question we want.

The check, the add, the move: in this order

  1. Is curr already in seen? → yes means we came back → return True.
  2. Otherwise add curr to the set, so it can be recognised next time.
  3. Move on: curr = curr.next.
Doubt 2: why must the add happen before the move?
→ After curr = curr.next, the old node is no longer in curr. If we never added it, it would never be in the set, and we could never recognise it when the loop brings us back.

When does the loop stop? At None

If there is a cycle, the check in step 1 eventually fires and returns. But if there is no cycle, that check never fires. Without another way out we'd be stuck. The teacher's reasoning: a list without a cycle is a straight line, so it must end in None. When curr reaches None, we know there's no cycle.

Loop conditionwhile curr is not None: … and after the loop, return False.
Doubt 3: what about an empty list?
→ curr = head = None, the loop never runs, and we return False. No special check needed.

5Approach steps

  1. Make an empty set seen and a pointer curr = head.
  2. While curr is not None: if curr is in seen → return True.
  3. Otherwise add curr to seen and move curr = curr.next.
  4. If the loop ends (we hit None) → return False.

6Code (Python)

Linked List Cycle, brute force with a hash set
class Solution:
    def hasCycle(self, head):
        seen = set()               # nodes we have already visited
        curr = head                # alias, so head stays untouched
        while curr is not None:
            if curr in seen:       # been here before -> loop
                return True
            seen.add(curr)         # tick this node
            curr = curr.next       # walk one step
        return False               # reached None -> straight list

7Code line by line

linewhat it means
seen = set()The "tick marks". It holds node objects. This is the O(n) extra space.
curr = headA walking pointer. We never move head itself.
while curr is not None:Keep walking until we fall off the end. This is the only way out when there is no cycle.
if curr in seen: return TrueWe are standing on a node we already ticked. Only a loop can bring us back, so there is a cycle.
seen.add(curr)Tick the current node before leaving it.
curr = curr.nextStep to the next node.
return FalseWe reached None. The list is a straight line, so no cycle.

8Dry run

List: 1 → 2 → 3 → 4 → 5 → 6 → back to 2.

stepcurrin seen?seen after this stepanswer so far
11no{1}keep going
22no{1, 2}keep going
33no{1, 2, 3}keep going
44no{1, 2, 3, 4}keep going
55no{1, 2, 3, 4, 5}keep going
66no{1, 2, 3, 4, 5, 6}6.next is 2, so curr → 2
72yes(unchanged)return True
step 7:
[1] → [2] → [3] → [4] → [5] → [6]
       ↑                         │
       └─────────────────────────┘
       curr   (node 2 is already ticked → True)

Without a cycle, e.g. 1 → 2 → 3 → None: we tick 1, 2, 3, then curr becomes None, the loop ends, and we return False.

9Complexity & remember

The time is already as good as it gets for linked lists. The only thing to improve is the extra set. The interviewer will ask: "can you do it without extra space?"

Remember brute forceWalk with curr. Seen before → True. Otherwise add, move. Hit None → False. Store nodes, never values.

Part B · Optimal: fast and slow pointers (Floyd's cycle check)

1The question (same as Part A)

Same input and output. New goal: O(1) extra space, so no set, just a couple of pointers.

2What the constraints tell us

3Intuition: two runners on a track

The teacher's picture: two people start a race at the same point. One runs at double the speed of the other.

So: if slow and fast ever stand on the same node → cycle → True. If fast reaches the end → no cycle → False.

4Building the logic from examples

Start both at head

slow = head, fast = head. Same starting point, like the race.

The loop condition: same as the middle-of-list problem

Fast is the one that runs ahead, so fast is the one that would hit the end. As we saw in Part 0, for an even-length list fast becomes None, and for an odd-length list fast.next becomes None. Both must be checked:

Loop conditionwhile fast is not None and fast.next is not None:
If this ever fails, the list is a straight line → after the loop, return False.
Doubt 1: why don't we check slow for None?
→ Slow is always behind fast (or on the same node). Any node slow will step on, fast has already passed. If the path ahead were None, fast would have found it first.
Doubt 2: why must fast is not None come first in the and?
→ If fast is None, reading fast.next would crash. Python checks the left side first and stops if it's False, so the second check only runs when fast is a real node.

Inside the loop: move, then compare

Doubt 3: why compare after moving and not before?
→ At the very start both pointers are on head, so they are "equal" before anyone has moved. Comparing first would say "cycle!" for every list. We only care about meeting again, after they have started running.
Doubt 4: can fast jump over slow inside the cycle and miss it forever?
→ No. Once both are inside the loop, look at the gap from fast up to slow (counted forward along the loop). Every step, slow moves 1 forward (the gap grows by 1) and fast moves 2 forward (the gap shrinks by 2). Net change: the gap gets 1 smaller each step. A gap that drops by exactly 1 each time must hit 0. It can't skip from 1 to −1. So they land on the same node. (This is a small extra on top of the teacher's race picture.)
Doubt 5: do we compare values or nodes?
→ Nodes, for the same reason as in Part A. Two different nodes can hold the same number. In Python, slow is fast checks "same box". (slow == fast also works for ListNode, since it has no custom equality, but is says exactly what we mean.)

5Approach steps

  1. Set slow = head and fast = head.
  2. While fast and fast.next both exist: move slow 1 step and fast 2 steps.
  3. If slow and fast are now the same node → return True.
  4. If the loop ends → fast reached the end → return False.

6Code (Python)

Linked List Cycle, fast and slow pointers
class Solution:
    def hasCycle(self, head):
        slow = head
        fast = head
        while fast is not None and fast.next is not None:
            slow = slow.next          # 1 jump
            fast = fast.next.next     # 2 jumps
            if slow is fast:          # the runners met -> loop
                return True
        return False                  # fast fell off the end

7Code line by line

linewhat it means
slow = head fast = headBoth runners start at the same place.
while fast is not None and fast.next is not None:Fast can still take two steps. If not, the list ended, so there's no cycle. Also covers the empty list.
slow = slow.nextSlow runner: one node forward.
fast = fast.next.nextFast runner: two nodes forward. Safe, because the loop condition checked fast.next exists.
if slow is fast: return TrueBoth on the same node after moving. Only a loop allows the faster runner to come back to the slower one.
return FalseFast reached None, so the list has an end.

8Dry run

The teacher's list: 1 → 2 → 3 → 4 → 5 → 6 → back to 2. The cycle is 2 → 3 → 4 → 5 → 6 → 2 (5 nodes).

stepslowfasthow fast movedsame node?
start11—(not checked)
1231 → 2 → 3no
2353 → 4 → 5no
3425 → 6 → 2 (wrapped round)no
4542 → 3 → 4no
5664 → 5 → 6yes → return True
after step 3 (fast has lapped round and is now behind slow):
[1] → [2] → [3] → [4] → [5] → [6]
       ↑            ↑            │
      fast         slow          │
       └─────────────────────────┘

after step 5 (they meet):
[1] → [2] → [3] → [4] → [5] → [6]
       ↑                         │
       └─────────────────────────┘
                              ↑
                          slow, fast  (both on node 6)

Notice at step 3 fast is "behind" slow on the loop (fast on 2, slow on 4; the gap from fast forward to slow is 2). Step 4 → gap 1. Step 5 → gap 0. That's Doubt 4 in action. The meeting node is 6. It is not where the cycle starts (that's 2). Finding the start is the next problem.

A no-cycle run: 1 → 2 → 3 → 4 → 5 → None

stepslowfastloop check next
start11fast and fast.next exist → go
123go
235fast.next is None → stop → return False

Edge cases

9Complexity & remember

The teacher's tip: whenever a question is about a middle point or a meeting point, and two things moving at different speeds could find it, think of fast and slow pointers.

Remember Floyd's checkslow = fast = head · while fast and fast.next · slow 1 step, fast 2 steps · then compare: same node → True · loop ends → False.

Part C · Revision page

Hash set (brute force)Fast & slow (optimal)
ideatick every node; a ticked node seen again = looptwo runners at speed 1 and 2; they meet only on a loop
what we storenode objects (never values)nothing, just 2 pointers
loop conditionwhile currwhile fast and fast.next
cycle found whencurr in seenslow is fast (after moving)
no cycle whencurr reaches Nonefast or fast.next reaches None
time / spaceO(n) / O(n)O(n) / O(1)
If you remember only 5 lines 1. A cycle means some node's next points back to an earlier node, so there's no None at the end.
2. Brute force: put each node in a set; meeting one again means a cycle.
3. Optimal: slow moves 1, fast moves 2, both from head.
4. Loop while fast and fast.next; compare after moving.
5. Meet → True. Fast falls off → False. O(n) time, O(1) space.
Mistakes to avoid ✗ storing values in the set (two nodes with equal values look like a cycle)
✗ comparing slow and fast before the first move (always equal at head)
✗ checking only fast.next without fast (crash on even length)
✗ adding to the set after moving curr (you add the wrong node)
✗ moving head itself instead of a copy
test it yourself (paste under either solution above)
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def build(values, pos=-1):
    """Build a list; if pos >= 0, the last node links back to node at index pos."""
    nodes = [ListNode(v) for v in values]
    for a, b in zip(nodes, nodes[1:]):
        a.next = b
    if pos >= 0 and nodes:
        nodes[-1].next = nodes[pos]
    return nodes[0] if nodes else None

s = Solution()
print(s.hasCycle(build([1, 2, 3, 4, 5, 6], 1)))   # True
print(s.hasCycle(build([1, 2, 3, 4, 5])))          # False
print(s.hasCycle(build([1, 2, 2])))                # False (equal values, no loop)
print(s.hasCycle(build([7], 0)))                   # True  (points to itself)
print(s.hasCycle(None))                            # False

Based on this video: Linked List Cycle | Fast and Slow Pointer