Part I · Arrays, Strings and Pointers Pattern 3 4 problems

Fast and Slow Pointers

Two walkers on the same track at different speeds. If the track loops, the fast one laps the slow one, and that collision is a cycle detector using no memory at all.

Also called Floyd’s tortoise and hare. It answers a question that looks like it needs a hash set, have I been here before, using two pointers and nothing else. The interviewer is usually testing whether you can get from the O(n)-space answer to the O(1)-space answer.

Contents

  1. When to use
  2. Core idea
  3. Why they must meet, and where
  4. The templates
  5. Common mistakes
  6. Linked List Cycle
  7. Middle of the Linked List
  8. Happy Number
  9. Find the Duplicate Number
  10. Recap

When to use

The trigger. There is a successor function: from any state there is exactly one next state. A linked list node has one next. An index i maps to nums[i]. A number maps to the sum of the squares of its digits. Any such function traces a path that must eventually repeat, and the question is about that repetition or about a position along the path.
SignalWhat it looks like
Cycle language“does the list have a cycle”, “where does the loop start”, “does it repeat forever”.
Positional language“the middle node”, “the k-th from the end”, “the second half”. One pass, no length count.
Space constraint“O(1) extra space” on a problem where a visited set is the obvious answer.
Read-only array“you must not modify the array” turns an array problem into an implicit linked list.

The mental move that unlocks the array problems

An array can be a linked list. If every value is a valid index, then i → nums[i] is a next pointer and the array is a linked structure. Once you see that, “find the duplicate” becomes “find the start of the cycle”. Recognising this disguise is the single highest-value thing on this page.

Core idea

Move slow one step and fast two steps per iteration. If the path is finite and has no cycle, fast falls off the end and you are done. If there is a cycle, both pointers end up inside it, and since fast gains exactly one position on slow every iteration, the gap shrinks by one each time and must hit zero. They cannot jump past each other.
h S tail, length μ = 3 cycle start M M = meeting point cycle length λ = 5 slow walks μ + k steps fast walks 2(μ + k) steps so μ + k is a multiple of λ
Figure 3.1 — The rho shape. Every successor-function path looks like this: a tail of length μ feeding a cycle of length λ. Either part may be empty.

Why they must meet, and where

Part 1: they meet

Once both pointers are inside the cycle, let d be the number of positions fast is behind slow, measured going forward around the cycle. Each iteration fast advances 2 and slow advances 1, so d decreases by exactly 1, modulo the cycle length. It therefore reaches 0 within λ iterations. Because the change is exactly one per step, they cannot step over each other, which is why the speeds must be 1 and 2 and not, say, 1 and 3.

Part 2: the meeting point locates the cycle start

Let μ be the tail length and λ the cycle length. When they meet, slow has taken μ + k steps for some k, and fast has taken 2(μ + k). Their difference, μ + k, must be a whole number of laps, so μ + k ≡ 0 (mod λ).

Now restart one pointer at the head and leave the other at the meeting point, and advance both one step at a time. After μ steps the first is at the cycle start. The second has taken μ + k + μ steps in total, and since μ + k is a multiple of λ, that lands it exactly μ steps into the cycle too. They meet at the cycle start.

This second phase is what turns cycle detection into cycle location, and it is the whole trick behind Find the Duplicate Number.

The templates

Node definition used on this page
from dataclasses import dataclass


@dataclass
class ListNode:
    """A singly linked list node."""

    val: int = 0
    next: "ListNode | None" = None
Template A — detect a cycle
def detect(head: ListNode | None) -> ListNode | None:
    """Return the meeting node if a cycle exists, else None."""
    slow = fast = head

    while fast is not None and fast.next is not None:
        slow = slow.next          # 1 step
        fast = fast.next.next     # 2 steps
        if slow is fast:
            return slow

    return None                   # fast ran off the end: no cycle

The guard fast is not None and fast.next is not None covers both odd and even list lengths. Test fast before dereferencing fast.next.

Template B — locate the cycle start
def cycle_start(head: ListNode | None) -> ListNode | None:
    """Return the first node of the cycle, or None if there is none."""
    meeting = detect(head)
    if meeting is None:
        return None

    walker = head
    while walker is not meeting:      # both move one step at a time
        walker = walker.next
        meeting = meeting.next

    return walker

Common mistakes

The problems

1. Linked List Cycle Easy

Problem

Given the head of a linked list, return True if the list has a cycle in it. Solve it with O(1) extra memory.

Approach

Solution

def has_cycle(head: ListNode | None) -> bool:
    """Return True if the linked list contains a cycle.

    Args:
        head: First node, or None for an empty list.

    Returns:
        True if following .next repeats a node forever.
    """
    slow = fast = head

    while fast is not None and fast.next is not None:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:          # identity, not equality
            return True

    return False

Walkthrough

List 3 → 2 → 0 → -4 with -4 pointing back to 2:

iterationslow atfast at
start33
120
202
3-4-4

They meet at -4, so the answer is True.

TimeO(n)SpaceO(1)

Time bound: slow takes at most μ steps to enter the cycle, then at most λ more before fast catches it, so at most μ + λ ≤ n iterations.

Edge cases to raise

Say this out loud: “The gap between them shrinks by exactly one per step, so the fast pointer cannot skip over the slow one. That is why the speeds have to be 1 and 2.”

2. Middle of the Linked List Easy

Problem

Return the middle node of a singly linked list. If there are two middles, return the second one.

Approach

Solution

def middle_node(head: ListNode | None) -> ListNode | None:
    """Return the middle node; the second middle if the length is even.

    Args:
        head: First node, or None.

    Returns:
        The middle node, or None for an empty list.
    """
    slow = fast = head

    while fast is not None and fast.next is not None:
        slow = slow.next
        fast = fast.next.next

    return slow

The two variants, and how to remember them

Loop guardEven-length resultUse it when
while fast and fast.nextsecond middleThe problem says “return the second middle”.
while fast.next and fast.next.nextfirst middleYou need the node before the split, for example to cut the list in two for merge sort.

The second variant needs a non-empty list, so guard head first. For a list of length 4 the first gives node 3 and the second gives node 2.

Walkthrough

List 1 → 2 → 3 → 4 → 5. After two iterations slow is at 3 and fast is at 5, whose next is None, so the loop stops. Answer 3. For 1 → 2 → 3 → 4, slow ends at 3, the second middle.

TimeO(n)SpaceO(1)

Where this shows up as a subroutine

Say this out loud: “One pass, and it works even if I can only read the list once, which the count-then-walk version cannot.”

3. Happy Number Easy

Problem

Start with a positive integer. Replace it by the sum of the squares of its digits, and repeat. The number is happy if this eventually reaches 1. Otherwise it loops forever without reaching 1. Return whether n is happy.

Approach

Solution

def is_happy(n: int) -> bool:
    """Return True if repeatedly summing squared digits reaches 1.

    Args:
        n: A positive integer.

    Returns:
        True if n is a happy number.

    Example:
        >>> is_happy(19)
        True
    """

    def next_value(value: int) -> int:
        """Sum of the squares of the decimal digits of value."""
        total = 0
        while value > 0:
            value, digit = divmod(value, 10)
            total += digit * digit
        return total

    slow, fast = n, next_value(n)

    # Stop on success (fast reaches the 1 fixed point) or on a collision.
    while fast != 1 and slow != fast:
        slow = next_value(slow)
        fast = next_value(next_value(fast))

    return fast == 1

Walkthrough

n = 19. The sequence is 19, 82, 68, 100, 1.

slowfastnote
1982initial
82100fast took two steps
681loop exits, happy

For n = 2 the sequence enters the cycle 4, 16, 37, 58, 89, 145, 42, 20, 4, the pointers collide inside it, and the answer is False.

TimeO(log n)SpaceO(1)

The first next_value call is O(log n) in the number of digits and collapses n to at most 243 immediately, after which the work is bounded by a constant.

Edge cases to raise

Say this out loud: “There is no list, but there is a deterministic successor over a finite state space, so the sequence has to end in a cycle. That is the only precondition Floyd needs.”

4. Find the Duplicate Number Medium

Problem

An array nums of n + 1 integers holds values in the range 1..n. Exactly one value is repeated, possibly many times. Find it, without modifying the array and using O(1) extra space.

Why the constraints matter

ApproachBlocked by
Sort, then scan neighbours“do not modify the array”
Hash set of seen values“O(1) extra space”
Cyclic sort, swapping values home“do not modify the array”
Sum formulaThe value may repeat more than twice
Floyd on the index graphNothing. This is the intended answer.
Binary search on the value range is the other accepted answer: for each candidate m, count how many values are ≤ m; if that count exceeds m, the duplicate is at or below m. That is O(n log n) time and O(1) space, and it is easier to derive under pressure. Have it as your backup.

Approach

Solution

def find_duplicate(nums: list[int]) -> int:
    """Find the one repeated value in nums, read-only and in O(1) space.

    Treats nums as a functional graph i -> nums[i]. The repeated value is
    the entrance of the cycle that walk falls into.

    Args:
        nums: n + 1 integers, each in 1..n, with exactly one value repeated.

    Returns:
        The repeated value.

    Example:
        >>> find_duplicate([1, 3, 4, 2, 2])
        2
    """
    # Phase 1: find a meeting point inside the cycle.
    slow = fast = nums[0]
    while True:
        slow = nums[slow]
        fast = nums[nums[fast]]
        if slow == fast:
            break

    # Phase 2: walk one pointer from the start; they meet at the entrance.
    finder = nums[0]
    while finder != slow:
        finder = nums[finder]
        slow = nums[slow]

    return finder

Walkthrough

nums = [1, 3, 4, 2, 2]. The walk from index 0 is 0 → 1 → 3 → 2 → 4 → 2 → 4 → …, a tail of 0, 1, 3 feeding the cycle 2, 4. Two arrows point at node 2, from index 3 and from index 4, and those are the two positions holding the value 2.

Phaseslowfast / finder
1 start11
1 step 132
1 step 222
2 start21
2 step 143
2 step 222

They meet at 2, the answer.

TimeO(n)SpaceO(1)Writesnone

Edge cases to raise

The do-while shape matters. Phase 1 must move before comparing, which is why it is while True with a break rather than a normal while slow != fast. The pointers start equal; a top-tested loop would exit immediately.
Say this out loud: “Values are in 1 to n and indices in 0 to n, so nums is a next pointer and the array is a linked list. Two indices holding the same value are two arrows into one node, which is a cycle entrance, so the duplicate is exactly the cycle start.”

Recap

The six things to carry forward

Where this goes next

Pattern 4, Merge Intervals, leaves pointers behind. It is the first pattern where the whole insight is what to sort by, and where a greedy sweep replaces a search.


2 — Two Pointers 4 — Merge Intervals