CodeChallenge LogoCodeChallenge

Two-Pointer Technique - 6 Patterns That Turn O(n²) Into O(n)

The two-pointer technique replaces nested loops with a single scan. Learn 6 patterns: opposite direction, same direction, sliding window, three-way partition, fast and slow, and merge.

July 21, 2026
9 min read
#two-pointer #algorithms #arrays #javascript #problem-solving
Two-Pointer Technique - 6 Patterns That Turn O(n²) Into O(n)

I remember the first time a senior engineer looked at my pull request and asked: “Why are you using two loops here?”

I had written a nested loop to find a pair that sums to a target. It passed the tests. The code was clean. I was proud of it. But it was O(n²). He opened a new tab, wrote the same solution with two moving pointers, and ran it on ten thousand items. Mine took seconds. His finished in milliseconds.

That moment changed how I approach array problems. Two pointers are not a single trick — they are a family of patterns. Each has a distinct structure and a specific type of problem it solves. Remove duplicates, merge sorted lists, detect cycles, partition arrays, track subarrays — all of them use coordinated indices that avoid revisiting elements.

The six patterns below are the ones I reach for most often, arranged from the most common to the most specialized. Learn them one at a time, and you will start recognizing the right pattern before you write the first line of code.

Short version: Two pointers work when the array is sorted (or can be sorted), when you are comparing pairs, or when you need a subarray in one pass. The six patterns below cover 90% of two-pointer problems you will encounter.

Pattern 1: Opposite Direction (Sorted Two Sum)

This is the classic pattern. You have a sorted array and two pointers: one at the start, one at the end. You check the sum. If it is too small, you move the left pointer up. If it is too big, you move the right pointer down. If it matches, you are done.

function twoSum(numbers, target) {
  let left = 0
  let right = numbers.length - 1

  while (left < right) {
    const sum = numbers[left] + numbers[right]
    if (sum === target) return [left, right]
    if (sum < target) left++
    else right--
  }
  return [-1, -1]
}

The insight is that the sorted order gives you information. Moving left increases the sum. Moving right decreases it. You are effectively narrowing down the search space by one element at each step, never revisiting anything. That is the whole reason it is O(n) instead of O(n²).

This pattern also solves Container With Most Water, where you calculate the area between left and right pointers and move the shorter side inward.

Pattern 2: Same Direction (Remove Duplicates)

Here both pointers start at the beginning, but one moves faster. The slow pointer marks the “write position” while the fast pointer scans ahead looking for unique values. When fast finds a value different from the one at slow, slow advances and takes that value.

function removeDuplicates(nums) {
  if (nums.length === 0) return 0
  let slow = 0
  for (let fast = 1; fast < nums.length; fast++) {
    if (nums[fast] !== nums[slow]) {
      slow++
      nums[slow] = nums[fast]
    }
  }
  return slow + 1
}

The slow pointer stays behind while the fast pointer runs ahead. This is the standard in-place array modification pattern. It preserves the relative order and uses no extra space.

Variations of this pattern solve problems like move zeroes to the end and remove a specific value from an array. The structure is always the same: one pointer tracks where to write, the other scans for the next valid element.

Pattern 3: Sliding Window (Fixed Size)

A sliding window is a two-pointer pattern where both pointers move in sync, maintaining a fixed distance between them. This is useful when you need to process every subarray of size k in O(n) instead of O(n × k).

function maxSumSubarray(arr, k) {
  let windowSum = 0
  for (let i = 0; i < k; i++) windowSum += arr[i]
  let maxSum = windowSum
  for (let i = k; i < arr.length; i++) {
    windowSum += arr[i] - arr[i - k]
    maxSum = Math.max(maxSum, windowSum)
  }
  return maxSum
}

The trick is that instead of recomputing the sum of each window from scratch, you add the new element and subtract the old element. The left pointer is implicit: it is always i - k. This pattern is the foundation for more advanced variable-size sliding window problems.

Pattern 4: Three-Way Partition (Dutch National Flag)

Sometimes two pointers are not enough. The Dutch National Flag problem asks you to sort an array containing only 0s, 1s, and 2s in a single pass. You need three pointers: low, mid, and high. The invariant is that everything before low is 0, everything after high is 2, and mid scans through the middle.

function sortColors(nums) {
  let low = 0, mid = 0, high = nums.length - 1
  while (mid <= high) {
    if (nums[mid] === 0) {
      [nums[low], nums[mid]] = [nums[mid], nums[low]]
      low++; mid++
    } else if (nums[mid] === 1) {
      mid++
    } else {
      [nums[mid], nums[high]] = [nums[high], nums[mid]]
      high--
    }
  }
}

This is the only pattern on this list that uses three pointers instead of two. But the thinking is the same: each pointer has a defined responsibility, and the algorithm maintains a set of invariants that guarantee correctness without nested loops.

The three-way partition pattern also appears in quicksort optimizations (the “Dutch flag” partition) and in problems that partition data into three categories.

Pattern 5: Fast and Slow (Cycle Detection)

Instead of both pointers moving at the same speed, one moves twice as fast as the other. If there is a cycle in a linked list, the fast pointer will eventually lap the slow pointer and they will meet. If there is no cycle, the fast pointer reaches the end.

function hasCycle(head) {
  if (!head) return false
  let slow = head, fast = head
  while (fast && fast.next) {
    slow = slow.next
    fast = fast.next.next
    if (slow === fast) return true
  }
  return false
}

This is not an array problem, but it belongs in the same mental family because it uses two pointers moving through a sequence. The fast pointer effectively “skips” elements, which gives it a different view of the data than the slow pointer. Comparing their positions reveals information you cannot get from a single traversal.

Beyond cycle detection, this pattern solves find the middle of a linked list (fast reaches the end when slow is at the middle) and find the start of a cycle (reset one pointer to head after they meet, then move both at the same speed).

Pattern 6: Two Sequences (Merge)

When you have two sorted arrays and you need to merge them into one, two pointers each on a different array is the natural solution. At each step, you take the smaller of the two values and advance the pointer that produced it.

function mergeSortedArrays(arr1, arr2) {
  let i = 0, j = 0, result = []
  while (i < arr1.length && j < arr2.length) {
    if (arr1[i] < arr2[j]) result.push(arr1[i++])
    else result.push(arr2[j++])
  }
  while (i < arr1.length) result.push(arr1[i++])
  while (j < arr2.length) result.push(arr2[j++])
  return result
}

This pattern extends naturally to problems like find the intersection of two arrays and find the median of two sorted arrays. The key is that each array has its own pointer, and you decide which one to advance based on the values you see.

How to Spot a Two-Pointer Problem

Not every array problem benefits from two pointers. The table below maps each of the six patterns to the signal that tells you it is the right choice, along with classic problems you can practice on CodeChallenge.

Pattern Signal Classic Problem
Opposite Direction Array is sorted, looking for a pair Two Sum II, Container With Most Water
Same Direction In-place modification or filtering Remove Duplicates, Move Zeroes
Sliding Window Subarray of fixed or variable size Maximum Sum Subarray, Longest Substring
Three-Way Array with three distinct values Sort Colors (Dutch National Flag)
Fast & Slow Linked list, unknown length or cycle Linked List Cycle, Middle of Linked List
Two Sequences Two sorted arrays to combine Merge Sorted Arrays, Intersection of Two Arrays

If your problem matches any of these signals, ask yourself: “Can I walk through this with two positions instead of nested loops?” The answer is yes more often than you think. Start with the pattern that fits the shape of your data, and the implementation will follow naturally.

The CodeChallenge Connection

The two-pointer technique is not just an interview trick — it is embedded in several CodeChallenge challenges across multiple categories.

Array Category — Opposite and Same Direction

The Find Pair with Difference K challenge in the Array category is a direct application of the opposite-direction pattern. The array is sorted, and you adjust two pointers based on whether the difference is too large or too small. The Remove Duplicates from Sorted Array challenge uses the same-direction pattern: one pointer writes, one scans.

Sorting Category — Preparation Step

Many two-pointer problems require sorting the array first. The Sorting category on CodeChallenge builds the foundation. Once you are comfortable sorting, the two-pointer problems become much easier because you can rely on the order to guide your pointer movements. The Merge Intervals challenge, for example, sorts intervals by start time before merging them in a single pass — a two-pointer mindset applied to a different data structure.

Sliding Window and Advanced Patterns

The Maximum Sum Subarray challenge tests the fixed-size sliding window pattern. For the three-way partition pattern, try the Sort Colors challenge, which sorts an array of 0s, 1s, and 2s in a single pass using three pointers. The Data Table Grid challenge does not use two pointers directly, but the habit of processing data with coordinated read and write positions applies to its sorting and filtering logic.

Key Takeaway

Two pointers replace nested loops with a single scan. The six patterns cover opposite direction, same direction, sliding window, three-way partition, fast and slow, and two sequences. Each has a distinct visual structure: converging from ends, lagging behind, moving in sync, partitioning with three pointers, lapping at different speeds, and advancing independently.

The next time you write a nested loop over an array, stop and ask: can I reframe this as two positions moving through the data? If yes, you just saved an order of magnitude in runtime.

Share:
Found this helpful?

Related Articles