Two Pointer Technique — Core Concepts
Why two pointers work
The two pointer technique reduces O(n²) brute-force pair checking to O(n) by exploiting structure in the data — usually sorted order or linked list topology. Instead of examining every possible pair, two pointers narrow the search space by eliminating large groups of candidates with each comparison.
Pattern 1: Converging pointers
Place one pointer at the start and one at the end. Move them toward each other based on a condition.
When to use: Sorted arrays, pair problems, palindrome checks.
Two Sum in sorted array
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]
elif current < target:
left += 1 # need a bigger sum
else:
right -= 1 # need a smaller sum
return []
print(two_sum_sorted([1, 3, 5, 7, 9, 11], 12)) # [1, 4] → 3+9
Why it works: When the sum is too small, increasing the left pointer (to a larger value) is the only way to increase the sum. When too large, decreasing the right pointer is the only way to decrease it. Every step eliminates an entire row or column of possibilities.
Palindrome check
def is_palindrome(s):
s = s.lower()
left, right = 0, len(s) - 1
while left < right:
# Skip non-alphanumeric
while left < right and not s[left].isalnum():
left += 1
while left < right and not s[right].isalnum():
right -= 1
if s[left] != s[right]:
return False
left += 1
right -= 1
return True
print(is_palindrome("A man, a plan, a canal: Panama")) # True
Container with most water
def max_area(height):
left, right = 0, len(height) - 1
best = 0
while left < right:
area = min(height[left], height[right]) * (right - left)
best = max(best, area)
if height[left] < height[right]:
left += 1
else:
right -= 1
return best
Pattern 2: Same-direction pointers
Both pointers start at the beginning and move right, but at different rates or under different conditions.
When to use: In-place array modification, removing duplicates, partitioning.
Remove duplicates from sorted array (in-place)
def remove_duplicates(nums):
if not nums:
return 0
write = 1 # slow pointer: where to write next unique value
for read in range(1, len(nums)): # fast pointer: scans ahead
if nums[read] != nums[read - 1]:
nums[write] = nums[read]
write += 1
return write # number of unique elements
nums = [1, 1, 2, 2, 3, 4, 4]
k = remove_duplicates(nums)
print(nums[:k]) # [1, 2, 3, 4]
Move zeros to end
def move_zeroes(nums):
write = 0
for read in range(len(nums)):
if nums[read] != 0:
nums[write], nums[read] = nums[read], nums[write]
write += 1
nums = [0, 1, 0, 3, 12]
move_zeroes(nums)
print(nums) # [1, 3, 12, 0, 0]
Pattern 3: Fast and slow pointers
Two pointers move at different speeds through a linked list or sequence. Classic application: cycle detection.
When to use: Linked list cycles, finding middle elements, detecting patterns in sequences.
Linked list cycle detection (Floyd’s algorithm)
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next # moves 1 step
fast = fast.next.next # moves 2 steps
if slow == fast:
return True
return False
Why it works: If there is a cycle, the fast pointer will eventually catch up to the slow pointer inside the cycle. If there is no cycle, the fast pointer reaches the end. The key mathematical insight: in a cycle of length L, the fast pointer closes the gap by 1 node each step, so they must meet within L steps of the slow pointer entering the cycle.
Find middle of linked list
def find_middle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow # middle node
When fast reaches the end, slow is at the middle. This avoids the need to count the list length first.
The 3Sum pattern
3Sum combines sorting with converging two pointers:
def three_sum(nums):
nums.sort()
result = []
for i in range(len(nums) - 2):
if i > 0 and nums[i] == nums[i-1]:
continue # skip duplicates
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]])
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
print(three_sum([-1, 0, 1, 2, -1, -4]))
# [[-1, -1, 2], [-1, 0, 1]]
Time: O(n²) — one loop × two pointer scan. This is optimal for 3Sum; no known O(n log n) solution exists.
How to identify two pointer problems
- “Pair in sorted array” → converging pointers
- “In-place modification” → same-direction (read/write pointers)
- “Palindrome” → converging from both ends
- “Cycle detection” → fast/slow pointers
- “K-sum” → sort + nested converging pointers
Common misconception
“Two pointers only work on sorted arrays.” Two pointers work on any data with exploitable structure. Sorted order is the most common, but linked list topology (fast/slow), partition invariants (Dutch national flag), and monotonic conditions also enable two-pointer solutions.
One thing to remember: The two pointer technique works because each pointer movement eliminates possibilities that cannot lead to a better answer. The art is choosing what condition controls each pointer’s movement.
See Also
- Python Backtracking Algorithms How computers solve puzzles by trying every path, backing up when stuck, and never giving up until they find the answer.
- Python Big O Complexity Analysis Why some programs finish in a blink while others take forever — explained with pizza delivery and toy cleanup.
- Python Binary Search Implementation Find anything in a sorted list insanely fast using the same trick you already use with dictionaries and phone books.
- Python Dynamic Programming The clever trick of remembering answers you already figured out so you never solve the same puzzle twice.
- Python Graph Algorithms How computers navigate maps, friendships, and connections using dots and lines — explained without any math.