Skip to Content
Course content

458: Whiteboard Practice: Fast and Slow Pointers

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a junior dev who was building a custom dependency graph for a build system. The goal was to ensure there were no circular dependencies—basically, making sure Package A didn't depend on Package B, which depended back on Package A. He had implemented a check using a massive hash set to keep track of every node he'd visited. It worked, but as the graph grew to thousands of nodes, the memory overhead started to lag the entire CI pipeline. When I asked him if he could do it without the extra memory, he looked at me like I was asking him to solve cold fusion. That's when we sat down at the whiteboard to talk about the "Tortoise and the Hare."

The Logic of Different Speeds

The fast and slow pointer technique, formally known as Floyd's Cycle-Finding Algorithm, is a bit of a brain-bender the first time you see it, but it's incredibly elegant. Instead of using a separate data structure to remember where you've been, you use two pointers moving through the sequence at different speeds. I like to think of it as a race track. If you have two runners and the track is a straight line, the fast runner just disappears into the distance. But if the track is a loop, the fast runner will eventually lap the slow runner.

In Python, we implement this by initializing two variables (usually called slow and fast) to the head of the list. In each iteration of a while loop, the slow pointer moves one step forward, and the fast pointer moves two. If the fast pointer ever hits a None value, you know there's no cycle. But if slow == fast at any point? You've found your loop. It's a purely mathematical certainty that the fast pointer will "catch" the slow one if a cycle exists, and the best part is that it happens in $O(1)$ space. No sets, no dictionaries, just two references.

def has_cycle(head):
    slow = head
    fast = head
    
    while fast and fast.next:
        slow = slow.next          # Move 1 step
        fast = fast.next.next     # Move 2 steps
        
        if slow == fast:
            return True
            
    return False

Finding the Midpoint in One Pass

While cycle detection is the classic "interview" use case, I actually use this pattern more often to find the middle of a linked list. Normally, you'd be tempted to iterate through the whole list to count the elements, divide by two, and then iterate again to reach the middle. That's two passes. It's not a disaster, but it's inefficient.

By using the same fast and slow pointer logic, you can find the middle in a single pass. Because the fast pointer moves exactly twice as fast as the slow one, by the time the fast pointer reaches the end of the list, the slow pointer is exactly halfway there. I've found this particularly useful when implementing merge sort on linked lists, where splitting the list in half is the first requirement of the divide-and-conquer step.

def find_middle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    # When fast reaches the end, slow is at the middle
    return slow

One quick tip: always be careful with your while condition. If you only check while fast:, your fast.next.next call will throw an AttributeError the moment you hit the tail of the list. Always check both fast and fast.next to keep your code from crashing on lists with an even number of elements.




📋 Practical Task

Exercise: The Circular Reference Detector

You are tasked with writing a utility for a memory management tool. You need to implement a function detect_loop(node) that determines if a singly linked list contains a cycle. To make this a real challenge, you must do this without using any additional data structures (no lists, sets, or dictionaries) to track visited nodes.

Below is a basic Node class for your implementation. Your goal is to return True if a loop exists and False otherwise.

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

def detect_loop(node):
    # Your code here
    pass

# Test Case 1: Linear List (No Loop)
node1 = Node(1)
node2 = Node(2)
node3 = Node(3)
node1.next = node2
node2.next = node3
print(f"Test 1: {detect_loop(node1)}") # Expected: False

# Test Case 2: Circular List (Loop)
node_a = Node("A")
node_b = Node("B")
node_c = Node("C")
node_a.next = node_b
node_b.next = node_c
node_c.next = node_a # Cycle created here
print(f"Test 2: {detect_loop(node_a)}") # Expected: True
Rating
0 0

There are no comments for now.

to be the first to leave a comment.