What you'll learn
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 TrueContainer 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 prefixThis 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.
