Two Pointer Technique — Core Concepts

Why two pointers work

The two pointer technique reduces O(n²) brute-force pair checking to O(n) by exploiting structure in the data — usually sorted order or linked list topology. Instead of examining every possible pair, two pointers narrow the search space by eliminating large groups of candidates with each comparison.

Pattern 1: Converging pointers

Place one pointer at the start and one at the end. Move them toward each other based on a condition.

When to use: Sorted arrays, pair problems, palindrome checks.

Two Sum in sorted array

def two_sum_sorted(nums, target):
    left, right = 0, len(nums) - 1

    while left < right:
        current = nums[left] + nums[right]
        if current == target:
            return [left, right]
        elif current < target:
            left += 1      # need a bigger sum
        else:
            right -= 1     # need a smaller sum

    return []

print(two_sum_sorted([1, 3, 5, 7, 9, 11], 12))  # [1, 4] → 3+9

Why it works: When the sum is too small, increasing the left pointer (to a larger value) is the only way to increase the sum. When too large, decreasing the right pointer is the only way to decrease it. Every step eliminates an entire row or column of possibilities.

Palindrome check

def is_palindrome(s):
    s = s.lower()
    left, right = 0, len(s) - 1

    while left < right:
        # Skip non-alphanumeric
        while left < right and not s[left].isalnum():
            left += 1
        while left < right and not s[right].isalnum():
            right -= 1

        if s[left] != s[right]:
            return False
        left += 1
        right -= 1

    return True

print(is_palindrome("A man, a plan, a canal: Panama"))  # True

Container with most water

def max_area(height):
    left, right = 0, len(height) - 1
    best = 0

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

        if height[left] < height[right]:
            left += 1
        else:
            right -= 1

    return best

Pattern 2: Same-direction pointers

Both pointers start at the beginning and move right, but at different rates or under different conditions.

When to use: In-place array modification, removing duplicates, partitioning.

Remove duplicates from sorted array (in-place)

def remove_duplicates(nums):
    if not nums:
        return 0

    write = 1  # slow pointer: where to write next unique value

    for read in range(1, len(nums)):  # fast pointer: scans ahead
        if nums[read] != nums[read - 1]:
            nums[write] = nums[read]
            write += 1

    return write  # number of unique elements

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

Move zeros to end

def move_zeroes(nums):
    write = 0

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

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

Pattern 3: Fast and slow pointers

Two pointers move at different speeds through a linked list or sequence. Classic application: cycle detection.

When to use: Linked list cycles, finding middle elements, detecting patterns in sequences.

Linked list cycle detection (Floyd’s algorithm)

def has_cycle(head):
    slow = fast = head

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

        if slow == fast:
            return True

    return False

Why it works: If there is a cycle, the fast pointer will eventually catch up to the slow pointer inside the cycle. If there is no cycle, the fast pointer reaches the end. The key mathematical insight: in a cycle of length L, the fast pointer closes the gap by 1 node each step, so they must meet within L steps of the slow pointer entering the cycle.

Find middle of linked list

def find_middle(head):
    slow = fast = head

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

    return slow  # middle node

When fast reaches the end, slow is at the middle. This avoids the need to count the list length first.

The 3Sum pattern

3Sum combines sorting with converging two pointers:

def three_sum(nums):
    nums.sort()
    result = []

    for i in range(len(nums) - 2):
        if i > 0 and nums[i] == nums[i-1]:
            continue  # skip duplicates

        left, right = i + 1, len(nums) - 1
        while left < right:
            total = nums[i] + nums[left] + nums[right]
            if total == 0:
                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 total < 0:
                left += 1
            else:
                right -= 1

    return result

print(three_sum([-1, 0, 1, 2, -1, -4]))
# [[-1, -1, 2], [-1, 0, 1]]

Time: O(n²) — one loop × two pointer scan. This is optimal for 3Sum; no known O(n log n) solution exists.

How to identify two pointer problems

  • “Pair in sorted array” → converging pointers
  • “In-place modification” → same-direction (read/write pointers)
  • “Palindrome” → converging from both ends
  • “Cycle detection” → fast/slow pointers
  • “K-sum” → sort + nested converging pointers

Common misconception

“Two pointers only work on sorted arrays.” Two pointers work on any data with exploitable structure. Sorted order is the most common, but linked list topology (fast/slow), partition invariants (Dutch national flag), and monotonic conditions also enable two-pointer solutions.

One thing to remember: The two pointer technique works because each pointer movement eliminates possibilities that cannot lead to a better answer. The art is choosing what condition controls each pointer’s movement.

pythonalgorithmstwo-pointers

See Also