You’ve probably seen both patterns filed under “pointer techniques” and assumed they’re one idea with two names. They aren’t. They look similar in code, but each one is justified by a different argument, and that is what tells you which to reach for on a new problem.
This guide shows you the brute-force version of each problem, then the two patterns side by side, with a dry-run trace, reusable templates in Python and C++, five variations, 12 practice problems in order, and the bugs that cost people interviews. If you want the complexity notation refreshed first, read our guide to time and space complexity.
Phase 1: The Problem (Two Brute Forces)
Question A: you’re given a sorted array and a target. Find two numbers that add up to it.
The brute force checks every pair:
def two_sum_brute(nums, target):
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
return []
That is O(n²) time. At a million elements it means roughly half a trillion pair checks, and it ignores the fact that the array is sorted.
Question B: given the head of a linked list, does it contain a cycle?
The brute force remembers every node it has seen:
def has_cycle_brute(head):
seen = set()
while head:
if head in seen:
return True
seen.add(head)
head = head.next
return False
This is O(n) time but O(n) extra space. When the list is huge, or when you’re asked for constant memory, it fails the constraint. A related problem, finding the middle of a list, tempts you into two passes: count the nodes, then walk half the count. That works, but a single pass is possible.
Both problems have the same shape. You’re wasting information. Sorted order tells you which candidates to discard, and relative motion tells you where a loop must be.
Phase 2: The Pattern
The two patterns, side by side
Two pointers (converging or same-direction). Place pointers at positions chosen by the structure of the data, usually the two ends of a sorted array, or a “reader” and a “writer” both moving right. At each step, a comparison tells you which pointer to move, and every move eliminates candidates for good.
Fast and slow pointers (Floyd’s tortoise and hare). Both pointers start together and move in the same direction at different speeds, typically one step versus two. You aren’t comparing values to decide moves. You’re using the speed difference to detect structure: a loop, a midpoint, or a repeated value.
Analogy: two pointers is two people reading a shelf of alphabetized books from opposite ends, and each can ignore the books the other has already ruled out. Fast and slow is two runners on a track who start together. If the track is a loop, the faster one laps the slower one. If it’s a straight road, the faster one finishes first, and when it does, the slower one is at the halfway mark.
Trigger phrases
Reach for two pointers when the statement says:
- “sorted array” plus “pair / triplet that sums to …”
- “palindrome”, “reverse in place”, “merge two sorted …”
- “remove duplicates / move zeros in place”, “O(1) extra space”
- “maximize area / minimize difference between two elements”
Reach for fast and slow when you see:
- “detect a cycle”, “find where the cycle begins”
- “middle of a linked list”, “kth node from the end” (a variant with a fixed gap)
- “find the duplicate” in an array of values within a known range, without modifying it
- a process that repeats (like digit-square sums) and you must decide whether it loops
The decision in one picture
What most tutorials get wrong: they present both as “move two pointers cleverly” and skip why each is correct, which is exactly the part you need to adapt them.
- Converging pointers are correct because of a monotonicity argument. If
nums[l] + nums[r] < target, then pairingnums[l]with any index at or belowrgives an even smaller sum, solcan never be part of an answer and is safe to discard. No sorted order, no argument, no algorithm. - Fast and slow pointers are correct because of a relative speed argument. If the fast pointer gains exactly one step per iteration, the gap between them changes by exactly 1 each time, so it can’t jump over zero. That is why 1-versus-2 is the standard choice. With speeds 1 and 3, the gap grows by 2, and in a cycle of even length the pointers can miss each other forever depending on where they enter. Speed 2 isn’t magic, but a speed difference of 1 is what guarantees a meeting.
Phase 3: Step-by-Step Walkthrough
Trace 1: Two pointers, sorted Two Sum
Array [1, 2, 4, 6, 10], target 8.
| step | l | r | nums[l] | nums[r] | sum | decision |
|---|---|---|---|---|---|---|
| 1 | 0 | 4 | 1 | 10 | 11 | too big → r -= 1 |
| 2 | 0 | 3 | 1 | 6 | 7 | too small → l += 1 |
| 3 | 1 | 3 | 2 | 6 | 8 | match, return [1, 3] |
Three steps instead of ten pair checks. Each move threw away a whole row or column of the pair table.
Trace 2: Fast and slow, cycle detection and entry
Linked list 1 → 2 → 3 → 4 → 5 → 6 → back to 3. Both pointers start at node 1.
| step | slow | fast |
|---|---|---|
| 0 | 1 | 1 |
| 1 | 2 | 3 |
| 2 | 3 | 5 |
| 3 | 4 | 3 (via 6) |
| 4 | 5 | 5 (via 4) → meet |
They meet at node 5, so a cycle exists. Now find where the cycle starts. Put one pointer back at the head and leave the other at the meeting point, then move both one step at a time:
| step | from head | from meeting point |
|---|---|---|
| 0 | 1 | 5 |
| 1 | 2 | 6 |
| 2 | 3 | 3 → meet at entry |
Why this works. Let a be the distance from the head to the cycle entry, b the distance from the entry to the meeting point along the cycle, and L the cycle length. When they meet, slow has traveled a + b. Fast has traveled 2(a + b), which is also a + b plus some whole number k of laps, kL. So a + b = kL, which gives a = kL − b = (k−1)L + (L − b). In words, a equals the distance from the meeting point forward to the entry, plus whole laps. In our trace, a = 2, b = 2, L = 4, and a + b = 4 = 1·L. ✓
Phase 4: Code Templates
Pure Python 3, no dependencies.
Template A: Converging two pointers
def two_sum_sorted(nums, target):
l, r = 0, len(nums) - 1
while l < r: # strict <: never pair an element with itself
s = nums[l] + nums[r]
if s == target:
return [l, r]
if s < target:
l += 1
else:
r -= 1
return [] # O(n) time, O(1) space
#include <vector>
using namespace std;
vector<int> twoSumSorted(const vector<int>& nums, long long target) {
int l = 0, r = (int)nums.size() - 1;
while (l < r) {
long long s = (long long)nums[l] + nums[r]; // avoid int overflow
if (s == target) return {l, r};
if (s < target) ++l; else --r;
}
return {}; // O(n) time, O(1) space
}
What problem it solves: finding a pair, or optimizing a quantity defined by two positions, in a sorted or otherwise monotone structure.
Key parts: the while l < r condition, one comparison per step, and exactly one pointer moving per iteration.
Production notes: if the data isn’t sorted, sorting costs O(n log n), which may still beat O(n²) but can lose to a hash map’s O(n) (at O(n) extra space). Pick based on whether you must keep the original indices and whether memory is tight.
Template B: Same-direction reader and writer
def remove_duplicates_sorted(nums):
if not nums:
return 0
w = 1 # next write position
for r in range(1, len(nums)):
if nums[r] != nums[w - 1]:
nums[w] = nums[r]
w += 1
return w # O(n) time, O(1) space; nums[:w] is the result
What problem it solves: compacting an array in place, with a reader scanning every element and a writer marking where the next kept element goes.
Key parts: the writer never passes the reader, so you never overwrite data you haven’t read yet.
Production notes: this in-place pattern is why many streaming and buffer-compaction routines run in constant extra memory, but it destroys the original order of discarded items, so don’t use it if you need them later.
Template C: Fast and slow on a linked list
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def has_cycle(head):
slow = fast = head
while fast and fast.next: # check fast first, then fast.next
slow = slow.next
fast = fast.next.next
if slow is fast: # identity, not value equality
return True
return False # O(n) time, O(1) space
def cycle_start(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
p = head
while p is not slow:
p = p.next
slow = slow.next
return p # entry node
return None
def middle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow # second middle when length is even
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(nullptr) {}
};
ListNode* detectCycle(ListNode* head) {
ListNode *slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
ListNode* p = head;
while (p != slow) { p = p->next; slow = slow->next; }
return p; // O(n) time, O(1) space
}
}
return nullptr;
}
What problem it solves: loop detection, entry finding, and midpoint finding in constant memory.
Key parts: the loop condition fast and fast.next prevents a null dereference. Compare nodes, not values, because two different nodes can hold equal values.
Production notes: the algorithm is named after Robert Floyd, but the Wikipedia article on cycle detection notes that the attribution may be a folk theorem, since Knuth’s 1969 statement is the first known appearance in print. It is used in practice where the sequence is generated by a function and you can’t afford to store it, such as Pollard’s rho factorization.
Phase 5: Variations
1. 3Sum (two pointers inside a loop). Sort, fix one element, then run Template A on the rest. The extra work is skipping duplicates.
def three_sum(nums):
nums.sort()
res = []
for i in range(len(nums) - 2):
if i and nums[i] == nums[i - 1]:
continue # skip duplicate anchors
l, r = i + 1, len(nums) - 1
while l < r:
s = nums[i] + nums[l] + nums[r]
if s < 0:
l += 1
elif s > 0:
r -= 1
else:
res.append([nums[i], nums[l], nums[r]])
l += 1
while l < r and nums[l] == nums[l - 1]:
l += 1
return res # O(n^2) time
2. Container With Most Water (greedy convergence). The area is min(h[l], h[r]) * (r - l). Move the shorter side: the width is shrinking, so only a taller wall can improve the bound, and moving the taller side cannot.
def max_area(h):
l, r, best = 0, len(h) - 1, 0
while l < r:
best = max(best, min(h[l], h[r]) * (r - l))
if h[l] < h[r]:
l += 1
else:
r -= 1
return best
3. Remove duplicates (reader and writer). This is Template B. It’s a two-pointer pattern where the pointers don’t converge.
4. Find the Duplicate Number (fast and slow on an array). For n + 1 values in 1..n, treat index i as pointing to nums[i]. Since no value is 0, index 0 is a tail outside any cycle, and the duplicate value is the cycle’s entry.
def find_duplicate(nums):
slow = fast = 0
while True:
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast:
break
slow = 0
while slow != fast:
slow = nums[slow]
fast = nums[fast]
return slow # O(n) time, O(1) space, array untouched
5. Happy Number (fast and slow on a number sequence). Repeatedly replace n with the sum of the squares of its digits. The sequence either reaches 1 or loops forever.
def is_happy(n):
def nxt(x):
return sum(int(d) ** 2 for d in str(x))
slow, fast = n, nxt(n)
while fast != 1 and slow != fast:
slow = nxt(slow)
fast = nxt(nxt(fast))
return fast == 1
Notice that variations 4 and 5 have no linked list. Fast and slow works on any sequence where each element determines the next.
Phase 6: Practice Problems (Easy to Hard)
Problem numbers are LeetCode’s.
| # | Problem | Pattern | One-line hint |
|---|---|---|---|
| 1 | Valid Palindrome (125) | Converging | Skip non-alphanumeric characters from both ends. |
| 2 | Two Sum II, Input Array Is Sorted (167) | Converging | Template A, but note the 1-based indices. |
| 3 | Remove Duplicates from Sorted Array (26) | Reader/writer | Template B. |
| 4 | Middle of the Linked List (876) | Fast/slow | When the length is even, decide which middle you want. |
| 5 | Linked List Cycle (141) | Fast/slow | Template C, has_cycle. |
| 6 | Happy Number (202) | Fast/slow | Detect the loop in the digit-square sequence. |
| 7 | Palindrome Linked List (234) | Fast/slow + reverse | Find the middle, reverse the second half, compare. |
| 8 | Container With Most Water (11) | Converging | Move the shorter wall. |
| 9 | 3Sum (15) | Converging | Sort, anchor one value, skip duplicates. |
| 10 | Linked List Cycle II (142) | Fast/slow | Reset one pointer to the head. |
| 11 | Find the Duplicate Number (287) | Fast/slow on array | Treat nums[i] as the next pointer. |
| 12 | Trapping Rain Water (42) | Converging | Track the running max from each side and move the lower side. |
For the next step up in this family, contiguous-window problems, move on to our sliding window practice problems. Converging pointers also work on a binary search tree through an in-order traversal. See the BST implementation guide if you want to try Two Sum IV that way.
Phase 7: Common Mistakes and Edge Cases
- Unsorted input to converging pointers. The monotonicity argument disappears and the algorithm returns wrong answers silently. Sort first, or use a hash map.
l < rversusl <= r. For pair problems you need<. For binary-search-like scans where a single element can be the answer, you may need<=.- Null dereference in fast/slow.
fast.next.nextcrashes whenfast.nextisNone. Guard withwhile fast and fast.next. - Comparing values instead of nodes. A list
1 → 1 → 1has equal values and no cycle. Use identity (isin Python, pointer equality in C++). - Forgetting to reset a pointer when finding the cycle entry, or resetting to the wrong node. Reset one pointer to the head and keep the other at the meeting point.
- Empty and one-node lists.
head = Noneshould returnFalseorNonecleanly. A single node pointing to itself is a valid cycle of length 1. - Duplicates in 3Sum. Skip duplicate anchors and duplicate left values after recording a match, or you’ll emit repeated triplets.
- Integer overflow. In C++ and Java, summing two large
intvalues can overflow. Cast tolong longbefore adding. - Even-length midpoints. Template C returns the second middle. If a problem needs the first middle (for example to split a list in half and detach it), stop one step earlier with
fast.next and fast.next.next. - Fast/slow on arrays with out-of-range values. Find-the-duplicate only works if every value is a valid index. Check the constraints before applying it.
Phase 8: Where This Appears in Real Systems
- Integer factorization. Pollard’s rho algorithm relies on cycle detection in a pseudo-random sequence modulo an unknown factor, which is why Floyd’s and Brent’s methods are used where storing the whole sequence isn’t feasible.
- Sorted-merge operations. Merging two sorted inputs by advancing a pointer in each is the core step of merge sort and of sort-merge joins in databases. Each pointer only moves forward, which is why these operations stream well over large data.
For interview prep context, these two patterns are among the first ones screeners test, and they sit early in the roadmap in our AI engineer career guide.
Key Takeaways
- Two pointers moves based on comparisons and discards candidates, so it needs sorted or monotone data to be correct.
- Fast and slow pointers moves based on speed and detects structure: loops, midpoints, repeated values. It needs no sorting.
- A speed difference of exactly 1 guarantees the pointers meet inside a loop. Other ratios can skip over each other.
- To find the cycle entry, reset one pointer to the head and move both one step at a time. The proof is
a = (k−1)L + (L − b). - Fast and slow works on any “each element determines the next” sequence, not just linked lists. Find the Duplicate Number and Happy Number prove it.
- Most bugs are boundary bugs: null checks,
<versus<=, comparing values instead of nodes, and unsorted input.
FAQ
What is the difference between two pointers and fast and slow pointers? Two pointers moves based on comparisons, usually over sorted data, and each move eliminates candidates. Fast and slow pointers moves at different speeds through the same sequence, and the gap between them reveals loops or midpoints. They differ in why they work, not just in how they look.
Why does Floyd’s algorithm use speeds of 1 and 2? A speed difference of one means the gap changes by exactly one per step, so the pointers can’t jump past each other inside a loop. Speeds like 1 and 3 can miss each other when the cycle length and the starting gap don’t line up. The 2:1 ratio is also what makes the entry-finding proof work out cleanly.
Can two pointers work on an unsorted array? Converging pointers generally can’t, because the discard argument depends on order. Reader-and-writer pointers do work on unsorted arrays, since they rely on position rather than order. Otherwise sort first (O(n log n)) or use a hash map.
What is the time and space complexity of fast and slow pointers? Cycle detection runs in O(n) time with O(1) extra space. The pointers are guaranteed to meet within a number of steps proportional to the list length, which beats the O(n) space of a hash-set approach.
How do I find the start of a cycle in a linked list? Run fast and slow until they meet. Move one pointer back to the head, then advance both one step at a time. They meet at the cycle’s entry node.
