Two Pointers Technique in Python: Patterns, Examples and When to Use It

Two nested loops compare every pair of positions, and that is usually too slow. Two indexes that move by a simple rule can often do the same job in one pass. The technique shows up with sorted arrays, palindromes, in-place edits and linked lists. Below are the two main patterns, four worked problems with tested code, and a way to tell when it applies.

What is the two pointers technique?

A pointer here is just an index into an array or a string. Using two of them lets you look at two places at once and decide, based on what you see, which one to move. The power comes from the fact that each pointer only moves in one direction, so the total number of moves is at most about 2n. There are two common shapes: pointers that start at opposite ends and move toward each other, and pointers that start together and move in the same direction at different speeds.

Pattern 1: pointers from opposite ends

This pattern fits sorted arrays and problems about symmetry. Place one pointer at the start and one at the end, compare, and move one of them inward.

Two Sum II (sorted array)

Given a sorted array, find two numbers that add up to a target. If the current sum is too small, the only way to increase it is to move the left pointer right. If it is too large, move the right pointer left.

def two_sum_sorted(numbers, target):
    lo, hi = 0, len(numbers) - 1
    while lo < hi:
        s = numbers[lo] + numbers[hi]
        if s == target:
            return [lo + 1, hi + 1]      # LeetCode uses 1-based indexes
        if s < target:
            lo += 1
        else:
            hi -= 1
    return []

For [2, 7, 11, 15] and target 9, we start with 2 + 15 = 17, which is too large, so hi moves left. Then 2 + 11 = 13, still too large. Then 2 + 7 = 9, and we return [1, 2]. The time is O(n) and the extra space is O(1).

Valid Palindrome

Check whether a string reads the same forwards and backwards, ignoring punctuation and letter case.

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

Skipping non-alphanumeric characters with inner loops avoids building a cleaned copy of the string, so the space stays O(1).

Container With Most Water

Given vertical lines of various heights, choose two lines that together with the x-axis hold the most water. The area is the smaller height multiplied by the distance between the lines.

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

Why is it safe to move the shorter line? Because the width always shrinks when you move a pointer, the only chance to find a larger area is to replace the limiting (shorter) line with a taller one. Moving the taller line can never help, since the shorter line still limits the height. For [1, 8, 6, 2, 5, 4, 8, 3, 7] the best area is 49.

Pattern 2: pointers in the same direction

Here one pointer reads and the other writes, or one pointer moves faster than the other. A typical example is removing duplicates from a sorted array in place.

def remove_duplicates(nums):
    if not nums:
        return 0
    write = 1
    for read in range(1, len(nums)):
        if nums[read] != nums[write - 1]:
            nums[write] = nums[read]
            write += 1
    return write

The read pointer scans every element, and the write pointer marks where the next unique value should be placed. After the loop, the first write elements contain the unique values in order. The time is O(n) and the space is O(1).

Bringing it all together: 3Sum

3Sum asks for all unique triplets that add up to zero. Sort the array, then fix one number at a time and use the opposite-ends pattern on the remainder. Skip repeated values to avoid duplicate triplets. The total time is O(n²), which is a big improvement over the O(n³) brute force.

A harder example: Trapping Rain Water

Given bars of different heights, how much rain water stays trapped between them? Water above a bar is limited by the shorter of the tallest bar to its left and the tallest bar to its right. The brute force computes both maxima for every position, which costs O(n²), or O(n) extra memory if you precompute them. Two pointers get it down to O(1) memory.

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

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

The reasoning is the same kind of safe-move argument as in Container With Most Water. If the left bar is shorter than the right bar, then the right side already holds something at least as tall as the left bar, so the water above the left position depends only on left_max. We can settle that position now and move lo inward. The mirror case handles the right side. Each step finalises one position, so the loop runs n times in total.

How to recognise a two pointers problem

  • The array is sorted, or sorting it would not break the problem.
  • You need a pair or triplet that meets a condition on sum, difference or product.
  • You must modify an array in place with O(1) extra space.
  • The problem involves symmetry, such as palindromes.
  • You are working with a linked list and need the middle element or a cycle (fast and slow pointers).

Complexity summary

Problem Time Space
Two Sum II O(n) O(1)
Valid Palindrome O(n) O(1)
Container With Most Water O(n) O(1)
Remove Duplicates O(n) O(1)
3Sum O(n²) O(1) extra, plus sorting

How to prove a pointer move is safe

Every opposite-ends solution rests on one argument: the pointer you are about to move cannot belong to a better answer that you have not seen yet. Writing that argument as a single sentence is the quickest test of whether you understand the problem.

In Two Sum II, suppose the current sum is too small. The right pointer already holds the largest value still in play, so no other partner for the left element could make the sum bigger. The left element is finished, and moving lo up is safe. In Container With Most Water, the area is the width times the shorter wall. Keep the shorter wall and move the taller one inward, and the width shrinks while the height is still capped by the same shorter wall, so the area cannot improve. That is why you always move the shorter side.

If you cannot produce a sentence like that for your pointer rule, you are guessing, and an interviewer who asks “why that pointer?” will notice.

Mistakes that look fine until the second test

The first sample usually passes. The trouble shows up later. In 3Sum, a triple that is found without skipping equal neighbours on both sides is reported twice. Writing lo <= hi where lo < hi is needed pairs an element with itself. A branch that forgets to move either pointer produces a loop that never ends, and the quickest way to catch it is to ask, for every branch, “which pointer moved?”

When the rule depends on a running total over a range rather than a pair, check sliding window or prefix sums instead. The same two-ended thinking drives binary search on sorted data, and the fast and slow pointers that find a cycle live in linked list problems.