18. 4Sum
Problem Description
You are given an array nums
containing n
integers and a target
value. Your task is to find all unique quadruplets (groups of four numbers) from the array that sum up to the target value.
A valid quadruplet [nums[a], nums[b], nums[c], nums[d]]
must satisfy these conditions:
- All four indices
a
,b
,c
, andd
are different (you cannot use the same element twice) - The indices must be within the valid range:
0 <= a, b, c, d < n
- The sum of the four elements equals the target:
nums[a] + nums[b] + nums[c] + nums[d] == target
- The quadruplet must be unique (no duplicate quadruplets in the result)
The solution uses a combination of sorting and the two-pointer technique. After sorting the array, it fixes the first two elements through nested loops and uses two pointers to find the remaining two elements. The sorting helps in:
- Efficiently skipping duplicate values to ensure unique quadruplets
- Using the two-pointer approach for the last two elements
The algorithm works by:
- Iterating through possible values for the first element (index
i
) - For each
i
, iterating through possible values for the second element (indexj
wherej > i
) - Using two pointers
k
andl
(wherek
starts right afterj
andl
starts at the end) to find pairs that complete the quadruplet - Adjusting pointers based on whether the current sum is less than, greater than, or equal to the target
- Skipping duplicate values at each level to avoid duplicate quadruplets in the result
The time complexity is O(nยณ)
due to the three nested loops (two explicit loops and one implicit loop from the two-pointer traversal).
Intuition
The key insight is recognizing this as an extension of the classic Two Sum and Three Sum problems. When we need to find combinations that sum to a target, sorting the array opens up powerful optimization opportunities.
Think about how we'd solve this naively: we'd need four nested loops to check every possible combination of four numbers, giving us O(nโด)
time complexity. But can we do better?
The breakthrough comes from realizing that once we fix two numbers, finding the other two becomes a Two Sum problem on a sorted array. In a sorted array, we can use two pointers moving from opposite ends to efficiently find pairs that sum to a specific value.
Why does this work? When we have a sorted array and two pointers at opposite ends:
- If the current sum is too small, we need a larger number, so we move the left pointer right (to a larger value)
- If the current sum is too large, we need a smaller number, so we move the right pointer left (to a smaller value)
- If we find the target sum, we record it and move both pointers inward
The beauty of sorting first is that it also helps us handle duplicates elegantly. When we encounter the same value multiple times, we can simply skip over the duplicates since we've already processed all combinations with that value. This prevents duplicate quadruplets in our result without needing a separate deduplication step.
So our strategy becomes:
- Sort the array to enable two-pointer technique and easy duplicate handling
- Fix the first two elements using nested loops
- For each pair of fixed elements, use two pointers to find all valid pairs for the remaining two positions
- Skip duplicates at each level to ensure unique quadruplets
This reduces our time complexity from O(nโด)
to O(nยณ)
, which is a significant improvement for large arrays.
Learn more about Two Pointers and Sorting patterns.
Solution Approach
Let's walk through the implementation step by step, following the sorting + double pointers approach.
Step 1: Initial Setup and Sorting
First, we sort the array to enable the two-pointer technique and make duplicate handling easier:
nums.sort()
We also check if the array has fewer than 4 elements, as we need at least 4 numbers to form a quadruplet.
Step 2: Fix the First Element
We iterate through the array to fix the first element nums[i]
:
for i in range(n - 3): # Stop at n-3 since we need 3 more elements
if i and nums[i] == nums[i - 1]:
continue # Skip duplicates for the first element
The condition i and nums[i] == nums[i - 1]
ensures we skip duplicate values for the first position (except when i = 0
).
Step 3: Fix the Second Element
For each fixed first element, we iterate to fix the second element nums[j]
:
for j in range(i + 1, n - 2): # Start after i, stop at n-2
if j > i + 1 and nums[j] == nums[j - 1]:
continue # Skip duplicates for the second element
Step 4: Two Pointers for the Last Two Elements
With the first two elements fixed, we use two pointers to find valid pairs for positions 3 and 4:
k, l = j + 1, n - 1 # k starts right after j, l starts at the end while k < l: x = nums[i] + nums[j] + nums[k] + nums[l]
Step 5: Adjust Pointers Based on Sum
We calculate the sum x
and compare it with the target:
- If
x < target
: The sum is too small, so we movek
forward to get a larger value - If
x > target
: The sum is too large, so we movel
backward to get a smaller value - If
x == target
: We found a valid quadruplet!
When we find a valid quadruplet:
ans.append([nums[i], nums[j], nums[k], nums[l]]) k, l = k + 1, l - 1 # Move both pointers inward
Step 6: Skip Duplicates for the Two Pointers
After finding a valid quadruplet, we skip duplicate values to avoid duplicate results:
while k < l and nums[k] == nums[k - 1]: k += 1 # Skip duplicate values for k while k < l and nums[l] == nums[l + 1]: l -= 1 # Skip duplicate values for l
Note: There's a small bug in the reference code - the condition nums[l] == nums[l + 1]
should be nums[l] == nums[l - 1]
since we're moving l
leftward.
Time and Space Complexity
- Time Complexity:
O(nยณ)
- We have two nested loops for fixing the first two elementsO(nยฒ)
, and for each pair, we use two pointers that traverse the remaining arrayO(n)
. - Space Complexity:
O(1)
if we don't count the output array, as we only use a constant amount of extra space for pointers and temporary variables.
The sorting step takes O(n log n)
time, but it's dominated by the O(nยณ)
main algorithm.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to see how the algorithm works.
Example Input:
nums = [1, 0, -1, 0, -2, 2]
target = 0
Step 1: Sort the array
After sorting: nums = [-2, -1, 0, 0, 1, 2]
Step 2: Fix first element (i=0, nums[i]=-2)
Iteration 1.1: Fix second element (j=1, nums[j]=-1)
- First two elements:
-2, -1
(sum = -3) - Need remaining two to sum to:
0 - (-3) = 3
- Two pointers:
k=2
(nums[k]=0),l=5
(nums[l]=2)- Sum:
-2 + (-1) + 0 + 2 = -1
< 0 - Move k right:
k=3
- Sum:
-2 + (-1) + 0 + 2 = -1
< 0 - Move k right:
k=4
- Sum:
-2 + (-1) + 1 + 2 = 0
โ Found! - Add
[-2, -1, 1, 2]
to result - Move both pointers:
k=5
,l=4
(k >= l, stop)
- Sum:
Iteration 1.2: Fix second element (j=2, nums[j]=0)
- First two elements:
-2, 0
(sum = -2) - Two pointers:
k=3
(nums[k]=0),l=5
(nums[l]=2)- Sum:
-2 + 0 + 0 + 2 = 0
โ Found! - Add
[-2, 0, 0, 2]
to result - Move both pointers:
k=4
,l=4
(k >= l, stop)
- Sum:
Iteration 1.3: Fix second element (j=3, nums[j]=0)
- Skip! (nums[3] == nums[2], duplicate)
Step 3: Fix first element (i=1, nums[i]=-1)
Iteration 2.1: Fix second element (j=2, nums[j]=0)
- First two elements:
-1, 0
(sum = -1) - Two pointers:
k=3
(nums[k]=0),l=5
(nums[l]=2)- Sum:
-1 + 0 + 0 + 2 = 1
> 0 - Move l left:
l=4
- Sum:
-1 + 0 + 0 + 1 = 0
โ Found! - Add
[-1, 0, 0, 1]
to result - Move both pointers:
k=4
,l=3
(k >= l, stop)
- Sum:
Step 4: Fix first element (i=2, nums[i]=0)
- First element is 0
- No valid quadruplets found (remaining elements don't sum to target)
Step 5: Fix first element (i=3, nums[i]=0)
- Skip! (nums[3] == nums[2], duplicate)
Final Result: [[-2, -1, 1, 2], [-2, 0, 0, 2], [-1, 0, 0, 1]]
The algorithm successfully finds all unique quadruplets that sum to 0, avoiding duplicates through the skip logic at each level.
Solution Implementation
1class Solution:
2 def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
3 # Get the length of the input array
4 n = len(nums)
5 # Initialize the result list to store all unique quadruplets
6 result = []
7
8 # If array has less than 4 elements, no quadruplet possible
9 if n < 4:
10 return result
11
12 # Sort the array to enable two-pointer technique and skip duplicates
13 nums.sort()
14
15 # First pointer: iterate through array leaving space for 3 more elements
16 for i in range(n - 3):
17 # Skip duplicate values for the first element
18 if i > 0 and nums[i] == nums[i - 1]:
19 continue
20
21 # Second pointer: iterate from i+1 leaving space for 2 more elements
22 for j in range(i + 1, n - 2):
23 # Skip duplicate values for the second element
24 if j > i + 1 and nums[j] == nums[j - 1]:
25 continue
26
27 # Use two pointers for the remaining two elements
28 left = j + 1
29 right = n - 1
30
31 # Two-pointer approach to find pairs that sum to the remaining target
32 while left < right:
33 # Calculate the sum of all four elements
34 current_sum = nums[i] + nums[j] + nums[left] + nums[right]
35
36 if current_sum < target:
37 # Sum too small, move left pointer right to increase sum
38 left += 1
39 elif current_sum > target:
40 # Sum too large, move right pointer left to decrease sum
41 right -= 1
42 else:
43 # Found a valid quadruplet, add to result
44 result.append([nums[i], nums[j], nums[left], nums[right]])
45
46 # Move both pointers inward
47 left += 1
48 right -= 1
49
50 # Skip duplicate values for the third element
51 while left < right and nums[left] == nums[left - 1]:
52 left += 1
53
54 # Skip duplicate values for the fourth element
55 while left < right and nums[right] == nums[right + 1]:
56 right -= 1
57
58 return result
59
1class Solution {
2 public List<List<Integer>> fourSum(int[] nums, int target) {
3 int length = nums.length;
4 List<List<Integer>> result = new ArrayList<>();
5
6 // Edge case: need at least 4 numbers
7 if (length < 4) {
8 return result;
9 }
10
11 // Sort the array to enable two-pointer technique
12 Arrays.sort(nums);
13
14 // First number: iterate through possible first elements
15 for (int first = 0; first < length - 3; first++) {
16 // Skip duplicate values for the first number
17 if (first > 0 && nums[first] == nums[first - 1]) {
18 continue;
19 }
20
21 // Second number: iterate through possible second elements
22 for (int second = first + 1; second < length - 2; second++) {
23 // Skip duplicate values for the second number
24 if (second > first + 1 && nums[second] == nums[second - 1]) {
25 continue;
26 }
27
28 // Two-pointer approach for the remaining two numbers
29 int left = second + 1;
30 int right = length - 1;
31
32 while (left < right) {
33 // Use long to prevent integer overflow
34 long sum = (long) nums[first] + nums[second] + nums[left] + nums[right];
35
36 if (sum < target) {
37 // Sum is too small, move left pointer right
38 left++;
39 } else if (sum > target) {
40 // Sum is too large, move right pointer left
41 right--;
42 } else {
43 // Found a valid quadruplet
44 result.add(List.of(nums[first], nums[second], nums[left], nums[right]));
45 left++;
46 right--;
47
48 // Skip duplicate values for the third number
49 while (left < right && nums[left] == nums[left - 1]) {
50 left++;
51 }
52
53 // Skip duplicate values for the fourth number
54 while (left < right && nums[right] == nums[right + 1]) {
55 right--;
56 }
57 }
58 }
59 }
60 }
61
62 return result;
63 }
64}
65
1class Solution {
2public:
3 vector<vector<int>> fourSum(vector<int>& nums, int target) {
4 int n = nums.size();
5 vector<vector<int>> result;
6
7 // Need at least 4 numbers to form a quadruplet
8 if (n < 4) {
9 return result;
10 }
11
12 // Sort the array to enable two-pointer technique
13 sort(nums.begin(), nums.end());
14
15 // First number: iterate through possible values
16 for (int i = 0; i < n - 3; ++i) {
17 // Skip duplicate values for the first number
18 if (i > 0 && nums[i] == nums[i - 1]) {
19 continue;
20 }
21
22 // Second number: iterate through possible values
23 for (int j = i + 1; j < n - 2; ++j) {
24 // Skip duplicate values for the second number
25 if (j > i + 1 && nums[j] == nums[j - 1]) {
26 continue;
27 }
28
29 // Use two pointers for the remaining two numbers
30 int left = j + 1;
31 int right = n - 1;
32
33 while (left < right) {
34 // Use long long to prevent integer overflow
35 long long sum = static_cast<long long>(nums[i]) + nums[j] + nums[left] + nums[right];
36
37 if (sum < target) {
38 // Sum is too small, move left pointer to increase sum
39 ++left;
40 } else if (sum > target) {
41 // Sum is too large, move right pointer to decrease sum
42 --right;
43 } else {
44 // Found a valid quadruplet
45 result.push_back({nums[i], nums[j], nums[left], nums[right]});
46
47 // Move both pointers and skip duplicates
48 ++left;
49 --right;
50
51 // Skip duplicate values for the third number
52 while (left < right && nums[left] == nums[left - 1]) {
53 ++left;
54 }
55
56 // Skip duplicate values for the fourth number
57 while (left < right && nums[right] == nums[right + 1]) {
58 --right;
59 }
60 }
61 }
62 }
63 }
64
65 return result;
66 }
67};
68
1/**
2 * Finds all unique quadruplets in the array that sum up to the target value
3 * @param nums - The input array of numbers
4 * @param target - The target sum to find
5 * @returns Array of quadruplets that sum to target
6 */
7function fourSum(nums: number[], target: number): number[][] {
8 const length: number = nums.length;
9 const result: number[][] = [];
10
11 // Need at least 4 numbers to form a quadruplet
12 if (length < 4) {
13 return result;
14 }
15
16 // Sort the array to enable two-pointer technique
17 nums.sort((a: number, b: number) => a - b);
18
19 // First number in quadruplet
20 for (let firstIndex: number = 0; firstIndex < length - 3; firstIndex++) {
21 // Skip duplicate values for first number
22 if (firstIndex > 0 && nums[firstIndex] === nums[firstIndex - 1]) {
23 continue;
24 }
25
26 // Second number in quadruplet
27 for (let secondIndex: number = firstIndex + 1; secondIndex < length - 2; secondIndex++) {
28 // Skip duplicate values for second number
29 if (secondIndex > firstIndex + 1 && nums[secondIndex] === nums[secondIndex - 1]) {
30 continue;
31 }
32
33 // Use two pointers for the remaining two numbers
34 let leftPointer: number = secondIndex + 1;
35 let rightPointer: number = length - 1;
36
37 while (leftPointer < rightPointer) {
38 const currentSum: number = nums[firstIndex] + nums[secondIndex] +
39 nums[leftPointer] + nums[rightPointer];
40
41 if (currentSum < target) {
42 // Sum is too small, move left pointer right
43 leftPointer++;
44 } else if (currentSum > target) {
45 // Sum is too large, move right pointer left
46 rightPointer--;
47 } else {
48 // Found a valid quadruplet
49 result.push([
50 nums[firstIndex],
51 nums[secondIndex],
52 nums[leftPointer],
53 nums[rightPointer]
54 ]);
55
56 // Move both pointers
57 leftPointer++;
58 rightPointer--;
59
60 // Skip duplicate values for third number
61 while (leftPointer < rightPointer && nums[leftPointer] === nums[leftPointer - 1]) {
62 leftPointer++;
63 }
64
65 // Skip duplicate values for fourth number
66 while (leftPointer < rightPointer && nums[rightPointer] === nums[rightPointer + 1]) {
67 rightPointer--;
68 }
69 }
70 }
71 }
72 }
73
74 return result;
75}
76
Time and Space Complexity
Time Complexity: O(nยณ)
The algorithm uses three nested loops:
- The outermost loop iterates through index
i
from0
ton-3
, which isO(n)
iterations - The second loop iterates through index
j
fromi+1
ton-2
, which isO(n)
iterations in the worst case - The innermost while loop uses two pointers
k
andl
that move towards each other, traversing at mostO(n)
elements
Since these loops are nested, the overall time complexity is O(n) ร O(n) ร O(n) = O(nยณ)
.
The sorting operation at the beginning takes O(n log n)
time, but this is dominated by the O(nยณ)
complexity of the main algorithm.
Space Complexity: O(log n)
The space complexity comes from:
- The sorting algorithm (typically Timsort in Python) uses
O(log n)
space for its recursive call stack - The output list
ans
stores the results, but this is not counted as part of the space complexity since it's the required output - The algorithm uses only a constant amount of extra variables (
i
,j
,k
,l
,x
), which isO(1)
Therefore, the dominant space usage is from the sorting operation, making the overall space complexity O(log n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow When Calculating Sum
One critical pitfall that's often overlooked is integer overflow when calculating the sum of four numbers. In languages with fixed integer sizes, adding four large integers can cause overflow. Even in Python (which handles arbitrary precision), other languages like Java or C++ can face this issue.
Problem Example:
# Consider nums = [1000000000, 1000000000, 1000000000, 1000000000] # target = 4000000000 # In languages with 32-bit integers, the sum would overflow current_sum = nums[i] + nums[j] + nums[left] + nums[right] # Potential overflow
Solution: Convert to a larger data type or use careful arithmetic:
# For languages with overflow concerns, cast to long/int64: current_sum = long(nums[i]) + nums[j] + nums[left] + nums[right] # Or rearrange the comparison to avoid computing the full sum: # Instead of: sum == target # Use: nums[left] + nums[right] == target - nums[i] - nums[j]
2. Incorrect Duplicate Skipping Logic
A very common mistake is the boundary check when skipping duplicates for the right pointer. The code shows:
while left < right and nums[right] == nums[right + 1]: # BUG! right -= 1
This will cause an index out of bounds error when right
is at position n-1
initially.
Correct Solution:
# Since we're moving right pointer leftward, compare with right-1 while left < right and nums[right] == nums[right - 1]: right -= 1
3. Missing Early Termination Optimizations
The algorithm doesn't include pruning conditions that could significantly improve performance for certain inputs:
Problem:
# The code processes all combinations even when it's impossible to reach target
for i in range(n - 3):
# Even if nums[i] + smallest_three > target, we continue
# Even if nums[i] + largest_three < target, we continue
Solution with Optimizations:
for i in range(n - 3):
if i > 0 and nums[i] == nums[i - 1]:
continue
# Early termination: smallest possible sum with current i
if nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target:
break # All subsequent sums will be larger
# Skip if largest possible sum with current i is too small
if nums[i] + nums[n - 3] + nums[n - 2] + nums[n - 1] < target:
continue # Try next i
for j in range(i + 1, n - 2):
if j > i + 1 and nums[j] == nums[j - 1]:
continue
# Similar optimizations for j
if nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target:
break
if nums[i] + nums[j] + nums[n - 2] + nums[n - 1] < target:
continue
4. Handling Edge Cases with Target Value
The code doesn't handle edge cases where the target or array values might be negative or zero properly in terms of optimization.
Problem:
# With negative numbers, early termination logic becomes complex # nums = [-5, -4, -3, 1, 2, 3], target = -8 # Simple min/max comparisons don't work as expected
Solution: Be careful with pruning optimizations when dealing with negative numbers, or disable them when the array contains negative values:
# Check if array has negative numbers has_negative = nums[0] < 0 # Apply optimizations only if all numbers are non-negative if not has_negative: # Apply early termination optimizations if nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target: break
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
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!