Skip to content
intermediate

3Sum

A reliable 3Sum solution comes from turning a cubic search into a sequence of sorted two-sum scans—and proving why each pointer move is safe.

Published 2026-09-02Updated 2026-09-1210 min read
Close-up of hands coding on a laptop, showcasing software development in action.
Close-up of hands coding on a laptop, showcasing software development in action. Photo by cottonbro studio on Pexels.
Problem

3Sum

Difficulty: MediumAcceptance rate: 39.8%

Given an integer array, return every distinct triplet of values taken from three different indices whose sum is zero.

ArrayTwo PointersSorting

Constraints

  • 3 <= nums.length <= 3000
  • -10^5 <= nums[i] <= 10^5

Important details

  • No duplicate triplets may appear in the result.
  • The ordering of the returned triplets and the values within each triplet does not matter.
  • The three selected indices must be distinct.

A reliable 3Sum solution comes from turning a cubic search into a sequence of sorted two-sum scans—and proving why each pointer move is safe.

Start with the obligations

Given an integer array, return every unique triplet of values from three different indices whose sum is zero. The result must not repeat a value triplet, even when the input contains repeated values.

There are three obligations to keep visible while designing the algorithm:

  1. Choose three different positions.
  2. Make their values sum to zero.
  3. Return each value composition once.

For example, [-1, 0, 1] and [1, -1, 0] describe the same triplet. Sorting gives every discovered triplet a canonical order, which makes both duplicate handling and reasoning much cleaner.

The optimized plan is short:

  1. Sort the array.
  2. Fix one value at index i.
  3. Search the suffix for two values summing to -nums[i].
  4. Move two pointers according to whether the current sum is too small or too large.
  5. Skip repeated values at the fixed position and after each match.

The important idea is not merely that two pointers are fast. Sorting makes the remaining search monotonic, so one movement can eliminate a whole group of impossible pairs.

The brute-force baseline

The direct approach chooses indices in increasing order:

i < j < k

For each combination, calculate:

nums[i] + nums[j] + nums[k]

Save the triplet when the sum is zero. This checks every possible combination and takes O(n³) time.

A duplicate-free version can sort the input, store each matching triplet as a tuple, and use a set to remove repeated value compositions. That is a valid baseline because it establishes coverage: every combination is considered, and the three indices are distinct by construction.

But it spends work on pairs that sorted order could reject immediately. The optimized version keeps the same obligations while shrinking the candidate space. That is the real transition from brute force to two pointers.

Sort, fix, and reduce to 2Sum

Sort the values in ascending order. For a fixed index i, the remaining equation is:

nums[left] + nums[right] == -nums[i]

Initialize the pointers like this:

left = i + 1
right = n - 1

Because left starts after i and the loop runs only while left < right, the three selected indices are always different.

The outer loop stops at n - 3, because two positions must remain after the fixed value. In Python, range(n - 2) expresses that boundary.

There is also a safe early stop. Once nums[i] > 0, every later value is positive because the array is sorted. Any later triplet therefore has a positive sum, so it cannot equal zero.

if nums[i] > 0:
    break

This guard is an optimization, not the central proof. The central proof is the monotonic behavior of the sorted suffix.

Why the pointers move in one direction

A sorted array segment with a fixed index and left and right pointers feeding into a sum comparison: a negative sum advances the left pointer, a positive sum retreats the right pointer, and a zero sum records the triplet before moving both pointers inward.
Because the suffix is sorted, each comparison safely eliminates a whole group of pairs and moves the scan in one direction.

For a fixed i, compute:

current_sum = nums[i] + nums[left] + nums[right]

The sorted values tell us which boundary can still improve the sum.

The sum is too small

If current_sum < 0, moving right left would choose an equal or smaller value and could only keep the sum the same or make it smaller. It cannot produce zero.

Move left right instead:

left += 1

The next left value is at least as large and is the only movement that can increase the sum.

The sum is too large

If current_sum > 0, moving left right would choose an equal or larger value and could only keep the sum the same or make it larger. It cannot produce zero.

Move right left:

right -= 1

The next right value is at most as large and is the only movement that can decrease the sum.

The sum is zero

Record the triplet, then move both pointers inward. The current pair has been consumed, and the same two positions cannot be reused.

The pointer rule is not a heuristic: a negative sum makes the left boundary the only useful boundary to advance, while a positive sum makes the right boundary the only useful boundary to retreat.

For one fixed i, each pointer moves inward and never reverses direction. The inner scan is therefore linear. Across all fixed positions, the algorithm takes O(n²) time after sorting.

Duplicate skipping has two separate jobs

Duplicate handling is easier when treated as a pair of invariants rather than as a cleanup step.

Skip repeated fixed values

At the start of the outer loop:

if i > 0 and nums[i] == nums[i - 1]:
    continue

The earlier occurrence already searched the relevant suffix for this value. Repeating the same fixed value would reproduce the same value triplets.

Do not skip every repeated value indiscriminately. Repeated values can be required inside a valid answer. Both [-1, -1, 2] and [0, 0, 0] are legitimate triplets because their values come from distinct indices.

Skip repeated pointer values after a match

After recording a match, advance both pointers first. Then skip equal neighbors:

left += 1
right -= 1

while left < right and nums[left] == nums[left - 1]:
    left += 1

while left < right and nums[right] == nums[right + 1]:
    right -= 1

The comparisons use the values just consumed. The bounds checks prevent the duplicate scan from leaving the active range.

For [-2, 0, 0, 2, 2], fixing -2 produces [-2, 0, 2]. The second zero and second two are different positions, but they do not produce a new value composition. Skipping them prevents a repeated output.

This distinction matters:

  • pointer boundaries guarantee distinct indices;
  • duplicate skipping guarantees unique value triplets.

They solve different problems.

Correctness through invariants

For a fixed i, maintain this invariant:

Before each inner-loop iteration, every discarded pair is unable to form a new valid pair for this fixed value, and every unexamined pair lies within the current pointer range.

The invariant survives each branch:

  • If the sum is too small, every pair using the current left with a smaller right position is also too small. Advancing left discards only impossible pairs.
  • If the sum is too large, every pair using the current right with a larger left position is also too large. Decreasing right discards only impossible pairs.
  • If the sum is zero, the pair is valid and is recorded before both positions are consumed.

Now consider the outer loop. Any valid triplet can be written in sorted value order as a <= b <= c. The first occurrence of a is processed as the fixed value. Later equal occurrences are skipped, but they would search the same value-based combinations and add no new result.

The inner scan finds the pair for that fixed value because it never discards a pair that could still reach the target. Duplicate skipping does not remove the first necessary occurrence of a value; it removes only later equivalent representations after a valid composition has already been recorded.

Finally, left > i and left < right guarantee three different indices. Every loop iteration advances a pointer, so the scan terminates.

The proof is a state-management proof: name what remains possible, name what has been ruled out, and show that each transition preserves those claims.

Dry run: [-1, 0, 1, 2, -1, -4]

After sorting:

[-4, -1, -1, 0, 1, 2]

A few states expose the mechanism:

Fixed valueLeft valueRight valueSumAction
-4-12-3Too small; advance left
-402-2Too small; advance left
-412-1Too small; advance left
-1-120Record [-1, -1, 2]
-1010Record [-1, 0, 1]

When -4 is fixed, even the largest available pair cannot raise the sum to zero. The pointers discover this without enumerating every pair.

At the first -1, the pair [-1, 2] gives zero. After both pointers move, [0, 1] gives the second result. The next outer value is another -1, so that fixed position is skipped.

For [0, 0, 0, 0], the first zero uses two later positions to produce [0, 0, 0]. After the match, equal pointer values are skipped, and later outer zeros are skipped as well. The result contains one triplet, while the selected indices remain distinct.

Python implementation

def three_sum(nums: list[int]) -> list[list[int]]:
    nums.sort()
    result: list[list[int]] = []
    n = len(nums)

    for i in range(n - 2):
        if nums[i] > 0:
            break

        if i > 0 and nums[i] == nums[i - 1]:
            continue

        left = i + 1
        right = n - 1

        while left < right:
            current_sum = nums[i] + nums[left] + nums[right]

            if current_sum < 0:
                left += 1
            elif current_sum > 0:
                right -= 1
            else:
                result.append([nums[i], nums[left], nums[right]])

                left += 1
                right -= 1

                while left < right and nums[left] == nums[left - 1]:
                    left += 1

                while left < right and nums[right] == nums[right + 1]:
                    right -= 1

    return result

The code follows the proof directly:

  • i selects the first value;
  • left and right bound the unexamined pair range;
  • the sign of current_sum chooses the only useful pointer movement;
  • a match advances both pointers before duplicate skipping.

nums.sort() mutates the input list. If the surrounding function must preserve the original list, use a sorted copy instead:

values = sorted(nums)

The algorithm is unchanged; only the input-storage decision differs.

Complexity and edge cases

Sorting costs O(n log n). For each fixed value, the two pointers move across the suffix in linear time, so the scans cost O(n²) overall. The total is:

O(n log n + n²) = O(n²)

Apart from the returned triplets, the algorithm uses O(1) auxiliary space when the sort is in place. The output itself is output-dependent and should be counted separately.

Check these boundaries:

  • fewer than three values: return an empty list;
  • all positive values: stop at the first positive fixed value;
  • all negative values: the sums remain too small and no result is found;
  • repeated zeros: return [0, 0, 0] once;
  • repeated negative or positive values: skip duplicates at both levels;
  • several matches for one fixed value: move both pointers, then continue scanning.

Common bugs are useful diagnostic signals. Reusing unrestricted indices violates the contract. Skipping repeated values before their first necessary use can remove [-1, -1, 2]. Moving the wrong pointer moves the sum away from zero. Skipping only the outer duplicate can still produce repeated results after multiple matches.

The transferable recognition rule

When you see a fixed-size sum problem with an exact target, ask:

  1. Can sorting make the candidate values monotonic?
  2. Can I fix one dimension and reduce the rest to a two-pointer search?
  3. Which entire group of states becomes impossible after each comparison?

For 3Sum, the answers are yes, yes, and “the pairs on the wrong side of the current sum.”

Sort. Fix. Scan. Prove the discarded states. Handle duplicates as a separate invariant.

If you can explain why a negative sum advances left, why a positive sum retreats right, and why duplicate skipping begins only after preserving the first necessary occurrence, you understand the 3Sum solution. The code is then a translation of the reasoning rather than a spell memorized from one example.

References

  1. LeetCode 15 3Sum Solution & Explanation | NeetCodeneetcode.io
7sources checked
7source domains
5searches run

Research updated Sep 5, 2026

Related sites

Strengthen the language foundations behind the solution

Use LearnPyFast and LearnJSFast when you want to reinforce the language mechanics that support interview implementations.

Python tutorialstutorial

LearnPyFast

Beginner-friendly Python tutorials, examples, and learning paths for practical programming foundations.

PythonProgrammingBeginners
Visit LearnPyFast
JavaScript tutorialstutorial

LearnJSFast

Beginner-friendly JavaScript tutorials for practical web development and self-taught developers.

JavaScriptFrontendWeb development
Visit LearnJSFast

Keep grinding

Related coding interview problems

Continue with nearby problems that reuse the same data-structure, invariant, or optimization pattern.

Professional team discussing analytics and brainstorming ideas in a meeting room.
intermediate
12 min read

3Sum Closest

The target does not identify the winning triplet. It tells each pointer which direction is still worth exploring.

View solution
Visual abstraction of neural networks in AI technology, featuring data flow and algorithms.
advanced
13 min read

4Sum

Four choices suggest an O(n^4) search. Sorting changes the last two choices into a controlled walk.

View solution
Laptop on green grass in sunlight. Ideal for remote work and technology themes.
intermediate
9 min read

Container With Most Water

The hard part is not calculating width × shorter_height. It is proving why one whole family of pairs can be discarded without checking them.

View solution