Two Pointers Explained Visually: One Pass Instead of Two Loops
6 min readBytePatterns
Why two indices walking a sorted array turn an O(n²) pair search into O(n), the elimination argument behind it, and the three shapes the pattern takes.
Two pointers is not one algorithm. It is a shape that a surprising number of array questions collapse into once you notice it — and noticing it is worth more in an interview than memorising any single solution built on top of it.
The problem it solves
Take the plainest version. Given a sorted array and a target, find two values that add up to the target.
The obvious answer is every pair: for each index, try every later index. That is O(n²) and it works. It also ignores the fact that the array is sorted, which — as with binary search — is where the saving is hiding.
The intuition
Put one index at each end. Add the two values it points at and compare with the target.
- Sum is too small. The right-hand value is already the largest one available, so no pair using the current left value can ever reach the target. That left value is finished. Move left inward.
- Sum is too big. By the same argument, the left value is the smallest available, so the current right value cannot be part of any valid pair. Move right inward.
- Sum matches. Done.
That is the whole correctness argument, and it is worth stating exactly like that: each step eliminates a value permanently, with a reason. Nothing is revisited, so the pointers can only move toward each other. Each of the n positions is discarded at most once, which makes the pass O(n).
The sortedness is doing all the work. It is what licenses "the largest available" and "the smallest available" — and it is why the same loop on an unsorted array is simply wrong rather than merely slow.
Watch it run
Coral marks the pair currently being added; the readout shows the running sum against the target. Watch which pointer moves after each comparison, and ask yourself why the other one stayed still.
Two Pointers
Step 1 of 9
The array is sorted, so start as wide as possible: one pointer at each end.
The same interactive animation as the lesson — step through it with the controls.
Every frame is a value being ruled out. On an array of six, the pass ends in at most five comparisons — where the nested-loop version would do fifteen.
The code
def pair_sum(nums, target):
"""nums must be sorted ascending. Returns (i, j) or None."""
left, right = 0, len(nums) - 1
while left < right: # never let them cross or meet
total = nums[left] + nums[right]
if total == target:
return (left, right)
if total < target:
left += 1 # need a bigger value
else:
right -= 1 # need a smaller value
return None
print(pair_sum([1, 3, 4, 6, 8, 11], 10)) # (2, 3) -> 4 + 6
print(pair_sum([1, 3, 4, 6, 8, 11], 100)) # None
print(pair_sum([2, 2], 4)) # (0, 1)
while left < right and not <=: at equality both pointers are on the same element, and using one value twice is almost never what the question means.
The three shapes it comes in
The converging pair above is one of three, and interviewers rotate between them.
- Converging. Start at both ends, move inward. Pair sums, palindrome checks, "container with most water", reversing in place.
- Same direction, one slow and one fast. A reader index runs ahead while a writer index marks where the kept values go. This is how you remove duplicates or filter an array in place with
O(1)extra space. - Two sequences at once. One pointer per array, both moving forward. Merging two sorted lists, intersecting them, comparing them.
Where it goes wrong
- Forgetting the array must be sorted. The elimination argument depends on it entirely. If sorting is allowed, the total becomes
O(n log n)— still far better than quadratic, but say the number. - Returning values when the question wants indices. Sorting destroys original positions. If the caller needs the original indices of an unsorted input, sorting first is the wrong move and a hash map is the right one.
<=instead of<. Lets a single element pair with itself.- Moving both pointers on a match. When the question asks for all pairs rather than one, advancing only one side re-finds the same pair. Move both, and skip duplicates on each side.
- Applying it to an unsortable relation. The trick needs "bigger sum means move this way" to be true. If the quantity you are comparing is not monotonic in the pointer positions, there is no valid elimination and the loop silently returns nonsense.
How to say it in an interview
Lead with the elimination, not the pointers:
"Because the array is sorted, the largest possible partner for the leftmost value is the rightmost one. If that sum is still short of the target, the leftmost value cannot be in any answer, so I discard it and move left inward. Symmetrically for the other side. Every step retires exactly one element, so the scan is O(n) time and O(1) space — against O(n²) for checking every pair. If the input were not sorted I would either sort first, making it O(n log n), or use a hash map for a single O(n) pass at the cost of O(n) space."
Three approaches, their costs, and a reason for the choice. That is the answer; the loop is just typing.