Three Sum as Composition

Find all unique triplets in [-1, 0, 1, 2, -1, -4] that sum to zero.

This is the single most-asked two-pointer interview question on LeetCode, and it trips up even experienced candidates. Not because the algorithm is complicated, but because the framing is misleading. When you read “find three numbers that sum to zero,” your brain tries to invent a three-pointer approach from scratch. That's the wrong instinct.

The real question isn't “how do I handle three elements at once?” It's "how do I avoid handling three elements at once?"

You already know an efficient technique for finding two numbers that hit a target. What if the three-element problem was secretly a two-element problem in disguise?

The decomposition

Here's the sorted array: [-4, -1, -1, 0, 1, 2]. Three numbers summing to zero sounds like it needs something entirely new. But does it?

Try this: tap any element to “lock it in.” Then look at what's left and what you need from it.

Tap any element to fix it. Then see what's left.
0
1
2
3
4
5
Try fixing different elements to see how the subproblem changes.

Every time you fix an element, the remaining problem has a familiar shape. You just discovered the decomposition by doing it. Can you describe the general strategy in your own words before moving on?

Think about it: you picked one element, and the leftover was a Two Sum problem you already know how to solve. What would a full algorithm look like if you repeated that idea for every possible “fixed” element? And what might go wrong if the same triplet gets found more than once?

This decomposition pattern -- reduce a bigger problem by fixing one variable and solving a smaller version -- shows up everywhere in algorithm design. It's the same idea behind dynamic programming, divide-and-conquer, and mathematical induction. Master it here and you'll recognize it instantly when it reappears.

Watch it compose

Here's the full Three Sum running on [-4, -1, -1, 0, 1, 2]. The outer loop fixes element i in violet. For each fixed element, two pointers run Two Sum on the remaining subarray.

But this isn't a passive demo. Before each step, you'll predict what happens next. Will the algorithm find a triplet? Move a pointer? Skip a duplicate? Think about the sum, then commit to your prediction.

Step 1/11
i
-4
0
L
-1
1
-1
2
0
3
1
4
R
2
5
-4 + -1 + 2 = -3
What happens next?

Your turn

Now you drive the inner Two Sum. For each fixed element, you decide which pointer should move. The outer loop advances automatically — your job is the convergence logic.

Remember the rule: if the current sum is too low, you need a larger value — so the left pointer should advance rightward. If it's too high, you need a smaller value — so the right pointer should retreat leftward. If it matches, you've found a triplet.

The sum and its distance from the target are shown. Use them to reason about which direction corrects the deficit.

Target sum: 0Step 1/11
i
-2
0
L
-1
1
0
2
1
3
2
4
R
3
5
-2 + -1 + 3 = 0
What do you think?

The sum is 0. Is this a match?

See Four Sum

If Three Sum is “one for-loop wrapping Two Sum,” what's Four Sum?

Think about it: you need four elements summing to a target. If you fix one element, what problem remains? A three-element problem — which you just learned to solve. And inside that Three Sum, fixing another element reduces to... Two Sum.

Each layer fixes one number and reduces the problem by one dimension. Four Sum fixes the first, leaving Three Sum. Three Sum fixes the second, leaving Two Sum. Two Sum is the base case — the converging pointer walk you already know cold. The whole thing nests like Russian dolls:

1
// Four Sum: fix nums[i], solve Three Sum on remainder
2
for (let i = 0; i < n - 3; i++) {
3
  // Three Sum: fix nums[j], solve Two Sum on remainder
4
  for (let j = i + 1; j < n - 2; j++) {
5
    // Two Sum: converging pointers
6
    let left = j + 1, right = n - 1;
7
    while (left < right) { /* ... */ }
8
  }
9
}

Notice how each inner loop starts after the outer loop's index. The j loop starts at i + 1, not 0. The left pointer starts at j + 1, not i + 1. Each layer operates on the remaining subarray after fixing its element — this is what prevents the same combination from being found in different orders.

Four Sum has three layers. Before each layer is revealed, predict what it does.

Peel back the layers of Four Sum
for i
Fix first element
-2
-1
0
0
1
2

The outer loop fixes one element. What does the next layer do?

The reduction pattern

Three Sum = for loop + Two Sum. That's the core trick. But the real insight is bigger than Three Sum — and it's worth pausing to name it clearly.

Before reading further, predict: if Five Sum reduces to Four Sum, and Four Sum reduces to Three Sum, what's the time complexity of Five Sum? You know Three Sum is O(n^2) and each layer adds one for-loop. Can you generalize the pattern to k-Sum?

The code mirrors the nesting: for loops wrapping for loops, each one handling a reduction layer. The outermost loop says “what if I fix this element?” The next loop says "okay, with that fixed, what if I fix this one too?" And so on, peeling away dimensions until you reach the two-element base case where converging pointers take over.

Four Sum = for loop + Three Sum. Five Sum = for loop + Four Sum. In general, k-Sum reduces to (k-1)-Sum by fixing one element. It's the same trick at every level, nested deeper.

4Sumfor iO(n³)3Sumfor jO(n²)2SumL→ ←RO(n)

The outermost loop fixes one element. The next loop fixes another. You keep peeling layers until you're down to Two Sum — the converging pointer walk you already know cold. The only new concern at each layer is duplicate skipping, and that follows the same pattern everywhere: if (i > start && nums[i] === nums[i-1]) continue.

Let's make this concrete with some complexity analysis. For Two Sum on a sorted array, converging pointers run in O(n). Each additional layer adds one for-loop, multiplying by n:

  • Two Sum: O(n)
  • Three Sum: O(n) * O(n) = O(n^2)
  • Four Sum: O(n) * O(n^2) = O(n^3)
  • k-Sum: O(n^(k-1))

This is the best you can do for general k-Sum without hashing tricks. And the structure of every solution is identical: k - 2 nested for-loops wrapping a Two Sum core.

Arrange the code

Now you'll build the Three Sum solution from scattered pieces. The full implementation is scrambled below — tap lines in the correct order to reassemble it.

This isn't about memorizing syntax. It's about understanding the structure. Think about the composition pattern: what must come first? The array needs to be sorted before pointers can converge — that's the precondition. Then the outer loop that fixes elements, one at a time. Then the inner Two Sum machinery — pointer initialization, the while loop, sum computation, and the three-way branch (too small, too big, match). And woven throughout: the duplicate skips you just learned.

If you pick a wrong line, you'll see exactly why it can't go there yet — the error message tells you what dependency you violated.

Tap lines below in the correct order
____________________
Available lines
0/9 placed

Fill Four Sum

Here's a Four Sum template with three blanks — all at the critical reduction points where the layers connect to each other.

The blanks test whether you understand the nesting pattern concretely, not just abstractly. Each blank sits at a boundary between layers: where does the inner j loop start? Where does the left pointer initialize? How does the sum combine all four elements?

If you internalized the reduction pattern, every blank has exactly one correct answer. The inner loop starts after the outer loop's index — because you don't want to reuse the element you already fixed. The left pointer starts after the inner loop's index — same reason, one layer deeper. And the sum must include all four elements: the two fixed by the for-loops and the two pointed to by left and right.

1
// The pattern at each boundary:
2
// outer loop fixes nums[i]     → i starts at 0
3
// inner loop fixes nums[j]     → j starts at ___  (after i)
4
// left pointer                  → starts at ___    (after j)
5
// sum = nums[i] + nums[j] + nums[left] + nums[right]
function fourSum(nums, target) {
nums.sort((a, b) => a - b)
for (let i = 0; i < nums.length - 3; i++) {
// skip duplicate i
for (; j < nums.length - 2; j++) {
// skip duplicate j
, r = nums.length - 1
while (l < r) {
const sum =
if (sum === target) { /* found */ }
else if (sum < target) l++
else r--
}
}
}
}

Spot the reduction

The k-Sum reduction is powerful, but it has a specific shape. It works when you need exactly k unordered elements from an array that sum to a target. It does not work when the constraint involves contiguity (subarray sum problems), ordering (sequence problems), or when k equals 2 (that's already the base case — you don't reduce Two Sum, you solve it directly with converging pointers).

The tell is in the problem statement. “Find k elements” with a sum target and no adjacency constraint? k-Sum reduction. “Find a contiguous subarray with sum X”? Sliding window or prefix sums — totally different structure. “Find pairs with a property”? Already at the base case.

Being able to classify a problem before coding is what separates pattern recognition from pattern memorization. Four problem descriptions follow. Classify each: is it a k-Sum reduction, or something structurally different?

Question 1/4

Find all unique quadruplets summing to T

Given an array of n integers, return all unique groups of four elements whose values add up to a target T.

Can this be solved with fix-one-element reduction?

Synthesis

Every k-Sum problem is the same trick nested k - 2 times. The outer loops fix elements. The innermost pair runs Two Sum. The only new complexity at each level is duplicate skipping — and you've already seen how that works.

Before reading the summary, test yourself: if someone asked you to implement Six Sum, how many nested for-loops would you write, and what would the innermost structure be? If you can answer without hesitating, you own the pattern.

Here's the mental model to carry forward:

When you see "find k elements that sum to target," don't invent a k-pointer technique. Instead, ask: “What if I fixed one?” That reduces the problem to (k-1)-Sum. Keep fixing until you're at Two Sum, then deploy converging pointers.

Three Sum isn't special. It's a for-loop wrapping Two Sum. Four Sum is a for-loop wrapping Three Sum. Once you see the reduction, k-Sum for any k is mechanical.

The structure maps cleanly to complexity analysis: each “fix one” layer adds an O(n) loop, so the total is always O(n^(k-1)). And the code structure is always the same — k - 2 nested for-loops wrapping a Two Sum core, with duplicate skipping at every level following the identical if (idx > start && nums[idx] === nums[idx-1]) continue pattern.

exponent of n2SumO(n)3SumO(n²)4SumO(n³)5SumO(n⁴)

The decomposition principle — reduce a problem by fixing one variable and solving the rest — is one of the most powerful tools in your algorithmic toolkit. It's the same idea that powers mathematical induction ("assume it works for n-1, prove it for n"), divide-and-conquer (“split the problem, solve the halves”), and backtracking (“make one choice, recurse on the rest”). You'll recognize it the moment it appears in those contexts — because you've already wielded it here.

This is the first time you're seeing the pattern, but it won't be the last. And the next time a problem says “find k things satisfying a constraint,” your first instinct should be: what if I fixed one?