LeetCode 15 - 3Sum
The problem gives us an integer array nums, and we need to find every unique triplet of numbers whose sum is exactly 0. A triplet consists of three different indices: - i != j - i != k - j != k The values themselves may be equal, but the indices must be different.
Difficulty: 🟡 Medium
Topics: Array, Two Pointers, Sorting
Solution
LeetCode 15 - 3Sum
Problem Understanding
The problem gives us an integer array nums, and we need to find every unique triplet of numbers whose sum is exactly 0.
A triplet consists of three different indices:
i != ji != kj != k
The values themselves may be equal, but the indices must be different. For example, [0,0,0] is valid if the array contains at least three zeros.
The output should contain all distinct triplets. Two triplets are considered duplicates if they contain the same values, regardless of order. For example:
[-1,0,1][0,-1,1]
represent the same triplet and only one of them should appear in the final answer.
The constraints are important:
3 <= nums.length <= 3000-10^5 <= nums[i] <= 10^5
A length of 3000 immediately tells us that cubic algorithms are probably too slow. An O(n^3) solution would require up to:
$$3000^3 = 27,000,000,000$$
operations in the worst case, which is far beyond acceptable limits.
The problem also contains several tricky edge cases:
- Arrays with many duplicate values
- Arrays containing only zeros
- Arrays with no valid triplets
- Negative and positive values mixed together
- Multiple different triplets sharing some numbers
A naive implementation often fails because duplicate handling becomes complicated. Preventing duplicate triplets is one of the core challenges of this problem.
Approaches
Brute Force Approach
The brute force solution checks every possible combination of three elements.
We can use three nested loops:
- The first loop chooses the first element.
- The second loop chooses the second element.
- The third loop chooses the third element.
For every triplet, we compute the sum. If the sum equals zero, we add the triplet to the answer.
Since the output cannot contain duplicates, we also need a way to avoid repeated triplets. One common approach is:
- Sort each valid triplet before storing it.
- Insert the triplet into a set.
This guarantees correctness because every possible combination is checked.
The major issue is performance. Three nested loops produce O(n^3) time complexity, which is too slow for n = 3000.
Optimal Approach, Sorting + Two Pointers
The key insight is that sorting allows us to search for pairs efficiently.
Suppose we fix one number nums[i]. Then the problem becomes:
Find two numbers after index
iwhose sum equals-nums[i].
After sorting the array:
- If the current sum is too small, we move the left pointer rightward to increase the sum.
- If the current sum is too large, we move the right pointer leftward to decrease the sum.
This transforms the inner search from O(n) pair enumeration into an efficient two pointer scan.
Sorting also makes duplicate handling much easier because equal values become adjacent.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n^3) |
O(k) |
Checks every triplet, too slow for large inputs |
| Optimal | O(n^2) |
O(1) excluding output |
Uses sorting and two pointers to efficiently find pairs |
Here, k represents the number of stored triplets.
Algorithm Walkthrough
Optimal Algorithm
- Sort the array.
Sorting is essential because the two pointer technique only works on ordered data. It also allows us to skip duplicates efficiently.
2. Iterate through the array using index i.
At each step, nums[i] becomes the first element of the triplet.
3. Skip duplicate values for i.
If nums[i] == nums[i - 1], we skip this iteration because we would generate the same triplets again.
4. Initialize two pointers.
left = i + 1right = len(nums) - 1
These pointers search for the remaining two values. 5. Compute the current sum.
total = nums[i] + nums[left] + nums[right]
- If the sum is too small, move
leftrightward.
Since the array is sorted, increasing left increases the total sum.
7. If the sum is too large, move right leftward.
Since the array is sorted, decreasing right decreases the total sum.
8. If the sum equals zero, record the triplet.
Add:
[nums[i], nums[left], nums[right]]
to the answer. 9. Move both pointers inward.
After finding a valid triplet, we continue searching for more combinations.
10. Skip duplicate values for left and right.
This prevents repeated triplets from entering the result.
11. Continue until left >= right.
At that point, all possible pairs for the current i have been checked.
Why it works
After sorting, every movement of the pointers changes the sum in a predictable direction.
- Moving
leftincreases the sum. - Moving
rightdecreases the sum.
This guarantees that every possible pair is examined exactly once for each fixed i.
Skipping duplicates works because equal values are adjacent in the sorted array. Once we process one occurrence, processing the next identical value would generate identical triplets.
Together, these properties ensure:
- Every valid triplet is found.
- No duplicate triplets are produced.
- The algorithm runs in
O(n^2)time.
Python Solution
class Solution:
def threeSum(self, nums: list[int]) -> list[list[int]]:
nums.sort()
result = []
n = len(nums)
for i in range(n - 2):
# Skip duplicate first elements
if i > 0 and nums[i] == nums[i - 1]:
continue
left = i + 1
right = n - 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
# Skip duplicate second elements
while left < right and nums[left] == nums[left - 1]:
left += 1
# Skip duplicate third elements
while left < right and nums[right] == nums[right + 1]:
right -= 1
return result
The implementation begins by sorting the array. This enables both efficient searching and duplicate elimination.
The outer loop fixes the first number of the triplet. Before processing each value, we check whether it matches the previous element. If it does, we skip it because it would generate duplicate triplets.
The two pointers then search the remaining subarray.
When the current sum is:
- Less than zero, we increase the sum by moving
left - Greater than zero, we decrease the sum by moving
right - Equal to zero, we record the triplet
After finding a valid triplet, both pointers move inward. Duplicate values are skipped immediately afterward so the same triplet cannot be added multiple times.
The algorithm naturally explores all valid combinations without revisiting unnecessary states.
Go Solution
package main
import "sort"
func threeSum(nums []int) [][]int {
sort.Ints(nums)
result := [][]int{}
n := len(nums)
for i := 0; i < n-2; i++ {
// Skip duplicate first elements
if i > 0 && nums[i] == nums[i-1] {
continue
}
left := i + 1
right := n - 1
for left < right {
total := nums[i] + nums[left] + nums[right]
if total < 0 {
left++
} else if total > 0 {
right--
} else {
result = append(result, []int{
nums[i],
nums[left],
nums[right],
})
left++
right--
// Skip duplicate second elements
for left < right && nums[left] == nums[left-1] {
left++
}
// Skip duplicate third elements
for left < right && nums[right] == nums[right+1] {
right--
}
}
}
}
return result
}
The Go implementation follows the same logic as the Python version.
Go slices are used to store triplets dynamically. The sort.Ints function sorts the array in ascending order.
Unlike Python, Go does not have built in dynamic list literals with automatic resizing semantics identical to Python lists, so triplets are appended using append.
Integer overflow is not a concern here because the constraints are well within Go's int range.
Returning an empty slice is acceptable when no triplets exist.
Worked Examples
Example 1
Input:
[-1,0,1,2,-1,-4]
Step 1: Sort the array
[-4,-1,-1,0,1,2]
Iteration Details
| i | nums[i] | left | right | Triplet | Sum | Action |
|---|---|---|---|---|---|---|
| 0 | -4 | 1 | 5 | [-4,-1,2] | -3 | Move left |
| 0 | -4 | 2 | 5 | [-4,-1,2] | -3 | Move left |
| 0 | -4 | 3 | 5 | [-4,0,2] | -2 | Move left |
| 0 | -4 | 4 | 5 | [-4,1,2] | -1 | Move left |
| 1 | -1 | 2 | 5 | [-1,-1,2] | 0 | Record triplet |
| 1 | -1 | 3 | 4 | [-1,0,1] | 0 | Record triplet |
Result:
[[-1,-1,2],[-1,0,1]]
The next -1 at index 2 is skipped because it would create duplicate triplets.
Example 2
Input:
[0,1,1]
Sorted array:
[0,1,1]
| i | left | right | Sum | Action |
|---|---|---|---|---|
| 0 | 1 | 2 | 2 | Move right |
No valid triplets exist.
Result:
[]
Example 3
Input:
[0,0,0]
Sorted array:
[0,0,0]
| i | left | right | Sum | Action |
|---|---|---|---|---|
| 0 | 1 | 2 | 0 | Record triplet |
Result:
[[0,0,0]]
After recording the triplet, duplicate skipping prevents additional identical results.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n^2) |
Outer loop runs n times, two pointers scan linearly |
| Space | O(1) excluding output |
Only pointer variables are used |
Sorting costs O(n log n), but the dominant part of the algorithm is the nested two pointer traversal, which takes O(n^2) time.
The algorithm uses constant auxiliary space because it only stores a few indices and temporary variables. The output list itself is not counted in auxiliary space complexity.
Test Cases
def normalize(result):
return sorted(sorted(triplet) for triplet in result)
sol = Solution()
# Example 1
assert normalize(sol.threeSum([-1,0,1,2,-1,-4])) == \
normalize([[-1,-1,2],[-1,0,1]]) # standard mixed case
# Example 2
assert normalize(sol.threeSum([0,1,1])) == \
normalize([]) # no valid triplets
# Example 3
assert normalize(sol.threeSum([0,0,0])) == \
normalize([[0,0,0]]) # all zeros
# Minimum valid length
assert normalize(sol.threeSum([-1,0,1])) == \
normalize([[-1,0,1]]) # exactly one triplet
# Multiple duplicates
assert normalize(sol.threeSum([-2,0,0,2,2])) == \
normalize([[-2,0,2]]) # duplicate handling
# No solution with negatives only
assert normalize(sol.threeSum([-5,-4,-3,-2])) == \
normalize([]) # impossible to sum to zero
# No solution with positives only
assert normalize(sol.threeSum([1,2,3,4])) == \
normalize([]) # impossible to sum to zero
# Multiple valid triplets
assert normalize(sol.threeSum([-4,-2,-2,-2,0,1,2,2,2,3,3,4,4,6,6])) == \
normalize([
[-4,-2,6],
[-4,0,4],
[-4,1,3],
[-4,2,2],
[-2,-2,4],
[-2,0,2]
]) # stress duplicate skipping
# Large duplicate block
assert normalize(sol.threeSum([0,0,0,0,0])) == \
normalize([[0,0,0]]) # many identical zeros
# Mixed negatives and positives
assert normalize(sol.threeSum([-3,-1,0,1,2,-1,-4])) == \
normalize([
[-3,1,2],
[-1,-1,2],
[-1,0,1]
]) # multiple distinct solutions
Test Summary
| Test | Why |
|---|---|
[-1,0,1,2,-1,-4] |
Standard example with duplicates |
[0,1,1] |
No valid triplets |
[0,0,0] |
Single valid triplet with identical values |
[-1,0,1] |
Minimum valid array size |
[-2,0,0,2,2] |
Duplicate elimination correctness |
[-5,-4,-3,-2] |
All negative values |
[1,2,3,4] |
All positive values |
| Large mixed duplicate case | Stress tests pointer movement and duplicate skipping |
[0,0,0,0,0] |
Repeated zeros |
[-3,-1,0,1,2,-1,-4] |
Multiple distinct triplets |
Edge Cases
Arrays Containing Many Duplicates
Duplicate handling is the most common source of bugs in this problem.
Consider:
[-2,0,0,2,2]
Without careful skipping logic, the algorithm might produce:
[-2,0,2]
[-2,0,2]
multiple times.
The implementation avoids this in two places:
- The outer loop skips duplicate starting elements.
- The inner loop skips duplicate
leftandrightvalues after recording a valid triplet.
Together, these checks guarantee uniqueness.
Arrays Consisting Entirely of Zeros
An input like:
[0,0,0,0]
contains many possible index combinations, but only one distinct triplet:
[0,0,0]
A naive implementation often inserts the same triplet repeatedly.
After recording the first valid triplet, the algorithm advances both pointers and skips all adjacent duplicate zeros, preventing repeated output.
Arrays With No Possible Triplets
Examples include:
[1,2,3,4]
or
[-5,-4,-3]
Since all numbers have the same sign, no three values can sum to zero.
The algorithm handles this naturally. The two pointer search eventually terminates without finding any valid sums, and the result remains empty.
Minimum Length Arrays
The smallest allowed input size is 3.
For example:
[-1,0,1]
The implementation still works correctly because:
- The outer loop runs once.
- The two pointers examine exactly one pair.
- The valid triplet is returned.
No special case logic is required.