Loading learning content...
Knowing how to implement a two-pointer solution is only half the battle. The other half—arguably the more valuable half—is knowing when to use it.
Pattern recognition is the skill that separates experienced engineers from beginners. An expert sees a problem and immediately thinks "this is a two-pointer problem" or "this won't work with two-pointer, I need dynamic programming." A beginner wastes time trying approaches that don't fit.
This page teaches you to recognize:
By the end of this page, you'll have a mental checklist for evaluating whether two-pointer applies to a given problem. This pattern recognition ability accelerates your problem-solving significantly.
Two-pointer is not a universal technique. It requires specific structural properties in the problem. Let's enumerate these prerequisites clearly.
The Monotonicity Principle:
This is the most important prerequisite. Two-pointer works when:
Moving one pointer in a direction consistently increases or decreases some target metric.
For example, in a sorted array searching for a pair sum:
left right → increases the sum (sorted property)right left → decreases the sum (sorted property)This monotonicity lets you make informed decisions about which pointer to move based on whether the current sum is too large or too small.
If the array is unsorted, moving left or right has unpredictable effects on the sum. You might move left right and get a smaller sum (if a large value is replaced by a small one). Without monotonicity, converging two-pointer doesn't work.
Some problem patterns almost always indicate a two-pointer solution. Learn to recognize these signals:
| Signal | Example Problem | Pointer Type |
|---|---|---|
| "Sorted array" mentioned | Two Sum II | Converging |
| "In-place" modification required | Remove Duplicates | Same-direction |
| "O(1) space" constraint | Move Zeroes | Same-direction |
| "Find pair/triple satisfying X" | 3Sum | Converging |
| "Palindrome" check | Valid Palindrome | Converging |
| "Container/Area" optimization | Container With Most Water | Converging |
| "Cycle detection" in sequence | Linked List Cycle | Fast-slow |
| "Partition" or "segregate" | Sort Colors | Same-direction |
| "Merge two sorted X" | Merge Sorted Arrays | Two pointers, one per array |
Signal: "Find two elements that satisfy a condition in sorted array"
This is the classic indicator. When you see:
Immediately think: converging two-pointer.
Signal: "Modify array in-place with O(1) space"
When you need to:
Think: same-direction two-pointer with reader/writer positions.
Signal: "Check symmetry"
Palindrome, mirrored structure, comparing ends:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
# SIGNAL: "sorted array" + "find pair"# Immediate thought: converging two-pointer # Example: Find pair with difference = kdef find_pair_with_difference(arr: list[int], k: int) -> tuple[int, int] | None: """ In sorted array, find pair with difference = k. Approach: Start both pointers at 0, not opposite ends. If difference < k, advance right (increase difference). If difference > k, advance left (decrease difference). """ arr.sort() left = 0 right = 1 while right < len(arr): diff = arr[right] - arr[left] if diff == k and left != right: return (arr[left], arr[right]) elif diff < k: right += 1 else: left += 1 if left == right: right += 1 return None # SIGNAL: "in-place" + "O(1) space"# Immediate thought: same-direction reader/writer # Example: Remove elements in-placedef remove_value(nums: list[int], val: int) -> int: writer = 0 for reader in range(len(nums)): if nums[reader] != val: nums[writer] = nums[reader] writer += 1 return writer # SIGNAL: "check if X is palindrome"# Immediate thought: converging from both ends # Example: Check if array is palindromicdef is_palindromic_array(arr: list[int]) -> bool: left, right = 0, len(arr) - 1 while left < right: if arr[left] != arr[right]: return False left += 1 right -= 1 return TrueEqually important is recognizing when two-pointer is the wrong approach. Here are red flags:
Example: Longest Increasing Subsequence (LIS)
Can we solve LIS with two-pointer? Let's analyze:
Two-pointer fails because:
Example: Subarray with Given Sum (Unsorted)
For positive-only arrays, sliding window (a form of two-pointer) works. For arrays with negatives... it doesn't! Why?
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
# RED FLAG: Longest Increasing Subsequence# Two-pointer CANNOT solve this efficiently # Wrong approach: trying two-pointerdef lis_two_pointer_wrong(nums): # This doesn't work! Two-pointer can't find optimal subsequence # because LIS elements may not be at the ends pass # Correct approach: Dynamic Programming O(n^2) or O(n log n)def lis_correct(nums: list[int]) -> int: if not nums: return 0 dp = [1] * len(nums) # dp[i] = LIS ending at index i for i in range(1, len(nums)): for j in range(i): if nums[j] < nums[i]: dp[i] = max(dp[i], dp[j] + 1) return max(dp) # RED FLAG: Subarray sum with negatives# Two-pointer/sliding window fails due to lack of monotonicity # For positive-only: sliding window worksdef subarray_sum_positive(nums: list[int], target: int) -> list[int]: """Works for positive-only arrays.""" left = 0 current_sum = 0 for right in range(len(nums)): current_sum += nums[right] while current_sum > target and left <= right: current_sum -= nums[left] left += 1 if current_sum == target: return [left, right] return [] # For arrays with negatives: need different approachdef subarray_sum_any(nums: list[int], target: int) -> list[int]: """Works for any array using prefix sums.""" prefix_sum = 0 seen = {0: -1} # prefix_sum -> index for i, num in enumerate(nums): prefix_sum += num if prefix_sum - target in seen: return [seen[prefix_sum - target] + 1, i] seen[prefix_sum] = i return [] # Example where two-pointer fails:# nums = [1, -2, 3, -1, 2], target = 3# Subarray [3] or [1, -2, 3, -1, 2] both sum to 3 or 3# But sliding window would fail because adding -2 decreases sum# then adding 3 increases it - no monotonicity!Ask yourself: 'If I move the left pointer right, does the target metric always increase (or always decrease)? Same for the right pointer moving left.' If the answer is 'it depends' or 'sometimes yes, sometimes no', two-pointer likely won't work.
Some problems aren't obviously two-pointer, but can be transformed into two-pointer problems with the right insight:
The classic Two Sum problem with an unsorted array:
Option 1: Sort + Two-Pointer
Option 2: Hash Map
For unsorted Two Sum, the hash map approach is often better (linear time). But if you need k-sum or the data is already sorted, two-pointer shines.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
# BORDERLINE: Two Sum on unsorted array# Can be solved with two-pointer after sorting def two_sum_with_indices(nums: list[int], target: int) -> list[int]: """ Two Sum returning original indices, using sort + two-pointer. Time: O(n log n) Space: O(n) for storing index pairs """ # Store (value, original_index) pairs indexed = [(val, idx) for idx, val in enumerate(nums)] indexed.sort(key=lambda x: x[0]) # Sort by value left, right = 0, len(indexed) - 1 while left < right: current_sum = indexed[left][0] + indexed[right][0] if current_sum == target: return [indexed[left][1], indexed[right][1]] elif current_sum < target: left += 1 else: right -= 1 return [] # BORDERLINE: Subarray with maximum sum of length k# This is sliding window (same-direction two-pointer) def max_sum_subarray_k(nums: list[int], k: int) -> int: """ Find max sum of subarray of length exactly k. Sliding window: maintain window of size k. Time: O(n) Space: O(1) """ if len(nums) < k: return 0 # Initialize with first window window_sum = sum(nums[:k]) max_sum = window_sum # Slide window for i in range(k, len(nums)): window_sum += nums[i] - nums[i - k] # Add new, remove old max_sum = max(max_sum, window_sum) return max_sum # BORDERLINE: Two arrays, find pair closest to targetdef two_array_closest_pair(arr1: list[int], arr2: list[int], target: int) -> tuple: """ Find one element from each array such that their sum is closest to target. Approach: Sort both, use converging-like logic. """ arr1.sort() arr2.sort() i = 0 j = len(arr2) - 1 closest = float('inf') best_pair = (arr1[0], arr2[0]) while i < len(arr1) and j >= 0: current_sum = arr1[i] + arr2[j] if abs(current_sum - target) < abs(closest - target): closest = current_sum best_pair = (arr1[i], arr2[j]) if current_sum < target: i += 1 elif current_sum > target: j -= 1 else: return best_pair # Exact match return best_pair # Testprint(two_sum_with_indices([3, 2, 4], 6)) # [1, 2] (2 + 4 = 6)print(max_sum_subarray_k([1, 4, 2, 10, 2, 3, 1, 0, 20], 4)) # 24 (10+2+3+1=16? Let's see... 2+3+1+0=6, 3+1+0+20=24)print(two_array_closest_pair([1, 4, 5, 7], [10, 20, 30, 40], 32)) # (7, 30) sum=37 or (4, 30) sum=34When two-pointer doesn't apply, here are the common alternatives and when to use each:
| Technique | When to Use | Example Problems |
|---|---|---|
| Hash Map/Set | Need O(1) lookups, unsorted data, counting | Two Sum, subarray sum with negatives |
| Dynamic Programming | Optimal substructure + overlapping subproblems | LIS, edit distance, coin change |
| Binary Search | Sorted data, looking for specific element | Search in rotated array, peak finding |
| Sliding Window | Contiguous subarray/substring optimization | Max sum subarray of size k |
| Stack | Matching pairs, next greater element, parentheses | Valid parentheses, daily temperatures |
| Prefix Sum | Range sum queries, subarray sums | Subarray sum equals k |
| Divide and Conquer | Problem splits into independent subproblems | Merge sort, closest pair of points |
| Backtracking | Need to explore all possibilities | Permutations, combinations, N-queens |
Decision Flowchart:
Is the data sorted or can you sort it?
├─ YES → Is it a pair/triple finding problem?
│ ├─ YES → Use CONVERGING TWO-POINTER
│ └─ NO → Is it searching for a specific value?
│ ├─ YES → Use BINARY SEARCH
│ └─ NO → Consider SLIDING WINDOW or other
│
└─ NO → Need O(1) lookups or counting?
├─ YES → Use HASH MAP/SET
└─ NO → Does problem have optimal substructure?
├─ YES → Use DYNAMIC PROGRAMMING
└─ NO → Need to explore all possibilities?
├─ YES → Use BACKTRACKING
└─ NO → Analyze further...
Many problems combine techniques. For example, 3Sum uses a loop (fix one element) + two-pointer (find pairs). Four Sum uses two loops + two-pointer. Binary search + two-pointer appears in problems like 'find k closest elements'. Don't limit yourself to one technique per problem.
When you encounter a new problem, run through this checklist:
Quick Heuristics:
| If you see... | Think... |
|---|---|
| "Sorted array" | Converging two-pointer |
| "In-place" | Same-direction reader/writer |
| "Find pair/triple" | Two-pointer or hash map |
| "Palindrome" | Converging from both ends |
| "O(1) space" | Two-pointer or careful in-place |
| "Cycle in list" | Fast-slow pointers |
| "Merge sorted" | One pointer per array |
| "Partition/segregate" | Same-direction swap pointer |
To solidify pattern recognition, here are problems categorized by technique:
| Problem | Difficulty | Key Insight |
|---|---|---|
| Two Sum II (sorted) | Easy | Classic converging sum search |
| 3Sum | Medium | Fix one + two-pointer for pairs |
| Container With Most Water | Medium | Move shorter wall inward |
| Valid Palindrome | Easy | Compare from both ends |
| Trapping Rain Water | Hard | Two-pointer with max tracking |
| 4Sum | Medium | Two loops + two-pointer |
| Problem | Difficulty | Key Insight |
|---|---|---|
| Remove Duplicates from Sorted Array | Easy | Reader-writer paradigm |
| Remove Element | Easy | Skip unwanted values |
| Move Zeroes | Easy | Swap non-zeros forward |
| Sort Colors | Medium | Dutch National Flag, 3 pointers |
| Partition List (linked list) | Medium | Two separate lists merged |
| Problem | Difficulty | Key Insight |
|---|---|---|
| Linked List Cycle | Easy | Floyd's cycle detection |
| Linked List Cycle II (find start) | Medium | Two-phase: detect + find start |
| Find the Duplicate Number | Medium | Array as linked list |
| Middle of the Linked List | Easy | Fast reaches end, slow is middle |
| Happy Number | Easy | Sequence forms cycle or terminates |
| Problem | Correct Approach | Why Not Two-Pointer |
|---|---|---|
| Longest Increasing Subsequence | DP or binary search | Non-contiguous, needs optimal substructure |
| Subarray Sum (with negatives) | Hash map + prefix sum | No monotonicity |
| Minimum Window Substring | Sliding window + hash map | Need tracking structure, not pure two-pointer |
| Edit Distance | Dynamic programming | 2D optimization problem |
You now have a comprehensive framework for recognizing when two-pointer applies. Let's consolidate:
The Master Heuristic:
Two-pointer works when you can guarantee that each pointer movement eliminates many candidates from consideration, and you never need to revisit eliminated candidates.
If both conditions hold, two-pointer likely applies. If either fails, look elsewhere.
Congratulations! You've completed the Two-Pointer Technique module. You now understand both variants (same-direction and converging), can implement classic problems, and—most importantly—know when to apply this technique and when to reach for alternatives. This pattern recognition skill will accelerate your problem-solving throughout your career.