LeetCopilot Logo
LeetCopilot
LeetCode Pattern/Two Pointers/The Complete Two Pointers Guide: Master All Patterns, Templates, and Techniques

The Complete Two Pointers Guide: Master All Patterns, Templates, and Techniques

LeetCopilot Team
Dec 9, 2025
25 min read
Two PointersComplete GuideAll PatternsTemplatesInterview PrepLeetCode
The ultimate comprehensive guide to two pointers. Learn all variants, when to use each pattern, complete templates in multiple languages, and a systematic approach to solve any two pointers problem.

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:

python
# 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 sum

The 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:

code
[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 meet

Real-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:

code
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 speeds

Real-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:

code
[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 W

Real-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:

code
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 simpler

Quick 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

python
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 None

JavaScript:

javascript
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:

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

python
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 False

JavaScript:

javascript
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:

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

python
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 length

JavaScript:

javascript
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:

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)

python
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

python
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 True

Why it works: Palindrome reads same forward and backward. Compare from both ends.

3. Container With Most Water

python
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_area

Why 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

python
def hasCycle(head):
    slow = fast = head
    
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        
        if slow == fast:
            return True
    
    return False

Why it works: If there's a cycle, fast will eventually lap slow, like runners on a track.

2. Middle of Linked List

python
def middleNode(head):
    slow = fast = head
    
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    
    return slow

Why it works: When fast reaches end (2x speed), slow is at middle (1x speed).

3. Find Cycle Start

python
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 slow

Why 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

python
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 write

Why it works: Write pointer marks where to place next unique element.

2. Move Zeroes

python
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] = 0

Why it works: Two-phase approach preserves order of non-zeros.

3. Remove Element

python
def removeElement(nums, val):
    write = 0
    
    for read in range(len(nums)):
        if nums[read] != val:
            nums[write] = nums[read]
            write += 1
    
    return write

Why 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)

python
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 result

Pattern: Fix one element, use two pointers on rest. O(n²) instead of O(n³).

See detailed guide: Advanced Multi-Pointer

2. Trapping Rain Water

python
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 water

Pattern: Track max heights from both sides, process the smaller side.

3. Palindrome Linked List

python
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 True

Pattern: Find middle with fast/slow, reverse second half, compare.

Common Mistakes

1. Wrong Loop Condition

Wrong:

python
while left <= right:  # Processes middle element twice

Correct:

python
while left < right:  # Stops before crossing

See guide: Off-by-One Errors

2. Not Checking fast.next

Wrong:

python
while fast:
    fast = fast.next.next  # Crashes if fast.next is None!

Correct:

python
while fast and fast.next:
    fast = fast.next.next

See guide: Linked List Pitfalls

3. Using Two Pointers on Unsorted Arrays

Wrong:

python
# Array is unsorted
left, right = 0, len(nums) - 1
# Two pointers won't work correctly!

Correct:

python
nums.sort()  # Sort first
left, right = 0, len(nums) - 1
# Or use hash map if indices needed

See guide: Unsorted Array Mistake

4. Not Skipping Duplicates in 3Sum

Wrong:

python
for i in range(len(nums)):
    # No duplicate check - generates duplicate triplets

Correct:

python
for i in range(len(nums)):
    if i > 0 and nums[i] == nums[i - 1]:
        continue

See 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

  1. Two Sum II (#167) ⭐
  2. Valid Palindrome (#125) ⭐
  3. Reverse String (#344)
  4. 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

PatternTimeSpaceUse Case
Opposite DirectionO(n)O(1)Sorted pairs, palindromes
Fast/SlowO(n)O(1)Linked lists, cycles
Read/WriteO(n)O(1)In-place manipulation
3SumO(n²)O(1)Triplets in sorted array
4SumO(n³)O(1)Quadruplets
k-SumO(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

  1. Opposite Direction: Sorted arrays, pairs, palindromes
  2. Fast/Slow: Linked lists, cycles, middle
  3. 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

python
# 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 += 1

Next Steps

Now that you've mastered the fundamentals, continue your journey:

  1. Practice systematically - Follow the roadmap above
  2. Debug effectively - Use debugging guide
  3. Master edge cases - See palindrome edge cases
  4. Learn proofs - Understand cycle detection proof
  5. 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:

  1. Identify data structure
  2. Check sorting
  3. Identify goal
  4. 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

Related Tutorials