3Sum
Find all unique triplets that sum to zero — sort + two pointers, deduping strategy, and complexity tradeoffs.
- dsa
- two-pointers
- arrays
- interview
- Meta
- Amazon
- Microsoft
- Adobe
The problem
Given an integer array nums, return all unique triplets [nums[i], nums[j], nums[k]] such that:
i,j,kare distinct indicesnums[i] + nums[j] + nums[k] === 0
Order of triplets (and order inside a triplet) usually does not matter; the set of triplets must be unique.
Input: nums = [-1, 0, 1, 2, -1, -4]
Output: [[-1, -1, 2], [-1, 0, 1]]
Input: nums = [0, 1, 1]
Output: []
Input: nums = [0, 0, 0]
Output: [[0, 0, 0]]
Two Sum’s big sibling. Pattern: sort → fix one index → two pointers on the rest, with careful deduping.
Brute force
Three nested loops, push every zero-sum triple, then unique with a set of sorted strings. O(n³) time, messy uniqueness. Mention and discard for n up to a few hundred interview-scale.
function threeSumBrute(nums: number[]): number[][] {
const n = nums.length;
const set = new Set<string>();
const out: number[][] = [];
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
for (let k = j + 1; k < n; k++) {
if (nums[i] + nums[j] + nums[k] === 0) {
const t = [nums[i], nums[j], nums[k]].sort((a, b) => a - b);
const key = t.join(",");
if (!set.has(key)) {
set.add(key);
out.push(t);
}
}
}
}
}
return out;
}
| Time | O(n³) + set overhead |
| Space | O(n) for the set of answers |
Optimal: sort + two pointers
- Sort ascending.
- For each
ifrom0ton - 3:- Skip duplicate
nums[i](same as previous). - Optional prune: if
nums[i] > 0, break (remaining sums positive). lo = i + 1,hi = n - 1.- While
lo < hi:sum = nums[i] + nums[lo] + nums[hi]- If sum === 0 → record; move lo/hi and skip duplicates
- If sum < 0 →
lo++ - If sum > 0 →
hi--
- Skip duplicate
function threeSum(nums: number[]): number[][] {
nums.sort((a, b) => a - b);
const res: number[][] = [];
const n = nums.length;
for (let i = 0; i < n - 2; i++) {
if (i > 0 && nums[i] === nums[i - 1]) continue;
if (nums[i] > 0) break;
let lo = i + 1;
let hi = n - 1;
while (lo < hi) {
const sum = nums[i] + nums[lo] + nums[hi];
if (sum === 0) {
res.push([nums[i], nums[lo], nums[hi]]);
lo++;
hi--;
while (lo < hi && nums[lo] === nums[lo - 1]) lo++;
while (lo < hi && nums[hi] === nums[hi + 1]) hi--;
} else if (sum < 0) {
lo++;
} else {
hi--;
}
}
}
return res;
}
| Time | O(n²) after O(n log n) sort |
| Space | O(1) or O(n) depending on sort; answer space separate |
Why sort first?
- Two pointers need order.
- Duplicates become adjacent — skip with
whileinstead of a global set of triples.
Hash-set per fixed i (alternate)
For each i, run Two Sum with a set on the suffix. Still O(n²), but deduping is easier after sort + skip-i, or with a set of triple keys. Interviewers usually prefer two pointers for 3Sum.
Edge cases
- Fewer than 3 elements →
[] - All zeros → one triple
[0,0,0] - No solution →
[] - Many duplicates — skip logic is the real test
- Negatives and positives mixed — sort handles ordering
Common bugs
- Skipping duplicates only on i but not on lo/hi after a hit
- Using
nums[lo] === nums[lo + 1]before moving (off-by-one) - Mutating indices incorrectly so
locrosseshi - Returning index triples instead of values (wrong problem)
Follow-ups
- 3Sum Closest — track best sum vs target
- 4Sum — fix two indices + two pointers → O(n³)
- 3Sum Smaller — count pairs under a bound
- Two Sum / Two Sum II — building blocks
Interview delivery
- Restate uniqueness + zero target.
- Brute O(n³) → sort + two pointers O(n²).
- Call out dedupe as the hard part.
- Code; dry-run
[-1,0,1,2,-1,-4]. - Complexities.
Related
- Two Sum
- Container With Most Water
- Valid Palindrome — two pointers flavor
- Merge Intervals — sort-then-scan family