The Two-Pointer Technique
Turn nested O(n²) scans into a single O(n) sweep. See both pointers move through the array, step by step.
Here is a problem that looks like it needs a nested loop: given a sorted array, find two numbers that add up to a target. The brute-force answer checks every pair — that’s O(n²). The two-pointer technique does it in a single O(n) sweep, and once you see it move, you’ll never forget it.
The idea: put one pointer at the far left (the smallest value) and one at the far right (the largest). Their sum tells you exactly which pointer to move.
Step through it. When the sum is too big, the only way to shrink it is to move the right pointer left (to a smaller value). When the sum is too small, move the left pointer right (to a bigger value). Every move eliminates a value that could never be part of a solution — which is why one linear pass is enough.
Why it works
Sortedness is the secret ingredient. Because the array is ordered, moving a pointer changes the sum in a predictable direction. That predictability is what lets you discard a candidate without ever revisiting it. Take away the sorted order and this trick evaporates — which is a strong hint that if a problem mentions “sorted,” two pointers should be the first pattern you reach for.
def two_sum_sorted(arr, target):
lo, hi = 0, len(arr) - 1
while lo < hi:
s = arr[lo] + arr[hi]
if s == target:
return (lo, hi)
if s > target:
hi -= 1 # sum too big → shrink from the right
else:
lo += 1 # sum too small → grow from the left
return None
The broader pattern
“Two pointers” is really a family of techniques where two indices move through a sequence, usually toward each other or in the same direction:
- Converging pointers (this lesson): start at both ends and meet in the middle. Great for pair-sum, container-with-most-water, and palindrome checks.
- Fast/slow pointers: two pointers moving at different speeds through a linked list — the classic cycle-detection trick.
- Read/write pointers: one scans ahead while another writes compacted results, used for in-place deduplication and partitioning.
When to reach for it
Reach for two pointers when you’re tempted to write a nested loop over a linear structure and there’s some order or symmetry you can exploit — sortedness, a target to converge on, or a partition point. If you can phrase the problem as “move the boundary that makes progress,” this pattern usually applies.
Interview tell: any time you catch yourself writing
for i ... for j > i, pause and ask whether a single pass with two pointers could replace it.
Pattern recognition
Variations
Worked problems
Notice how each solution eliminates a nested comparison by proving one pointer move is safe.
Valid Palindrome
Decide whether a string reads the same forward and backward after ignoring non-alphanumeric characters and case.
Approach. Put one pointer at each end. Skip characters that do not count, then compare lowercase versions. A mismatch proves the string is not a palindrome; crossing pointers means every relevant pair matched.
Show solution
def is_palindrome(s):
left = 0
right = len(s) - 1
while left < right:
while left < right and not s[left].isalnum():
left += 1
while left < right and not s[right].isalnum():
right -= 1
if s[left].lower() != s[right].lower():
return False
left += 1
right -= 1
return TrueComplexity. O(n) time and O(1) extra space. Each pointer only moves inward.
3Sum
Return all unique triplets of numbers that sum to zero. The order of triplets in the output does not matter.
Approach. Sort the numbers. Fix the first value, then use two pointers on the remaining suffix to find pairs that sum to its negation. Skip duplicate fixed values and duplicate pointer values so each triplet appears once.
Show solution
def three_sum(nums):
nums.sort()
ans = []
for i in range(len(nums) - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue
left = i + 1
right = len(nums) - 1
while left < right:
total = nums[i] + nums[left] + nums[right]
if total == 0:
ans.append([nums[i], nums[left], nums[right]])
left += 1
right -= 1
while left < right and nums[left] == nums[left - 1]:
left += 1
while left < right and nums[right] == nums[right + 1]:
right -= 1
elif total < 0:
left += 1
else:
right -= 1
return ansComplexity. O(n²) time. Auxiliary space is O(n) for Python’s sort in the worst case, plus the output.
Container With Most Water
Given vertical line heights, choose two lines that form a container with the x-axis and hold the most water. The area is width times the shorter height.
Approach. Start with the widest container. The shorter side limits the area, so moving the taller side inward cannot improve the limiting height and only shrinks width. Move the shorter side and keep the best area seen.
Show solution
def max_area(height):
left = 0
right = len(height) - 1
best = 0
while left < right:
width = right - left
best = max(best, width * min(height[left], height[right]))
if height[left] < height[right]:
left += 1
else:
right -= 1
return bestComplexity. O(n) time and O(1) extra space. Each step discards one boundary that cannot beat the current width with its limiting height.