Searching & Sorting · 11 of 36
Find four elements that sum to a given value
Find every different group of four numbers in a list that adds up to a target.
- Simple way
- time O(n^4)
room O(1) - Clever way
- time O(n^3)
room O(1)
The problem, simply
You get an array of numbers and a target. Find every group of four numbers, taken from four different positions, that adds up to the target.
Two groups with the same four values count as one, even if they use different positions. Return each group with its numbers in order from small to big. Extra room for the answer itself does not count.
A story to remember it
Picture it
Sorted numbers with the four fingers i, j, l and r. Here -2 + -1 + 1 + 2 = 0, so that group is saved.
- looking now
- answer
Examples
A normal case
- In
arr = [1, 0, -1, 0, -2, 2], target = 0- Out
[[-2, -1, 1, 2], [-2, 0, 0, 2], [-1, 0, 0, 1]]
The two 0s may both be used in one group because they sit at different positions.
Many answers
- In
arr = [10, 2, 3, 4, 5, 9, 7, 8], target = 23- Out
[[2, 3, 8, 10], [2, 4, 7, 10], [2, 4, 8, 9], [2, 5, 7, 9], [3, 4, 7, 9], [3, 5, 7, 8]]
Six different groups.
All the same
- In
arr = [2, 2, 2, 2, 2], target = 8- Out
[[2, 2, 2, 2]]
There are five ways to choose four of the 2s, but they are all the same group.
Overflow trap
- In
arr = [1000000000, 1000000000, 1000000000, 1000000000], target = -294967296- Out
[]
The real sum is 4 billion. In int it wraps around to exactly -294967296 and looks like a match. Add in long.
Too few numbers
- In
arr = [1, 2, 3], target = 6- Out
[]
You need four positions.
Way 1: the simple way
Try every group of four positions. Sort each matching group and drop it into a set, so copies disappear.
import java.util.*;
class Solution {
public List<List<Integer>> fourSum(int[] arr, int target) {
Set<List<Integer>> groups = new TreeSet<>((x, y) -> {
for (int k = 0; k < 4; k++) {
int c = Integer.compare(x.get(k), y.get(k));
if (c != 0) return c;
}
return 0;
});
int n = arr.length;
for (int a = 0; a < n; a++)
for (int b = a + 1; b < n; b++)
for (int c = b + 1; c < n; c++)
for (int d = c + 1; d < n; d++) {
long sum = (long) arr[a] + arr[b] + arr[c] + arr[d];
if (sum == target) {
List<Integer> g = new ArrayList<>(List.of(arr[a], arr[b], arr[c], arr[d]));
Collections.sort(g); // same values, same order
groups.add(g); // the set drops copies
}
}
return new ArrayList<>(groups);
}
}How long it takes: about n^4 / 24 groups, O(n^4). Extra room: O(1) besides the answer set.
Way 2: the clever way
Sort the array. Fix the first two numbers with two loops, then find the last two with two fingers that move toward each other, like two sum on a sorted list.
- Sort the array.
- Loop
iover the first number. Skip it if it equals the number before it. - Loop
jover the second number, afteri. Skip it if it equals the number before it (but not the one right afteri). - Put
l = j + 1andr = n - 1. Add the four numbers. - Equal to the target: save the group, then move
landrpast any copies of their numbers. - Too small: move
lright. Too big: moverleft.
Fix two numbers, squeeze the other two, skip copies
01 / 15
Sort first: [-2, -1, 0, 0, 1, 2]. Equal numbers now sit together, which makes skipping repeats easy.
- target
- 0
- looking now
- checking
- answer
- just added
- ignored
import java.util.*;
class Solution {
public List<List<Integer>> fourSum(int[] arr, int target) {
List<List<Integer>> out = new ArrayList<>();
int n = arr.length;
Arrays.sort(arr);
for (int i = 0; i < n - 3; i++) {
if (i > 0 && arr[i] == arr[i - 1]) continue; // same first number
for (int j = i + 1; j < n - 2; j++) {
if (j > i + 1 && arr[j] == arr[j - 1]) continue; // same second number
int l = j + 1, r = n - 1;
while (l < r) {
long sum = (long) arr[i] + arr[j] + arr[l] + arr[r];
if (sum == target) {
out.add(List.of(arr[i], arr[j], arr[l], arr[r]));
while (l < r && arr[l] == arr[l + 1]) l++; // skip copies
while (l < r && arr[r] == arr[r - 1]) r--;
l++;
r--;
} else if (sum < target) {
l++; // need more
} else {
r--; // need less
}
}
}
}
return out;
}
}How long it takes: sorting is O(n log n). Two loops times one finger walk is O(n^3). Extra room: O(1) besides the answer.
Watch out for
- Checking copies of
jwithj > 0instead ofj > i + 1. For[0, 0, 0, 0]and target 0, the firstjequalsarr[i]and gets skipped, so the only group[0, 0, 0, 0]is lost. - After a match, moving only one finger. With sorted values, if
lstays, the only partner that works is a copy ofarr[r], which gives the same group again. - Forgetting to sort each group in Way 1. Then
[0, -1, 1, 0]and[-1, 0, 0, 1]look different to the set.
What interviewers ask next
- Can you beat
O(n^3)? Store every pair sum in a hash map, then look uptarget - (pair sum)for each pair. That isO(n^2)on average for "does any group exist", but listing all distinct groups can still cost more, and keeping the four positions different is fiddly. - Generalize to k-sum. Write
kSum(start, k, target): fork == 2use two fingers, otherwise loop the first number and callkSum(i + 1, k - 1, target - arr[i]). TimeO(n^(k-1)). - Speed it up in practice: break out of the
iloop when the four smallest numbers fromialready pass the target, and skipiwhen the biggest numbers cannot reach it. - Count groups instead of listing them, with repeats allowed? Then pair sums in a hash map with counts work well.
Remember this
Grown-up words: the fingers are two pointers, the four numbers are a quadruplet, skipping equal neighbours is deduplication, and the recursive version is the general k-sum reduction.