Every interview prep list mentions the two pointer technique, and almost every explanation makes the same mistake: it shows you solutions without teaching you recognition. You read the Two Sum II solution, nod along, and then freeze in a real interview because nobody told you what a two pointer problem looks like before you know the answer.

Here’s the framing that fixes it: two pointers is a technique for eliminating candidate pairs (or subarrays) without checking them, the same way binary search eliminates candidates without visiting them. The brute-force approach to most pair problems checks all O(n²) pairs. Two pointers gets to O(n) by proving, at each step, that a whole batch of pairs can’t possibly be the answer.

In this post we’ll cover the three flavors of two pointers, the problem-statement signals that tell you to use it, and six problems from easy to hard, each in Python.


What Is the Two Pointer Technique?

You keep two indices into a sequence and move them according to some rule, instead of running two independent nested loops. Because each pointer only ever moves forward (or the pointers only ever move toward each other), the total work is O(n) instead of O(n²).

There are three distinct flavors, and knowing which one you’re in matters more than memorizing any single solution:

  1. Converging pointers. Start at both ends of a (usually sorted) array and move toward each other. Used for pair-sum problems, palindromes, and container/area problems.
  2. Fast and slow pointers (same direction). Both pointers start at the beginning; the fast one scans, the slow one marks a boundary. Used for in-place deduplication, partitioning, and linked-list cycle detection. The same idea powers the classic cycle check in Day 7: Introduction to Linked Lists.
  3. Parallel pointers across two sequences. One pointer per array, both moving forward. Used for merging sorted arrays (the heart of the merge step in Day 27: MergeSort) and subsequence checks.

If arrays themselves still feel shaky, do a quick pass through Day 4: Introduction to Arrays first; everything below assumes you’re comfortable indexing from both ends.


Pattern Recognition: When to Reach for Two Pointers

This is the part most guides skip. In a live interview, scan the problem statement for these signals:

  • The input is sorted, or you’re allowed to sort it. Sorted order is what makes “move left” or “move right” a provably safe elimination. If the problem hands you sorted data and asks about pairs, two pointers should be your first thought.
  • You’re asked about a pair, triplet, or subarray with a target property: “find two numbers that sum to X,” “the pair with the largest area,” “the closest sum to zero.”
  • The problem demands O(1) extra space. Hash maps solve many pair problems in O(n) time but O(n) space (see Hash Maps and Sets: The Most Underrated FAANG Topic). When the interviewer says “can you do it without extra memory?”, they’re usually steering you to two pointers.
  • In-place modification. “Remove duplicates in place,” “move zeros to the end,” “partition around a pivot”: all fast/slow pointer problems. The partition step of Day 26: QuickSort is exactly this pattern.
  • Symmetry from both ends. Palindromes and reversal problems practically shout “converging pointers.”

One honest anti-signal: if the array is unsorted, you can’t sort it (because indices matter), and you need exact lookups: that’s hash map territory, not two pointers. Two Sum (return the indices) is the canonical example: sorting destroys the indices, so a hash map wins.

Now let’s put the pattern to work, easiest problem first.


Problem 1: Two Sum II, Sorted Input (Easy)

Given a sorted array and a target, return the positions of two numbers that add up to the target.

Brute force checks all O(n²) pairs. Instead, put one pointer at each end and look at the sum:

 1def two_sum_sorted(numbers, target):
 2    left, right = 0, len(numbers) - 1
 3    while left < right:
 4        current = numbers[left] + numbers[right]
 5        if current == target:
 6            return [left + 1, right + 1]  # 1-indexed per problem statement
 7        elif current < target:
 8            left += 1    # sum too small: only a bigger left value can help
 9        else:
10            right -= 1   # sum too big: only a smaller right value can help
11    return []

The proof is the whole lesson. If numbers[left] + numbers[right] < target, then numbers[left] paired with anything to the left of right is even smaller, so every pair involving the current left is dead, and we can discard left entirely. One comparison eliminates n−1 candidate pairs. That’s the elimination engine that makes every converging-pointer solution O(n).

Problem 2: Valid Palindrome (Easy)

Return true if a string reads the same forward and backward, considering only alphanumeric characters and ignoring case.

Symmetry from both ends, the clearest two pointer signal there is:

 1def is_palindrome(s):
 2    left, right = 0, len(s) - 1
 3    while left < right:
 4        while left < right and not s[left].isalnum():
 5            left += 1
 6        while left < right and not s[right].isalnum():
 7            right -= 1
 8        if s[left].lower() != s[right].lower():
 9            return False
10        left += 1
11        right -= 1
12    return True

O(n) time, O(1) space, versus the tempting one-liner that builds a cleaned, reversed copy in O(n) space. Interviewers use this problem to check whether you handle the messy details (skipping non-alphanumerics) without breaking the pointer invariants. More string fundamentals live in Strings and String Manipulation.

Problem 3: Remove Duplicates from Sorted Array (Easy–Medium)

Remove duplicates in place from a sorted array and return the count of unique elements.

Different flavor: fast/slow, same direction. The slow pointer marks the end of the “clean” region; the fast pointer scans for the next new value:

1def remove_duplicates(nums):
2    if not nums:
3        return 0
4    slow = 0
5    for fast in range(1, len(nums)):
6        if nums[fast] != nums[slow]:
7            slow += 1
8            nums[slow] = nums[fast]
9    return slow + 1

The invariant to state out loud: everything up to and including slow is deduplicated. Once you have that sentence, the code writes itself, and the same skeleton solves Move Zeroes, Remove Element, and the QuickSort partition.

Problem 4: Container With Most Water (Medium)

Given heights of vertical lines, find two lines that together with the x-axis contain the most water.

The area between lines i and j is min(height[i], height[j]) * (j - i). Start with the widest container and converge:

 1def max_area(height):
 2    left, right = 0, len(height) - 1
 3    best = 0
 4    while left < right:
 5        area = min(height[left], height[right]) * (right - left)
 6        best = max(best, area)
 7        if height[left] < height[right]:
 8            left += 1
 9        else:
10            right -= 1
11    return best

Why is moving the shorter line safe? Keeping the shorter line while shrinking the width can never win: the height is capped by the shorter line, and the width only decreases. So every container that keeps the shorter endpoint is provably worse than one we’ve already measured: batch elimination again. If you can articulate that argument, this “medium” becomes a five-minute problem.

Problem 5: 3Sum (Medium, deceptively fiddly)

Find all unique triplets that sum to zero.

The classic composition: sort, fix one element, then run Two Sum II on the rest.

 1def three_sum(nums):
 2    nums.sort()
 3    result = []
 4    for i in range(len(nums) - 2):
 5        if i > 0 and nums[i] == nums[i - 1]:
 6            continue                      # skip duplicate anchors
 7        if nums[i] > 0:
 8            break                         # sorted: no zero-sum possible
 9        left, right = i + 1, len(nums) - 1
10        while left < right:
11            total = nums[i] + nums[left] + nums[right]
12            if total < 0:
13                left += 1
14            elif total > 0:
15                right -= 1
16            else:
17                result.append([nums[i], nums[left], nums[right]])
18                while left < right and nums[left] == nums[left + 1]:
19                    left += 1             # skip duplicate seconds
20                while left < right and nums[right] == nums[right - 1]:
21                    right -= 1
22                left += 1
23                right -= 1
24    return result

O(n²) total (one two pointer pass per anchor) versus O(n³) brute force. What actually fails candidates here isn’t the algorithm; it’s the deduplication. Three separate skips (anchor, left, right) are required, and missing any one of them produces duplicate triplets. Practice this one until the skips are muscle memory.

Problem 6: Trapping Rain Water (Hard)

Given an elevation map, compute how much rainwater it traps.

Water above position i is min(max_left, max_right) - height[i]. The O(n)-space solution precomputes both max arrays. The two pointer solution realizes you only ever need the smaller of the two maxes, and you can know which side that is without scanning ahead:

 1def trap(height):
 2    left, right = 0, len(height) - 1
 3    left_max, right_max = 0, 0
 4    water = 0
 5    while left < right:
 6        if height[left] < height[right]:
 7            left_max = max(left_max, height[left])
 8            water += left_max - height[left]
 9            left += 1
10        else:
11            right_max = max(right_max, height[right])
12            water += right_max - height[right]
13            right -= 1
14    return water

The key insight: when height[left] < height[right], we know right_max >= height[right] > height[left], so the water level at left is decided by left_max alone: the right side can’t be the bottleneck. O(n) time, O(1) space, and one of the most elegant eliminations in all of interview prep. If this proof feels slippery, that’s normal; walk through it with a small example like [4,2,0,3,2,5] until it clicks.


The Recognition Checklist

Before you write any code in an interview, run this:

Signal in the problemFlavor to try
Sorted array + pair/triplet with target sumConverging pointers
“Do it in place” / “O(1) extra space”Fast/slow pointers
Palindrome, reversal, symmetryConverging pointers
Max area / container between two endsConverging pointers with greedy move
Merge two sorted sequencesParallel pointers
Unsorted + exact lookup + indices matterNot two pointers: hash map

And when you commit to the technique, say the invariant out loud: “each step provably discards all pairs involving the pointer I move, so I never miss the answer.” Interviewers reward candidates who can justify the elimination, not just type the loop.

Two pointers also pairs naturally with its sibling technique for contiguous subarrays: if the problem says “longest substring” or “smallest window” instead of “pair,” you want the sliding window technique instead. The two get confused constantly; the difference is that sliding window maintains a window state between the pointers, while classic two pointers only cares about the endpoints.


Where to Go Next

Two pointers rewards structured practice more than almost any topic, because the three flavors cover so many problems once the elimination logic is instinct. In the 60 Days of Algorithms challenge, the groundwork lands early (arrays in Day 4 and Day 5, the partition idea in Day 26), so by the time you hit pair problems, the moves feel familiar. Create a free account to track your progress through all 60 days.



Happy coding. May your pointers always converge.