Skip to content

3Sum

Naseebullah AhmadiSenior Software Engineer, London

Find every triplet that sums to zero, without listing any twice. Three unknowns become two once you fix one, and the hard part turns out to be the duplicates, not the search.

11 min read
#algorithms

Input: [-1, 0, 1, 2, -1, -4]. Answer: [-1, -1, 2] and [-1, 0, 1], each exactly once. Checking every triple is O(n3)O(n^3).

Fix one, and it's Two Sum II

Sort first. Then pick a pivot nums[i], and the question "which three numbers sum to zero?" becomes "which two numbers, to the right of the pivot, sum to -nums[i]?" That's Two Sum II: two pointers closing in, one comparison deciding which side to drop.

  1. Index 0: -4
  2. Index 1: -1 (i)
  3. Index 2: -1 (L)
  4. Index 3: 0
  5. Index 4: 1
  6. Index 5: 2 (R)
Pivot -1, so L and R hunt for a pair summing to 1. -1 + 2 = 1: the first triplet, [-1, -1, 2].

After a hit, both pointers move in, and the sweep finds 0 + 1 = 1: the second triplet, [-1, 0, 1]. Running one O(n)O(n) sweep per pivot gives O(n2)O(n^2) overall, and the sort's O(nlog⁡n)O(n \log n) disappears inside that.

Where duplicates come from

Now the outer loop moves to the next pivot, index 2. It's another -1.

  1. Index 0: -4
  2. Index 1: -1 (done)
  3. Index 2: -1 (i: skip)
  4. Index 3: 0
  5. Index 4: 1
  6. Index 5: 2
Same pivot value as the last round. Its sweep can only find pairs the last round already found: [-1, 0, 1] again.

A pivot with the same value as the previous one hunts for the same target in a subset of the same numbers, so anything it finds is a repeat. Sorting put equal values side by side, which makes the fix one line: if nums[i] === nums[i - 1], skip it.

The same thing happens inside the sweep. After recording a hit, if the next L is the same value as the last one, it pairs with the same R partner and records the same triplet. So after each hit, step both pointers past equal neighbours before continuing.

RuleWithout it
Skip a pivot equal to the previous pivotthe whole sweep repeats its triplets
After a hit, skip equal neighbours of L and Rthe same triplet twice in one sweep
Start L at i + 1, not 0the pivot pairs with itself
Stop once nums[i] > 0wasted sweeps: three positives can't sum to 0

Two skips and a sweep

@itsnas three-sum.ts
codethree-sum.ts
function threeSum(nums: number[]): number[][] {
  nums.sort((a, b) => a - b)
  const result: number[][] = []
 
  for (let i = 0; i < nums.length - 2; i++) {
    if (nums[i] > 0) break
    if (i > 0 && nums[i] === nums[i - 1]) continue // skip pivot dupes
 
    let left = i + 1
    let right = nums.length - 1
 
    while (left < right) {
      const sum = nums[i] + nums[left] + nums[right]
      if (sum === 0) {
        result.push([nums[i], nums[left], nums[right]])
        while (left < right && nums[left] === nums[left + 1]) left++
        while (left < right && nums[right] === nums[right - 1]) right--
        left++
        right--
      } else if (sum < 0) {
        left++
      } else {
        right--
      }
    }
  }
 
  return result
}
main
Nas (@itsnas)

O(n2)O(n^2) time, O(1)O(1) extra space beyond the sort and the output. You can't do better than O(n2)O(n^2) in the worst case anyway: an input full of zeros and pairs can have O(n2)O(n^2) distinct triplets to return.

The shape generalises. 4Sum fixes two pivots and runs the same sweep inside; 3Sum Closest tracks the best difference instead of matching exactly. Valid Triangle Number flips it around and fixes the largest value, so that one comparison can count a whole range of pairs at once.