← Back to DevBytes

Solving 3Sum Problem in JavaScript: Step-by-Step Guide

Introduction to the 3Sum Problem

The 3Sum problem is one of the most classic algorithmic challenges you'll encounter in coding interviews and competitive programming. At its core, it asks you to find all unique triplets in an array that sum up to a target value (usually zero). While the problem statement sounds deceptively simple, crafting an efficient solution requires a solid understanding of sorting, two-pointer techniques, and careful handling of duplicates.

In this tutorial, we'll walk through everything you need to know about solving the 3Sum problem in JavaScript, from the brute-force approach to an optimized O(n²) solution. By the end, you'll have a production-ready implementation and a deep understanding of the underlying techniques.

What Is the 3Sum Problem?

Given an integer array nums, the goal is to return all unique triplets [nums[i], nums[j], nums[k]] such that i != j, i != k, and j != k, and nums[i] + nums[j] + nums[k] === 0. The solution set must not contain duplicate triplets.

For example, given the input [-1, 0, 1, 2, -1, -4], the expected output is [[-1, -1, 2], [-1, 0, 1]]. Notice that even though -1 appears twice in the array, the triplet [-1, 0, 1] only appears once in the result.

Why the 3Sum Problem Matters

The 3Sum problem is more than just an interview favorite — it teaches fundamental concepts that appear across many algorithmic challenges:

Mastering this problem gives you a reusable toolkit for a whole family of related challenges.

The Brute-Force Approach

Before optimizing, let's understand the naive solution. We can use three nested loops to check every possible triplet:

function threeSumBruteForce(nums) {
  const result = [];
  const seen = new Set();

  for (let i = 0; i < nums.length - 2; i++) {
    for (let j = i + 1; j < nums.length - 1; j++) {
      for (let k = j + 1; k < nums.length; k++) {
        if (nums[i] + nums[j] + nums[k] === 0) {
          const triplet = [nums[i], nums[j], nums[k]].sort((a, b) => a - b);
          const key = triplet.join(',');
          if (!seen.has(key)) {
            seen.add(key);
            result.push(triplet);
          }
        }
      }
    }
  }

  return result;
}

console.log(threeSumBruteForce([-1, 0, 1, 2, -1, -4]));
// Output: [[-1, -1, 2], [-1, 0, 1]]

While this works, it runs in O(n³) time, which becomes unusably slow for arrays larger than a few hundred elements. The sorting and Set operations add additional overhead. We can do much better.

The Optimized Two-Pointer Solution

The key insight is that if we sort the array first, we can use the two-pointer technique to find pairs that sum to a target value in linear time. Since we need triplets that sum to zero, for each element nums[i], we look for two other elements that sum to -nums[i].

Step-by-Step Algorithm

The Complete Implementation

function threeSum(nums) {
  const result = [];

  // Step 1: Sort the array
  nums.sort((a, b) => a - b);

  for (let i = 0; i < nums.length - 2; i++) {
    // Skip duplicate values for the first element
    if (i > 0 && nums[i] === nums[i - 1]) {
      continue;
    }

    // Early exit: if the smallest value is positive, no triplet can sum to zero
    if (nums[i] > 0) {
      break;
    }

    let left = i + 1;
    let right = nums.length - 1;

    while (left < right) {
      const sum = nums[i] + nums[left] + nums[right];

      if (sum === 0) {
        result.push([nums[i], nums[left], nums[right]]);

        // Skip duplicates for the second element
        while (left < right && nums[left] === nums[left + 1]) {
          left++;
        }
        // Skip duplicates for the third element
        while (left < right && nums[right] === nums[right - 1]) {
          right--;
        }

        left++;
        right--;
      } else if (sum < 0) {
        left++;
      } else {
        right--;
      }
    }
  }

  return result;
}

console.log(threeSum([-1, 0, 1, 2, -1, -4]));
// Output: [[-1, -1, 2], [-1, 0, 1]]

console.log(threeSum([0, 1, 1]));
// Output: []

console.log(threeSum([0, 0, 0]));
// Output: [[0, 0, 0]]

How the Two-Pointer Technique Works

Understanding why the two-pointer approach works is crucial. After sorting, the array is in ascending order. For a fixed nums[i], we need to find two numbers that sum to -nums[i]. Starting with the leftmost and rightmost available elements:

This reduces the inner search from O(n²) to O(n), bringing the total complexity down to O(n²).

Handling Duplicates Correctly

Duplicate handling is where most implementations go wrong. There are three places where duplicates must be addressed:

1. The Outer Loop (First Element)

After sorting, identical values are adjacent. If nums[i] equals nums[i - 1], we've already processed this value, so we skip it. This prevents generating the same triplet multiple times.

if (i > 0 && nums[i] === nums[i - 1]) {
  continue;
}

2. The Second Element After a Match

Once we find a valid triplet, we skip any consecutive duplicate values at the left pointer before moving forward.

while (left < right && nums[left] === nums[left + 1]) {
  left++;
}

3. The Third Element After a Match

Similarly, we skip duplicate values at the right pointer before moving backward.

while (left < right && nums[right] === nums[right - 1]) {
  right--;
}

After these skip loops, we still need to move both pointers inward once more to land on fresh values.

Complexity Analysis

Let's break down the time and space complexity of the optimized solution:

Compared to the brute-force O(n³) approach, this is a massive improvement. For an array of 1,000 elements, the optimized solution performs roughly 1,000,000 operations versus 1,000,000,000 for brute force.

Best Practices

Validate Input Early

Always check for edge cases before running the main algorithm. Arrays with fewer than three elements cannot produce any triplets.

function threeSum(nums) {
  if (nums.length < 3) {
    return [];
  }

  // ... rest of the implementation
}

Use Early Termination

Once the smallest value in the sorted array is greater than zero, no triplet can possibly sum to zero. Breaking early saves unnecessary iterations.

if (nums[i] > 0) {
  break;
}

Avoid Mutating the Input

If the caller expects the original array to remain unchanged, create a copy before sorting.

function threeSum(nums) {
  const sorted = [...nums].sort((a, b) => a - b);
  // Use 'sorted' instead of 'nums' throughout
}

Write Test Cases

Test your implementation against a variety of inputs, including edge cases:

const testCases = [
  { input: [-1, 0, 1, 2, -1, -4], expected: [[-1, -1, 2], [-1, 0, 1]] },
  { input: [], expected: [] },
  { input: [0], expected: [] },
  { input: [0, 0, 0], expected: [[0, 0, 0]] },
  { input: [0, 0, 0, 0], expected: [[0, 0, 0]] },
  { input: [-2, 0, 1, 1, 2], expected: [[-2, 0, 2], [-2, 1, 1]] },
  { input: [-4, -2, -2, -2, 0, 1, 2, 2, 2, 3, 3, 4, 4, 6, 6],
    expected: [[-4, -2, 6], [-4, 0, 4], [-4, 1, 3], [-4, 2, 2], [-2, -2, 4], [-2, 0, 2]] }
];

testCases.forEach(({ input, expected }) => {
  const result = threeSum(input);
  const passed = JSON.stringify(result) === JSON.stringify(expected);
  console.log(passed ? 'PASS' : 'FAIL', input);
});

Common Pitfalls to Avoid

Extending to Variants

Once you understand the 3Sum solution, several variants become straightforward:

3Sum Closest

Instead of finding triplets that sum to exactly zero, find the triplet whose sum is closest to a given target. Track the closest sum encountered during the two-pointer scan.

function threeSumClosest(nums, target) {
  nums.sort((a, b) => a - b);
  let closest = nums[0] + nums[1] + nums[2];

  for (let i = 0; i < nums.length - 2; i++) {
    let left = i + 1;
    let right = nums.length - 1;

    while (left < right) {
      const sum = nums[i] + nums[left] + nums[right];

      if (Math.abs(sum - target) < Math.abs(closest - target)) {
        closest = sum;
      }

      if (sum < target) {
        left++;
      } else if (sum > target) {
        right--;
      } else {
        return sum;
      }
    }
  }

  return closest;
}

console.log(threeSumClosest([-1, 2, 1, -4], 1)); // Output: 2

4Sum

For 4Sum, add another outer loop and reuse the two-pointer technique for the innermost pair. This generalizes to kSum using recursion.

function fourSum(nums, target) {
  const result = [];
  nums.sort((a, b) => a - b);

  for (let i = 0; i < nums.length - 3; i++) {
    if (i > 0 && nums[i] === nums[i - 1]) continue;

    for (let j = i + 1; j < nums.length - 2; j++) {
      if (j > i + 1 && nums[j] === nums[j - 1]) continue;

      let left = j + 1;
      let right = nums.length - 1;

      while (left < right) {
        const sum = nums[i] + nums[j] + nums[left] + nums[right];

        if (sum === target) {
          result.push([nums[i], nums[j], nums[left], nums[right]]);

          while (left < right && nums[left] === nums[left + 1]) left++;
          while (left < right && nums[right] === nums[right - 1]) right--;

          left++;
          right--;
        } else if (sum < target) {
          left++;
        } else {
          right--;
        }
      }
    }
  }

  return result;
}

console.log(fourSum([1, 0, -1, 0, -2, 2], 0));
// Output: [[-2, -1, 1, 2], [-2, 0, 0, 2], [-1, 0, 0, 1]]

Conclusion

The 3Sum problem is a perfect showcase of how sorting combined with the two-pointer technique can transform an O(n³) brute-force solution into an elegant O(n²) algorithm. By carefully handling duplicates, adding early termination checks, and validating edge cases, you can build a robust solution that handles any input gracefully. The patterns you learn here — sorted-array traversal, pointer manipulation, and duplicate skipping — extend naturally to a whole family of kSum problems and beyond. Practice this solution until the logic feels intuitive, and you'll be well-equipped to tackle a wide range of algorithmic challenges in your coding interviews and real-world projects.

— Ad —

Google AdSense will appear here after approval

← Back to all articles