15. 3Sum
Problem Description
You are given an integer array nums
. Your task is to find all unique triplets in the array where three numbers add up to zero.
Specifically, you need to return all triplets [nums[i], nums[j], nums[k]]
that satisfy these conditions:
- The three indices must be different:
i != j
,i != k
, andj != k
- The sum equals zero:
nums[i] + nums[j] + nums[k] == 0
- The solution set must not contain duplicate triplets (even if the same values appear multiple times in the array)
For example, if nums = [-1, 0, 1, 2, -1, -4]
, the output would be [[-1, -1, 2], [-1, 0, 1]]
. Notice that even though -1
appears twice in the array, we only include each unique triplet once in the result.
The key challenge is efficiently finding all such triplets while avoiding duplicates. The solution uses sorting combined with a two-pointer technique. After sorting the array, for each element nums[i]
, we use two pointers to find pairs in the remaining array that sum to -nums[i]
. This reduces the problem from finding three numbers that sum to zero to finding two numbers that sum to a target value.
The algorithm carefully skips duplicate values during iteration to ensure the result contains only unique triplets. When nums[i]
is positive, we can stop early since all remaining elements will also be positive (after sorting), making it impossible to achieve a sum of zero.
Intuition
The brute force approach would be to check all possible triplets using three nested loops, which would take O(nยณ)
time. However, we can optimize this significantly by observing that once we fix one number, we're essentially looking for two other numbers that sum to the negative of the fixed number.
This insight transforms our problem: instead of finding three numbers that sum to zero, we can fix one number nums[i]
and then find two numbers in the remaining array that sum to -nums[i]
. This is essentially the classic Two Sum problem, which can be solved efficiently using two pointers on a sorted array.
Why does sorting help? When the array is sorted, we can use the two-pointer technique effectively. If the current sum is too small, we know we need a larger number, so we move the left pointer right. If the sum is too large, we need a smaller number, so we move the right pointer left. This way, we can find all valid pairs in linear time for each fixed element.
The duplicate handling becomes straightforward with a sorted array. Since identical elements are grouped together after sorting, we can easily skip duplicates by checking if the current element equals the previous one. For the first element nums[i]
, we skip it if it's the same as nums[i-1]
. Similarly, after finding a valid triplet, we skip duplicate values for both pointers to avoid recording the same triplet multiple times.
An additional optimization comes from recognizing that if nums[i] > 0
in a sorted array, all elements to its right are also positive. Since we need three numbers to sum to zero, having all positive numbers makes this impossible, allowing us to terminate early.
This approach reduces the time complexity from O(nยณ)
to O(nยฒ)
- we iterate through each element once (O(n)
), and for each element, the two-pointer search takes O(n)
time.
Learn more about Two Pointers and Sorting patterns.
Solution Approach
The implementation follows a systematic approach using sorting and two pointers:
Step 1: Sort the Array
First, we sort the array in ascending order using nums.sort()
. This enables us to use the two-pointer technique and makes duplicate handling straightforward.
Step 2: Iterate Through First Element
We enumerate each element as the first element of a potential triplet. The loop runs from index 0
to n-2
(where n
is the array length) because we need at least three elements for a valid triplet.
Step 3: Early Termination
If nums[i] > 0
, we break immediately. Since the array is sorted, all subsequent elements will also be positive, making it impossible to find three numbers that sum to zero.
Step 4: Skip Duplicates for First Element
To avoid duplicate triplets, we check if i > 0
and nums[i] == nums[i-1]
. If true, we skip this iteration using continue
because we've already processed this value as the first element.
Step 5: Two-Pointer Search
For each valid nums[i]
, we initialize two pointers:
- Left pointer
j = i + 1
(starts right after the fixed element) - Right pointer
k = n - 1
(starts at the end of the array)
Step 6: Find Valid Triplets
While j < k
, we calculate the sum x = nums[i] + nums[j] + nums[k]
and compare it with zero:
- If
x < 0
: The sum is too small, so we incrementj
to get a larger value - If
x > 0
: The sum is too large, so we decrementk
to get a smaller value - If
x == 0
: We found a valid triplet! We add[nums[i], nums[j], nums[k]]
to our answer list
Step 7: Handle Duplicates for Two Pointers After finding a valid triplet, we need to skip duplicate values for both pointers:
- Move
j
forward:j += 1
, then continue incrementing whilej < k
andnums[j] == nums[j-1]
- Move
k
backward:k -= 1
, then continue decrementing whilej < k
andnums[k] == nums[k+1]
This ensures we don't add the same triplet multiple times even when duplicate values exist in the array.
Step 8: Return Results
After processing all possible first elements, we return the ans
list containing all unique triplets.
The time complexity is O(nยฒ)
: sorting takes O(n log n)
, and the main loop runs in O(nยฒ)
(outer loop O(n)
ร inner two-pointer search O(n)
). The space complexity is O(1)
if we don't count the output array, as we only use a constant amount of extra space for pointers.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the algorithm with nums = [-1, 0, 1, 2, -1, -4]
.
Step 1: Sort the array
After sorting: nums = [-4, -1, -1, 0, 1, 2]
Step 2-7: Iterate through each element as the first element
Iteration 1: i = 0, nums[i] = -4
- Not a duplicate (i = 0), not positive, so we proceed
- Set two pointers: j = 1 (nums[j] = -1), k = 5 (nums[k] = 2)
- Calculate sum: -4 + (-1) + 2 = -3 < 0
- Sum too small, move j right: j = 2 (nums[j] = -1)
- Calculate sum: -4 + (-1) + 2 = -3 < 0
- Sum too small, move j right: j = 3 (nums[j] = 0)
- Calculate sum: -4 + 0 + 2 = -2 < 0
- Sum too small, move j right: j = 4 (nums[j] = 1)
- Calculate sum: -4 + 1 + 2 = -1 < 0
- Sum too small, move j right: j = 5, but now j >= k, so stop
Iteration 2: i = 1, nums[i] = -1
- Not a duplicate (different from nums[0] = -4), not positive, so proceed
- Set two pointers: j = 2 (nums[j] = -1), k = 5 (nums[k] = 2)
- Calculate sum: -1 + (-1) + 2 = 0 โ
- Found triplet! Add
[-1, -1, 2]
to answer - Skip duplicates: j moves from 2 to 3 (nums[3] = 0, different from -1)
- k moves from 5 to 4 (nums[4] = 1, different from 2)
- Continue with j = 3, k = 4
- Calculate sum: -1 + 0 + 1 = 0 โ
- Found triplet! Add
[-1, 0, 1]
to answer - Skip duplicates: j moves from 3 to 4, but now j >= k, so stop
Iteration 3: i = 2, nums[i] = -1
- This is a duplicate (nums[2] = nums[1] = -1), so skip with continue
Iteration 4: i = 3, nums[i] = 0
- Not a duplicate, not positive, so proceed
- Set two pointers: j = 4 (nums[j] = 1), k = 5 (nums[k] = 2)
- Calculate sum: 0 + 1 + 2 = 3 > 0
- Sum too large, move k left: k = 4, but now j >= k, so stop
Iteration 5: i = 4, nums[i] = 1
- nums[i] > 0, so we break early (all remaining elements are positive)
Final Result: [[-1, -1, 2], [-1, 0, 1]]
The algorithm efficiently finds all unique triplets by:
- Using sorting to enable the two-pointer technique
- Skipping duplicate values at position i (like when i = 2)
- Finding valid triplets using two pointers that converge
- Skipping duplicate values at j and k after finding valid triplets
- Terminating early when nums[i] > 0
Solution Implementation
1class Solution:
2 def threeSum(self, nums: List[int]) -> List[List[int]]:
3 # Sort the array to enable two-pointer technique
4 nums.sort()
5 n = len(nums)
6 result = []
7
8 # Iterate through the array, fixing the first element
9 for i in range(n - 2):
10 # Early termination: if the smallest number is positive, no zero sum possible
11 if nums[i] > 0:
12 break
13
14 # Skip duplicate values for the first element to avoid duplicate triplets
15 if i > 0 and nums[i] == nums[i - 1]:
16 continue
17
18 # Use two pointers to find pairs that sum to -nums[i]
19 left, right = i + 1, n - 1
20
21 while left < right:
22 current_sum = nums[i] + nums[left] + nums[right]
23
24 if current_sum < 0:
25 # Sum is too small, move left pointer to increase the sum
26 left += 1
27 elif current_sum > 0:
28 # Sum is too large, move right pointer to decrease the sum
29 right -= 1
30 else:
31 # Found a valid triplet with sum equal to zero
32 result.append([nums[i], nums[left], nums[right]])
33
34 # Move both pointers inward
35 left += 1
36 right -= 1
37
38 # Skip duplicate values for the second element
39 while left < right and nums[left] == nums[left - 1]:
40 left += 1
41
42 # Skip duplicate values for the third element
43 while left < right and nums[right] == nums[right + 1]:
44 right -= 1
45
46 return result
47
1class Solution {
2 public List<List<Integer>> threeSum(int[] nums) {
3 // Sort the array to enable two-pointer technique
4 Arrays.sort(nums);
5
6 // Result list to store all unique triplets
7 List<List<Integer>> result = new ArrayList<>();
8 int length = nums.length;
9
10 // Iterate through the array as the first element of triplet
11 // Stop at length-2 since we need at least 3 elements
12 // Also stop early if current element is positive (sum can't be zero with all positive numbers)
13 for (int i = 0; i < length - 2 && nums[i] <= 0; i++) {
14 // Skip duplicate values for the first element to avoid duplicate triplets
15 if (i > 0 && nums[i] == nums[i - 1]) {
16 continue;
17 }
18
19 // Two-pointer approach for finding pairs that sum to -nums[i]
20 int left = i + 1;
21 int right = length - 1;
22
23 while (left < right) {
24 int sum = nums[i] + nums[left] + nums[right];
25
26 if (sum < 0) {
27 // Sum is too small, move left pointer to increase sum
28 left++;
29 } else if (sum > 0) {
30 // Sum is too large, move right pointer to decrease sum
31 right--;
32 } else {
33 // Found a valid triplet with sum equal to zero
34 result.add(List.of(nums[i], nums[left], nums[right]));
35 left++;
36 right--;
37
38 // Skip duplicate values for the second element
39 while (left < right && nums[left] == nums[left - 1]) {
40 left++;
41 }
42
43 // Skip duplicate values for the third element
44 while (left < right && nums[right] == nums[right + 1]) {
45 right--;
46 }
47 }
48 }
49 }
50
51 return result;
52 }
53}
54
1class Solution {
2public:
3 vector<vector<int>> threeSum(vector<int>& nums) {
4 // Sort the array to enable two-pointer technique
5 sort(nums.begin(), nums.end());
6
7 vector<vector<int>> result;
8 int size = nums.size();
9
10 // Iterate through the array as the first number
11 // Stop at size-2 since we need at least 3 numbers
12 // Also stop if current number > 0 (optimization: if smallest > 0, sum can't be 0)
13 for (int first = 0; first < size - 2 && nums[first] <= 0; ++first) {
14 // Skip duplicate values for the first number to avoid duplicate triplets
15 if (first > 0 && nums[first] == nums[first - 1]) {
16 continue;
17 }
18
19 // Use two pointers for the remaining two numbers
20 int left = first + 1;
21 int right = size - 1;
22
23 // Find all valid pairs for current first number
24 while (left < right) {
25 int currentSum = nums[first] + nums[left] + nums[right];
26
27 if (currentSum < 0) {
28 // Sum too small, move left pointer to increase sum
29 ++left;
30 } else if (currentSum > 0) {
31 // Sum too large, move right pointer to decrease sum
32 --right;
33 } else {
34 // Found a valid triplet with sum = 0
35 result.push_back({nums[first], nums[left], nums[right]});
36
37 // Move both pointers
38 ++left;
39 --right;
40
41 // Skip duplicate values for the second number
42 while (left < right && nums[left] == nums[left - 1]) {
43 ++left;
44 }
45
46 // Skip duplicate values for the third number
47 while (left < right && nums[right] == nums[right + 1]) {
48 --right;
49 }
50 }
51 }
52 }
53
54 return result;
55 }
56};
57
1/**
2 * Finds all unique triplets in the array that sum to zero
3 * @param nums - Input array of numbers
4 * @returns Array of triplets that sum to zero
5 */
6function threeSum(nums: number[]): number[][] {
7 // Sort the array in ascending order for two-pointer approach
8 nums.sort((a, b) => a - b);
9
10 const result: number[][] = [];
11 const length = nums.length;
12
13 // Iterate through the array, fixing the first element
14 // Stop at length-2 since we need at least 3 elements
15 // Also stop if current element > 0 (sum can't be zero with all positive numbers)
16 for (let firstIndex = 0; firstIndex < length - 2 && nums[firstIndex] <= 0; firstIndex++) {
17 // Skip duplicate values for the first element to avoid duplicate triplets
18 if (firstIndex > 0 && nums[firstIndex] === nums[firstIndex - 1]) {
19 continue;
20 }
21
22 // Use two pointers for the remaining elements
23 let leftPointer = firstIndex + 1;
24 let rightPointer = length - 1;
25
26 // Find pairs that sum with nums[firstIndex] to equal zero
27 while (leftPointer < rightPointer) {
28 const currentSum = nums[firstIndex] + nums[leftPointer] + nums[rightPointer];
29
30 if (currentSum < 0) {
31 // Sum is too small, move left pointer right to increase sum
32 leftPointer++;
33 } else if (currentSum > 0) {
34 // Sum is too large, move right pointer left to decrease sum
35 rightPointer--;
36 } else {
37 // Found a valid triplet
38 result.push([nums[firstIndex], nums[leftPointer], nums[rightPointer]]);
39
40 // Move both pointers and skip duplicates
41 leftPointer++;
42 rightPointer--;
43
44 // Skip duplicate values for the second element
45 while (leftPointer < rightPointer && nums[leftPointer] === nums[leftPointer - 1]) {
46 leftPointer++;
47 }
48
49 // Skip duplicate values for the third element
50 while (leftPointer < rightPointer && nums[rightPointer] === nums[rightPointer + 1]) {
51 rightPointer--;
52 }
53 }
54 }
55 }
56
57 return result;
58}
59
Time and Space Complexity
Time Complexity: O(nยฒ)
The algorithm sorts the array first, which takes O(n log n)
time. Then it uses a fixed pointer i
to iterate through the array from index 0 to n-2
, which is O(n)
iterations. For each fixed i
, it uses two pointers j
and k
to traverse the remaining array with a two-pointer technique. In the worst case, the two pointers together traverse the remaining n-i-1
elements once, making it O(n)
for each i
. Therefore, the nested loop structure gives us O(n) ร O(n) = O(nยฒ)
. Since O(nยฒ)
dominates O(n log n)
, the overall time complexity is O(nยฒ)
.
Space Complexity: O(log n)
The space complexity comes from the sorting algorithm. Python's built-in sort()
method uses Timsort, which has a worst-case space complexity of O(n)
but typically uses O(log n)
space for the recursion stack in practice. The algorithm itself only uses a constant amount of extra space for variables like i
, j
, k
, and x
. The output array ans
is not counted as part of the space complexity since it's the required output. Therefore, the space complexity is O(log n)
where n
is the length of the input array.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Duplicate Handling After Finding a Valid Triplet
The Pitfall: A frequent mistake is forgetting to move both pointers after finding a valid triplet, or only moving one pointer. This causes an infinite loop because the same valid triplet keeps being found repeatedly.
Incorrect Code Example:
if current_sum == 0: result.append([nums[i], nums[left], nums[right]]) # Forgot to move pointers! This creates an infinite loop # The while loop condition never changes
Correct Solution: Always move both pointers after finding a valid triplet, then skip duplicates:
if current_sum == 0: result.append([nums[i], nums[left], nums[right]]) left += 1 right -= 1 # Then skip duplicates for both pointers while left < right and nums[left] == nums[left - 1]: left += 1 while left < right and nums[right] == nums[right + 1]: right -= 1
2. Off-by-One Error in Duplicate Skipping Logic
The Pitfall: Using the wrong comparison indices when skipping duplicates, particularly comparing with the wrong neighboring element.
Incorrect Code Example:
# Wrong: comparing with nums[right - 1] instead of nums[right + 1] while left < right and nums[right] == nums[right - 1]: right -= 1
This compares the current right
pointer with the element we're moving toward, not the element we're moving away from. This can skip valid elements or fail to skip duplicates properly.
Correct Solution:
After decrementing right
, compare with nums[right + 1]
(the value we just moved from):
while left < right and nums[right] == nums[right + 1]: right -= 1
3. Forgetting to Check Bounds Before Skipping Duplicates
The Pitfall: Not checking if pointers are still within valid bounds during duplicate skipping can cause index out of bounds errors.
Incorrect Code Example:
# Missing the left < right check in the while condition while nums[left] == nums[left - 1]: # Can cause index errors left += 1
Correct Solution: Always include bounds checking in the condition:
while left < right and nums[left] == nums[left - 1]: left += 1
4. Skipping Duplicates for the First Element Incorrectly
The Pitfall: Starting duplicate checking from index 0 instead of index 1, which would skip the first occurrence of each value entirely.
Incorrect Code Example:
for i in range(n - 2):
if nums[i] == nums[i - 1]: # Problem when i = 0, nums[-1] is accessed
continue
Correct Solution:
Add a condition to check i > 0
before comparing with the previous element:
for i in range(n - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue
5. Not Handling Edge Cases for Array Length
The Pitfall: Processing arrays with fewer than 3 elements without proper validation.
Incorrect Code Example:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
n = len(nums)
# Directly starting the loop without checking if n >= 3
for i in range(n - 2): # If n < 3, range could be negative or empty
...
Correct Solution: Either add an explicit check or ensure the loop range handles it naturally:
def threeSum(self, nums: List[int]) -> List[List[int]]:
if len(nums) < 3:
return []
nums.sort()
# Or just let range(n - 2) handle it naturally when n < 3
What are the most two important steps in writing a depth first search function? (Select 2)
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donโt Miss This!