Facebook Pixel

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, and d 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:

  1. Efficiently skipping duplicate values to ensure unique quadruplets
  2. 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 (index j where j > i)
  • Using two pointers k and l (where k starts right after j and l 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).

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Sort the array to enable two-pointer technique and easy duplicate handling
  2. Fix the first two elements using nested loops
  3. For each pair of fixed elements, use two pointers to find all valid pairs for the remaining two positions
  4. 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 move k forward to get a larger value
  • If x > target: The sum is too large, so we move l 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 elements O(nยฒ), and for each pair, we use two pointers that traverse the remaining array O(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 Evaluator

Example 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)

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)

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)

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 from 0 to n-3, which is O(n) iterations
  • The second loop iterates through index j from i+1 to n-2, which is O(n) iterations in the worst case
  • The innermost while loop uses two pointers k and l that move towards each other, traversing at most O(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 is O(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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?


Recommended Readings

Want a Structured Path to Master System Design Too? Donโ€™t Miss This!

Load More