Quick Answer

The two pointer technique uses two indexes moving through an array instead of nested loops, turning many O(n squared) problems into O(n). There are two main shapes: pointers starting at opposite ends and moving toward each other, used on sorted arrays for pair-sum and palindrome problems, and two pointers moving in the same direction at different speeds, used for removing duplicates in place and for cycle detection. It works when moving a pointer lets you safely discard possibilities you never need to check again.

The Problem It Replaces

Take a common question: given a sorted array, find two numbers that add up to a target. The obvious solution checks every pair.

def two_sum_brute(arr, target):
    for i in range(len(arr)):
        for j in range(i + 1, len(arr)):
            if arr[i] + arr[j] == target:
                return [i, j]
    return []

That is O(n squared) — for a 100,000-element array it is five billion comparisons and will time out on any serious judge. The two pointer version does it in one pass.

def two_sum_sorted(arr, target):
    left, right = 0, len(arr) - 1
    while left < right:
        total = arr[left] + arr[right]
        if total == target:
            return [left, right]
        elif total < target:
            left += 1        # need a bigger sum, move the small end up
        else:
            right -= 1       # need a smaller sum, move the big end down
    return []

Each step moves one pointer, and the pointers only ever move toward each other, so the loop runs at most n times. O(n) time, O(1) extra space.

Why It Is Actually Correct

This is the part worth understanding, because it tells you when the technique applies at all. The code looks like it skips possibilities — and it does. The question is whether the skipped ones could ever have been answers.

Suppose the sum is too small. The smallest element is arr[left]. Pairing it with arr[right], the largest remaining value, already fell short. So arr[left] paired with anything else in the range is smaller still, and cannot reach the target. Every pair involving arr[left] is eliminated at once, which is why moving it forward is safe.

The mirror argument applies when the sum is too big: arr[right] paired with the smallest available value already overshoots, so it cannot work with anything.

Each step discards a whole row or column of the comparison grid rather than a single cell. That is where the factor of n disappears.

The requirement this exposes: the array must be sorted. On unsorted data the reasoning collapses, because a small sum no longer implies that everything to the left is smaller. If the input is unsorted and you need pair sums, use a hash set for O(n) time, or sort first and accept O(n log n).

Pattern One: Pointers at Opposite Ends

Start at both ends and converge. Use it when the answer depends on a pair, and moving one end changes the result predictably.

Palindrome check. Compare the ends, step inward, stop on the first mismatch.

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

Container with most water. Two lines form a container; area is the shorter height times the distance. Start wide and always move the shorter side inward — moving the taller one can only reduce the width without ever raising the limiting height.

Reversing in place. Swap the ends and step inward. O(n) time, O(1) space, no second array.

Three sum. Sort, fix one element, then run the two pointer scan on the rest. That gives O(n squared) overall — much better than the O(n cubed) triple loop.

Pattern Two: Both Pointers Moving Forward

Here both pointers start near the beginning and move the same way at different speeds. One is often called the slow or write pointer and the other fast or read.

Remove duplicates from a sorted array in place. The read pointer scans; the write pointer marks where the next unique value belongs.

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

This is the shape behind "modify the array in place and return the new length", a very common interview instruction. Building a new list is easier but uses O(n) extra space, which the question is usually trying to rule out.

Move zeroes to the end, partition around a value and merge two sorted arrays all use the same structure.

Fast and slow for cycle detection. In a linked list, advance one pointer by one node and the other by two. If there is a cycle they must eventually meet; if the fast one reaches the end, there is none. This is Floyd's algorithm, and it detects a cycle in O(n) time with O(1) space where the obvious solution needs a hash set.

When Not to Reach For It

The pattern is popular enough that people try to apply it everywhere. It fails in predictable ways.

  • Unsorted data with a pair condition. Without order, moving a pointer discards possibilities you cannot rule out. Use a hash set instead.
  • You need every pair, not one. If the task is to count or list all pairs satisfying something, you may genuinely need the nested loop — you cannot skip work when the output requires it.
  • The condition is not monotonic. Two pointers rely on moving in one direction making things reliably bigger or smaller. If the value jumps around, there is no safe direction to move.
  • Subarray sums with negative numbers. A common trap: the sliding-window variant assumes that extending a window increases the sum. Negatives break that, and the answer is a prefix-sum-plus-hash-map approach instead.

A quick test before committing: ask yourself what does moving this pointer eliminate, and am I certain none of it could be the answer? If you can state that in one sentence, the technique applies. If you cannot, it probably does not.

Frequently Asked Questions

Does the array need to be sorted for two pointers? For the opposite-ends pattern, almost always yes — the correctness argument depends on order. The same-direction pattern, such as removing duplicates or moving zeroes, does not always require sorting. If you need pair sums on unsorted data, a hash set gives O(n) without sorting.
What is the time complexity of the two pointer technique? Usually O(n), since each pointer traverses the array at most once. If you had to sort first, the overall complexity becomes O(n log n) because the sort dominates. Space is normally O(1), which is often the real reason the pattern is asked for.
What is the difference between two pointers and sliding window? Sliding window is a special case of two pointers where both move forward and the region between them is the thing you care about. General two pointers may move from opposite ends, and the region between them is not necessarily the answer.
How do fast and slow pointers detect a cycle? The fast pointer moves two steps per iteration and the slow one moves a single step. In a cycle the fast pointer gains one position per iteration on the slow one, so it must eventually land on it. Without a cycle, the fast pointer reaches the end first.
Which problems should I practise for this pattern? Two Sum on a sorted array, valid palindrome, reverse a string in place, remove duplicates from a sorted array, move zeroes, container with most water, three sum, and linked list cycle detection. Those cover both shapes and most of the variations interviewers ask.