The core idea: two indices, one pass
Most beginners reach for nested loops when a problem involves comparing elements — check every element against every other, an O(n squared) sweep. The two pointers pattern replaces that with two indices that move through the data deliberately, so you visit each position roughly once. Instead of re-scanning the whole array to ask 'what pairs with element i?', you let the second pointer's position carry information about what you have already ruled out.
The trick is that each pointer moves in only one direction and never backtracks. Because neither index resets, the total number of steps is bounded by the length of the array — that is what turns a quadratic brute force into a linear scan.
Two shapes: converging and fast–slow
There are two common layouts. In the converging (opposite-ends) form, one pointer starts at index 0 and the other at the last index, and they walk toward each other until they meet. This suits pair-sum searches on sorted arrays, in-place reversal, and palindrome checks.
In the fast–slow (same-direction) form, both pointers start near the front but advance at different rates or under different conditions. A slow 'write' pointer trails a fast 'read' pointer to compact an array in place — removing duplicates or zeros — while a fast pointer moving two steps to a slow pointer's one detects cycles in a linked list (Floyd's algorithm). Watching the two indices crawl through the array step by step makes which shape you need click faster than reading the loop.
Why it works on sorted data
For pair-sum problems the converging pattern relies on order. When the array is sorted, the sum of the two ends tells you exactly which way to move: if the sum is too small, the only way to increase it is to move the left pointer right toward larger values; if it is too big, move the right pointer left. Each comparison eliminates one candidate for good, so a single pass suffices.
This is also why sorting is sometimes worth the O(n log n) cost up front: it unlocks the linear scan for problems like 3Sum, where you fix one element and two-point the rest. On unsorted data without that structure, a hash table is often the better tool.
def two_sum_sorted(nums, target):
left, right = 0, len(nums) - 1
while left < right:
current = nums[left] + nums[right]
if current == target:
return [left, right]
if current < target:
left += 1 # sum too small, need a larger value
else:
right -= 1 # sum too big, need a smaller value
return [-1, -1] # no pair foundA single scan is O(n) time and O(1) extra space because neither pointer backtracks. If the input must be sorted first, the sort's O(n log n) dominates. Nested two-pointer variants like 3Sum are O(n squared).
When to use it
- Searching a sorted array for a pair or triplet whose values meet a sum or difference condition
- Reversing an array or string in place, or checking whether a string is a palindrome
- Removing duplicates, zeros, or unwanted elements from a sorted array in place with a read/write pointer
- Detecting a cycle or finding the midpoint of a linked list using fast and slow pointers
Watch out for
- Applying the converging sum trick to unsorted data — the 'move left or right' logic only works when the array is sorted, and sorting first adds O(n log n).
- Getting the loop condition wrong: using left <= right when an element must not pair with itself, or advancing the wrong pointer, which causes infinite loops or missed answers.
- Forgetting to skip over duplicate values in problems like 3Sum, which produces repeated result tuples.
Two pointers is one of the highest-frequency patterns in coding interviews, especially for array and string rounds at product companies and campus placement drives. Classic prompts include Two Sum II (sorted input), valid palindrome, reverse string, remove duplicates from a sorted array, container with most water, and 3Sum. Interviewers rarely say 'use two pointers' by name — they expect you to notice that a sorted input or an in-place requirement is the hint. What they are really watching is whether you can justify why each pointer moves the way it does, state the O(n) time and O(1) space out loud, and handle edge cases like empty input, duplicates, and the pointers crossing.