Skip to content

Two Sum II

Naseebullah AhmadiSenior Software Engineer, London

Two pointers on a sorted array finds the pair in one pass. But why is it safe to throw a number away after one bad sum? Draw every pair as a grid and the proof becomes a picture.

7 min read
#algorithms

The problem: given a sorted array and a target, return the positions of the two values that add up to it. Take [1, 3, 4, 5, 7, 11] and a target of 9.

Every pair, as a grid

Lay out every possible pair as a grid: rows are the smaller value, columns the larger. Brute force visits all 15 cells. Two pointers starts at the top-right corner, the smallest value with the largest, and the sum it finds there decides which way to move.

left ╲ right345711
122221
34431
4✓31
531
71

Each number is the step that ruled that cell out:

  1. 11 + 11 = 12, too big. 1 is the smallest value there is, so 11 is too big with anyone. The whole 11 column goes.
  2. 21 + 7 = 8, too small. 7 is now the largest value left, so 1 is too small with anyone. The whole 1 row goes.
  3. 33 + 7 = 10, too big. The 7 column goes.
  4. 43 + 5 = 8, too small. The 3 row goes.
  5. 54 + 5 = 9. Found it, at positions 3 and 4.

Five comparisons, and every one deleted an entire row or column of the grid, not a single cell. That's why it's safe: you never throw away a pair you haven't proven can't work. And it's why the array must be sorted: "1 is the smallest" is the sentence that kills the column.

Move the wrong pointer (left when the sum is too big) and you delete a row that might hold the answer, walking straight past it.

One loop, 1-indexed answer

@itsnas two-sum.ts
codetwo-sum.ts
// Returns 1-indexed positions, per the sorted Two Sum convention.
function twoSum(
  numbers: number[],
  target: number,
): [number, number] | null {
  let left = 0
  let right = numbers.length - 1
 
  while (left < right) {
    const sum = numbers[left] + numbers[right]
    if (sum === target) return [left + 1, right + 1]
    if (sum < target) left++ // left is too small for anything remaining
    else right-- // right is too big for anything remaining
  }
 
  return null
}
main
Nas (@itsnas)

O(n)O(n) time, O(1)O(1) space. The classic version wants 1-indexed positions, so add one to each index before returning: a correct walk with the wrong offset still fails every test. The guard is left < right because a pair needs two distinct slots.

When the array isn't sorted

Without order, nothing tells you a value is "the smallest", so no comparison can delete a row. Use a hash map instead: for each value, check whether target - value was already seen. That's O(n)O(n) time but O(n)O(n) space; sorting first and walking the pointers trades that space for O(nlog⁡n)O(n \log n) time.

The grid is worth remembering. 3Sum runs this exact walk once per fixed value, and "closest to target" walks the same grid while tracking the best difference instead of stopping on a match.