Files
codetutor/backend/data/questions/three-sum.yaml

188 lines
7.8 KiB
YAML

title: 3Sum
slug: three-sum
difficulty: medium
leetcode_id: 15
leetcode_url: https://leetcode.com/problems/3sum/
categories:
- arrays
- two-pointers
- sorting
patterns:
- two-pointers
function_signature: "def three_sum(nums: list[int]) -> list[list[int]]:"
test_cases:
visible:
- input: { nums: [-1, 0, 1, 2, -1, -4] }
expected: [[-1, -1, 2], [-1, 0, 1]]
- input: { nums: [0, 1, 1] }
expected: []
- input: { nums: [0, 0, 0] }
expected: [[0, 0, 0]]
hidden:
- input: { nums: [-2, 0, 1, 1, 2] }
expected: [[-2, 0, 2], [-2, 1, 1]]
- input: { nums: [1, 2, -2, -1] }
expected: []
- input: { nums: [-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]]
description: |
Given an integer array `nums`, return all the triplets `[nums[i], nums[j], nums[k]]` such that `i != j`, `i != k`, and `j != k`, and `nums[i] + nums[j] + nums[k] == 0`.
Notice that the solution set must **not contain duplicate triplets**.
constraints: |
- `3 <= nums.length <= 3000`
- `-10^5 <= nums[i] <= 10^5`
examples:
- input: "nums = [-1,0,1,2,-1,-4]"
output: "[[-1,-1,2],[-1,0,1]]"
explanation: "The distinct triplets that sum to zero are [-1,-1,2] and [-1,0,1]."
- input: "nums = [0,1,1]"
output: "[]"
explanation: "No triplet sums to zero."
- input: "nums = [0,0,0]"
output: "[[0,0,0]]"
explanation: "The only triplet [0,0,0] sums to zero."
explanation:
intuition: |
Finding three numbers that sum to zero seems complex, but we can reduce it to a simpler problem we already know how to solve.
Think of it like this: if we **fix** one number (call it `a`), then we need to find two numbers that sum to `-a`. This is exactly the Two Sum problem! But instead of using a hash map (which makes duplicate handling tricky), we can use two pointers on a **sorted** array.
Sorting gives us two superpowers:
1. **Two pointers work**: With a sorted array, if our sum is too small, move left pointer right; if too big, move right pointer left
2. **Easy duplicate skipping**: Adjacent duplicates become neighbours, so `if nums[i] == nums[i-1]: skip`
The algorithm: for each element `nums[i]`, use two pointers on the remaining array to find pairs summing to `-nums[i]`.
approach: |
We solve this using **Sort + Two Pointers**:
**Step 1: Sort the array**
- Sorting enables two-pointer technique and easy duplicate detection
- Time: O(n log n), which doesn't affect overall O(n²) complexity
&nbsp;
**Step 2: Fix the first element and find pairs**
- For each `i` from 0 to n-3:
- Skip if `nums[i] == nums[i-1]` (avoid duplicate triplets)
- **Early termination**: If `nums[i] > 0`, stop — no triplet can sum to zero (all remaining elements are positive)
- Set `left = i + 1`, `right = n - 1`
- Find pairs using two pointers
&nbsp;
**Step 3: Two-pointer search for each fixed element**
- Calculate `total = nums[i] + nums[left] + nums[right]`
- If `total < 0`: we need larger values, move `left` right
- If `total > 0`: we need smaller values, move `right` left
- If `total == 0`: found a triplet!
- Add `[nums[i], nums[left], nums[right]]` to result
- Skip duplicates for both pointers: `while nums[left] == nums[left+1]: left++`
- Move both pointers inward
&nbsp;
**Step 4: Return all unique triplets**
Duplicate skipping happens at three levels: the outer loop, left pointer, and right pointer.
common_pitfalls:
- title: Not Handling Duplicates Properly
description: |
Without careful duplicate skipping, you'll return duplicate triplets like `[-1,-1,2]` multiple times.
Duplicates must be handled at **all three levels**:
1. Outer loop: `if i > 0 and nums[i] == nums[i-1]: continue`
2. Left pointer: `while left < right and nums[left] == nums[left+1]: left += 1`
3. Right pointer: `while left < right and nums[right] == nums[right-1]: right -= 1`
wrong_approach: "Using a set of tuples (works but slower)"
correct_approach: "Skip adjacent duplicates at each level"
- title: Duplicate Skipping Order After Finding Triplet
description: |
After finding a valid triplet, skip duplicates **before** moving both pointers. A common bug is skipping duplicates incorrectly, leading to missing triplets or infinite loops.
The sequence should be: (1) add triplet, (2) skip left duplicates, (3) skip right duplicates, (4) move both `left++` and `right--`.
wrong_approach: "Moving pointers before skipping duplicates"
correct_approach: "Skip duplicates first, then move both pointers"
- title: Missing Early Termination
description: |
Once `nums[i] > 0` in a sorted array, no valid triplet can exist (all remaining elements are non-negative, so the smallest possible sum is positive).
This optimisation can significantly speed up cases with many positive numbers.
wrong_approach: "Continuing to search when nums[i] > 0"
correct_approach: "if nums[i] > 0: break"
key_takeaways:
- "**Reduce N-sum to (N-1)-sum**: Fix one element and solve a smaller problem — this pattern extends to 4Sum, kSum"
- "**Sorting enables two pointers**: Transforms O(n²) lookup per element into O(n)"
- "**Multi-level duplicate handling**: When returning all unique solutions, handle duplicates at every decision point"
- "**Time complexity is O(n²)**: Can't do better when returning all triplets (there can be O(n²) triplets)"
time_complexity: "O(n²). Sorting is O(n log n), then for each of n elements, the two-pointer search is O(n)."
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 three_sum(nums: list[int]) -> list[list[int]]:
nums.sort() # Enable two pointers and duplicate detection
result = []
n = len(nums)
for i in range(n - 2):
# Skip duplicates for the first element
if i > 0 and nums[i] == nums[i - 1]:
continue
# Early termination: if smallest element is positive, no solution
if nums[i] > 0:
break
# Two pointers for the remaining array
left, right = i + 1, n - 1
while left < right:
total = nums[i] + nums[left] + nums[right]
if total < 0:
# Need larger sum, move left pointer
left += 1
elif total > 0:
# Need smaller sum, move right pointer
right -= 1
else:
# Found a triplet
result.append([nums[i], 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 for next pair
left += 1
right -= 1
return result
explanation: |
**Time Complexity:** O(n²) — O(n log n) sort + O(n) two-pointer search for each of O(n) elements.
**Space Complexity:** O(log n) to O(n) — Sorting space; output not counted.
We sort the array, then for each element, use two pointers to find pairs that complete the triplet. Careful duplicate skipping at all three levels ensures we return only unique triplets.