Fast and Slow Pointers Pattern
The Fast and Slow Pointers pattern, also known as the Floyd's Cycle Detection algorithm or the Tortoise and Hare algorithm, uses two pointers moving at different speeds through a data structure. This technique is particularly effective for cycle detection and finding middle elements.
When to Use Fast and Slow Pointers
Use this pattern when:
- Detecting cycles in linked lists or sequences
- Finding the middle of a linked list
- Determining if a structure is a palindrome
- Finding the start of a cycle
- Problems involving circular or cyclic data
Core Approach
The Concept
- Slow Pointer: Moves one step at a time
- Fast Pointer: Moves two steps at a time (or some other ratio)
Key Insight: If there's a cycle, fast pointer will eventually catch up to slow pointer. If no cycle, fast pointer reaches end.
Why It Works for Cycle Detection
In a cycle: - Fast pointer enters cycle first - Eventually both pointers are in cycle - Fast pointer gains on slow pointer by 1 position per iteration - They must meet (can't skip over each other)
Problem 1: Linked List Cycle
Difficulty: Easy
LeetCode: #141
Problem Statement
Given head of a linked list, determine if the list has a cycle in it.
Example:
Approach
Use two pointers: - Slow moves one step per iteration - Fast moves two steps per iteration - If they meet, there's a cycle - If fast reaches end (None), no cycle
Solution
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def has_cycle(head):
"""
Detect if linked list has a cycle.
Time Complexity: O(n) - visit each node at most once
Space Complexity: O(1) - only two pointers
"""
if not head or not head.next:
return False
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True # Cycle detected
return False # Reached end, no cycle
# Test helper to create cycle
def create_cycle_list(values, pos):
"""Create linked list with cycle at position pos (-1 for no cycle)."""
if not values:
return None
head = ListNode(values[0])
current = head
cycle_node = None
if pos == 0:
cycle_node = head
for i in range(1, len(values)):
current.next = ListNode(values[i])
current = current.next
if i == pos:
cycle_node = current
if cycle_node:
current.next = cycle_node
return head
# Test
head = create_cycle_list([3, 2, 0, -4], 1)
print(has_cycle(head)) # Output: True
Step-by-Step Walkthrough
List: 3 -> 2 -> 0 -> -4 -> (back to 2)
Initial: slow = 3, fast = 3
Iteration 1:
slow = slow.next = 2
fast = fast.next.next = 0
slow != fast, continue
Iteration 2:
slow = slow.next = 0
fast = fast.next.next = 2
slow != fast, continue
Iteration 3:
slow = slow.next = -4
fast = fast.next.next = -4
slow == fast ✓ Cycle detected!
Result: True
Problem 2: Middle of the Linked List
Difficulty: Easy
LeetCode: #876
Problem Statement
Given a linked list, return the middle node. If there are two middle nodes, return the second one.
Example:
Approach
Use two pointers: - When fast pointer reaches end, slow pointer is at middle - Fast moves 2x speed, so slow is at halfway point when fast finishes
Solution
def middle_node(head):
"""
Find middle node of linked list.
Time Complexity: O(n) - traverse list once
Space Complexity: O(1) - only two pointers
"""
if not head:
return None
slow = head
fast = head
# When fast reaches end, slow is at middle
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
# Test helper
def create_list(values):
"""Create linked list from values."""
if not values:
return None
head = ListNode(values[0])
current = head
for val in values[1:]:
current.next = ListNode(val)
current = current.next
return head
def list_to_array(head):
"""Convert linked list to array."""
result = []
while head:
result.append(head.val)
head = head.next
return result
# Test
head = create_list([1, 2, 3, 4, 5])
middle = middle_node(head)
print(list_to_array(middle)) # Output: [3, 4, 5]
Step-by-Step Walkthrough
List: 1 -> 2 -> 3 -> 4 -> 5 -> None
Initial: slow = 1, fast = 1
Iteration 1:
slow = slow.next = 2
fast = fast.next.next = 3
fast and fast.next exist, continue
Iteration 2:
slow = slow.next = 3
fast = fast.next.next = 5
fast and fast.next exist, continue
Iteration 3:
slow = slow.next = 4
fast = fast.next.next = None
fast is None, exit loop
Result: slow points to 3 (middle node)
Problem 3: Linked List Cycle II
Difficulty: Medium
LeetCode: #142
Problem Statement
Given a linked list, return the node where the cycle begins. If no cycle, return null.
Example:
Approach
Two-phase algorithm:
Phase 1: Detect cycle (same as Problem 1) - If no cycle, return None
Phase 2: Find cycle start - Reset one pointer to head - Move both pointers one step at a time - Where they meet is the cycle start
Why this works: Mathematical proof based on distances traveled.
Solution
def detect_cycle(head):
"""
Find the node where cycle begins.
Time Complexity: O(n) - two passes through list
Space Complexity: O(1) - only two pointers
"""
if not head or not head.next:
return None
# Phase 1: Detect if cycle exists
slow = head
fast = head
has_cycle_flag = False
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
has_cycle_flag = True
break
if not has_cycle_flag:
return None
# Phase 2: Find cycle start
# Reset slow to head, move both one step at a time
slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow # This is the cycle start
# Test
head = create_cycle_list([3, 2, 0, -4], 1)
cycle_start = detect_cycle(head)
print(cycle_start.val if cycle_start else None) # Output: 2
Step-by-Step Walkthrough
List: 3 -> 2 -> 0 -> -4 -> (back to 2)
Phase 1: Detect cycle
slow = 3, fast = 3
slow = 2, fast = 0
slow = 0, fast = 2
slow = -4, fast = -4
Cycle detected at node -4
Phase 2: Find start
Reset slow to head (3)
fast stays at -4
Both move one step:
slow = 2, fast = 2
They meet at node 2!
Result: Node with value 2 (cycle start)
Why it works:
- Let's say distance from head to cycle start is 'a'
- Distance from cycle start to meeting point is 'b'
- Cycle length is 'c'
- When they meet: slow traveled a+b, fast traveled a+b+c
- Since fast is 2x speed: 2(a+b) = a+b+c
- Simplifies to: a = c-b
- So moving from head 'a' steps and from meeting point 'a' steps
both lead to cycle start!
Key Takeaways
- Speed Ratio: Typically 2:1, but can vary based on problem
- Cycle Detection: Fast and slow pointers guaranteed to meet in cycle
- Middle Finding: Fast reaches end when slow at middle
- Space Efficiency: O(1) space vs O(n) for hash set approach
- Multiple Phases: Some problems require cycle detection followed by additional work
Common Patterns
Cycle Detection
- Linked list cycle (I and II)
- Happy number problem
- Circular array loop
Middle Finding
- Middle of linked list
- Palindrome linked list (find middle, then reverse)
- Reorder linked list
Other Applications
- Find nth node from end (fast pointer n steps ahead)
- Intersection of two linked lists
- Remove nth node from end
Practice Tips
- Draw the list and trace pointer movements
- Understand the math behind cycle detection
- Consider edge cases: no cycle, single node, two nodes
- Practice both phases of cycle detection
- Think about why fast pointer moves 2 steps (not 3, 4, etc.)
Template Pattern
def fast_slow_pattern(head):
"""General template for fast/slow pointer problems."""
if not head:
return None
slow = head
fast = head
# Move pointers at different speeds
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# Check condition (cycle, middle, etc.)
if slow == fast:
# Found what we're looking for
break
# Post-processing if needed
# (e.g., find cycle start, reverse second half)
return result
Common Mistakes
- Not checking if fast.next exists before moving fast
- Initializing pointers at wrong positions
- Forgetting to handle empty list or single node
- Wrong movement ratio (e.g., moving slow by 2, fast by 3)
- Not handling case when no cycle exists
Essential Linked List Algorithms: From Traversal to Reversal
Linked lists are fundamental data structures that require careful pointer manipulation. Master these essential techniques to solve any linked list problem.
Technique 1: Reversal (Iterative)
Core skill: Rewiring pointers while maintaining references.
def reverse_linked_list(head):
"""
Reverse linked list iteratively.
Time: O(n)
Space: O(1)
Key: Three pointers to track prev, current, next
"""
prev = None
current = head
while current:
# Save next node
next_node = current.next
# Reverse pointer
current.next = prev
# Move pointers forward
prev = current
current = next_node
return prev # New head
# Visualization:
# Before: 1 -> 2 -> 3 -> None
# After: None <- 1 <- 2 <- 3
# ^head
Recursive approach:
def reverse_recursive(head):
"""
Reverse linked list recursively.
Time: O(n)
Space: O(n) - call stack
Pattern: Reverse rest of list, then fix current node
"""
# Base case
if not head or not head.next:
return head
# Reverse rest of list
new_head = reverse_recursive(head.next)
# Fix current node's next pointer
head.next.next = head
head.next = None
return new_head
Technique 2: Reversal in Range
Problem: Reverse nodes from position m to n.
def reverse_between(head, m, n):
"""
Reverse linked list from position m to n.
Example: 1->2->3->4->5, m=2, n=4
Result: 1->4->3->2->5
Time: O(n)
Space: O(1)
"""
if not head or m == n:
return head
dummy = ListNode(0)
dummy.next = head
prev = dummy
# Move to node before m
for _ in range(m - 1):
prev = prev.next
# Reverse from m to n
reverse_start = prev.next
current = reverse_start.next
for _ in range(n - m):
# Move current to after prev
reverse_start.next = current.next
current.next = prev.next
prev.next = current
current = reverse_start.next
return dummy.next
Technique 3: Merge Two Sorted Lists
Pattern: Compare and link nodes alternately.
def merge_two_lists(l1, l2):
"""
Merge two sorted linked lists.
Time: O(n + m)
Space: O(1) - reuses existing nodes
"""
dummy = ListNode(0)
current = dummy
while l1 and l2:
if l1.val < l2.val:
current.next = l1
l1 = l1.next
else:
current.next = l2
l2 = l2.next
current = current.next
# Attach remaining nodes
current.next = l1 if l1 else l2
return dummy.next
# Recursive approach (elegant but O(n) space)
def merge_recursive(l1, l2):
"""Recursive merge."""
if not l1:
return l2
if not l2:
return l1
if l1.val < l2.val:
l1.next = merge_recursive(l1.next, l2)
return l1
else:
l2.next = merge_recursive(l1, l2.next)
return l2
Technique 4: Reorder List
Problem: L0 → L1 → ... → Ln-1 → Ln becomes L0 → Ln → L1 → Ln-1 → ...
def reorder_list(head):
"""
Reorder list by interleaving first and second halves.
Example: 1->2->3->4->5 becomes 1->5->2->4->3
Strategy:
1. Find middle using fast/slow pointers
2. Reverse second half
3. Merge two halves alternately
Time: O(n)
Space: O(1)
"""
if not head or not head.next:
return
# Step 1: Find middle
slow = fast = head
while fast.next and fast.next.next:
slow = slow.next
fast = fast.next.next
# Step 2: Reverse second half
second = slow.next
slow.next = None # Split list
second = reverse_linked_list(second)
# Step 3: Merge alternately
first = head
while second:
temp1 = first.next
temp2 = second.next
first.next = second
second.next = temp1
first = temp1
second = temp2
Technique 5: Remove Nth Node From End
Pattern: Two-pass vs one-pass with gap.
def remove_nth_from_end_one_pass(head, n):
"""
Remove nth node from end in one pass.
Strategy: Maintain gap of n between two pointers
Time: O(L) where L is list length
Space: O(1)
"""
dummy = ListNode(0)
dummy.next = head
fast = slow = dummy
# Move fast ahead by n steps
for _ in range(n):
fast = fast.next
# Move both until fast reaches end
while fast.next:
fast = fast.next
slow = slow.next
# Remove node
slow.next = slow.next.next
return dummy.next
Technique 6: Deep Copy with Random Pointer
Problem: Clone list where nodes have both next and random pointers.
class Node:
def __init__(self, val, next=None, random=None):
self.val = val
self.next = next
self.random = random
def copy_random_list(head):
"""
Clone list with random pointers.
Strategy: Interweave original and copied nodes
Time: O(n)
Space: O(1) - excluding output
"""
if not head:
return None
# Step 1: Create copy nodes interweaved with originals
# Original: A -> B -> C
# Result: A -> A' -> B -> B' -> C -> C'
current = head
while current:
copy = Node(current.val)
copy.next = current.next
current.next = copy
current = copy.next
# Step 2: Set random pointers for copies
current = head
while current:
if current.random:
current.next.random = current.random.next
current = current.next.next
# Step 3: Separate original and copy lists
current = head
copy_head = head.next
while current:
copy = current.next
current.next = copy.next
if copy.next:
copy.next = copy.next.next
current = current.next
return copy_head
# Alternative: Hash map approach (O(n) space but simpler)
def copy_random_list_hash(head):
"""Clone using hash map."""
if not head:
return None
# Map original -> copy
old_to_new = {}
# First pass: create all nodes
current = head
while current:
old_to_new[current] = Node(current.val)
current = current.next
# Second pass: link nodes
current = head
while current:
if current.next:
old_to_new[current].next = old_to_new[current.next]
if current.random:
old_to_new[current].random = old_to_new[current.random]
current = current.next
return old_to_new[head]
Technique 7: Sort List
Problem: Sort linked list in O(n log n) time.
def sort_list(head):
"""
Sort linked list using merge sort.
Time: O(n log n)
Space: O(log n) - recursion stack
Why merge sort?
- No random access → can't use quicksort efficiently
- Linked list merge is O(1) space
"""
if not head or not head.next:
return head
# Find middle using fast/slow
slow = fast = head
prev = None
while fast and fast.next:
prev = slow
slow = slow.next
fast = fast.next.next
# Split into two halves
prev.next = None
# Recursively sort each half
left = sort_list(head)
right = sort_list(slow)
# Merge sorted halves
return merge_two_lists(left, right)
Technique 8: Add Two Numbers
Pattern: Traverse both lists simultaneously with carry.
def add_two_numbers(l1, l2):
"""
Add two numbers represented as linked lists.
Example: (2 -> 4 -> 3) + (5 -> 6 -> 4) = (7 -> 0 -> 8)
Represents: 342 + 465 = 807
Time: O(max(n, m))
Space: O(max(n, m))
"""
dummy = ListNode(0)
current = dummy
carry = 0
while l1 or l2 or carry:
# Get values (0 if list exhausted)
val1 = l1.val if l1 else 0
val2 = l2.val if l2 else 0
# Calculate sum and carry
total = val1 + val2 + carry
carry = total // 10
digit = total % 10
# Create new node
current.next = ListNode(digit)
current = current.next
# Move to next nodes
if l1:
l1 = l1.next
if l2:
l2 = l2.next
return dummy.next
Technique 9: Flatten Multilevel List
Problem: Flatten list with child pointers into single level.
def flatten(head):
"""
Flatten doubly linked list with child pointers.
Strategy: DFS to process child lists recursively
Time: O(n)
Space: O(n) - recursion stack
"""
if not head:
return head
def flatten_dfs(node):
"""
Flatten starting from node.
Returns tail of flattened list.
"""
current = node
tail = None
while current:
next_node = current.next
# Process child
if current.child:
child_tail = flatten_dfs(current.child)
# Insert child list
current.next = current.child
current.child.prev = current
current.child = None
# Link to rest of list
if next_node:
child_tail.next = next_node
next_node.prev = child_tail
tail = child_tail
current = next_node
else:
tail = current
current = next_node
return tail
flatten_dfs(head)
return head
Common Patterns Summary
| Pattern | When to Use | Key Technique |
|---|---|---|
| Reversal | Reorder nodes | Three pointers (prev, curr, next) |
| Merge | Combine sorted lists | Dummy node + comparison |
| Reorder | Interleave halves | Find middle + reverse + merge |
| Remove | Delete nodes | Fast pointer ahead + gap |
| Copy | Clone complex structures | Interweave or hash map |
| Sort | Order nodes | Merge sort (O(n log n)) |
| Add | Arithmetic on lists | Carry propagation |
| Flatten | Multilevel to single | DFS with tail tracking |
Practice Strategy
- Master reversal first - Foundation for many problems
- Use dummy nodes - Simplifies edge case handling
- Draw diagrams - Visualize pointer changes
- Test edge cases: Empty, single node, two nodes
- Check for cycles - Prevent infinite loops
Further Learning
The techniques above cover essential linked list manipulations for interviews and real-world applications. For additional perspectives:
- Theoretical Analysis: Academic resources may provide formal proofs of algorithm correctness
- Language-Specific Optimizations: Different memory management strategies in C++, Java, Python
- Advanced Topics: Skip lists, XOR linked lists, and memory-efficient variants
The key is understanding pointer manipulation and being able to trace through operations step-by-step on paper before coding.