Skip to content
C Codeloom
DSA

The Two Pointers Technique: A Practical Guide

A practical guide to the two pointers technique — opposite-end and same-direction patterns, when to use each, and six classic interview problems with worked solutions.

·9 min read · By Yash Kesharwani
Intermediate 12 min read

What you'll learn

  • When the two pointers technique applies
  • The opposite-end (converging) pattern
  • The same-direction (fast/slow) pattern
  • How to convert nested loops into linear scans
  • Six classic problems and their idiomatic solutions

Prerequisites

  • Comfortable with Python lists and loops
  • Basic understanding of Big-O notation

The two pointers technique is one of the most quietly useful tricks in algorithm design. It turns many problems that look like they need a nested loop — O(n^2) — into a single linear pass — O(n). The idea is simple: maintain two indices into an array (or string), and move them according to a rule that makes progress on every step.

This post covers the two main patterns, when to reach for each, and a handful of classic problems where the technique shines.

The core idea

A pointer is just an index. With two pointers, you can compare or process two positions at once. The art is choosing a rule for moving them so that each step either solves the problem or rules out a chunk of the search space.

There are two patterns you will see again and again:

  1. Opposite ends — start at the two ends and move toward each other.
  2. Same direction — start at the same end, move at different speeds (often “fast” and “slow”).

Pattern 1: Opposite ends (converging)

You begin with left = 0 and right = len(arr) - 1, then move inward. This pattern fits sorted arrays, palindrome checks, and pair-sum style problems.

index: 0 1 2 3 4 5 arr: [ 1, 3, 4, 5, 7, 10 ] ↑ ↑ L R

step 1: arr[L] + arr[R] = 1 + 10 = 11 > target=9 → move R left step 2: arr[L] + arr[R] = 1 + 7 = 8 < target=9 → move L right step 3: arr[L] + arr[R] = 3 + 7 = 10 > target=9 → move R left step 4: arr[L] + arr[R] = 3 + 5 = 8 < target=9 → move L right step 5: arr[L] + arr[R] = 4 + 5 = 9 = target=9 → found!

Opposite-end pointers converging on a sorted array

Here is the canonical pair-sum on a sorted array:

def pair_sum_sorted(arr, target):
    L, R = 0, len(arr) - 1
    while L < R:
        s = arr[L] + arr[R]
        if s == target:
            return (L, R)
        if s < target:
            L += 1
        else:
            R -= 1
    return None

print(pair_sum_sorted([1, 3, 4, 5, 7, 10], 9))
# output: (2, 3)

Every iteration either finds the answer or shrinks the window by one. That is O(n) time and O(1) extra space.

Why does it work?

If arr[L] + arr[R] < target, then no pairing of R with anything to the left of L can reach target either (since those are smaller). So we can safely retire L. The symmetric argument retires R when the sum is too large. That is the heart of two pointers: each move eliminates a whole row or column of the implicit n x n search.

Pattern 2: Same direction (fast / slow)

Both pointers start at the left. One moves faster than the other, or one waits for a condition. This pattern fits in-place array modification, removing duplicates, and window-style scanning.

input: [ 1, 1, 2, 2, 3, 4 ] ↑ i (writes unique values)

scan: [ 1, 1, 2, 2, 3, 4 ] ↑ ↑ w r

after one pass: [ 1, 2, 3, 4, 3, 4 ] ↑ w points past the unique prefix

Same-direction pointers compacting an array in place

Here is “remove duplicates from a sorted array” in place:

def remove_duplicates(arr):
    if not arr:
        return 0
    w = 1  # write index
    for r in range(1, len(arr)):
        if arr[r] != arr[r - 1]:
            arr[w] = arr[r]
            w += 1
    return w  # length of the unique prefix

nums = [1, 1, 2, 2, 3, 4]
k = remove_duplicates(nums)
print(k, nums[:k])
# output: 4 [1, 2, 3, 4]

The read pointer r walks through every element; the write pointer w only advances when we find something new. O(n) time, O(1) space.

When to reach for it

Look for these signals in a problem:

  • The input is sorted (or can be sorted cheaply).
  • You are asked about pairs, triplets, or subarrays.
  • A naive solution is O(n^2) with two nested loops over the same array.
  • The problem mentions in-place modification.
  • You are checking a palindrome or any “mirror” property.

Try it yourself. Before reading on, write a function is_palindrome(s) that uses two pointers (no s[::-1] shortcuts). Trace it on "racecar" and "hello" to convince yourself the loop terminates correctly.

Palindrome check — the gentlest example

def is_palindrome(s):
    L, R = 0, len(s) - 1
    while L < R:
        if s[L] != s[R]:
            return False
        L += 1
        R -= 1
    return True

print(is_palindrome("racecar"))   # output: True
print(is_palindrome("hello"))     # output: False

The structure — two pointers, a comparison, symmetric moves — is the template you will reuse over and over.

A subtler example: three sum

The 3Sum problem (find all unique triplets that sum to zero) at first looks like O(n^3). With sorting plus two pointers it drops to O(n^2):

def three_sum(nums):
    nums.sort()
    result = []
    n = len(nums)
    for i in range(n - 2):
        if i > 0 and nums[i] == nums[i - 1]:
            continue  # skip duplicates for the fixed element
        L, R = i + 1, n - 1
        target = -nums[i]
        while L < R:
            s = nums[L] + nums[R]
            if s == target:
                result.append([nums[i], nums[L], nums[R]])
                L += 1
                R -= 1
                while L < R and nums[L] == nums[L - 1]:
                    L += 1
                while L < R and nums[R] == nums[R + 1]:
                    R -= 1
            elif s < target:
                L += 1
            else:
                R -= 1
    return result

print(three_sum([-1, 0, 1, 2, -1, -4]))
# output: [[-1, -1, 2], [-1, 0, 1]]

The outer loop fixes one number; the inner two-pointer scan finds pairs that complete the triple. Sorting first is the move that unlocks the technique.

Try it yourself. Modify three_sum to return the count of unique triplets instead of the triplets themselves. Then extend it to four_sum — you should need two nested loops plus the two-pointer scan, giving O(n^3).

Practice problems

Each problem comes with a short description and a worked solution. Try each one for ten minutes before peeking.

1. Valid Palindrome

Given a string, consider only alphanumeric characters and ignore case. Return whether it is a palindrome.

def is_valid_palindrome(s):
    L, R = 0, len(s) - 1
    while L < R:
        while L < R and not s[L].isalnum():
            L += 1
        while L < R and not s[R].isalnum():
            R -= 1
        if s[L].lower() != s[R].lower():
            return False
        L += 1
        R -= 1
    return True

print(is_valid_palindrome("A man, a plan, a canal: Panama"))
# output: True

2. Two Sum II — Input Array Sorted

Given a 1-indexed sorted array and a target, return the indices of the two numbers that add to the target.

def two_sum_sorted(numbers, target):
    L, R = 0, len(numbers) - 1
    while L < R:
        s = numbers[L] + numbers[R]
        if s == target:
            return [L + 1, R + 1]
        if s < target:
            L += 1
        else:
            R -= 1

print(two_sum_sorted([2, 7, 11, 15], 9))
# output: [1, 2]

3. 3Sum

Already shown above. The key tricks are sorting once, fixing one index in an outer loop, and deduplicating by skipping equal neighbours.

4. Container With Most Water

Given heights, find two lines that together with the x-axis form the container that holds the most water.

def max_area(height):
    L, R = 0, len(height) - 1
    best = 0
    while L < R:
        area = (R - L) * min(height[L], height[R])
        best = max(best, area)
        if height[L] < height[R]:
            L += 1
        else:
            R -= 1
    return best

print(max_area([1, 8, 6, 2, 5, 4, 8, 3, 7]))
# output: 49

Why move the smaller side? Because moving the taller side can only shrink the area (the width drops, and the height is still capped by the shorter side).

5. Trapping Rain Water

Given an elevation map, compute how much water it can trap.

def trap(height):
    L, R = 0, len(height) - 1
    left_max = right_max = 0
    water = 0
    while L < R:
        if height[L] < height[R]:
            left_max = max(left_max, height[L])
            water += left_max - height[L]
            L += 1
        else:
            right_max = max(right_max, height[R])
            water += right_max - height[R]
            R -= 1
    return water

print(trap([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]))
# output: 6

The trick: at each step, the smaller side is the bottleneck, so we can safely commit to its water level.

6. Remove Duplicates from Sorted Array

Already shown above. The same-direction pattern with a write pointer that lags the read pointer.

Recap

You now know:

  • Two pointers turns many O(n^2) scans into O(n).
  • Opposite-end pointers suit sorted arrays, palindromes, and pair-sum problems.
  • Same-direction pointers suit in-place modification and dedup work.
  • Sorting first often unlocks the technique (3Sum, pair-sum).
  • The correctness argument is always the same: each move eliminates values that cannot be part of the answer.

Next steps

The sliding window technique is a close cousin of same-direction two pointers — same shape, but with the invariant managed inside a moving window. That is the next post.

→ Next: The Sliding Window Technique

Questions or feedback? Email codeloomdevv@gmail.com.