Skip to content
Codeloom
LeetCode

LeetCode Two Pointer Patterns: The Complete Guide

Master every two pointer pattern for LeetCode: opposite-end, same-direction, and fast/slow pointers with templates, complexity analysis, and 15+ mapped problems.

·7 min read · By Codeloom
Intermediate 14 min read

What you'll learn

  • The three core two pointer patterns and when each applies
  • Opposite-end pointers for sorted arrays and palindromes
  • Same-direction pointers for sliding window and partitioning
  • Fast/slow pointers for cycle detection and linked list problems
  • Templates with complexity analysis for each pattern

Prerequisites

  • Basic array and linked list operations
  • Understanding of O(n) vs O(n log n) complexity
  • Python fundamentals
Diagram showing opposite-end, same-direction, and fast/slow two pointer patterns

Two pointers is one of the most versatile techniques in algorithm interviews. The core idea is simple: instead of brute-forcing with nested loops (O(n^2)), you maintain two indices that move through the data structure in a coordinated way, reducing the problem to O(n). This guide covers every major variant.

Pattern 1: Opposite-End Pointers

Place one pointer at the start and one at the end. Move them toward each other based on a condition. This pattern works on sorted arrays and palindrome checks.

Template

def opposite_end(arr: list[int]) -> ...:
    left, right = 0, len(arr) - 1

    while left < right:
        if condition(arr[left], arr[right]):
            # found answer or update result
            left += 1
            right -= 1
        elif need_bigger_value:
            left += 1
        else:
            right -= 1

Complexity: O(n) time, O(1) space.

Example: Two Sum II (LeetCode #167)

Given a 1-indexed sorted array, find two numbers that sum to a target.

def two_sum(numbers: list[int], target: int) -> list[int]:
    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 []

print(two_sum([2, 7, 11, 15], 9))   # [1, 2]
print(two_sum([2, 3, 4], 6))        # [1, 3]

Example: Container With Most Water (LeetCode #11)

def max_area(height: list[int]) -> int:
    left, right = 0, len(height) - 1
    best = 0

    while left < right:
        w = right - left
        h = min(height[left], height[right])
        best = max(best, w * h)

        # move the shorter side inward
        if height[left] < height[right]:
            left += 1
        else:
            right -= 1

    return best

print(max_area([1, 8, 6, 2, 5, 4, 8, 3, 7]))  # 49

Mapped Problems

ProblemKey Insight
#167 Two Sum IISum too small? Move left. Too big? Move right.
#11 Container With Most WaterAlways move the shorter wall inward.
#15 3SumFix one element, run two pointers on the rest.
#125 Valid PalindromeCompare characters from both ends.
#42 Trapping Rain WaterTrack max height from each side.

Pattern 2: Same-Direction Pointers (Slow/Fast Reader-Writer)

Both pointers start at the beginning. One (fast) scans every element; the other (slow) tracks the write position or window boundary. This pattern handles in-place deduplication, partitioning, and sliding windows.

Template: Remove Duplicates

def remove_duplicates(nums: list[int]) -> int:
    if not nums:
        return 0

    slow = 0  # write pointer

    for fast in range(1, len(nums)):
        if nums[fast] != nums[slow]:
            slow += 1
            nums[slow] = nums[fast]

    return slow + 1

arr = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4]
k = remove_duplicates(arr)
print(arr[:k])  # [0, 1, 2, 3, 4]

Example: Move Zeroes (LeetCode #283)

def move_zeroes(nums: list[int]) -> None:
    slow = 0  # position for next non-zero

    for fast in range(len(nums)):
        if nums[fast] != 0:
            nums[slow], nums[fast] = nums[fast], nums[slow]
            slow += 1

arr = [0, 1, 0, 3, 12]
move_zeroes(arr)
print(arr)  # [1, 3, 12, 0, 0]

Mapped Problems

ProblemKey Insight
#26 Remove Duplicates from Sorted ArraySlow writes unique values, fast scans ahead.
#27 Remove ElementSame reader-writer; skip target value.
#283 Move ZeroesSwap non-zeros to the slow pointer position.
#75 Sort ColorsDutch National Flag: three pointers (low, mid, high).
#80 Remove Duplicates IIAllow at most two of each; track count.

Pattern 3: Fast/Slow (Floyd’s Tortoise and Hare)

One pointer moves one step at a time; the other moves two. If there is a cycle, they will meet. This pattern is essential for cycle detection in linked lists and for finding the middle node.

Template: Cycle Detection

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def has_cycle(head: ListNode) -> bool:
    slow = fast = head

    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            return True

    return False

Why it works: In a cycle, the fast pointer gains one step per iteration. The gap between them decreases by one each time, so they must eventually collide.

Example: Find the Cycle Start (LeetCode #142)

def detect_cycle(head: ListNode) -> ListNode:
    slow = fast = head

    # phase 1: detect cycle
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:
            break
    else:
        return None  # no cycle

    # phase 2: find entry point
    slow = head
    while slow is not fast:
        slow = slow.next
        fast = fast.next

    return slow

The math behind phase 2: if the distance from head to cycle start is a, and the meeting point is b steps into the cycle, then a = c where c is the remaining cycle length after the meeting point.

Example: Find Middle of Linked List (LeetCode #876)

def middle_node(head: ListNode) -> ListNode:
    slow = fast = head

    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next

    return slow  # slow is at the middle

Mapped Problems

ProblemKey Insight
#141 Linked List CycleBasic fast/slow detection.
#142 Linked List Cycle IITwo-phase: detect, then find entry.
#876 Middle of the Linked ListWhen fast reaches end, slow is at middle.
#202 Happy NumberTreat the sequence as a linked list; detect cycle.
#287 Find the Duplicate NumberArray as implicit linked list; Floyd’s algorithm.

Choosing the Right Pattern

Use this decision tree:

  1. Sorted array + searching for a pair? Opposite-end pointers.
  2. In-place modification or partitioning? Same-direction reader-writer.
  3. Cycle detection or finding a midpoint? Fast/slow pointers.
  4. Substring or subarray with a constraint? Sliding window (same-direction variant).

Complexity Summary

PatternTimeSpaceTypical Use
Opposite-endO(n)O(1)Pair search, palindromes
Same-directionO(n)O(1)Dedup, partition, filter
Fast/slowO(n)O(1)Cycles, midpoints

Common Pitfalls

Off-by-one errors: Always clarify whether your loop condition is left < right or left <= right. For pair-finding, use strict < to avoid using the same element twice.

Forgetting to sort: Opposite-end pointers require sorted input. If the input is unsorted, sort first (O(n log n)) or use a hash set instead.

Infinite loops: Make sure at least one pointer moves in every iteration. If your condition does not advance either pointer, the loop never terminates.

Two pointers is a pattern family, not a single trick. Once you recognize which variant applies, the implementation follows a predictable template. Practice mapping problems to these three categories and the solutions will come naturally.