Skip to content

Two Pointers Technique

The Two Pointers technique uses two pointers to traverse a data structure, often moving towards each other or in the same direction. It’s a powerful pattern that can reduce O(n²) solutions to O(n).

When to Use Two Pointers

  • Sorted arrays - Find pairs, triplets, or subarrays
  • Palindrome problems - Check from both ends
  • Partitioning - Separate elements by condition
  • Merging - Combine sorted arrays
  • Removing duplicates - In-place modifications

Pattern 1: Opposite Direction

Two pointers start at opposite ends and move towards each other.

Array: [1, 2, 3, 4, 5, 6, 7]
↑ ↑
left right

Example: Two Sum (Sorted Array)

Given a sorted array, find two numbers that sum to a target.

def two_sum_sorted(nums: list[int], target: int) -> list[int]:
"""
Find indices of two numbers that sum to target.
Time: O(n), Space: O(1)
"""
left, right = 0, len(nums) - 1
while left < right:
current_sum = nums[left] + nums[right]
if current_sum == target:
return [left, right]
elif current_sum < target:
left += 1 # Need larger sum
else:
right -= 1 # Need smaller sum
return [] # No solution found
# Example
nums = [1, 2, 3, 4, 6]
print(two_sum_sorted(nums, 6)) # [1, 3] -> 2 + 4 = 6

Example: Valid Palindrome

Check if a string is a palindrome (ignoring non-alphanumeric characters).

def is_palindrome(s: str) -> bool:
"""
Check if string is palindrome.
Time: O(n), Space: O(1)
"""
left, right = 0, len(s) - 1
while left < right:
# Skip non-alphanumeric characters
while left < right and not s[left].isalnum():
left += 1
while left < right and not s[right].isalnum():
right -= 1
# Compare characters (case-insensitive)
if s[left].lower() != s[right].lower():
return False
left += 1
right -= 1
return True
# Examples
print(is_palindrome("A man, a plan, a canal: Panama")) # True
print(is_palindrome("race a car")) # False

Example: Container With Most Water

Find two lines that form a container holding the most water.

def max_area(height: list[int]) -> int:
"""
Find max water container area.
Time: O(n), Space: O(1)
"""
left, right = 0, len(height) - 1
max_water = 0
while left < right:
# Calculate current area
width = right - left
h = min(height[left], height[right])
current_area = width * h
max_water = max(max_water, current_area)
# Move the shorter line inward
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_water
# Example
heights = [1, 8, 6, 2, 5, 4, 8, 3, 7]
print(max_area(heights)) # 49

Pattern 2: Same Direction (Fast & Slow)

Both pointers move in the same direction, often at different speeds.

Array: [1, 1, 2, 2, 3, 4, 4]
↑ ↑
slow fast

Example: Remove Duplicates (In-Place)

Remove duplicates from a sorted array in-place.

def remove_duplicates(nums: list[int]) -> int:
"""
Remove duplicates in-place, return new length.
Time: O(n), Space: O(1)
"""
if not nums:
return 0
slow = 0 # Points to last unique element
for fast in range(1, len(nums)):
if nums[fast] != nums[slow]:
slow += 1
nums[slow] = nums[fast]
return slow + 1
# Example
nums = [1, 1, 2, 2, 3, 4, 4]
length = remove_duplicates(nums)
print(nums[:length]) # [1, 2, 3, 4]
print(length) # 4

Example: Move Zeroes

Move all zeroes to the end while maintaining order of other elements.

def move_zeroes(nums: list[int]) -> None:
"""
Move zeroes to end in-place.
Time: O(n), Space: O(1)
"""
slow = 0 # Position for next non-zero element
# Move all non-zero elements to the front
for fast in range(len(nums)):
if nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1
# Example
nums = [0, 1, 0, 3, 12]
move_zeroes(nums)
print(nums) # [1, 3, 12, 0, 0]

Pattern 3: Three Pointers (3Sum)

Extension of two pointers for finding triplets.

def three_sum(nums: list[int]) -> list[list[int]]:
"""
Find all unique triplets that sum to zero.
Time: O(n²), Space: O(1) excluding output
"""
nums.sort()
result = []
for i in range(len(nums) - 2):
# Skip duplicates for first element
if i > 0 and nums[i] == nums[i - 1]:
continue
left, right = i + 1, len(nums) - 1
while left < right:
total = nums[i] + nums[left] + nums[right]
if total == 0:
result.append([nums[i], nums[left], nums[right]])
# Skip duplicates
while left < right and nums[left] == nums[left + 1]:
left += 1
while left < right and nums[right] == nums[right - 1]:
right -= 1
left += 1
right -= 1
elif total < 0:
left += 1
else:
right -= 1
return result
# Example
nums = [-1, 0, 1, 2, -1, -4]
print(three_sum(nums)) # [[-1, -1, 2], [-1, 0, 1]]

Complexity Analysis

ProblemBrute ForceTwo Pointers
Two Sum (sorted)O(n²)O(n)
Valid PalindromeO(n) with extra spaceO(n) in-place
Container With Most WaterO(n²)O(n)
Remove DuplicatesO(n) with extra spaceO(n) in-place
3SumO(n³)O(n²)

Common Mistakes

  1. Forgetting to handle edge cases - Empty arrays, single elements
  2. Off-by-one errors - Check left < right vs left <= right
  3. Not skipping duplicates - Important for unique results
  4. Infinite loops - Always ensure pointers move

Practice Problems

ProblemDifficultyPattern
Two Sum IIEasyOpposite Direction
Valid PalindromeEasyOpposite Direction
Remove DuplicatesEasySame Direction
Move ZeroesEasySame Direction
Container With Most WaterMediumOpposite Direction
3SumMediumThree Pointers
Trapping Rain WaterHardTwo Pointers + Precompute

Key Takeaways

  1. Sorted arrays are ideal for two pointers (opposite direction)
  2. In-place modifications use same-direction pointers
  3. Always consider what condition moves each pointer
  4. Watch for duplicates when collecting unique results
  5. Two pointers often reduces O(n²) to O(n)

Next Steps