title: 4Sum
slug: four-sum
difficulty: medium
leetcode_id: 18
leetcode_url: https://leetcode.com/problems/4sum/
categories:
  - arrays
  - two-pointers
  - sorting
patterns:
  - two-pointers

description: |
  Given an array `nums` of `n` integers, return *an array of all the **unique** quadruplets* `[nums[a], nums[b], nums[c], nums[d]]` such that:

  - `0 <= a, b, c, d < n`
  - `a`, `b`, `c`, and `d` are **distinct**
  - `nums[a] + nums[b] + nums[c] + nums[d] == target`

  You may return the answer in **any order**.

constraints: |
  - `1 <= nums.length <= 200`
  - `-10^9 <= nums[i] <= 10^9`
  - `-10^9 <= target <= 10^9`

examples:
  - input: "nums = [1,0,-1,0,-2,2], target = 0"
    output: "[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]"
    explanation: "Three unique quadruplets sum to 0: [-2,-1,1,2], [-2,0,0,2], and [-1,0,0,1]."
  - input: "nums = [2,2,2,2,2], target = 8"
    output: "[[2,2,2,2]]"
    explanation: "The only quadruplet that sums to 8 is [2,2,2,2]."

explanation:
  intuition: |
    If you've solved **3Sum**, 4Sum follows the same reduction strategy: fix one element and solve a smaller problem.

    Think of it like peeling an onion. 3Sum reduces to 2Sum by fixing one element. Similarly, **4Sum reduces to 3Sum** by fixing the first element, which then reduces to 2Sum. Each layer peels away one dimension of complexity.

    The key insight is that after sorting, we can use **two nested loops** to fix the first two elements, then apply the familiar two-pointer technique to find the remaining pair. This gives us O(n³) time — the best we can do when there can be O(n³) valid quadruplets.

    Sorting remains essential for two reasons:
    1. **Two pointers work**: Adjusting sum by moving pointers left or right
    2. **Duplicate skipping**: Adjacent duplicates become neighbours we can easily skip

  approach: |
    We solve this using **Sort + Two Nested Loops + Two Pointers**:

    **Step 1: Sort the array**

    - Sorting enables two-pointer technique and easy duplicate detection
    - Time: O(n log n), dominated by the O(n³) main algorithm

    &nbsp;

    **Step 2: Fix the first element**

    - For each `i` from 0 to n-4:
      - Skip if `nums[i] == nums[i-1]` (avoid duplicate quadruplets)
      - **Early termination**: If `nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target`, break (smallest possible sum exceeds target)
      - **Skip if too small**: If `nums[i] + nums[n-3] + nums[n-2] + nums[n-1] < target`, continue (largest possible sum is still less than target)

    &nbsp;

    **Step 3: Fix the second element**

    - For each `j` from i+1 to n-3:
      - Skip if `nums[j] == nums[j-1]` and `j > i + 1` (avoid duplicates)
      - Apply similar early termination and skip optimisations

    &nbsp;

    **Step 4: Two-pointer search for remaining pair**

    - Set `left = j + 1`, `right = n - 1`
    - Calculate `total = nums[i] + nums[j] + nums[left] + nums[right]`
    - If `total < target`: move `left` right
    - If `total > target`: move `right` left
    - If `total == target`: found a quadruplet!
      - Add to result, skip duplicates, move both pointers

    &nbsp;

    **Step 5: Return all unique quadruplets**

    Duplicate skipping happens at all four levels: outer loop, second loop, left pointer, and right pointer.

  common_pitfalls:
    - title: Integer Overflow
      description: |
        With constraints `-10^9 <= nums[i] <= 10^9` and `-10^9 <= target <= 10^9`, the sum of four numbers can reach `4 × 10^9`, which **overflows 32-bit integers**.

        In languages like C++ or Java, you must use `long long` or `long` types for the sum calculation. In Python, integers have arbitrary precision, so this isn't an issue — but be aware when porting to other languages.
      wrong_approach: "Using int for sum in C++/Java"
      correct_approach: "Use long/long long or cast during addition"

    - title: Incorrect Duplicate Skipping at Second Level
      description: |
        When skipping duplicates for the second element `j`, you must check `j > i + 1` before comparing `nums[j] == nums[j-1]`.

        Without this check, you might skip the very first valid `j` after `i`, missing valid quadruplets.

        Example: `nums = [0,0,0,0]`, `target = 0` — if you skip when `j == i + 1`, you'd incorrectly skip `j = 1` when comparing to `nums[0]`.
      wrong_approach: "if nums[j] == nums[j-1]: continue (always)"
      correct_approach: "if j > i + 1 and nums[j] == nums[j-1]: continue"

    - title: Missing Early Termination Optimisations
      description: |
        Unlike 3Sum where you can break when `nums[i] > 0` (since target is 0), 4Sum has a variable target. The optimisations become:

        - **Break** if `nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target` — smallest sum exceeds target
        - **Continue** if `nums[i] + nums[n-3] + nums[n-2] + nums[n-1] < target` — largest sum too small

        Without these, you may TLE on edge cases with skewed distributions.
      wrong_approach: "No early termination checks"
      correct_approach: "Check minimum and maximum possible sums at each level"

  key_takeaways:
    - "**Generalise N-sum**: Fix k-2 elements with nested loops, then apply two pointers — this pattern works for any kSum"
    - "**Time complexity is O(n^(k-1))**: For 4Sum, it's O(n³); for kSum in general, O(n^(k-1)) is optimal when there can be that many solutions"
    - "**Early termination matters**: Checking minimum and maximum possible sums can dramatically prune the search space"
    - "**Duplicate handling at every level**: Each nested loop needs its own duplicate skip logic with the correct boundary check"

  time_complexity: "O(n³). Sorting is O(n log n), then two nested O(n) loops each contain an O(n) two-pointer search."
  space_complexity: "O(log n) to O(n). Depends on the sorting algorithm — O(log n) for in-place sorts, O(n) for others. The output is not counted as extra space."

solutions:
  - approach_name: Sort + Two Pointers
    is_optimal: true
    code: |
      def four_sum(nums: list[int], target: int) -> list[list[int]]:
          nums.sort()  # Enable two pointers and duplicate detection
          result = []
          n = len(nums)

          for i in range(n - 3):
              # Skip duplicates for first element
              if i > 0 and nums[i] == nums[i - 1]:
                  continue

              # Early termination: smallest possible sum exceeds target
              if nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target:
                  break

              # Skip: largest possible sum with nums[i] is still too small
              if nums[i] + nums[n - 3] + nums[n - 2] + nums[n - 1] < target:
                  continue

              for j in range(i + 1, n - 2):
                  # Skip duplicates for second element (note: j > i + 1)
                  if j > i + 1 and nums[j] == nums[j - 1]:
                      continue

                  # Early termination for inner loop
                  if nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target:
                      break

                  # Skip if largest sum with nums[i], nums[j] is too small
                  if nums[i] + nums[j] + nums[n - 2] + nums[n - 1] < target:
                      continue

                  # Two pointers for remaining pair
                  left, right = j + 1, n - 1

                  while left < right:
                      total = nums[i] + nums[j] + nums[left] + nums[right]

                      if total < target:
                          left += 1
                      elif total > target:
                          right -= 1
                      else:
                          # Found a quadruplet
                          result.append([nums[i], nums[j], nums[left], nums[right]])

                          # Skip duplicates for left pointer
                          while left < right and nums[left] == nums[left + 1]:
                              left += 1
                          # Skip duplicates for right pointer
                          while left < right and nums[right] == nums[right - 1]:
                              right -= 1

                          # Move both pointers
                          left += 1
                          right -= 1

          return result
    explanation: |
      **Time Complexity:** O(n³) — O(n log n) sort + two nested O(n) loops with O(n) two-pointer search inside.

      **Space Complexity:** O(log n) to O(n) — Sorting space; output not counted.

      We sort the array, then use two nested loops to fix the first two elements. For each pair, two pointers find the remaining pair that completes the target sum. Early termination and skip optimisations prune many unnecessary iterations.

  - approach_name: Brute Force
    is_optimal: false
    code: |
      def four_sum(nums: list[int], target: int) -> list[list[int]]:
          n = len(nums)
          result = set()  # Use set to avoid duplicates

          # Try all possible quadruplets
          for i in range(n):
              for j in range(i + 1, n):
                  for k in range(j + 1, n):
                      for l in range(k + 1, n):
                          if nums[i] + nums[j] + nums[k] + nums[l] == target:
                              # Sort tuple to handle duplicates
                              quad = tuple(sorted([nums[i], nums[j], nums[k], nums[l]]))
                              result.add(quad)

          return [list(q) for q in result]
    explanation: |
      **Time Complexity:** O(n⁴) — Four nested loops checking all combinations.

      **Space Complexity:** O(k) — Where k is the number of unique quadruplets stored in the set.

      This naive approach checks every possible combination of four elements. While correct, it's too slow for larger inputs. With n=200, this means up to 64 million iterations. The optimal solution reduces this to O(n³) by using sorting and two pointers.
