3Sum
Sort, fix one number, and find the other two with two pointers that move toward each other. Solved two ways, brute force in O(n³) and sort plus two pointers in O(n²), with a step-through, code in five languages and the Big O worked out from the code.
The problem
Section titled “The problem”You are given an array nums of whole numbers (call its length n). Find every group of three numbers, taken from three
different positions, whose sum is 0. Return each such group of values once.
Two groups count as the same if they hold the same three values, even when the values come from different positions. The groups, and the order inside each group, may come back in any order.
Example 1
Input: nums = [-3, 1, 2, -1, 0, 2]
Output: [[-3, 1, 2], [-1, 0, 1]]
−3 + 1 + 2 = 0 and −1 + 0 + 1 = 0. There are two 2s, so −3, 1, 2 can be built twice, but it is returned once.
Example 2
Input: nums = [1, 2, -4]
Output: []
The only group is 1 + 2 − 4 = −1, which is not 0.
Example 3
Input: nums = [0, 0, 0, 0]
Output: [[0, 0, 0]]
Many groups of positions give 0 + 0 + 0, but they all hold the same values.
Constraints
Section titled “Constraints”- 3 ≤ n ≤ 3,000
- −100,000 ≤ nums[i] ≤ 100,000
Clarifying questions
Section titled “Clarifying questions”- Do we return positions or values?
- Values. That is what makes sorting safe here.
- Can one value be used twice?
- Only if it appears twice in the array. Each group uses three different positions.
- How are repeats handled?
- A group of values appears once in the output, however many ways it can be built.
- How large is n?
- Up to 3,000. So O(n²), about 9 million steps, is fine, but O(n³) is not.
Spot the pattern
Section titled “Spot the pattern”Fix one number, and the problem becomes Two Sum on the rest: find two numbers that add up to minus the fixed one. On a sorted array, Two Sum is the two-pointer trick: one pointer at each end, and the sum tells you which one to move. Sorting also puts equal values side by side, which makes skipping repeats easy.
Solutions
Section titled “Solutions”A hash set for the third number also reaches O(n²), but removing repeated groups is messier. Sorting handles the search and the repeats at once.
Brute force O(n³)
The idea
Section titled “The idea”Try every group of three positions i < j < k. If the three numbers add up to 0, sort the three
values and put them in a set. Sorting the group makes [2, −3, 1] and [−3, 1, 2] look the same, so the
set keeps only one copy.
It is correct, but the number of groups grows like n³.
Walkthrough
Section titled “Walkthrough”- result = set()
- for i in range(len(nums)):
- for j in range(i + 1, len(nums)):
- for k in range(j + 1, len(nums)):
- if nums[i] + nums[j] + nums[k] == 0:
- result.add(tuple(sorted((nums[i], nums[j], nums[k]))))
- return [list(triple) for triple in result]
Try every group of three positions i < j < k. With 6 numbers that is 20 groups.
class Solution { // Brute force: try every group of three positions. // About n^3 / 6 groups, so O(n^3) time. A set drops repeated triples. public List<List<Integer>> threeSum(int[] nums) { int numsLength = nums.length; Set<List<Integer>> result = new HashSet<>(); for (int i = 0; i < numsLength; i++) { for (int j = i + 1; j < numsLength; j++) { for (int k = j + 1; k < numsLength; k++) { if (nums[i] + nums[j] + nums[k] == 0) { // Sorting the triple makes equal triples look the same. List<Integer> triple = new ArrayList<>(List.of(nums[i], nums[j], nums[k])); Collections.sort(triple); result.add(triple); } } } } return new ArrayList<>(result); }}# Brute force: try every group of three positions.# About n^3 / 6 groups, so O(n^3) time. A set drops repeated triples.def three_sum(nums: list[int]) -> list[list[int]]: nums_length = len(nums) result = set() for i in range(nums_length): for j in range(i + 1, nums_length): for k in range(j + 1, nums_length): if nums[i] + nums[j] + nums[k] == 0: # Sorting the triple makes equal triples look the same. result.add(tuple(sorted((nums[i], nums[j], nums[k])))) return [list(triple) for triple in result]public class Solution{ // Brute force: try every group of three positions. // About n^3 / 6 groups, so O(n^3) time. A set drops repeated triples. public IList<IList<int>> ThreeSum(int[] nums) { int numsLength = nums.Length; var seen = new HashSet<string>(); var result = new List<IList<int>>(); for (int i = 0; i < numsLength; i++) { for (int j = i + 1; j < numsLength; j++) { for (int k = j + 1; k < numsLength; k++) { if (nums[i] + nums[j] + nums[k] == 0) { // Sorting the triple makes equal triples look the same. var triple = new List<int> { nums[i], nums[j], nums[k] }; triple.Sort(); if (seen.Add(string.Join(",", triple))) result.Add(triple); } } } } return result; }}// Brute force: try every group of three positions.// About n^3 / 6 groups, so O(n^3) time. A set drops repeated triples.func threeSum(nums []int) [][]int { numsLength := len(nums) seen := make(map[[3]int]bool) result := [][]int{} for i := 0; i < numsLength; i++ { for j := i + 1; j < numsLength; j++ { for k := j + 1; k < numsLength; k++ { if nums[i]+nums[j]+nums[k] == 0 { // Sorting the triple makes equal triples look the same. triple := []int{nums[i], nums[j], nums[k]} sort.Ints(triple) key := [3]int{triple[0], triple[1], triple[2]} if !seen[key] { seen[key] = true result = append(result, triple) } } } } } return result}// Brute force: try every group of three positions.// About n^3 / 6 groups, so O(n^3) time. A set drops repeated triples.function threeSum(nums: number[]): number[][] { const numsLength = nums.length; const seen = new Set<string>(); const result: number[][] = []; for (let i = 0; i < numsLength; i++) { for (let j = i + 1; j < numsLength; j++) { for (let k = j + 1; k < numsLength; k++) { if (nums[i] + nums[j] + nums[k] === 0) { // Sorting the triple makes equal triples look the same. const triple = [nums[i], nums[j], nums[k]].sort((first, second) => first - second); const key = triple.join(","); if (!seen.has(key)) { seen.add(key); result.push(triple); } } } } } return result;}1 Find the work that repeats
The lit line adds three numbers and compares the sum with 0. It sits inside three loops. When the sum is 0, sorting three values and adding them to the set is also a fixed amount of work. So each run is 1 step.
- result = set()
- for i in range(len(nums)):
- for j in range(i + 1, len(nums)):
- for k in range(j + 1, len(nums)):
- if nums[i] + nums[j] + nums[k] == 0:
- result.add(tuple(sorted((nums[i], nums[j], nums[k]))))
- return [list(triple) for triple in result]
2 Count the runs on Example 1
Example 1 has 6 numbers. With
i = 0, the inner two loops pick 2 of the 5 numbers after it: 10 groups. Withi = 1it is 6, then 3, then 1.10 + 6 + 3 + 1 = 20 runs.
i 0 1 2 3 groups with this i 10 6 3 1 total10 + 6 + 3 + 1 = 203 Write the count for any n
The loops visit every way to choose 3 positions out of n:
n × (n − 1) × (n − 2) / 6 = ⅙n³ − ½n² + ⅓nCheck it with Example 1: 6 × 5 × 4 / 6 = 20.
4 Keep only the part that grows fastest
- Drop the −½n² and +⅓n. Next to ⅙n³ they soon stop mattering.
- Drop the ⅙. It does not change how fast the count grows. When n doubles, the work grows 8 times either way.
What is left is n³. At n = 3,000 that is about 4.5 billion groups.
So the time is O(n³).
S Space: count the extra memory the same way
Besides the answer, the code keeps
i,jandk. The set is the answer, which does not count as extra memory. So the space is O(1).result (the answer)ijk
Sort and two pointers O(n²)
The idea
Section titled “The idea”Sort the array. Then fix the first number of the group, nums[i], from left to right. For each one,
look for two numbers after it that add up to −nums[i].
Put left just after i and right at the end. Look at the sum of the three:
- Too small:
nums[right]is already the biggest number left, soleftmust grow. Moveleftone step to the right. - Too big:
nums[left]is already the smallest number left, sorightmust shrink. Moverightone step to the left. - Zero: record the group, then move both pointers inward.
Two small checks handle repeats. Skip a fixed number equal to the one before it, since it would find
the same groups. After a match, skip any left value equal to the one just used. And once nums[i] is above
0, stop: everything after it is positive too.
Why it never misses a group: each move only throws away a number that cannot be in any zero-sum
group with the current nums[i], so no group is skipped.
Walkthrough
Section titled “Walkthrough”- nums.sort(); result = []
- for i in range(len(nums) - 2):
- if nums[i] > 0: break
- if i > 0 and nums[i] == nums[i - 1]: continue
- left, right = i + 1, len(nums) - 1
- while left < right:
- total = nums[i] + nums[left] + nums[right]
- if total < 0: left += 1
- elif total > 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
- return result
Sort first. Equal numbers become neighbors, and the two-pointer trick needs order.
class Solution { // Sort, fix one number, then find the other two with two pointers. // n fixed numbers times an O(n) scan, so O(n^2) time. Sorting is O(n log n). public List<List<Integer>> threeSum(int[] nums) { Arrays.sort(nums); int numsLength = nums.length; List<List<Integer>> result = new ArrayList<>(); for (int i = 0; i < numsLength - 2; i++) { // Everything after a positive number is positive too: no zero sum left. if (nums[i] > 0) break; // The same fixed number would find the same triples again. if (i > 0 && nums[i] == nums[i - 1]) continue; int left = i + 1, right = numsLength - 1; while (left < right) { int total = nums[i] + nums[left] + nums[right]; // Sorted order tells us which pointer to move. if (total < 0) left++; else if (total > 0) right--; else { result.add(List.of(nums[i], nums[left], nums[right])); left++; right--; // Skip equal values, so the same pair is not used twice. while (left < right && nums[left] == nums[left - 1]) left++; } } } return result; }}# Sort, fix one number, then find the other two with two pointers.# n fixed numbers times an O(n) scan, so O(n^2) time. Sorting is O(n log n).def three_sum(nums: list[int]) -> list[list[int]]: nums.sort() nums_length = len(nums) result = [] for i in range(nums_length - 2): # Everything after a positive number is positive too: no zero sum left. if nums[i] > 0: break # The same fixed number would find the same triples again. if i > 0 and nums[i] == nums[i - 1]: continue left, right = i + 1, nums_length - 1 while left < right: total = nums[i] + nums[left] + nums[right] # Sorted order tells us which pointer to move. if total < 0: left += 1 elif total > 0: right -= 1 else: result.append([nums[i], nums[left], nums[right]]) left += 1 right -= 1 # Skip equal values, so the same pair is not used twice. while left < right and nums[left] == nums[left - 1]: left += 1 return resultpublic class Solution{ // Sort, fix one number, then find the other two with two pointers. // n fixed numbers times an O(n) scan, so O(n^2) time. Sorting is O(n log n). public IList<IList<int>> ThreeSum(int[] nums) { Array.Sort(nums); int numsLength = nums.Length; var result = new List<IList<int>>(); for (int i = 0; i < numsLength - 2; i++) { // Everything after a positive number is positive too: no zero sum left. if (nums[i] > 0) break; // The same fixed number would find the same triples again. if (i > 0 && nums[i] == nums[i - 1]) continue; int left = i + 1, right = numsLength - 1; while (left < right) { int total = nums[i] + nums[left] + nums[right]; // Sorted order tells us which pointer to move. if (total < 0) left++; else if (total > 0) right--; else { result.Add(new List<int> { nums[i], nums[left], nums[right] }); left++; right--; // Skip equal values, so the same pair is not used twice. while (left < right && nums[left] == nums[left - 1]) left++; } } } return result; }}// Sort, fix one number, then find the other two with two pointers.// n fixed numbers times an O(n) scan, so O(n^2) time. Sorting is O(n log n).func threeSum(nums []int) [][]int { sort.Ints(nums) numsLength := len(nums) result := [][]int{} for i := 0; i < numsLength-2; i++ { // Everything after a positive number is positive too: no zero sum left. if nums[i] > 0 { break } // The same fixed number would find the same triples again. if i > 0 && nums[i] == nums[i-1] { continue } left, right := i+1, numsLength-1 for left < right { total := nums[i] + nums[left] + nums[right] // Sorted order tells us which pointer to move. if total < 0 { left++ } else if total > 0 { right-- } else { result = append(result, []int{nums[i], nums[left], nums[right]}) left++ right-- // Skip equal values, so the same pair is not used twice. for left < right && nums[left] == nums[left-1] { left++ } } } } return result}// Sort, fix one number, then find the other two with two pointers.// n fixed numbers times an O(n) scan, so O(n^2) time. Sorting is O(n log n).function threeSum(nums: number[]): number[][] { nums.sort((first, second) => first - second); const numsLength = nums.length; const result: number[][] = []; for (let i = 0; i < numsLength - 2; i++) { // Everything after a positive number is positive too: no zero sum left. if (nums[i] > 0) break; // The same fixed number would find the same triples again. if (i > 0 && nums[i] === nums[i - 1]) continue; let left = i + 1; let right = numsLength - 1; while (left < right) { const total = nums[i] + nums[left] + nums[right]; // Sorted order tells us which pointer to move. if (total < 0) left++; else if (total > 0) right--; else { result.push([nums[i], nums[left], nums[right]]); left++; right--; // Skip equal values, so the same pair is not used twice. while (left < right && nums[left] === nums[left - 1]) left++; } } } return result;}1 Find the work that repeats
Two things cost time. The sort at the start takes about n log n steps. Then, for each fixed
i, the while loop adds three numbers and moves at least one pointer. Each run is 1 step.- nums.sort(); result = []
- for i in range(len(nums) - 2):
- if nums[i] > 0: break
- if i > 0 and nums[i] == nums[i - 1]: continue
- left, right = i + 1, len(nums) - 1
- while left < right:
- total = nums[i] + nums[left] + nums[right]
- if total < 0: left += 1
- elif total > 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
- return result
2 Count the runs on Example 1
Example 1 has 6 numbers. The sort takes about 6 × log₂ 6 ≈ 16 steps. Then the while loop runs 3 times for
i = 0, 3 times fori = 1and 2 times fori = 2. Ati = 3the fixed number is positive, so the loop stops.About 16 + 8 = 24 steps.
i 0 1 2 3 nums[i] -3 -1 0 1 while runs 3 3 2 stop totalsort ≈ 16, then 3 + 3 + 2 = 83 Write the count for any n
For a fixed
i, the gap betweenleftandrightstarts at n − i − 2 and every run shrinks it by at least 1. Adding that up for everyi:n log n + (n − 2) + (n − 3) + … + 1 ≈ n log n + ½n²Check it with Example 1: the gaps start at 4, 3 and 2, so at most 9 runs. The real 8 runs is just below that.
4 Keep only the part that grows fastest
- Drop the n log n. The sort is smaller than n² for any large n, so the pointer loops are the biggest part.
- Drop the ½. It does not change how fast the count grows.
What is left is n².
So the time is O(n²).
S Space: count the extra memory the same way
The code keeps
i,left,rightandtotal, a fixed few numbers. The answer does not count, so the space is O(1), plus whatever your language’s sort uses inside.result (the answer)ileftrighttotal
Tested at the limits
Section titled “Tested at the limits”2,000 random arrays of 3 to 9 numbers from −4 to 4, so zeros, repeats and repeated triples are common. The brute force and the sort and two pointers gave the same triples every time.
| Largest inputs | Result |
|---|---|
| 3,000 numbers (the most allowed), all odd, so no triple adds to 0 and the pointers scan the whole range each time. Sort and two pointers. | 0.3 s, and about 0.01 MB of extra memory: it sorts in place, and the sort borrows a little room of its own. |
| 200 numbers. Brute force, about 1.3 million triples. | 0.06 s. At 3,000 numbers it would check 4.5 billion triples, which is why the two pointers matter. |
The designbeyondcode digest, every Saturday
New deep dives, interview tips and site news in one short email. Free. Leave with one click.
We never sell or share your email. We only use it to send this newsletter.