Two Pointers — The Complete Guide
Master the Two Pointers pattern from first principles. Learn to recognize it instantly, evolve brute-force into optimal solutions, and confidently solve interview questions.
Table of Contents
Intuition from Scratch
Why does this pattern exist?
Imagine you have a sorted list of numbers and someone asks: "Find two numbers that add up to 15." The brute-force way is to check every possible pair — pick the first number, then scan the rest. That's O(n²). Painfully slow for large inputs.
But wait — the list is sorted. That's a massive clue. If you place one finger at the start (smallest number) and another at the end (largest number), you can make intelligent decisions:
- Sum too small? Move the left finger right (need a bigger number)
- Sum too big? Move the right finger left (need a smaller number)
- Sum just right? Found it.
Each step eliminates an entire row or column of possibilities. You never go backwards. One pass, O(n). That's the Two Pointers pattern — using two indices that move strategically to avoid redundant work.
Analogies to Build Intuition
The Dictionary Search
Looking for a word in a physical dictionary. You open to the middle — too far? Go left. Not far enough? Go right. Two Pointers is like having two bookmarks, one near the front and one near the back, and moving them inward based on what you find.
Two People Walking Toward Each Other
Imagine two people at opposite ends of a hallway. They walk toward each other, each adjusting their speed based on a condition. They'll eventually meet in the middle. That's the opposite-direction variant — you process the entire array in one pass.
The Slow and Fast Runner
Two runners on a track, one fast and one slow. If the track has a loop, the fast runner will eventually lap the slow one — that's how you detect cycles. If there's no loop, the fast runner reaches the end first — that's how you find the middle.
What kind of problems does it solve?
Two Pointers is your go-to when:
- You're working with a sorted array or string
- You need to find pairs or triplets with a specific property
- You need to partition or rearrange elements in-place
- You need to compare or merge two sequences
- You want to reduce O(n²) brute force to O(n)
The core insight
Two Pointers works because each pointer movement eliminates possibilities that you'd otherwise check in a nested loop. The sorted order (or some other structural property) guarantees that skipped possibilities can't contain the answer.
Pattern Recognition
Recognizing Two Pointers in the wild is the most important skill. Here are the signals to look for and the traps to avoid.
Keywords & Signals in the Problem Statement
Use Two Pointers when you see...
- ✅"Sorted array" + find pair/triplet with sum X
- ✅"In-place" removal or rearrangement
- ✅"Palindrome" checking or manipulation
- ✅"Merge two sorted arrays/lists"
- ✅"Container with most water" / area problems
- ✅"Remove duplicates from sorted array"
- ✅"Partition array" around a pivot
- ✅"Subsequence" check (is A a subsequence of B?)
- ✅"Closest pair" to a target value
- ✅O(1) extra space constraint on array problems
Don't use Two Pointers when...
- ❌Array is unsorted AND you can't sort it (use Hashing instead)
- ❌You need contiguous subarray sum/length (use Sliding Window)
- ❌Problem requires all combinations/subsets (use Backtracking)
- ❌You need to find elements by value in unsorted data (use Hash Map)
- ❌The problem involves a tree or graph structure
- ❌You need the count of subarrays (usually Prefix Sum or Sliding Window)
Two Pointers vs. Similar Patterns
| Pattern | When to Use | Key Difference |
|---|---|---|
| Two Pointers | Sorted data, pairs, in-place operations | Pointers move based on comparison logic, not window size |
| Sliding Window | Contiguous subarray/substring with constraint | Window expands/shrinks to maintain a condition — both pointers move right |
| Binary Search | Find a specific value or boundary in sorted data | Halves the search space each step — only one "pointer" (mid) moves |
| Hashing | Unsorted data, need O(1) lookup | Trades space for time — no pointer movement, just map lookups |
The sorting question
If the input isn't sorted but the problem screams "Two Pointers," ask yourself: "Can I sort it first?" If sorting doesn't break the problem (e.g., you don't need original indices), sort it and then apply Two Pointers. This is exactly how 3Sum works.
Brute Force → Optimized Thinking
Let's walk through the thinking evolution using a concrete example: Two Sum II — given a sorted array, find two numbers that add up to a target.
Brute Force — Check Every Pair
The most naive approach: for each element, scan every element after it and check if they sum to the target. This works but is slow because we're doing redundant comparisons.
function twoSum(nums: number[], target: number): number[] { for (let i = 0; i < nums.length; i++) { for (let j = i + 1; j < nums.length; j++) { if (nums[i] + nums[j] === target) return [i + 1, j + 1]; } } return []; } // Problem: We're ignoring the fact that the array is SORTED.
Better — Binary Search for Complement
Since the array is sorted, for each element nums[i], we can binary search for (target - nums[i]) in the remaining array. This uses the sorted property but still has an outer loop.
function twoSum(nums: number[], target: number): number[] { for (let i = 0; i < nums.length; i++) { const complement = target - nums[i]; // Binary search for complement in nums[i+1..end] let lo = i + 1, hi = nums.length - 1; while (lo <= hi) { const mid = lo + Math.floor((hi - lo) / 2); if (nums[mid] === complement) return [i + 1, mid + 1]; if (nums[mid] < complement) lo = mid + 1; else hi = mid - 1; } } return []; } // Better! But we can do even better by using BOTH ends.
Optimal — Two Pointers from Both Ends
The key insight: place one pointer at the start (smallest) and one at the end (largest). The sum of these two tells us exactly which pointer to move. If the sum is too small, we need a bigger number → move left pointer right. If too big → move right pointer left. Each step eliminates one candidate, so we process the entire array in a single pass.
function twoSum(nums: number[], target: number): number[] { let left = 0, right = nums.length - 1; while (left < right) { const sum = nums[left] + nums[right]; if (sum === target) return [left + 1, right + 1]; if (sum < target) left++; // need bigger sum → move left forward else right--; // need smaller sum → move right backward } return []; } // Perfect: O(n) time, O(1) space. Uses the sorted property fully.
Why Does Each Optimization Work?
Brute → Binary Search
We realized the inner loop is just 'searching for a value in a sorted array' — that's exactly what binary search does. We replaced O(n) inner scan with O(log n) binary search.
Binary Search → Two Pointers
We realized that after checking a pair, the sorted order tells us WHICH direction to move. We don't need to restart the search — we can maintain two pointers and converge. This eliminates the outer loop entirely.
The General Principle
Each optimization exploits more of the problem's structure. Brute force ignores everything. Binary search uses 'sorted'. Two Pointers uses 'sorted' + 'the answer about the current pair tells us which pointer to move'. The more structure you exploit, the faster the solution.
The thinking process matters more than the code
In interviews, walking through this brute → better → optimal progression shows the interviewer how you think. Even if you know the optimal solution immediately, briefly mention the brute force and explain why you're optimizing. It demonstrates depth of understanding.
Core Templates
Two Pointers has three main templates. Memorize these structures — most problems are variations of one of them.
Template 1: Opposite Direction (Converging)
Pointers start at both ends and move toward each other. Used for pair-finding in sorted arrays, palindrome checks, and container problems.
function oppositeDirection(arr: number[]): Result { let left = 0; let right = arr.length - 1; while (left < right) { // Compute something with arr[left] and arr[right] const current = compute(arr[left], arr[right]); if (current === target) { return result; // found answer } else if (current < target) { left++; // need bigger → move left forward } else { right--; // need smaller → move right backward } } return notFound; } // When to use: sorted array + pair search, palindrome, container/area
Template 2: Same Direction (Fast & Slow / Read & Write)
Both pointers start at the beginning. One moves faster or conditionally. Used for in-place removal, deduplication, and partitioning.
function sameDirection(arr: number[]): number { let slow = 0; // write pointer — marks the "good" portion for (let fast = 0; fast < arr.length; fast++) { // fast = read pointer, scans every element if (shouldKeep(arr[fast])) { arr[slow] = arr[fast]; // write to the "good" portion slow++; } } return slow; // length of the "good" portion } // When to use: remove duplicates, remove element, move zeroes
Template 3: Two Sequences
One pointer per sequence. Used for merging sorted arrays, checking subsequences, and comparing strings.
function twoSequences(arr1: number[], arr2: number[]): Result { let i = 0; // pointer for arr1 let j = 0; // pointer for arr2 while (i < arr1.length && j < arr2.length) { if (arr1[i] < arr2[j]) { // process arr1[i] i++; } else if (arr1[i] > arr2[j]) { // process arr2[j] j++; } else { // arr1[i] === arr2[j] — match found // process both i++; j++; } } // Handle remaining elements in either array return result; } // When to use: merge sorted arrays, intersection, is-subsequence
Which template to pick?
Ask yourself: (1) Am I searching for a pair in one sorted array? → Opposite Direction. (2) Am I filtering/rearranging elements in-place? → Same Direction. (3) Am I comparing or merging two separate sequences? → Two Sequences.
Variations & Sub-Patterns
Two Pointers isn't a single trick — it's a family of techniques. Here are the most common variations and how the approach changes for each.
| Variation | Pointer Movement | Classic Example |
|---|---|---|
| Pair Sum (sorted) | left→ and ←right converge based on sum comparison | Two Sum II, 3Sum |
| Palindrome Check | left→ and ←right converge, comparing characters | Valid Palindrome, Palindrome Linked List |
| Container / Area | left→ and ←right converge, move the shorter side | Container With Most Water, Trapping Rain Water |
| Remove / Deduplicate | slow (write) + fast (read) move same direction | Remove Duplicates, Remove Element, Move Zeroes |
| Partition (Dutch Flag) | Three pointers: low, mid, high | Sort Colors (0s, 1s, 2s) |
| Merge Two Sorted | One pointer per array, advance the smaller | Merge Sorted Array, Merge Two Sorted Lists |
| Subsequence Check | One pointer per string, advance both on match | Is Subsequence |
| Three Sum / K-Sum | Fix one element, two-pointer on the rest | 3Sum, 4Sum, 3Sum Closest |
Deep Dive: The 3Sum Technique
3Sum is the most important Two Pointers extension. The idea: reduce a 3-element problem to a 2-element problem by fixing one element and running Two Pointers on the rest. This generalizes to K-Sum.
Problems mentioned above
function threeSum(nums: number[]): number[][] { nums.sort((a, b) => a - b); const result: number[][] = []; for (let i = 0; i < nums.length - 2; i++) { // Skip duplicates for the fixed element if (i > 0 && nums[i] === nums[i - 1]) continue; let left = i + 1; let right = nums.length - 1; const target = -nums[i]; // we need nums[left] + nums[right] = -nums[i] while (left < right) { const sum = nums[left] + nums[right]; if (sum === target) { result.push([nums[i], nums[left], nums[right]]); // Skip duplicates while (left < right && nums[left] === nums[left + 1]) left++; while (left < right && nums[right] === nums[right - 1]) right--; left++; right--; } else if (sum < target) { left++; } else { right--; } } } return result; } // Time: O(n²) — can't do better for 3Sum // Space: O(1) ignoring output
Deep Dive: The Dutch National Flag (3-Way Partition)
When you need to partition an array into three groups (e.g., Sort Colors: 0s, 1s, 2s), use three pointers: low, mid, and high. Elements before low are group 1, between low and mid are group 2, after high are group 3.
function sortColors(nums: number[]): void { let low = 0, mid = 0, high = nums.length - 1; while (mid <= high) { if (nums[mid] === 0) { [nums[low], nums[mid]] = [nums[mid], nums[low]]; low++; mid++; } else if (nums[mid] === 1) { mid++; } else { [nums[mid], nums[high]] = [nums[high], nums[mid]]; high--; // Don't advance mid — the swapped element needs checking } } } // Time: O(n) single pass | Space: O(1)
Visual Walkthrough
Let's trace through Container With Most Water step by step to see Two Pointers in action. Input: [1, 8, 6, 2, 5, 4, 8, 3, 7].
Array: [1, 8, 6, 2, 5, 4, 8, 3, 7] Index: 0 1 2 3 4 5 6 7 8 Step 1: L=0(1), R=8(7) → area = min(1,7) × 8 = 8 → move L (1 < 7) Step 2: L=1(8), R=8(7) → area = min(8,7) × 7 = 49 → move R (7 < 8) ★ best so far Step 3: L=1(8), R=7(3) → area = min(8,3) × 6 = 18 → move R (3 < 8) Step 4: L=1(8), R=6(8) → area = min(8,8) × 5 = 40 → move R (equal, either works) Step 5: L=1(8), R=5(4) → area = min(8,4) × 4 = 16 → move R (4 < 8) Step 6: L=1(8), R=4(5) → area = min(8,5) × 3 = 15 → move R (5 < 8) Step 7: L=1(8), R=3(2) → area = min(8,2) × 2 = 4 → move R (2 < 8) Step 8: L=1(8), R=2(6) → area = min(8,6) × 1 = 6 → move R (6 < 8) L=1, R=1 → L not < R, stop. Answer: 49 (between index 1 and 8) Key insight: We always move the SHORTER wall because: - Area = min(left, right) × width - Width always decreases by 1 - Moving the taller wall: min can only stay same or decrease → area decreases - Moving the shorter wall: min might increase → area might increase So moving the shorter wall is the only move that has a CHANCE of improving.
Trapping Rain Water — The Harder Variant
Array: [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1] Index: 0 1 2 3 4 5 6 7 8 9 10 11 leftMax = 0, rightMax = 0, water = 0 Step 1: L=0, R=11 | leftMax=0, rightMax=1 height[L]=0 <= height[R]=1 → process left leftMax = max(0, 0) = 0 | water += 0 - 0 = 0 | L++ Step 2: L=1, R=11 | leftMax=1, rightMax=1 height[L]=1 <= height[R]=1 → process left leftMax = max(0, 1) = 1 | water += 1 - 1 = 0 | L++ Step 3: L=2, R=11 | leftMax=1, rightMax=1 height[L]=0 <= height[R]=1 → process left leftMax = 1 | water += 1 - 0 = 1 ★ | L++ ...continues until L meets R... Final answer: 6 units of water The trick: always process the side with the smaller max. That side's water level is determined — the other side is at least as tall.
Practice Problems
These 7 problems are carefully ordered from easy to hard. Each one teaches a different aspect of Two Pointers. Solve them in order.
LC 125. Valid Palindrome
Why this pattern:
Opposite direction — compare characters from both ends, skip non-alphanumeric. This is the simplest Two Pointers problem and builds the core muscle of converging pointers.
Key idea: Initialize left = 0, right = end. Skip non-alphanumeric characters. Compare lowercase versions. If mismatch → not a palindrome. If pointers cross → it is.
LC 167. Two Sum II — Input Array Is Sorted
Why this pattern:
Opposite direction — the sorted property means the sum of left + right tells you exactly which pointer to move. This is the textbook Two Pointers problem.
Key idea: left = 0, right = end. If sum < target, move left right. If sum > target, move right left. The sorted order guarantees you won't miss the answer.
LC 283. Move Zeroes
Why this pattern:
Same direction (read/write) — fast pointer reads every element, slow pointer writes non-zero elements. After the loop, fill the rest with zeroes.
Key idea: slow = write position for non-zero values. Fast scans everything. When fast finds a non-zero, swap it to the slow position. This maintains relative order of non-zero elements.
LC 15. 3Sum
Why this pattern:
Sort + fix one element + opposite-direction Two Pointers on the rest. The key challenge is handling duplicates — skip duplicate values for all three positions.
Key idea: Sort the array. For each nums[i], run Two Pointers on nums[i+1..end] looking for pairs that sum to -nums[i]. Skip duplicates at every level to avoid duplicate triplets.
LC 11. Container With Most Water
Why this pattern:
Opposite direction — the width decreases as pointers converge, so you must move the shorter wall (it's the bottleneck). Moving the taller wall can never increase the area.
Key idea: Area = min(height[left], height[right]) × (right - left). Always move the pointer pointing to the shorter wall. The shorter wall limits the area, so keeping it can't help.
LC 75. Sort Colors
Why this pattern:
Dutch National Flag — three pointers (low, mid, high) partition the array into three sections in a single pass. This is the 3-way partition variant.
Key idea: low tracks where the next 0 goes, high tracks where the next 2 goes, mid scans. Swap 0s to low, 2s to high, skip 1s. Don't advance mid after swapping with high (the swapped value is unchecked).
LC 42. Trapping Rain Water
Why this pattern:
Opposite direction — at each position, the water trapped depends on the minimum of the max height to its left and right. Two pointers track these maxes from both ends, converging inward.
Key idea: Maintain leftMax and rightMax. Process the side with the smaller max (it's the bottleneck). Water at position = currentMax - height[position]. Move that pointer inward. The other side's max can only be >= current, so the calculation is safe.
Practice strategy
For each problem: (1) Identify which template applies before coding. (2) Write the brute force first. (3) Optimize using Two Pointers. (4) After solving, write one sentence: "This is [template] because [signal]."
Common Mistakes
These are the traps that catch people on Two Pointers problems. Learn them here so you don't learn them in an interview.
Forgetting to skip duplicates in 3Sum
You get the right triplets but with duplicates like [-1,0,1] appearing multiple times. The interviewer asks you to fix it and you panic.
✅After finding a valid triplet, advance both pointers past duplicate values: while (left < right && nums[left] === nums[left+1]) left++. Do the same for the outer loop's fixed element.
Using left <= right instead of left < right
When pointers can be equal, you might process the same element twice (e.g., using the same number twice in Two Sum when it shouldn't be allowed).
✅For pair problems, use left < right (strict). The only time you'd use <= is when you need to process the element where they meet (rare).
Applying Two Pointers to unsorted data
You see 'find a pair with sum X' and immediately use Two Pointers, but the array isn't sorted. The pointer movement logic breaks because there's no monotonic property.
✅Always check: is the data sorted? If not, either sort it first (if you don't need original indices) or use a Hash Map instead.
Not advancing mid after swapping with low in Dutch Flag
In Sort Colors, you swap nums[mid] with nums[low] but forget to advance mid. This causes an infinite loop or incorrect results.
✅When swapping with low, advance BOTH low and mid (the swapped value from low is already processed). When swapping with high, only decrement high (the swapped value from high is unprocessed).
Moving the wrong pointer
In Container With Most Water, you move the taller wall's pointer instead of the shorter one. Or in Two Sum, you move left when you should move right.
✅Always reason about what each pointer movement does to the result. In container problems: moving the shorter wall might increase area, moving the taller wall never can. Write this reasoning as a comment before coding.
Confusing Two Pointers with Sliding Window
You use converging Two Pointers on a problem that needs a sliding window (both pointers moving right). The approaches look similar but solve different problem types.
✅Two Pointers: pointers move toward each other OR one is fast/slow. Sliding Window: both pointers move in the same direction maintaining a 'window'. If the problem says 'contiguous subarray with constraint', it's Sliding Window.
Interview Strategy
Knowing the pattern is half the battle. Here's how to present it in an interview setting to maximize your score.
The 5-Step Interview Flow
Clarify & Confirm
'The array is sorted, correct? And I need to find a pair that sums to the target? Can there be duplicates? Should I return indices or values?' — Clarifying shows you're thorough and buys you thinking time.
State the Brute Force
'The brute force would be to check every pair — that's O(n²). But since the array is sorted, I can do better.' — This shows you understand the baseline and sets up the optimization.
Explain the Optimization
'I'll use Two Pointers — one at the start, one at the end. Since the array is sorted, if the sum is too small I move the left pointer right, if too big I move the right pointer left. This gives me O(n).' — Name the pattern explicitly.
Code It Clean
Write the solution. Use descriptive variable names (left/right, not i/j). Add a brief comment for the key decision point. Keep it concise — no unnecessary variables or logic.
Test & Analyze
Trace through an example: 'For [2,7,11,15] with target 9: left=0(2), right=3(15), sum=17 > 9 → right--. left=0(2), right=2(11), sum=13 > 9 → right--. left=0(2), right=1(7), sum=9 = target ✓'. State complexity: O(n) time, O(1) space.
Common Follow-Up Questions
| Follow-Up | How to Handle |
|---|---|
| "What if the array isn't sorted?" | Use a Hash Map for O(n) time, O(n) space. Or sort first for O(n log n) time, O(1) space if indices don't matter. |
| "Can you find all pairs, not just one?" | Same Two Pointers approach, but don't return early — collect all valid pairs. Handle duplicates by skipping equal values. |
| "Extend to 3Sum / 4Sum?" | Fix one (or two) elements with an outer loop, then run Two Pointers on the rest. Time becomes O(n^(k-1)) for k-Sum. |
| "What about duplicates?" | After finding a match, skip duplicate values: while (left < right && nums[left] === nums[left+1]) left++. Same for right and the outer loop. |
| "Can you do it with less space?" | Two Pointers already uses O(1) space — that's the best possible. Mention this as an advantage over the Hash Map approach. |
| "What if there are multiple valid answers?" | Clarify: do you want all of them, the closest one, or any one? This changes whether you return early or continue scanning. |
The meta-skill
Interviewers aren't just testing if you know Two Pointers. They're testing if you can (1) recognize when to use it, (2) explain WHY it works, and (3) adapt when the problem changes. Practice explaining your reasoning out loud — it's a different skill from solving silently.
Summary & Cheat Sheet
Pin this section. Review it before mock interviews and contests.
Quick Revision Cheat Sheet
Core idea: Two indices move strategically to avoid nested loops — O(n²) → O(n)
Prerequisite: Usually requires sorted data. If unsorted, sort first or use hashing
Opposite direction: left=0, right=end, converge → pair sum, palindrome, container
Same direction: slow=write, fast=read → remove duplicates, move zeroes, partition
Two sequences: One pointer per array → merge sorted, is-subsequence, intersection
3Sum trick: Sort + fix one element + Two Pointers on rest → O(n²)
Dutch Flag: Three pointers (low, mid, high) → 3-way partition in single pass
Move shorter side: In container/water problems, always move the pointer at the shorter wall
Skip duplicates: After finding a match: while (nums[ptr] === nums[ptr±1]) ptr±±
Complexity: Time: O(n) or O(n²) for k-Sum. Space: almost always O(1)
Mental trigger: "Sorted array + pair" or "in-place + O(1) space" → Two Pointers
Decision Flowchart
Is the data sorted (or can you sort it)? ├── YES → Do you need to find a pair/triplet with a property? │ ├── YES → Opposite Direction Two Pointers │ └── NO → Do you need to remove/rearrange elements in-place? │ ├── YES → Same Direction (read/write) Two Pointers │ └── NO → Are you comparing two sorted sequences? │ ├── YES → Two Sequences Two Pointers │ └── NO → Consider Binary Search or Sliding Window └── NO → Do you need original indices? ├── YES → Use Hash Map instead └── NO → Sort first, then apply Two Pointers
One last thing
Two Pointers is one of the most versatile patterns in DSA. It appears in easy problems (Valid Palindrome) and hard ones (Trapping Rain Water). The difference isn't the technique — it's recognizing WHEN and HOW to apply it. Solve the 7 practice problems in Section 07, and you'll have that intuition locked in.