Two pointers
Use two indices (or pointers) that move through the same array, often from opposite ends or at different speeds. Common when the input is sorted or when you’re looking for pairs/subarrays.
When to apply
- Input is sorted, or you sort it yourself
- Finding pairs that sum to a target
- Reversing / partitioning in place
- Removing duplicates from a sorted array
- Squeezing from both ends
Pattern: opposite ends
def two_sum_sorted(arr: list[int], target: int) -> tuple[int, int] | None:
"""In a sorted array, find indices of two values summing to target."""
left, right = 0, len(arr) - 1
while left < right:
s = arr[left] + arr[right]
if s == target:
return (left, right)
elif s < target:
left += 1
else:
right -= 1
return None
O(n) time, O(1) space. The sortedness is what makes the directional move correct: if the sum is too small, only moving left rightward can increase it.
Pattern: same direction (slow + fast)
def remove_duplicates(arr: list[int]) -> int:
"""In-place dedupe of a sorted array. Returns new length."""
if not arr:
return 0
slow = 0
for fast in range(1, len(arr)):
if arr[fast] != arr[slow]:
slow += 1
arr[slow] = arr[fast]
return slow + 1
slow tracks the next position to write; fast scans for new values.
Pattern: meet in the middle (palindrome)
def is_palindrome(s: str) -> bool:
s = "".join(c.lower() for c in s if c.isalnum())
left, right = 0, len(s) - 1
while left < right:
if s[left] != s[right]:
return False
left += 1
right -= 1
return True
Problem: container with most water
Given height[], find two lines that form a container with most water:
def max_area(height: list[int]) -> int:
left, right = 0, len(height) - 1
best = 0
while left < right:
h = min(height[left], height[right])
best = max(best, h * (right - left))
# Move the shorter wall — moving the taller one can never improve
if height[left] < height[right]:
left += 1
else:
right -= 1
return best
Greedy insight: width shrinks each step, so we must increase height — only achievable by moving the shorter side.
Problem: 3-sum
Find all unique triplets that sum to zero:
def three_sum(nums: list[int]) -> list[list[int]]:
nums.sort()
result = []
for i in range(len(nums) - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue # skip duplicates for first element
target = -nums[i]
left, right = i + 1, len(nums) - 1
while left < right:
s = nums[left] + nums[right]
if s == target:
result.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 s < target:
left += 1
else:
right -= 1
return result
O(n²) — sort + n × two-pointer.
Pitfalls
- Off-by-one on
while left < rightvs<=— for pairs you almost always want strict<. - Forgetting to handle duplicates in result-collecting problems (3-sum, 4-sum).
- Sorting destroys original indices — keep them if needed.
Interview angle
- “When do you reach for two pointers?” - a sorted array where you need a pair or triple meeting a condition, in-place partitioning or removal, or comparing from both ends. The signal is that moving one pointer lets you rule out a whole range without checking it.
- “Why is it
O(n)rather thanO(n^2)?” - each pointer only moves forward, so together they take at most2nsteps. That monotonic movement is what replaces the nested loop. - “Two-sum on a sorted array?” - pointers at both ends; if the sum is too small move left up, too large move right down.
O(n)time andO(1)space, versusO(n)extra space for the hash-map approach on an unsorted array. - “Fast and slow pointers?” - cycle detection, finding the middle, and the k-th from the end in one pass over a linked list. See ../03_linked_lists/01_basics_reverse_cycle.md.