The Two-Pointer Technique: One Pattern, Dozens of Problems
How the two-pointer technique works, the two main variants, and how to spot two-pointer problems in interviews before you waste time on brute force.
Two pointers is one of those techniques that feels too simple to be useful — until you realize it's the key to solving a huge chunk of array and string problems in O(n) instead of O(n^2). It shows up in interviews constantly, and once you learn to recognize the pattern, problems that looked hard become almost mechanical.
The Basic Idea
You have two index variables (the "pointers") that move through an array or string. Instead of nested loops checking every pair (O(n^2)), the pointers move intelligently based on some condition, giving you O(n) total.
There are two main flavors:
- Opposite-direction pointers — one starts at the beginning, one at the end, they move toward each other
- Same-direction pointers — both start near the beginning, one moves faster (fast/slow) or one defines a window boundary
Opposite-Direction Pointers
This variant works when you have a sorted array (or something with a natural ordering) and you need to find pairs or boundaries.
Two Sum on a Sorted Array
Given a sorted array and a target, find two numbers that add up to the target.
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
Why this works: if the sum is too small, moving left right increases it (array is sorted). If too large, moving right left decreases it. You never miss a valid pair because you're always moving in the direction that could find the answer.
O(n) time, O(1) space. Compare to the hash map approach which is also O(n) time but O(n) space — on a sorted array, two pointers wins on space.
Container With Most Water
Classic medium problem. You have bars at different heights, pick two to form a container, maximize the water area.
def max_area(height):
left, right = 0, len(height) - 1
best = 0
while left < right:
width = right - left
h = min(height[left], height[right])
best = max(best, width * h)
if height[left] < height[right]:
left += 1
else:
right -= 1
return best
The insight: always move the shorter side inward. Moving the taller side can never increase the area (the height is limited by the shorter side, and the width is decreasing). Moving the shorter side might find a taller bar that increases the area.
Valid Palindrome
Check if a string is a palindrome (ignoring non-alphanumeric characters).
def is_palindrome(s):
left, right = 0, len(s) - 1
while left < right:
while left < right and not s[left].isalnum():
left += 1
while left < right and not s[right].isalnum():
right -= 1
if s[left].lower() != s[right].lower():
return False
left += 1
right -= 1
return True
Opposite pointers converging from both ends, comparing as they go. Straightforward.
Same-Direction Pointers
This is where things get more interesting. Both pointers move in the same direction, but at different speeds or with different roles.
Remove Duplicates In-Place
Given a sorted array, remove duplicates in-place and return the new length. You need O(1) extra space.
def remove_duplicates(nums):
if not nums: return 0
write = 1
for read in range(1, len(nums)):
if nums[read] != nums[read - 1]:
nums[write] = nums[read]
write += 1
return write
read scans through every element. write marks where the next unique element should go. The write pointer only advances when we find something new. This "read/write pointer" pattern shows up in any problem where you need to filter or compact an array in-place.
Sliding Window (Two Pointers in Disguise)
Sliding window is really just same-direction two pointers where left and right define a window boundary. The window expands (move right) and contracts (move left) based on some condition.
def length_of_longest_substring(s):
seen = set()
left = 0
best = 0
for right in range(len(s)):
while s[right] in seen:
seen.remove(s[left])
left += 1
seen.add(s[right])
best = max(best, right - left + 1)
return best
right expands the window. When we hit a duplicate, left contracts until the duplicate is gone. Each character is added and removed from the set at most once, so it's O(n) total despite the nested while loop.
Fast/Slow Pointers (Linked Lists)
Floyd's cycle detection: slow pointer moves one step, fast pointer moves two steps. If there's a cycle, they'll meet. If there isn't, the fast pointer hits null.
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
This same pattern finds the middle of a linked list (when fast reaches the end, slow is at the middle), detects cycles, and finds the start of cycles.
How to Recognize Two-Pointer Problems
This is the real skill. Here are the signals:
Opposite-direction pointers — look for:- Sorted array + finding pairs
- Palindrome checking
- "Container" or "trapping" problems with boundaries
- Any problem where brute force would check all pairs with nested loops on a sorted input
- "In-place" modification with O(1) space
- Substring/subarray problems with a condition (sliding window)
- "Longest/shortest subarray with property X"
- Linked list problems involving middle, cycle, or nth-from-end
Common Mistakes
Forgetting that opposite-direction pointers need sorted input. If the array isn't sorted and you try the shrinking window approach, you'll miss valid pairs. Sort first, or use a hash map instead. Off-by-one errors with window boundaries. Is your window[left, right] inclusive on both ends? Or left, right) exclusive on the right? Pick one convention and stick with it. Most sliding window implementations use inclusive on both ends.
Moving the wrong pointer. In opposite-direction problems, think carefully about which pointer to move and why. The reasoning is usually: "moving this pointer could improve the answer, moving the other one cannot."
Practice Strategy
Start with these problems in order:
- Two Sum II (sorted array) — basic opposite pointers
- Valid Palindrome — opposite with skipping
- Container With Most Water — opposite with greedy reasoning
- Remove Duplicates — same-direction read/write
- Longest Substring Without Repeating Characters — sliding window
- Linked List Cycle — fast/slow
Once you've done these six, you'll recognize the pattern in most new problems. [CodeUp organizes problems by technique, so you can drill two-pointer problems back to back until pattern recognition becomes second nature. That's the fastest way to get comfortable — not doing one two-pointer problem mixed in with fifty other random problems, but batching them until the pattern is obvious on sight.
Two pointers isn't fancy. It's just moving two indices intelligently instead of using brute force nested loops. But that simple shift in thinking unlocks O(n) solutions to problems that most people initially solve in O(n^2). And interviewers notice when you jump straight to the efficient approach.