Pattern 02 · Arrays & Strings

Two Pointers

Keep two indices into the data and move them in a coordinated way — converging from opposite ends, chasing in the same direction, or walking two arrays at once. By exploiting structure (usually sorted order), you skip combinations that cannot win and squeeze an O(n²) pair search into one pass.

time O(n)space O(1)variants converge · fast/slow · mergepractice 13 problems
01 core concept

Skip the pairs that can't possibly work

Brute force tries every pair with nested loops — O(n²). Two pointers keeps two references and moves them in a single coordinated sweep — O(n).

Brute force (nested loops): Two pointers (single pass): for i in range(n): L = 0, R = n-1 for j in range(i+1, n): while L < R: check(i, j) check(L, R) move L or R O(n^2) O(n)

The key insight: structure — sorted order, partitioning logic — tells you which pointer to move, and every move permanently eliminates one element from consideration.

1 / 7walkthrough — two sum II on a sorted array, target = 10
1L
3
4
6
8
11R
target 10
Sorted array. L starts at the smallest value, R at the largest. The amber band is the remaining search space — watch it shrink.
02 three types

The three pointer choreographies

Type 1Opposite direction (converging)

Start at both ends and move toward each other. Use when the array is sorted and you need pairs.

Array: [ 1 3 5 7 9 11 15 ] ^ ^ L ---------------------->R L moves right, R moves left They converge toward the middle

Type 2Same direction (fast / slow)

One pointer scans ahead (fast / read) while the other marks the next "good" position (slow / write). Use for partitioning, removing duplicates in place, and linked-list cycle problems.

Array: [ 0 0 1 1 2 3 3 ] ^ ^ S F-->-->-->-->-->--> Slow writes, Fast reads Fast scans ahead, Slow marks "good" position
1 / 6walkthrough — move zeroes to the end, in place
0W R
1
0
3
12
write 0
read 0
The rule: R reads every element; when it finds a non-zero, swap it into slot W and advance W. R = 0 reads a 0 → do nothing, only R moves.

Type 3Two arrays (merge style)

One pointer per input; compare and advance the smaller one. Use when merging sorted arrays, intersecting them, or comparing element by element.

Array A: [ 1 3 5 8 ] Array B: [ 2 4 6 7 ] ^ ^ i --> j --> Compare A[i] vs B[j], advance the smaller one
03 recognition

When to reach for it

Problem signalTwo pointer type
"pair in sorted array that sums to X"Opposite direction
"triplet that sums to zero"Sort + opposite direction
"is this a palindrome?"Opposite direction
"container with most water"Opposite direction
"remove duplicates in-place"Same direction (R/W)
"move zeroes to end"Same direction (R/W)
"linked list has a cycle?"Same direction (fast/slow)
"merge two sorted arrays"Two arrays
"intersection of sorted arrays"Two arrays
"trapping rain water"Opposite direction
"sort colors (Dutch flag)"Three pointers

Decision framework

Is the input SORTED (or should you sort it)? | YES --> Do you need PAIRS/TRIPLETS? | | | YES --> Opposite direction pointers | | (left at start, right at end) | | | NO --> Are you MERGING two sorted inputs? | | | YES --> One pointer per input | NO --> Consider binary search instead | NO --> Do you need to PARTITION or REMOVE IN-PLACE? | YES --> Same direction (read pointer + write pointer) | NO --> Is it a LINKED LIST cycle/middle problem? | YES --> Fast/slow pointers NO --> Maybe not a two pointer problem
When NOT to use it
Converging pointers depend on sorted order — that's what makes "move L to grow the sum, move R to shrink it" true. Unsorted input where you can't afford to sort (e.g. you must return original indices) → use a hash map instead. Not merging, not partitioning, no sorted structure to exploit? It's probably not a two pointer problem.
04 templates

The templates and the classics

T1Opposite direction

opposite_direction.pypython
def opposite_direction(arr, target):
    """Generic opposite-direction two pointer on sorted array."""
    left, right = 0, len(arr) - 1

    while left < right:
        current = arr[left] + arr[right]  # or whatever comparison

        if current == target:
            return [left, right]          # found answer
        elif current < target:
            left += 1                     # need larger sum -> move left up
        else:
            right -= 1                    # need smaller sum -> move right down

    return []  # no solution

T2Same direction (read / write)

same_direction.pypython
def same_direction(arr):
    """Generic same-direction two pointer for in-place filtering."""
    write = 0                              # write pointer (slow)

    for read in range(len(arr)):           # read pointer (fast)
        if should_keep(arr[read]):         # your condition here
            arr[write] = arr[read]
            write += 1

    return write  # new length of valid portion

Ex 1Two Sum II — sorted array

two_sum_sorted.py — LC 167python
def two_sum_sorted(numbers, target):
    """Find pair in sorted array that sums to target. O(n) time, O(1) space."""
    left, right = 0, len(numbers) - 1

    while left < right:
        total = numbers[left] + numbers[right]

        if total == target:
            return [left + 1, right + 1]  # 1-indexed
        elif total < target:
            left += 1       # sum too small, increase left
        else:
            right -= 1      # sum too large, decrease right
Why this never skips a valid pair
The array is sorted, so moving L up increases the sum and moving R down decreases it. If sum < target, no right pointer could fix it with the current left; if sum > target, no left pointer could fix it with the current right — the eliminated element could never be part of the answer.

Ex 2Three Sum

three_sum.py — LC 15python
def three_sum(nums):
    """Find all unique triplets that sum to zero."""
    nums.sort()                            # MUST sort first
    result = []

    for i in range(len(nums) - 2):
        # Skip duplicate values for first element
        if i > 0 and nums[i] == nums[i - 1]:
            continue

        # Two pointer search for remaining pair
        left, right = i + 1, len(nums) - 1
        target = -nums[i]                  # a + b + c = 0 means b + c = -a

        while left < right:
            total = nums[left] + nums[right]

            if total == target:
                result.append([nums[i], nums[left], nums[right]])
                # Skip duplicates for both pointers
                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 total < target:
                left += 1
            else:
                right -= 1

    return result
Three Sum: nums = [-1, 0, 1, 2, -1, -4] After sort: [-4, -1, -1, 0, 1, 2] i=0, nums[i]=-4, target=4: L=1, R=5: -1+2=1 < 4, L++ L=2, R=5: -1+2=1 < 4, L++ L=3, R=5: 0+2=2 < 4, L++ L=4, R=5: 1+2=3 < 4, L++ L >= R, done i=1, nums[i]=-1, target=1: L=2, R=5: -1+2=1 == 1 --> [-1,-1,2] found! skip dup L, skip dup R -> L=3, R=4 L=3, R=4: 0+1=1 == 1 --> [-1,0,1] found! L=4, R=3 -> done i=2, nums[i]=-1, SKIP (same as nums[1]) Result: [[-1,-1,2], [-1,0,1]]

Ex 3Container With Most Water

max_area.py — LC 11python
def max_area(height):
    """Find two lines that form container holding most water."""
    left, right = 0, len(height) - 1
    best = 0

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

        # KEY INSIGHT: move the shorter line inward
        # Moving the taller line can only decrease or maintain area
        # (width shrinks, and height is bounded by the shorter line)
        # Moving the shorter line MIGHT find a taller one -> bigger area
        if height[left] < height[right]:
            left += 1
        else:
            right -= 1

    return best
height = [1, 8, 6, 2, 5, 4, 8, 3, 7] Step 1: [1 8 6 2 5 4 8 3 7] area = 8 * min(1,7) = 8 L R height[L]=1 < height[R]=7, move L Step 2: [1 8 6 2 5 4 8 3 7] area = 7 * min(8,7) = 49 L R height[R]=7 < height[L]=8, move R Step 3: [1 8 6 2 5 4 8 3 7] area = 6 * min(8,3) = 18 L R height[R]=3 < height[L]=8, move R Step 4: [1 8 6 2 5 4 8 3 7] area = 5 * min(8,8) = 40 L R equal, move either (say R) ...continue until L >= R. Best = 49.

Ex 4Trapping Rain Water

trap.py — LC 42python
def trap(height):
    """Calculate trapped rainwater. O(n) time, O(1) space."""
    left, right = 0, len(height) - 1
    left_max, right_max = 0, 0
    water = 0

    #  left_max                    right_max
    #     |                            |
    #     v                            v
    #   |                          |
    #   |  water   |       |  water|
    #   |__|_______|_______|_______|
    #   L ->                    <- R

    while left < right:
        if height[left] < height[right]:
            # Water at left is bounded by left_max
            # (right side is guaranteed >= height[right] > height[left])
            left_max = max(left_max, height[left])
            water += left_max - height[left]   # water above this bar
            left += 1
        else:
            # Water at right is bounded by right_max
            right_max = max(right_max, height[right])
            water += right_max - height[right]
            right -= 1

    return water

Ex 5Remove Duplicates from Sorted Array

remove_duplicates.py — LC 26python
def remove_duplicates(nums):
    """Remove duplicates in-place, return new length. O(n) time, O(1) space."""
    if not nums:
        return 0

    write = 1  # first element is always kept

    for read in range(1, len(nums)):
        if nums[read] != nums[write - 1]:   # new unique value found
            nums[write] = nums[read]         # write it to the next slot
            write += 1

    return write

    # Example walkthrough:
    # [1, 1, 2, 2, 3]
    #  W  R                 nums[1]=1 == nums[0]=1, skip
    #  W     R              nums[2]=2 != nums[0]=1, write -> [1, 2, ...]
    #     W     R           nums[3]=2 == nums[1]=2, skip
    #     W        R        nums[4]=3 != nums[1]=2, write -> [1, 2, 3, ...]
    # Result: [1, 2, 3, _, _], return 3

Ex 6Linked List Cycle (fast / slow)

has_cycle.py — LC 141python
def has_cycle(head):
    """Detect cycle in linked list using Floyd's algorithm."""
    slow = fast = head

    while fast and fast.next:
        slow = slow.next          # 1 step
        fast = fast.next.next     # 2 steps

        if slow is fast:          # they meet -> cycle exists
            return True

    return False  # fast reached end -> no cycle

    #  S
    #  F
    #  1 -> 2 -> 3 -> 4 -> 5
    #                  ^    |
    #                  |    v
    #                  7 <- 6
    #
    #  Step 1: S=2, F=3
    #  Step 2: S=3, F=5
    #  Step 3: S=4, F=7
    #  Step 4: S=5, F=4
    #  Step 5: S=6, F=6  --> MEET! Cycle detected.

Complexity reference

ProblemTimeSpaceTechnique
Two Sum IIO(n)O(1)Opposite direction
Three SumO(n²)O(1)*Sort + opposite direction
Container With Most WaterO(n)O(1)Opposite, greedy move
Trapping Rain WaterO(n)O(1)Opposite, track max
Remove DuplicatesO(n)O(1)Same direction R/W
Move ZeroesO(n)O(1)Same direction swap
Merge Sorted ArraysO(n+m)O(1)**Two arrays
Linked List CycleO(n)O(1)Fast/slow
Palindrome CheckO(n)O(1)Opposite direction

* excluding output  ·  ** if merging into existing space

05 common mistakes

Where pointers go wrong

Mistake 1 — converging on unsorted input
"Move L to grow the sum, move R to shrink it" is only true on a sorted array. Three Sum's first line is nums.sort() — forget it and the elimination argument collapses, and you'll silently skip valid pairs.
Mistake 2 — not skipping duplicates in Three Sum
After recording a triplet you must advance both pointers past runs of equal values (and skip repeated nums[i]) — otherwise the result contains duplicate triplets.
Mistake 3 — moving the taller line in Container With Most Water
Always move the shorter line inward. Moving the taller one can only decrease or maintain area — width shrinks and height stays bounded by the shorter line. Moving the shorter line might find a taller one.
Mistake 4 — unsafe fast-pointer advance
In fast/slow loops, guard with while fast and fast.next before doing fast.next.next — otherwise you dereference None at the end of an acyclic list.
Mistake 5 — breaking order with the wrong write strategy
Same-direction problems that must preserve relative order (Move Zeroes) need the write-and-swap pattern, not clever overwrites. And when merging into existing space (LC 88), fill from the end so you never overwrite unread values.
10 practice

Practice ladder

In this order — easiest first. Tick each one as you solve it, and re-solve anything that took more than 25 minutes two days later.

13 problems
easyLC 125
Opposite direction, skip non-alphanumeric. The warm-up.
easyLC 283
Same direction swap — the viz above, live.
easyLC 26
Same direction read/write. Template T2.
easyLC 88
Two arrays — fill from the end to avoid overwrites.
easyLC 141
Fast/slow — Floyd's algorithm.
easyLC 876
Fast/slow — fast at 2&times; speed lands slow at the middle.
mediumLC 167
Opposite direction — the canonical converging pair.
mediumLC 11
Opposite direction + greedy "move the shorter line".
mediumLC 15
Sort + opposite direction + duplicate skipping.
mediumLC 16
Sort + opposite direction + track best difference.
mediumLC 75
Dutch National Flag — three pointers.
mediumLC 18
Sort + nested two pointers.
hardLC 42
The boss fight: opposite direction + left/right running max.