Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
3Sum
The rite of passage: find every unique triplet summing to zero without drowning in duplicates or O(n³). Sort once, fix one element, and let two pointers squeeze the rest — the pattern that generalizes to a whole family of k-sum problems.
The problem
Given nums, return all unique triplets [a, b, c] with a + b + c = 0. Each triplet must be sorted ascending, the list of triplets sorted lexicographically, and no triplet may appear twice — even if the input contains duplicates.
Your turn — write it
Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.
Implement three_sum(nums) → all unique zero-sum triplets, using sort + two pointers and duplicate-skipping at every level.
Examples and constraints 5 examples
nums = [-1, 0, 1, 2, -1, -4][[-1, -1, 2], [-1, 0, 1]]nums = [0, 0, 0, 0][[0, 0, 0]]nums = [1, 2, 3][]nums = [-2, -2, 0, 0, 2, 2][[-2, 0, 2]]nums = [1, -1][]- Triplets sorted ascending internally; the result list sorted lexicographically.
- No duplicate triplets in the output.
- Target O(n²): sort + fixed element + two pointers, not three nested loops.
Hints 4 ideas
- Sort first. Sorted order is what makes both the two pointers AND the dedup possible.
- Fix index i; find pairs in nums[i+1:] summing to -nums[i] with left/right pointers.
- Sum too small → move left up; too big → move right down; equal → record and move BOTH.
- Skip duplicates three times: at i, at left after a hit, at right after a hit.
Approach, complexity & discussion — open after you solve
Start from brute force
The obvious approach tries every triple of positions: three nested loops over i < j < k, and whenever nums[i] + nums[j] + nums[k] == 0, add the sorted triplet to a set so duplicates collapse, then sort the set for the output. It is correct, because every combination is checked, and it needs O(t) space for the t distinct triplets. But it is O(n³) time: at n = 3000, a common limit for this problem, that is about 4.5 billion triples, while the sorted two-pointer version does about 4.5 million pointer steps. The set also hides duplicate work rather than avoiding it: [0, 0, 0, 0] finds the same triplet four times.
The approach
Sorting turns the inner search into a two-pointer squeeze. Fix the smallest element a[i]; you need a pair in the rest that sums to −a[i]. With lo just after i and hi at the end, a sum that is too small can only grow by moving lo right, and one that is too big can only shrink by moving hi left, so each step discards a value that cannot be in any remaining pair. Sorting also makes duplicates adjacent, so skipping a value equal to its neighbor is enough to emit each triplet once, already in order.
The reference sorts into a, then for each i in range(n - 2) skips a[i] == a[i - 1]. Inside, it computes s = a[i] + a[lo] + a[hi]: below 0 it does lo += 1, above 0 hi -= 1, and at 0 it appends [a[i], a[lo], a[hi]], moves both pointers, and steps past repeats of the values just used. Because i and lo only increase, the triplets come out in lexicographic order with no final sort.
Walk through an example
Trace the first example, which sorts to a = [-4, -1, -1, 0, 1, 2].
| step | i, lo, hi | sum and move | out |
|---|---|---|---|
| 1 | i = 0 (−4), lo 1 → 5 | sums −3, −3, −2, −1: all too small, lo runs into hi | [] |
| 2 | i = 1 (−1), lo = 2, hi = 5 | −1 + −1 + 2 = 0: record, then lo = 3, hi = 4 | [[-1, -1, 2]] |
| 3 | i = 1, lo = 3, hi = 4 | −1 + 0 + 1 = 0: record, then lo = 4, hi = 3, stop | [[-1, -1, 2], [-1, 0, 1]] |
| 4 | i = 2 (−1) | equals a[1]: skipped | unchanged |
| 5 | i = 3 (0), lo = 4, hi = 5 | 0 + 1 + 2 = 3: too big, hi = 4, stop | unchanged |
| 6 | loop ends | range(n - 2) stops before i = 4 | [[-1, -1, 2], [-1, 0, 1]] |
Complexity
Optimal: O(n²) time, an O(n) two-pointer pass for each of n values of i after an O(n log n) sort; O(n) space for the sorted copy a, plus the output. Brute force (three nested loops and a set): O(n³) time and O(t) space for t distinct triplets.
Edge cases to test
- Fewer than three numbers:
[1, -1],[5]and[]all return[], becauserange(n - 2)is empty. - All zeros:
[0, 0, 0, 0]→[[0, 0, 0]], once rather than four times. - Heavy duplicates:
[-2, -2, 0, 0, 2, 2]→[[-2, 0, 2]]. All three skips matter: ati, atloand athi. - Equal values used together:
[-1, -1, 2]→[[-1, -1, 2]]. Duplicate skipping must remove repeated triplets, not stop two equal numbers from appearing in one triplet. - No negatives:
[1, 2, 3]→[]. Oncea[i] > 0no triplet can sum to zero, so abreakthere is a safe shortcut the reference does not take. - Large magnitudes: Python integers cannot overflow, but in a language with 32-bit integers
a[i] + a[lo] + a[hi]can, so use a 64-bit sum.
Common mistakes
- Brute-forcing all triples in O(n³) instead of collapsing the inner search with two pointers.
- Not skipping duplicates, so the result contains repeated triplets.
- Advancing only one pointer on a match, or forgetting to move past duplicate values after recording one.
- Skipping duplicates at
iby comparing witha[i + 1]instead ofa[i - 1]: that skips the first copy of a value, so the first example loses[-1, -1, 2].
Follow-ups an interviewer asks next
- How does this change for 3Sum Closest?
- Keep the same sort and two-pointer loop, but instead of looking for an exact zero, track the sum
swith the smallestabs(s - target)seen so far, and moveloorhidepending on whethersis below or above the target. Return early if a sum hits the target exactly. Duplicate skipping becomes optional, since only one number is returned. Time staysO(n²). - How do you generalize to 4Sum or k-Sum?
- Recursively: fix one element, skip its duplicates, and solve a (k − 1)-sum on the rest with the reduced target, down to the two-pointer base case. That gives
O(n^(k−1))time after one sort. For 4Sum it isO(n³); a hash map of pair sums can decide whether any quadruplet exists in aboutO(n²)average time, but listing every unique one that way is harder to deduplicate. - Can you solve it with a hash set instead of two pointers?
- Yes: for each
i, run a Two Sum pass over the rest with a set of seen values, looking for-(a[i] + x). It is alsoO(n²)but usesO(n)extra space per pass, and deduplication needs a set of sorted tuples. You still have to sort to produce the required output order, so the two-pointer version is usually simpler. - Can 3Sum be solved faster than O(n²)?
- Only by small factors. Algorithms exist that shave logarithmic factors off
n², but no algorithm running inO(n^(2−ε))for any constantε > 0is known. The 3SUM conjecture says none exists, and many problems in computational geometry are shown hard by reducing 3SUM to them.
Where this shows up
Three-sum is the canonical “sort, then two pointers to collapse a dimension” pattern — trading an O(n³) search for O(n²) by exploiting order. The same reduction recurs across pair-, triple-, and k-sum problems and in any search where sortedness lets you prune half the space each step.
Reference solution
Reference solution — Python
def three_sum(nums):
a = sorted(nums)
n = len(a)
out = []
for i in range(n - 2):
if i > 0 and a[i] == a[i - 1]:
continue
lo, hi = i + 1, n - 1
while lo < hi:
s = a[i] + a[lo] + a[hi]
if s < 0:
lo += 1
elif s > 0:
hi -= 1
else:
out.append([a[i], a[lo], a[hi]])
lo += 1
hi -= 1
while lo < hi and a[lo] == a[lo - 1]:
lo += 1
while lo < hi and a[hi] == a[hi + 1]:
hi -= 1
return out
print(three_sum([-1, 0, 1, 2, -1, -4])) # -> [[-1, -1, 2], [-1, 0, 1]]Reference solution — TypeScript
function threeSum(nums: number[]): number[][] {
const a = [...nums].sort((x, y) => x - y);
const n = a.length;
const out: number[][] = [];
for (let i = 0; i < n - 2; i++) {
if (i > 0 && a[i] === a[i - 1]) continue;
let lo = i + 1, hi = n - 1;
while (lo < hi) {
const s = a[i] + a[lo] + a[hi];
if (s < 0) lo++;
else if (s > 0) hi--;
else {
out.push([a[i], a[lo], a[hi]]);
lo++; hi--;
while (lo < hi && a[lo] === a[lo - 1]) lo++;
while (lo < hi && a[hi] === a[hi + 1]) hi--;
}
}
}
return out;
}
console.log(threeSum([-1, 0, 1, 2, -1, -4])); // -> [[-1,-1,2],[-1,0,1]]Questions about 3Sum
What is the time complexity of the 3Sum problem?
The standard solution runs in O(n squared) time. Sorting costs O(n log n), and then for each of the n possible first elements, a two-pointer pass over the rest of the array takes O(n), which dominates. Space is O(n) if you sort a copy of the input, as the reference does, or O(log n) to O(n) for an in-place sort, depending on the sort algorithm, plus the space for the output. The naive version with three nested loops is O(n cubed), which is why interviewers expect the sorted two-pointer approach.
How do you avoid duplicate triplets in 3Sum?
Sort the array so equal values sit next to each other, then skip repeats at three places. For the fixed element, skip any value equal to the previous one, because it would find exactly the same pairs. After recording a triplet, move the left pointer past every copy of its value and the right pointer past every copy of its value. Each distinct triplet is then generated once, without a set. The skip at the fixed element must compare with the previous value, not the next one, or the first copy is skipped and triplets like -1, -1, 2 disappear.
Why does the 3Sum solution sort the array first?
Sorting does two jobs at once. It makes the two-pointer search valid: in a sorted array, if the current sum is too small, the only way to increase it is to move the left pointer right, and if it is too large, the only way to decrease it is to move the right pointer left, so each step safely discards one candidate. It also groups equal values together, which is what makes skipping duplicates a simple comparison with a neighbor. As a bonus, triplets come out already sorted internally and in lexicographic order.
Can 3Sum be solved faster than O(n squared)?
Not by much, as far as anyone knows. There are algorithms that beat n squared by logarithmic factors, but no algorithm that runs in time n to the power 2 minus a positive constant has been found, and the 3SUM conjecture states that none exists. The problem is important in complexity theory for exactly this reason: many problems in computational geometry are proven hard by showing that a fast algorithm for them would also solve 3SUM quickly. In an interview, the sorted two-pointer O(n squared) solution is the expected answer.
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- Two SumThe classic warm-up: find the two numbers that add up to a target. Brute force is O(n²) — a hash map gets you to one pass, O(n). Solve it in Python or TypeScript, right in your browser, with hidden tests and a reveal-solution button.Read →
- Valid ParenthesesThe canonical stack problem: decide whether every bracket is closed by the right type, in the right order. A stack turns nested matching into a single pass. Solve it in Python or TypeScript with hidden tests.Read →
- Fizz BuzzThe famous screening question. Print 1…n, but multiples of 3 become "Fizz", of 5 become "Buzz", and of both become "FizzBuzz". Easy — the catch is testing divisibility in the right order. Solve it in Python or TypeScript.Read →
- Number of IslandsThe canonical connected-components question. Flood-fill each unvisited patch of land with BFS or DFS, count how many floods you started — grid traversal, visited bookkeeping, and the classic diagonal trap. Solve it in Python or TypeScript.Read →
- Merge IntervalsCalendar apps and memory allocators run on this: sort by start, sweep once, grow or close the current block. Touching endpoints merge, nested intervals vanish — the classic that punishes off-by-one thinking. Solve it in Python or TypeScript.Read →
- Top-K Frequent ElementsCount, then rank — the two-step pattern behind trending topics and hot-key detection. A hash map counts; a (-count, value) sort ranks with a clean tie-break; buckets get you to O(n) if you want the flex. Solve it in Python or TypeScript.Read →