Two pointers is one of the most powerful patterns in algorithmic problem solving. It transforms O(n²) brute force solutions into O(n) optimal ones. It's used in hundreds of LeetCode problems. And once you master it, you'll solve an entire class of problems in minutes.
But here's the challenge: two pointers isn't a single technique—it's a family of patterns, each with its own use cases, templates, and gotchas. Understanding when and how to apply each variant is what separates those who memorize solutions from those who truly master the pattern.
This comprehensive guide will teach you everything about two pointers: all variants, when to use each pattern, the intuition behind why they work, common mistakes to avoid, and a systematic decision framework that works every time.
What Is Two Pointers?
The Core Concept
Two pointers is a technique where you use two indices (pointers) to traverse a data structure, making intelligent decisions based on the values they point to. Instead of checking every possible pair or combination (which would be O(n²) or worse), we use the properties of the data structure—particularly sorted order or linked list structure—to eliminate large portions of the search space.
The fundamental insight: When data has certain properties (like being sorted), we can make greedy decisions about which direction to move our pointers. This allows us to skip checking impossible solutions and go straight to promising candidates.
Why Two Pointers Works
The power of two pointers comes from three key advantages:
1. Dramatic Time Complexity Reduction
Consider finding two numbers that sum to a target in a sorted array. The naive approach checks every pair:
- First number: n choices
- Second number: n-1 choices
- Total: n × (n-1) / 2 = O(n²) comparisons
With two pointers, we start from both ends and make one decision per step:
- Each step, we move exactly one pointer
- We make at most n steps total
- Total: O(n) comparisons
That's a reduction from quadratic to linear time!
2. Constant Space Complexity
Unlike hash map approaches that store seen elements (O(n) space), two pointers only uses two integer variables. This is crucial when:
- Memory is limited
- The problem explicitly requires O(1) space
- You're working with very large datasets
3. Enables Greedy Algorithms
On sorted data, two pointers enables greedy decision-making. For example, if the sum of two numbers is too small, we know we need a larger number—so we move the left pointer right. This certainty comes from the sorted property and makes the algorithm both simple and correct.
The Intuition: Why It's Not Magic
When you first see two pointers, it might seem like magic. How do we know we won't miss the answer? The key is understanding that we're systematically eliminating impossible solutions.
Example: Two Sum on a sorted array [1, 2, 3, 4, 5, 6, 7, 8, 9] with target 10
Start with pointers at both ends:
- Left at 1, Right at 9
- Sum = 1 + 9 = 10 ✓ Found it!
But what if the target was 11?
- Sum = 1 + 9 = 10 < 11 (too small)
- Key insight: With left at 1, the maximum possible sum is 1 + 9 = 10
- Any pair including 1 will be ≤ 10, so we can eliminate all pairs with 1
- Move left pointer to 2
This is why two pointers works: each decision eliminates a whole set of impossible solutions, allowing us to find the answer in linear time.
Comparing Approaches
Let's see the difference in practice with Two Sum on a sorted array:
Brute Force Approach (O(n²)):
Check every possible pair. For an array of size 10, that's 45 comparisons. For size 1000, that's 499,500 comparisons.
Two Pointers Approach (O(n)):
Start from both ends, make smart decisions. For an array of size 10, at most 10 steps. For size 1000, at most 1000 steps.
The code difference:
# Brute force: Check all pairs
for i in range(len(nums)):
for j in range(i+1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
# Two pointers: Smart elimination
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 sumThe two pointers version is not only faster—it's also more elegant and easier to understand once you grasp the intuition.
The Three Main Variants
Two pointers isn't one pattern—it's three distinct techniques that solve different types of problems. Understanding which variant to use is the key to mastering two pointers.
1. Opposite Direction (Converging Pointers)
The Pattern: Pointers start at opposite ends of an array and move toward each other, converging at the middle.
When to use:
- The array is sorted (or you can sort it)
- You're looking for pairs that meet a condition
- You're checking symmetry (like palindromes)
- You need to make greedy decisions based on endpoint values
Why it works:
The sorted property gives us a crucial guarantee: moving the left pointer right always gives us a larger value, and moving the right pointer left always gives us a smaller value. This monotonicity allows us to make greedy decisions without missing the optimal solution.
Visual representation:
[1, 2, 3, 4, 5, 6, 7, 8]
L R ← Start at opposite ends
L R ← Converge based on condition
L R ← Continue until they meetReal-world analogy: Imagine you're searching for two people whose combined age is exactly 50 years. If you line them up by age (youngest to oldest), you can start with the youngest and oldest. If their sum is too high, the oldest person is too old—try the next oldest. If too low, the youngest is too young—try the next youngest. You'll find the pair efficiently without checking everyone.
Example problems: Two Sum II (#167), Valid Palindrome (#125), Container With Most Water (#11)
2. Fast and Slow (Tortoise and Hare)
The Pattern: Both pointers start at the same position and move in the same direction, but at different speeds—typically one moves twice as fast as the other.
When to use:
- Working with linked lists
- Detecting cycles in a sequence
- Finding the middle of a linked list
- Removing the nth node from the end
Why it works:
The speed difference creates a mathematical relationship. In a cycle, the fast pointer will eventually "lap" the slow pointer—like runners on a circular track. For finding the middle, when the fast pointer (moving 2x speed) reaches the end, the slow pointer (moving 1x speed) is exactly at the middle.
Visual representation:
1 -> 2 -> 3 -> 4 -> 5 -> None
S,F ← Both start at head
S F ← Slow moves 1, Fast moves 2
S F ← Continue at different speedsReal-world analogy: Two runners on a track. If the track is circular (has a cycle), the faster runner will eventually lap the slower one. If the track has an end (no cycle), the fast runner reaches it first while the slow runner is at the midpoint.
Example problems: Linked List Cycle (#141), Middle of Linked List (#876), Happy Number (#202)
3. Read and Write (In-Place Manipulation)
The Pattern: Both pointers move in the same direction at the same speed, but the write pointer lags behind the read pointer, marking where to place the next valid element.
When to use:
- In-place array modification required
- Removing elements from an array
- Partitioning an array
- Problem says "without extra space" or "O(1) space"
Why it works:
The read pointer scans through all elements, while the write pointer marks the position for the next valid element. We're essentially building a new array in the same memory space, overwriting elements we no longer need.
Visual representation:
[1, 1, 2, 2, 3]
W R ← Read finds unique (1), write it at W
W R ← Read finds duplicate (1), skip
W R ← Read finds unique (2), write it at WReal-world analogy: Imagine cleaning out your bookshelf. You scan each book (read pointer) and decide if you want to keep it. If yes, you place it at the next available spot from the left (write pointer). Books you don't want are simply overwritten by the next book you keep.
Example problems: Remove Duplicates (#26), Move Zeroes (#283), Remove Element (#27)
When to Use Two Pointers
Choosing the right pattern is crucial. Here's a systematic decision framework:
The Decision Tree
Understanding which variant to use comes down to answering a few key questions about your problem:
Question 1: What data structure are you working with?
├─ Linked List → Almost always Fast/Slow
│ ├─ Cycle detection? → Fast/Slow ✓
│ ├─ Find middle? → Fast/Slow ✓
│ └─ Remove nth from end? → Fast/Slow ✓
│
└─ Array or String → Continue to Question 2
Question 2: What's the goal?
├─ In-place manipulation? → Read/Write ✓
├─ Finding pairs/triplets? → Continue to Question 3
├─ Checking symmetry? → Opposite Direction ✓
└─ Subarray/substring? → Sliding Window (not two pointers)
Question 3: Is the array sorted?
├─ Yes → Opposite Direction ✓
├─ Can sort without losing info? → Sort first, then Opposite Direction ✓
└─ No, and need original indices → Hash Map (not two pointers)
Question 4: Space constraints?
├─ Must be O(1)? → Two Pointers ✓
└─ O(n) allowed? → Hash Map might be simplerQuick Recognition Checklist
Use Opposite Direction when you see:
- ✅ "Sorted array" in problem description
- ✅ "Find two numbers that..." (pairs)
- ✅ "Palindrome" or "symmetric"
- ✅ "Container" or "water" problems
- ✅ Can sort without losing required information
Use Fast/Slow when you see:
- ✅ "Linked list" in problem description
- ✅ "Cycle" or "loop"
- ✅ "Middle" of a linked list
- ✅ "nth from the end"
- ✅ Any problem with implicit sequences (like Happy Number)
Use Read/Write when you see:
- ✅ "In-place" or "without extra space"
- ✅ "Remove" elements from array
- ✅ "Move all X to the end"
- ✅ "Partition" an array
- ✅ O(1) space requirement
Don't use Two Pointers when you see:
- ❌ "Return the indices" (and array is unsorted) → Use Hash Map
- ❌ "Longest substring" or "maximum subarray" → Use Sliding Window
- ❌ Tree or graph problems → Use DFS/BFS
- ❌ Need to track all elements between pointers → Use Sliding Window
Complete Templates
Template 1: Opposite Direction
def opposite_direction(arr, target):
"""
For: Sorted arrays, finding pairs, palindromes
Time: O(n)
Space: O(1)
"""
left, right = 0, len(arr) - 1
while left < right:
current = compute(arr[left], arr[right])
if current == target:
return [left, right]
elif current < target:
left += 1 # Need larger value
else:
right -= 1 # Need smaller value
return NoneJavaScript:
function oppositeDirection(arr, target) {
let left = 0, right = arr.length - 1;
while (left < right) {
const current = compute(arr[left], arr[right]);
if (current === target) {
return [left, right];
} else if (current < target) {
left++;
} else {
right--;
}
}
return null;
}Java:
public int[] oppositeDirection(int[] arr, int target) {
int left = 0, right = arr.length - 1;
while (left < right) {
int current = compute(arr[left], arr[right]);
if (current == target) {
return new int[]{left, right};
} else if (current < target) {
left++;
} else {
right--;
}
}
return new int[]{};
}Template 2: Fast and Slow
def fast_slow(head):
"""
For: Linked lists, cycle detection, finding middle
Time: O(n)
Space: O(1)
"""
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True # Cycle detected
return FalseJavaScript:
function fastSlow(head) {
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) {
return true;
}
}
return false;
}Java:
public boolean fastSlow(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
return true;
}
}
return false;
}Template 3: Read and Write
def read_write(arr):
"""
For: In-place manipulation, removing elements
Time: O(n)
Space: O(1)
"""
write = 0
for read in range(len(arr)):
if is_valid(arr[read]):
arr[write] = arr[read]
write += 1
return write # New lengthJavaScript:
function readWrite(arr) {
let write = 0;
for (let read = 0; read < arr.length; read++) {
if (isValid(arr[read])) {
arr[write] = arr[read];
write++;
}
}
return write;
}Java:
public int readWrite(int[] arr) {
int write = 0;
for (int read = 0; read < arr.length; read++) {
if (isValid(arr[read])) {
arr[write] = arr[read];
write++;
}
}
return write;
}Pattern 1: Opposite Direction
Core Problems
1. Two Sum II (Sorted Array)
def twoSum(numbers, target):
left, right = 0, len(numbers) - 1
while left < right:
current_sum = numbers[left] + numbers[right]
if current_sum == target:
return [left + 1, right + 1] # 1-indexed
elif current_sum < target:
left += 1
else:
right -= 1
return []Why it works: Sorted array allows greedy decisions. If sum too small, increase left. If too large, decrease right.
2. Valid Palindrome
def isPalindrome(s):
cleaned = ''.join(c.lower() for c in s if c.isalnum())
left, right = 0, len(cleaned) - 1
while left < right:
if cleaned[left] != cleaned[right]:
return False
left += 1
right -= 1
return TrueWhy it works: Palindrome reads same forward and backward. Compare from both ends.
3. Container With Most Water
def maxArea(height):
left, right = 0, len(height) - 1
max_area = 0
while left < right:
area = min(height[left], height[right]) * (right - left)
max_area = max(max_area, area)
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_areaWhy it works: Moving the shorter line is the only way to potentially increase area.
See detailed guide: Opposite Direction Template
Pattern 2: Fast and Slow
Core Problems
1. Linked List Cycle
def hasCycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return FalseWhy it works: If there's a cycle, fast will eventually lap slow, like runners on a track.
2. Middle of Linked List
def middleNode(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slowWhy it works: When fast reaches end (2x speed), slow is at middle (1x speed).
3. Find Cycle Start
def detectCycle(head):
# Phase 1: Detect cycle
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
break
else:
return None
# Phase 2: Find start
slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slowWhy it works: Mathematical proof shows distance from head to cycle start equals distance from meeting point to cycle start.
See detailed guide: Fast and Slow Pointers
Pattern 3: Read and Write
Core Problems
1. Remove Duplicates
def removeDuplicates(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 writeWhy it works: Write pointer marks where to place next unique element.
2. Move Zeroes
def moveZeroes(nums):
write = 0
# Copy non-zeros
for read in range(len(nums)):
if nums[read] != 0:
nums[write] = nums[read]
write += 1
# Fill zeros
for i in range(write, len(nums)):
nums[i] = 0Why it works: Two-phase approach preserves order of non-zeros.
3. Remove Element
def removeElement(nums, val):
write = 0
for read in range(len(nums)):
if nums[read] != val:
nums[write] = nums[read]
write += 1
return writeWhy it works: Only copy elements that aren't equal to val.
See detailed guide: In-Place Manipulation
Advanced Techniques
1. 3Sum (Fix One + Two Pointers)
def threeSum(nums):
nums.sort()
result = []
for i in range(len(nums) - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue
left, right = i + 1, len(nums) - 1
target = -nums[i]
while left < right:
current_sum = nums[left] + nums[right]
if current_sum == target:
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 current_sum < target:
left += 1
else:
right -= 1
return resultPattern: Fix one element, use two pointers on rest. O(n²) instead of O(n³).
See detailed guide: Advanced Multi-Pointer
2. Trapping Rain Water
def trap(height):
if not height:
return 0
left, right = 0, len(height) - 1
left_max, right_max = height[left], height[right]
water = 0
while left < right:
if height[left] < height[right]:
if height[left] >= left_max:
left_max = height[left]
else:
water += left_max - height[left]
left += 1
else:
if height[right] >= right_max:
right_max = height[right]
else:
water += right_max - height[right]
right -= 1
return waterPattern: Track max heights from both sides, process the smaller side.
3. Palindrome Linked List
def isPalindrome(head):
# Find middle
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# Reverse second half
prev = None
while slow:
next_node = slow.next
slow.next = prev
prev = slow
slow = next_node
# Compare
left, right = head, prev
while right:
if left.val != right.val:
return False
left = left.next
right = right.next
return TruePattern: Find middle with fast/slow, reverse second half, compare.
Common Mistakes
1. Wrong Loop Condition
❌ Wrong:
while left <= right: # Processes middle element twice✅ Correct:
while left < right: # Stops before crossingSee guide: Off-by-One Errors
2. Not Checking fast.next
❌ Wrong:
while fast:
fast = fast.next.next # Crashes if fast.next is None!✅ Correct:
while fast and fast.next:
fast = fast.next.nextSee guide: Linked List Pitfalls
3. Using Two Pointers on Unsorted Arrays
❌ Wrong:
# Array is unsorted
left, right = 0, len(nums) - 1
# Two pointers won't work correctly!✅ Correct:
nums.sort() # Sort first
left, right = 0, len(nums) - 1
# Or use hash map if indices neededSee guide: Unsorted Array Mistake
4. Not Skipping Duplicates in 3Sum
❌ Wrong:
for i in range(len(nums)):
# No duplicate check - generates duplicate triplets✅ Correct:
for i in range(len(nums)):
if i > 0 and nums[i] == nums[i - 1]:
continueSee guide: 3Sum Duplicates
Decision Framework
Step-by-Step Decision Process
Step 1: Identify Data Structure
- Linked list? → Consider fast/slow
- Array/string? → Continue to step 2
Step 2: Check Sorting
- Sorted? → Consider opposite direction
- Can sort without losing info? → Sort first
- Can't sort? → Consider hash map or read/write
Step 3: Identify Goal
- Finding pairs? → Opposite direction
- Checking palindrome? → Opposite direction
- Cycle detection? → Fast/slow
- In-place manipulation? → Read/write
- Subarray/substring? → Sliding window (not two pointers)
Step 4: Check Space Constraints
- Must be O(1)? → Two pointers
- O(n) allowed? → Hash map might be simpler
See complete guide: Pattern Recognition
Practice Roadmap
Beginner (Master the Basics)
Week 1: Opposite Direction
- Two Sum II (#167) ⭐
- Valid Palindrome (#125) ⭐
- Reverse String (#344)
- Squares of Sorted Array (#977)
Week 2: Fast/Slow
5. Linked List Cycle (#141) ⭐
6. Middle of Linked List (#876) ⭐
7. Happy Number (#202)
8. Remove Nth From End (#19)
Week 3: Read/Write
9. Remove Duplicates (#26) ⭐
10. Remove Element (#27) ⭐
11. Move Zeroes (#283) ⭐
12. Remove Duplicates II (#80)
Intermediate (Build Mastery)
Week 4-5: Advanced Patterns
13. 3Sum (#15) ⭐⭐
14. Container With Most Water (#11) ⭐⭐
15. Trapping Rain Water (#42) ⭐⭐
16. 3Sum Closest (#16)
17. 4Sum (#18)
18. Linked List Cycle II (#142)
19. Palindrome Linked List (#234)
20. Sort Colors (#75)
Advanced (Interview Ready)
Week 6-8: Complex Applications
21. Minimum Window Substring (#76) ⭐⭐⭐
22. Longest Substring Without Repeating (#3)
23. Find Duplicate Number (#287)
24. Intersection of Two Linked Lists (#160)
25. Valid Triangle Number (#611)
26. 3Sum Smaller (#259)
27. Subarray Product Less Than K (#713)
28. Partition Labels (#763)
⭐ = Must-solve
⭐⭐ = Very important
⭐⭐⭐ = Interview favorite
Complexity Analysis
| Pattern | Time | Space | Use Case |
|---|---|---|---|
| Opposite Direction | O(n) | O(1) | Sorted pairs, palindromes |
| Fast/Slow | O(n) | O(1) | Linked lists, cycles |
| Read/Write | O(n) | O(1) | In-place manipulation |
| 3Sum | O(n²) | O(1) | Triplets in sorted array |
| 4Sum | O(n³) | O(1) | Quadruplets |
| k-Sum | O(n^(k-1)) | O(1) | k elements |
Two Pointers vs Alternatives
vs Hash Map
Two Pointers:
- ✅ O(1) space
- ✅ Works on sorted data
- ❌ Requires sorting (O(n log n))
- ❌ Loses original indices
Hash Map:
- ✅ O(n) time
- ✅ Preserves indices
- ❌ O(n) space
- ✅ Works on unsorted data
When to use: See Hash Map vs Two Pointers
vs Sliding Window
Two Pointers (Opposite):
- Pointers converge
- Only endpoints matter
- For pairs, palindromes
Sliding Window:
- Both move same direction
- Window contents matter
- For subarrays, substrings
When to use: See Two Pointers vs Sliding Window
Key Takeaways
The Three Patterns
- Opposite Direction: Sorted arrays, pairs, palindromes
- Fast/Slow: Linked lists, cycles, middle
- Read/Write: In-place manipulation, removing elements
When to Use
- ✅ Sorted data (or can sort)
- ✅ Finding pairs/triplets
- ✅ O(1) space required
- ✅ Linked list problems
- ✅ In-place operations
Common Pitfalls
- ❌ Using on unsorted arrays (when indices needed)
- ❌ Wrong loop condition (
<=vs<) - ❌ Not checking
fast.next - ❌ Forgetting to skip duplicates
Templates to Memorize
# Opposite Direction
left, right = 0, len(arr) - 1
while left < right:
if condition:
return result
elif need_larger:
left += 1
else:
right -= 1
# Fast/Slow
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# Read/Write
write = 0
for read in range(len(arr)):
if valid(arr[read]):
arr[write] = arr[read]
write += 1Next Steps
Now that you've mastered the fundamentals, continue your journey:
- Practice systematically - Follow the roadmap above
- Debug effectively - Use debugging guide
- Master edge cases - See palindrome edge cases
- Learn proofs - Understand cycle detection proof
- Use LeetCopilot - Get smart hints and execution traces
Conclusion
Two pointers is a fundamental pattern that every software engineer must master. It's not just about memorizing templates—it's about understanding when to use each variant, why it works, and how to avoid common mistakes.
Master these three patterns:
- Opposite Direction (sorted pairs)
- Fast/Slow (linked lists)
- Read/Write (in-place)
Follow the decision framework:
- Identify data structure
- Check sorting
- Identify goal
- Check space constraints
Practice systematically:
- Start with beginner problems
- Build to intermediate
- Master advanced techniques
With this guide and consistent practice, you'll solve two pointers problems with confidence and elegance. The pattern will become second nature, and you'll recognize opportunities to use it instantly.
Happy coding, and may your pointers always converge correctly! 🎯
Want to Practice LeetCode Smarter?
LeetCopilot is a free browser extension that enhances your LeetCode practice with AI-powered hints, personalized study notes, and realistic mock interviews — all designed to accelerate your coding interview preparation.
Also compatible with Edge, Brave, and Opera
