Fast & Slow Pointers (Floyd's Tortoise and Hare)
You're given the head of a linked list that may loop back on itself. Find and return the exact node where that loop begins — or null if the list has no loop at all.
(A test harness may describe the loop internally with a 0-indexed pos marking where the tail's next pointer reconnects, using -1 for no loop, but that index is never handed to your function.)
You must leave the linked list unmodified.
Fast & Slow Pointers
slow (1x) vs fast (2x) (Velocity Differential)next = nums[i]) may contain a loop, or you need its middle in one pass with O(1) extra space.| 1 | from typing import Optional |
| 2 | Â |
| 3 | class ListNode: |
| 4 | def __init__(self, val: int = 0, next: Optional["ListNode"] = None): |
| 5 | self.val = val |
| 6 | self.next = next |
| 7 | Â |
| 8 | def find_cycle_entrance(head: Optional[ListNode]) -> Optional[ListNode]: |
| 9 | """Linked List Cycle II: Floyd's tortoise and hare, then find the cycle's first node. |
| 10 | Time: O(N) | Space: O(1) |
| 11 | """ |
| 12 | if not head or not head.next: |
| 13 | return None |
| 14 | Â |
| 15 | slow = fast = head # both pointers start together; fast will take exactly twice slow's steps |
| 16 | has_cycle = False |
| 17 | Â |
| 18 | # 1. Phase 1 - Detect Collision: |
| 19 | # fast gains exactly one step on slow per move, so inside a cycle it cannot jump over slow: they must meet. Testing fast and fast.next (not just fast) keeps fast.next.next from reading an attribute of None. |
| 20 | while fast and fast.next: |
| 21 | slow = slow.next # 1 step |
| 22 | fast = fast.next.next # 2 steps |
| 23 | if slow == fast: |
| 24 | has_cycle = True |
| 25 | break |
| 26 | Â |
| 27 | if not has_cycle: |
| 28 | return None # fast fell off the end: no cycle |
| 29 | Â |
| 30 | # 2. Phase 2 - Locate Cycle Origin: |
| 31 | # head and the meeting point are the same number of steps from the cycle's first node, up to whole laps, so walking both one step at a time lands them on it together. |
| 32 | ptr1 = head |
| 33 | ptr2 = slow |
| 34 | while ptr1 != ptr2: |
| 35 | ptr1 = ptr1.next |
| 36 | ptr2 = ptr2.next |
| 37 | Â |
| 38 | return ptr1 |
fast has always taken exactly twice as many steps as slow, so inside a cycle the gap between them shrinks by one on every move.Inside a cycle, fast gains exactly one step on slow per move, so it can't jump over it: if there is a cycle they meet, and if fast runs off the end there is none (and slow is then at the middle).
Walking from the head and walking from the meeting point reach the cycle's first node after the same number of steps (the second walk may add whole laps), so ptr1 from head and ptr2 from the meeting point, one step at a time, both land on it.
The trap is the loop test: write while fast and fast.next, because fast.next.next crashes when fast.next is None; testing only fast crashes on every list without a cycle that has an odd number of nodes.
See it run
Fast & Slow Pointers, step by step
slow node 0fast node 0
Each circle is a node and shows its value. An arrow points to the next node. Node 0 is the head, and a node number is its position in the list.
slowthe pointer that takes 1 stepfastthe pointer that takes 2 stepsptr1the walker that starts at the headptr2the walker that starts at the meeting nodetogethertwo pointers on the same node
Start: slow and fast both stand on the head. Each loop, slow takes 1 step and fast takes 2.
Gotcha 1 of 4
- The trap
Gotcha 1: Writing
while fastinstead ofwhile fast and fast.nextMistakewhile fast:Fixwhile fast and fast.next:What happens: Each step moves
fasttwo nodes withfast.next.next. Whenfastis the last node,fast.nextisNone. Thenfast.next.nextreads an attribute ofNone, and Python raises an AttributeError. This happens on every list without a cycle that has an odd number of nodes.Example: head = [1, 2, 3], pos = -1 (no cycle)Mistake: raises AttributeErrorFix: returns None
- The array is declared circular, so you already know where the loop is and must look past the end, as in Next Greater Element II (LC 503), instead of finding out whether a loop exists. See Circular Array
Theory
The question we build
What we know
The input is a linked list, possibly with a cycle.
fast moves two nodes for every one node slow moves, so inside a cycle the gap between them shrinks by exactly one node per move.
What the problem gives us
The input is head.
Return the node where the cycle begins, or None when there is no cycle.
The question we build
We ask: "does slow ever equal fast?"
If the list has no cycle, fast eventually reaches the end.
If the list has a cycle, the gap between slow and fast shrinks by one every move, so it must reach zero before fast can leave the cycle.
Narrowing down to the template
Why can fast never jump over slow inside a cycle?
The gap between them is a whole number of nodes, and it shrinks by exactly one node on every move, so it passes through every smaller value, including zero, before it could become negative.
So fast cannot skip past slow without first landing on it: if a cycle exists, the two pointers meet inside it.
That is exactly Fast & Slow Pointers: walk slow one step and fast two steps per move, and a meeting proves a cycle, a fast that falls off the end proves there is none.
Step 1: Track every node visited
Walk the list once, remembering every node seen. Correct: the first node visited twice is where the cycle starts. States the contract in one listing: find the first repeated node, or confirm there is none.
| 1 | def find_cycle_entrance_brute(head): |
| 2 | seen = set() |
| 3 | node = head |
| 4 | while node: |
| 5 | if id(node) in seen: |
| 6 | return node # The first node visited twice is the cycle's first node |
| 7 | seen.add(id(node)) |
| 8 | node = node.next |
| 9 | return None |
Cost: O(N) time, O(N) extra space for the visited set.
Step 2: Detect the cycle without extra memory
fast gains exactly one step on slow per move, so it can never jump over slow inside a cycle: they must meet.
So two pointers, no set, decide whether a cycle exists in O(1) space: walk slow by one node and fast by two, and stop on a meeting or on fast falling off the end.
The loop test has to be while fast and fast.next, because fast.next.next fails the moment fast.next is None, which happens on every cycle-free list with an odd number of nodes.
| 1 | def has_cycle(head): |
| 2 | if not head or not head.next: |
| 3 | return False |
| 4 | slow = fast = head |
| 5 | while fast and fast.next: # fast.next.next needs fast.next to exist |
| 6 | slow = slow.next # 1 step |
| 7 | fast = fast.next.next # 2 steps: fast gains exactly 1 step on slow per move |
| 8 | if slow == fast: |
| 9 | return True # fast could not jump over slow: they meet inside the cycle |
| 10 | return False # fast fell off the end: no cycle |
Cost: O(N) time, O(1) extra space, but it only answers yes or no.
Step 3: Locate where the cycle starts
Detecting a cycle is not the same as finding where it starts.
From the meeting point, fast has gone around the cycle exactly as many extra times as slow has made trips, and both have covered the same distance from head to the meeting point, up to whole laps of the cycle, so a second pair of one-step walkers, one from head and one from the meeting point, reaches the cycle's first node together.
| 1 | New line: from typing import Optional |
| 2 | |
| 3 | New line: class ListNode: |
| 4 | New line: def __init__(self, val: int = 0, next: Optional["ListNode"] = None): |
| 5 | New line: self.val = val |
| 6 | New line: self.next = next |
| 7 | |
| 8 | New line: def find_cycle_entrance(head: Optional[ListNode]) -> Optional[ListNode]: |
| 9 | New line: """Linked List Cycle II: Floyd's tortoise and hare, then find the cycle's first node. |
| 10 | New line: Time: O(N) | Space: O(1) |
| 11 | New line: """ |
| 12 | if not head or not head.next: |
| 13 | New line: return None |
| 14 | |
| 15 | slow = fast = head # both pointers start together; fast will take exactly twice slow's steps |
| 16 | New line: has_cycle = False |
| 17 | |
| 18 | New line: # 1. Phase 1 - Detect Collision: |
| 19 | New line: # fast gains exactly one step on slow per move, so inside a cycle it cannot jump over slow: they must meet. Testing fast and fast.next (not just fast) keeps fast.next.next from reading an attribute of None. |
| 20 | while fast and fast.next: |
| 21 | slow = slow.next # 1 step |
| 22 | fast = fast.next.next # 2 steps |
| 23 | if slow == fast: |
| 24 | New line: has_cycle = True |
| 25 | New line: break |
| 26 | |
| 27 | New line: if not has_cycle: |
| 28 | New line: return None # fast fell off the end: no cycle |
| 29 | |
| 30 | New line: # 2. Phase 2 - Locate Cycle Origin: |
| 31 | New line: # head and the meeting point are the same number of steps from the cycle's first node, up to whole laps, so walking both one step at a time lands them on it together. |
| 32 | New line: ptr1 = head |
| 33 | New line: ptr2 = slow |
| 34 | New line: while ptr1 != ptr2: |
| 35 | New line: ptr1 = ptr1.next |
| 36 | New line: ptr2 = ptr2.next |
| 37 | |
| 38 | New line: return ptr1 |
Cost: O(N) time, O(1) extra space, and this time it returns the exact node.
| Before | After | What changed |
|---|---|---|
return True (Phase 1 only) | has_cycle = True; break, then Phase 2's ptr1/ptr2 walk from head and the meeting point | A yes/no answer becomes the exact starting node, by walking two more one-step pointers until they meet. |
Practice
Fast & Slow Pointers (Floyd's Tortoise and Hare)
You're given the head of a linked list that may loop back on itself. Find and return the exact node where that loop begins — or null if the list has no loop at all.
(A test harness may describe the loop internally with a 0-indexed pos marking where the tail's next pointer reconnects, using -1 for no loop, but that index is never handed to your function.)
You must leave the linked list unmodified.
Fast & Slow Pointers
slow (1x) vs fast (2x) (Velocity Differential)next = nums[i]) may contain a loop, or you need its middle in one pass with O(1) extra space.| 1 | from typing import Optional |
| 2 | Â |
| 3 | class ListNode: |
| 4 | def __init__(self, val: int = 0, next: Optional["ListNode"] = None): |
| 5 | self.val = val |
| 6 | self.next = next |
| 7 | Â |
| 8 | def find_cycle_entrance(head: Optional[ListNode]) -> Optional[ListNode]: |
| 9 | """Linked List Cycle II: Floyd's tortoise and hare, then find the cycle's first node. |
| 10 | Time: O(N) | Space: O(1) |
| 11 | """ |
| 12 | if not head or not head.next: |
| 13 | return None |
| 14 | Â |
| 15 | slow = fast = head # both pointers start together; fast will take exactly twice slow's steps |
| 16 | has_cycle = False |
| 17 | Â |
| 18 | # 1. Phase 1 - Detect Collision: |
| 19 | # fast gains exactly one step on slow per move, so inside a cycle it cannot jump over slow: they must meet. Testing fast and fast.next (not just fast) keeps fast.next.next from reading an attribute of None. |
| 20 | while fast and fast.next: |
| 21 | slow = slow.next # 1 step |
| 22 | fast = fast.next.next # 2 steps |
| 23 | if slow == fast: |
| 24 | has_cycle = True |
| 25 | break |
| 26 | Â |
| 27 | if not has_cycle: |
| 28 | return None # fast fell off the end: no cycle |
| 29 | Â |
| 30 | # 2. Phase 2 - Locate Cycle Origin: |
| 31 | # head and the meeting point are the same number of steps from the cycle's first node, up to whole laps, so walking both one step at a time lands them on it together. |
| 32 | ptr1 = head |
| 33 | ptr2 = slow |
| 34 | while ptr1 != ptr2: |
| 35 | ptr1 = ptr1.next |
| 36 | ptr2 = ptr2.next |
| 37 | Â |
| 38 | return ptr1 |
fast has always taken exactly twice as many steps as slow, so inside a cycle the gap between them shrinks by one on every move.Inside a cycle, fast gains exactly one step on slow per move, so it can't jump over it: if there is a cycle they meet, and if fast runs off the end there is none (and slow is then at the middle).
Walking from the head and walking from the meeting point reach the cycle's first node after the same number of steps (the second walk may add whole laps), so ptr1 from head and ptr2 from the meeting point, one step at a time, both land on it.
The trap is the loop test: write while fast and fast.next, because fast.next.next crashes when fast.next is None; testing only fast crashes on every list without a cycle that has an odd number of nodes.
See it run
Fast & Slow Pointers, step by step
slow node 0fast node 0
Each circle is a node and shows its value. An arrow points to the next node. Node 0 is the head, and a node number is its position in the list.
slowthe pointer that takes 1 stepfastthe pointer that takes 2 stepsptr1the walker that starts at the headptr2the walker that starts at the meeting nodetogethertwo pointers on the same node
Start: slow and fast both stand on the head. Each loop, slow takes 1 step and fast takes 2.
Gotcha 1 of 4
- The trap
Gotcha 1: Writing
while fastinstead ofwhile fast and fast.nextMistakewhile fast:Fixwhile fast and fast.next:What happens: Each step moves
fasttwo nodes withfast.next.next. Whenfastis the last node,fast.nextisNone. Thenfast.next.nextreads an attribute ofNone, and Python raises an AttributeError. This happens on every list without a cycle that has an odd number of nodes.Example: head = [1, 2, 3], pos = -1 (no cycle)Mistake: raises AttributeErrorFix: returns None
- The array is declared circular, so you already know where the loop is and must look past the end, as in Next Greater Element II (LC 503), instead of finding out whether a loop exists. See Circular Array
Theory
The question we build
What we know
The input is a linked list, possibly with a cycle.
fast moves two nodes for every one node slow moves, so inside a cycle the gap between them shrinks by exactly one node per move.
What the problem gives us
The input is head.
Return the node where the cycle begins, or None when there is no cycle.
The question we build
We ask: "does slow ever equal fast?"
If the list has no cycle, fast eventually reaches the end.
If the list has a cycle, the gap between slow and fast shrinks by one every move, so it must reach zero before fast can leave the cycle.
Narrowing down to the template
Why can fast never jump over slow inside a cycle?
The gap between them is a whole number of nodes, and it shrinks by exactly one node on every move, so it passes through every smaller value, including zero, before it could become negative.
So fast cannot skip past slow without first landing on it: if a cycle exists, the two pointers meet inside it.
That is exactly Fast & Slow Pointers: walk slow one step and fast two steps per move, and a meeting proves a cycle, a fast that falls off the end proves there is none.
Step 1: Track every node visited
Walk the list once, remembering every node seen. Correct: the first node visited twice is where the cycle starts. States the contract in one listing: find the first repeated node, or confirm there is none.
| 1 | def find_cycle_entrance_brute(head): |
| 2 | seen = set() |
| 3 | node = head |
| 4 | while node: |
| 5 | if id(node) in seen: |
| 6 | return node # The first node visited twice is the cycle's first node |
| 7 | seen.add(id(node)) |
| 8 | node = node.next |
| 9 | return None |
Cost: O(N) time, O(N) extra space for the visited set.
Step 2: Detect the cycle without extra memory
fast gains exactly one step on slow per move, so it can never jump over slow inside a cycle: they must meet.
So two pointers, no set, decide whether a cycle exists in O(1) space: walk slow by one node and fast by two, and stop on a meeting or on fast falling off the end.
The loop test has to be while fast and fast.next, because fast.next.next fails the moment fast.next is None, which happens on every cycle-free list with an odd number of nodes.
| 1 | def has_cycle(head): |
| 2 | if not head or not head.next: |
| 3 | return False |
| 4 | slow = fast = head |
| 5 | while fast and fast.next: # fast.next.next needs fast.next to exist |
| 6 | slow = slow.next # 1 step |
| 7 | fast = fast.next.next # 2 steps: fast gains exactly 1 step on slow per move |
| 8 | if slow == fast: |
| 9 | return True # fast could not jump over slow: they meet inside the cycle |
| 10 | return False # fast fell off the end: no cycle |
Cost: O(N) time, O(1) extra space, but it only answers yes or no.
Step 3: Locate where the cycle starts
Detecting a cycle is not the same as finding where it starts.
From the meeting point, fast has gone around the cycle exactly as many extra times as slow has made trips, and both have covered the same distance from head to the meeting point, up to whole laps of the cycle, so a second pair of one-step walkers, one from head and one from the meeting point, reaches the cycle's first node together.
| 1 | New line: from typing import Optional |
| 2 | |
| 3 | New line: class ListNode: |
| 4 | New line: def __init__(self, val: int = 0, next: Optional["ListNode"] = None): |
| 5 | New line: self.val = val |
| 6 | New line: self.next = next |
| 7 | |
| 8 | New line: def find_cycle_entrance(head: Optional[ListNode]) -> Optional[ListNode]: |
| 9 | New line: """Linked List Cycle II: Floyd's tortoise and hare, then find the cycle's first node. |
| 10 | New line: Time: O(N) | Space: O(1) |
| 11 | New line: """ |
| 12 | if not head or not head.next: |
| 13 | New line: return None |
| 14 | |
| 15 | slow = fast = head # both pointers start together; fast will take exactly twice slow's steps |
| 16 | New line: has_cycle = False |
| 17 | |
| 18 | New line: # 1. Phase 1 - Detect Collision: |
| 19 | New line: # fast gains exactly one step on slow per move, so inside a cycle it cannot jump over slow: they must meet. Testing fast and fast.next (not just fast) keeps fast.next.next from reading an attribute of None. |
| 20 | while fast and fast.next: |
| 21 | slow = slow.next # 1 step |
| 22 | fast = fast.next.next # 2 steps |
| 23 | if slow == fast: |
| 24 | New line: has_cycle = True |
| 25 | New line: break |
| 26 | |
| 27 | New line: if not has_cycle: |
| 28 | New line: return None # fast fell off the end: no cycle |
| 29 | |
| 30 | New line: # 2. Phase 2 - Locate Cycle Origin: |
| 31 | New line: # head and the meeting point are the same number of steps from the cycle's first node, up to whole laps, so walking both one step at a time lands them on it together. |
| 32 | New line: ptr1 = head |
| 33 | New line: ptr2 = slow |
| 34 | New line: while ptr1 != ptr2: |
| 35 | New line: ptr1 = ptr1.next |
| 36 | New line: ptr2 = ptr2.next |
| 37 | |
| 38 | New line: return ptr1 |
Cost: O(N) time, O(1) extra space, and this time it returns the exact node.
| Before | After | What changed |
|---|---|---|
return True (Phase 1 only) | has_cycle = True; break, then Phase 2's ptr1/ptr2 walk from head and the meeting point | A yes/no answer becomes the exact starting node, by walking two more one-step pointers until they meet. |