Introduction
The two pointer technique is one of the most fundamental algorithmic patterns you'll encounter in coding interviews. It's elegant, efficient, and once you master it, you'll recognize opportunities to apply it everywhere.
At its core, the two pointer technique involves using two variables (pointers) to traverse a data structure—typically an array or string—in a coordinated way. Instead of using nested loops (O(n²)), two pointers often reduce the time complexity to O(n).
Why Two Pointers Matter
Consider this: if you're asked to find a pair of numbers in a sorted array that sum to a target, a naive approach would check every possible pair:
# Naive approach - O(n²)
def find_pair(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 []With two pointers, we can solve this in O(n):
# Two pointer approach - O(n)
def find_pair(arr, target):
left, right = 0, len(arr) - 1
while left < right:
current_sum = arr[left] + arr[right]
if current_sum == target:
return [left, right]
elif current_sum < target:
left += 1
else:
right -= 1
return []When to Use Two Pointers
Look for these signals that suggest a two pointer solution:
- Sorted array or string* - The most common indicator
- Finding pairs or triplets* with a specific sum or property
- Comparing elements from both ends* (palindromes, reversing)
- In-place modifications* without extra space
- Merging sorted arrays* or linked lists
- Removing duplicates* from sorted data
- Partitioning* elements based on a condition
The Key Insight
- Two pointers work because they systematically eliminate possibilities. In a sorted array looking for a target sum:
- If the current sum is too small, moving the left pointer right increases it
- If the current sum is too large, moving the right pointer left decreases it
- Each move eliminates one possibility, guaranteeing we check all valid pairs
Pattern 1: Opposite Direction (Start & End)
This is the most common pattern. Pointers start at opposite ends and move toward each other.
Template
def opposite_direction(arr):
left, right = 0, len(arr) - 1
while left < right:
# Process current pair
if condition_met(arr[left], arr[right]):
return result
# Move pointers based on comparison
if should_move_left(arr[left], arr[right]):
left += 1
else:
right -= 1
return default_resultExample: Container With Most Water
Given heights of vertical lines, find two lines that form a container holding the most water.
def max_area(height):
left, right = 0, len(height) - 1
max_water = 0
while left < right:
# Calculate current area
width = right - left
h = min(height[left], height[right])
max_water = max(max_water, width * h)
# Move the shorter line inward
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_waterWhy this works: The width decreases as pointers move inward, so we can only increase area by finding a taller line. Moving the taller line can never increase the area (height is limited by the shorter line), so we always move the shorter one.
Pattern 2: Same Direction (Fast & Slow)
Both pointers start at the same position but move at different speeds or under different conditions.
Template
def same_direction(arr):
slow = 0
for fast in range(len(arr)):
if should_include(arr[fast]):
arr[slow] = arr[fast]
slow += 1
return slow # Length of processed portionExample: Remove Duplicates from Sorted Array
Remove duplicates in-place and return the new length.
def remove_duplicates(nums):
if not nums:
return 0
slow = 1 # Position for next unique element
for fast in range(1, len(nums)):
if nums[fast] != nums[fast - 1]:
nums[slow] = nums[fast]
slow += 1
return slowExample: Move Zeroes
Move all zeros to the end while maintaining order of non-zero elements.
def move_zeroes(nums):
slow = 0 # Position for next non-zero
for fast in range(len(nums)):
if nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1Pattern 3: Two Arrays
Use one pointer for each array, useful for merging or comparing sorted arrays.
Example: Merge Sorted Arrays
def merge(nums1, m, nums2, n):
# Start from the end to avoid overwriting
p1, p2, p = m - 1, n - 1, m + n - 1
while p1 >= 0 and p2 >= 0:
if nums1[p1] > nums2[p2]:
nums1[p] = nums1[p1]
p1 -= 1
else:
nums1[p] = nums2[p2]
p2 -= 1
p -= 1
# Copy remaining elements from nums2
nums1[:p2 + 1] = nums2[:p2 + 1]Common Mistakes to Avoid
1. Off-by-One Errors Always clarify whether to use `left < right` or `left <= right`: - Use `<` when looking for pairs (different indices) - Use `<=` when processing single elements
2. Infinite Loops Ensure at least one pointer moves in each iteration:
# BAD - potential infinite loop
while left < right:
if condition:
# Do something but don't move pointers!
pass
# GOOD - always move at least one pointer
while left < right:
if condition:
result = process(left, right)
left += 1 # or right -= 13. Forgetting the Sorted Requirement Two pointers with opposite direction typically requires sorted input. If unsorted, either: - Sort first (adds O(n log n)) - Use a different approach (hash map, etc.)
4. Not Handling Edge Cases - Empty array - Single element - All elements are the same - No valid pairs exist
Practice Problems
Start with these problems to build your two pointer intuition:
- Beginner:*
- Two Sum II (sorted array)
- Valid Palindrome
- Remove Duplicates from Sorted Array
- Intermediate:*
- 3Sum
- Container With Most Water
- Trapping Rain Water
- Advanced:*
- 4Sum
- Minimum Window Substring (with sliding window)
Key Takeaways
- Recognize the pattern:* Sorted arrays, pair/triplet finding, in-place modifications
- Choose the right variant:* Opposite direction vs. same direction vs. two arrays
- Think about invariants:* What property do your pointers maintain?
- Practice the movement logic:* When should each pointer move?
- Handle edge cases:* Empty inputs, single elements, no solution
The two pointer technique is foundational—master it, and you'll have a powerful tool for dozens of interview problems.