3Sum in Java DSA: From the Triple Loop to the Two-Pointer Sweep
-
Last Updated: July 27, 2026
-
By: javahandson
-
Series
Learn Java in a easy way
Learn 3Sum in Java DSA step by step, from a simple brute-force triple loop to the clean sort-and-two-pointer solution, with dry runs and beginner-friendly explanations.
3Sum in Java is the problem where two-pointer thinking really starts to pay off. You have already met the two-pointer idea in easier problems. Here you get to use it on something with a bit more bite.
The task sounds short. You get an array of numbers. You must find every group of three numbers that adds up to zero.
One catch makes people sweat. You cannot repeat the same triplet twice. So [-1, 0, 1] should show up once, even if the array could form it in more than one way.
We build the answer in three steps, same as always. Brute force checks every possible trio with three loops. A cleaner version sorts first and uses a hash set for the third number. The final one sorts and then walks two pointers inward, which is the answer interviewers want to see.
Every approach comes with a full, step-by-step dry run on the same six numbers. Nothing is skipped, so you can follow exactly what each line does and what changes at every step.
Let us pin down the rules before touching code.
For our array the answer is [[-1, -1, 2], [-1, 0, 1]]. The array has two copies of -1, so a careless solution prints the same triplet twice. Handling that is the real challenge.
We sort the array before we start, so [-1, 0, 1, 2, -1, -4] becomes [-4, -1, -1, 0, 1, 2]. Sorting helps in two ways.
Fix one number, then chase the other two with two pointers. One pointer lower starts just after the fixed number. The other pointer higher starts at the far right end.
Once you fix the first number, you only need two more that sum to its negative. So 3Sum is really “pick one number, then solve 2Sum on the rest.”
| 💡 Interview Insight A common opener is “how would you avoid duplicate triplets?” Say two things: sort first, then skip equal neighbours. Naming both shows you see the real difficulty. |
Try every possible group of three numbers. Keep the ones that sum to zero. It is slow, but it proves you understand the goal.
sort nums
result = empty set of triplets
for a from 0 to n-1: // first number
for b from a+1 to n-1: // second number
for c from b+1 to n-1: // third number
if nums[a] + nums[b] + nums[c] == 0:
add sorted [nums[a], nums[b], nums[c]] to result
return result as a listimport java.util.*;
public class ThreeSumBrute {
public static List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
Set<List<Integer>> found = new HashSet<>();
for (int a = 0; a < nums.length - 2; a++) {
for (int b = a + 1; b < nums.length - 1; b++) {
for (int c = b + 1; c < nums.length; c++) {
if (nums[a] + nums[b] + nums[c] == 0) {
found.add(Arrays.asList(nums[a], nums[b], nums[c]));
}
}
}
}
return new ArrayList<>(found);
}
public static void main(String[] args) {
int[] nums = { -1, 0, 1, 2, -1, -4 };
System.out.println(threeSum(nums)); // [[-1, -1, 2], [-1, 0, 1]]
}
}Sorted array: [-4, -1, -1, 0, 1, 2]. The three loops try every trio in order. We trace all 20 of them, so nothing is hidden. Watch the set on the right.
| Step | a (val) | b (val) | c (val) | sum | Zero? | result set after |
|---|---|---|---|---|---|---|
| 1 | 0 (-4) | 1 (-1) | 2 (-1) | -6 | No | { } |
| 2 | 0 (-4) | 1 (-1) | 3 (0) | -5 | No | { } |
| 3 | 0 (-4) | 1 (-1) | 4 (1) | -4 | No | { } |
| 4 | 0 (-4) | 1 (-1) | 5 (2) | -3 | No | { } |
| 5 | 0 (-4) | 2 (-1) | 3 (0) | -5 | No | { } |
| 6 | 0 (-4) | 2 (-1) | 4 (1) | -4 | No | { } |
| 7 | 0 (-4) | 2 (-1) | 5 (2) | -3 | No | { } |
| 8 | 0 (-4) | 3 (0) | 4 (1) | -3 | No | { } |
| 9 | 0 (-4) | 3 (0) | 5 (2) | -2 | No | { } |
| 10 | 0 (-4) | 4 (1) | 5 (2) | -1 | No | { } |
| 11 | 1 (-1) | 2 (-1) | 3 (0) | -2 | No | { } |
| 12 | 1 (-1) | 2 (-1) | 4 (1) | -1 | No | { } |
| 13 | 1 (-1) | 2 (-1) | 5 (2) | 0 | Yes | { [-1,-1,2] } |
| 14 | 1 (-1) | 3 (0) | 4 (1) | 0 | Yes | { [-1,-1,2], [-1,0,1] } |
| 15 | 1 (-1) | 3 (0) | 5 (2) | 1 | No | { [-1,-1,2], [-1,0,1] } |
| 16 | 1 (-1) | 4 (1) | 5 (2) | 2 | No | { [-1,-1,2], [-1,0,1] } |
| 17 | 2 (-1) | 3 (0) | 4 (1) | 0 | Yes | same, copy ignored |
| 18 | 2 (-1) | 3 (0) | 5 (2) | 1 | No | { [-1,-1,2], [-1,0,1] } |
| 19 | 2 (-1) | 4 (1) | 5 (2) | 2 | No | { [-1,-1,2], [-1,0,1] } |
| 20 | 3 (0) | 4 (1) | 5 (2) | 3 | No | { [-1,-1,2], [-1,0,1] } |
Let us walk the whole trace, group by group, and see what the loops are doing.
Steps 1 to 10: a is fixed at -4.
The outer loop parks a on the -4 at index 0. Now b and c sweep the rest of the array looking for two numbers that add up to 4, because -4 needs +4 to reach zero.
The two largest numbers left are 1 and 2, which only add to 3, and 3 is less than 4. So -4 can never find its partners, and the set stays empty.
Steps 11 to 16: a is fixed at the first -1 (index 1).
Now a is -1, so b and c need to add up to 1. This is where the answers live.
Notice the pattern: once the sum passes zero, larger c values only make it bigger. The brute force cannot use that hint, so it checks them anyway.
Steps 17 to 20: a is fixed at the second -1 (index 2), then 0.
This group holds the one moment that matters most for correctness.
Step 17 is the whole reason we sort and use a set. Both copies of -1 can build [-1, 0, 1], so without the set we would print it twice. Twenty trios were tested in all, yet only two unique triplets survived.
Twenty trios for six numbers is fine. For a few thousand it crawls, which is why we improve it next.
Drop the third loop. Fix the first number, walk the rest, and let a hash set remember what we have already seen. For each value we ask the set whether the partner we need has already gone by.
sort nums
result = empty set of triplets
for i from 0 to n-3:
seen = empty hash set
for j from i+1 to n-1:
need = -(nums[i] + nums[j]) // partner we want
if need is in seen:
add sorted [nums[i], need, nums[j]] to result
add nums[j] to seen
return result as a listimport java.util.*;
public class ThreeSumHashSet {
public static List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
Set<List<Integer>> result = new HashSet<>();
for (int i = 0; i < nums.length - 2; i++) {
Set<Integer> seen = new HashSet<>();
for (int j = i + 1; j < nums.length; j++) {
int need = -(nums[i] + nums[j]);
if (seen.contains(need)) {
result.add(Arrays.asList(nums[i], need, nums[j]));
}
seen.add(nums[j]);
}
}
return new ArrayList<>(result);
}
public static void main(String[] args) {
int[] nums = { -1, 0, 1, 2, -1, -4 };
System.out.println(threeSum(nums)); // [[-1, -1, 2], [-1, 0, 1]]
}
}Sorted array: [-4, -1, -1, 0, 1, 2]. We trace every i pass and every j step, so nothing is skipped. seen resets to empty at the top of each pass.
Pass i = 0, nums[i] = -4, seen = { }:
| j (val) | need | seen before | In seen? | Action | seen after |
|---|---|---|---|---|---|
| 1 (-1) | 5 | { } | No | nothing | { -1 } |
| 2 (-1) | 5 | { -1 } | No | nothing | { -1 } |
| 3 (0) | 4 | { -1 } | No | nothing | { -1, 0 } |
| 4 (1) | 3 | { -1, 0 } | No | nothing | { -1, 0, 1 } |
| 5 (2) | 2 | { -1, 0, 1 } | No | nothing | { -1, 0, 1, 2 } |
Nothing matched with -4 fixed, because no needed partner was ever in the notebook first.
Pass i = 1, nums[i] = -1, seen = { }:
| j (val) | need | seen before | In seen? | Action | seen after |
|---|---|---|---|---|---|
| 2 (-1) | 2 | { } | No | nothing | { -1 } |
| 3 (0) | 1 | { -1 } | No | nothing | { -1, 0 } |
| 4 (1) | 0 | { -1, 0 } | Yes (0 seen) | save [-1, 0, 1] | { -1, 0, 1 } |
| 5 (2) | -1 | { -1, 0, 1 } | Yes (-1 seen) | save [-1, -1, 2] | { -1, 0, 1, 2 } |
This pass finds both triplets. At j=4 we needed 0, which was already noted, so [-1, 0, 1]. At j=5 we needed -1, also noted, so [-1, -1, 2].
Pass i = 2, nums[i] = -1, seen = { }:
| j (val) | need | seen before | In seen? | Action | seen after |
|---|---|---|---|---|---|
| 3 (0) | 1 | { } | No | nothing | { 0 } |
| 4 (1) | 0 | { 0 } | Yes (0 seen) | match [-1, 0, 1], already have it | { 0, 1 } |
| 5 (2) | -1 | { 0, 1 } | No | nothing | { 0, 1, 2 } |
At j=4 the trio [-1, 0, 1] forms again, this time from the second -1. The outer result set already holds it, so it is ignored. This is the duplicate guard doing its job.
Pass i = 3, nums[i] = 0, seen = { }:
| j (val) | need | seen before | In seen? | Action | seen after |
|---|---|---|---|---|---|
| 4 (1) | -1 | { } | No | nothing | { 1 } |
| 5 (2) | -2 | { 1 } | No | nothing | { 1, 2 } |
No partner is ever found here, so this pass adds nothing. Passes for i = 4 and beyond have too few numbers left to form a triplet.
Let us go pass by pass and watch how the notebook decides each match.
Pass i=0, first number -4.
With -4 fixed, every partner we need is a large positive number. At j=1 we need 5, at j=3 we need 4, and so on. But the notebook only ever fills with -1, 0, 1, 2, so the number we need is never in it.
Pass i=1, first number -1.
Now seen starts empty again, and we hunt for two numbers that add up to 1.
Both triplets fall out of this single pass. Each match happened because the partner had already been written down on an earlier step of the same pass.
Pass i=2, first number -1 again.
This is the duplicate pass, and it shows why we keep an outer result set.
So this pass rediscovers a triplet we already had. The result set is what stops it from appearing twice.
Pass i=3, first number 0.
Nothing matches, and passes for later i values have fewer than two numbers left, so they add nothing either.
One timing rule ties it all together. We write nums[j] into the notebook only after we check it. That is why at j=4 the 0 recorded back at j=3 is ready and waiting, and why a number can never wrongly match itself.
| 💡 Interview Insight Asked why seen is emptied for every new i, say this: it stops numbers from an earlier first-element pass leaking in and forming false triplets. |
A big jump over brute force. But it still carries a notebook each pass, and interviewers usually want the leaner version next.
On a sorted array we do not need a notebook. Fix the first number, then send two pointers inward from both ends. The sort tells the pointers which way to move. This is the answer interviewers hope to see.
sort nums
result = empty list
for i from 0 to n-3:
if i > 0 and nums[i] == nums[i-1]:
continue // skip duplicate first number
lower = i + 1
higher = n - 1
while lower < higher:
sum = nums[i] + nums[lower] + nums[higher]
if sum < 0: lower = lower + 1
else if sum > 0: higher = higher - 1
else:
add [nums[i], nums[lower], nums[higher]] to result
skip lower right past equal values
skip higher left past equal values
lower = lower + 1
higher = higher - 1
return resultimport java.util.*;
public class ThreeSumTwoPointer {
public static List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> result = new ArrayList<>();
for (int i = 0; i < nums.length - 2; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue; // skip duplicate first number
}
int lower = i + 1, higher = nums.length - 1;
while (lower < higher) {
int sum = nums[i] + nums[lower] + nums[higher];
if (sum < 0) {
lower++;
} else if (sum > 0) {
higher--;
} else {
result.add(Arrays.asList(nums[i], nums[lower], nums[higher]));
while (lower < higher && nums[lower] == nums[lower + 1]) lower++;
while (lower < higher && nums[higher] == nums[higher - 1]) higher--;
lower++;
higher--;
}
}
}
return result;
}
public static void main(String[] args) {
int[] nums = { -1, 0, 1, 2, -1, -4 };
System.out.println(threeSum(nums)); // [[-1, -1, 2], [-1, 0, 1]]
}
}Sorted array: [-4, -1, -1, 0, 1, 2]. We trace every i pass and every while step, including the passes that find nothing and the one we skip.
Pass i = 0, nums[i] = -4, lower = 1, higher = 5:
| lower (val) | higher (val) | sum | vs 0 | Action |
|---|---|---|---|---|
| 1 (-1) | 5 (2) | -3 | less | move lower right |
| 2 (-1) | 5 (2) | -3 | less | move lower right |
| 3 (0) | 5 (2) | -2 | less | move lower right |
| 4 (1) | 5 (2) | -1 | less | move lower right |
| lower=5, higher=5 | — | — | — | lower not < higher, pass ends |
The sum stayed negative all the way, so lower kept sliding right until it met higher. No triplet with -4.
Pass i = 1, nums[i] = -1, lower = 2, higher = 5:
| lower (val) | higher (val) | sum | vs 0 | Action |
|---|---|---|---|---|
| 2 (-1) | 5 (2) | 0 | equal | save [-1, -1, 2], move both in → lower=3, higher=4 |
| 3 (0) | 4 (1) | 0 | equal | save [-1, 0, 1], move both in → lower=4, higher=3 |
| lower=4, higher=3 | — | — | — | lower not < higher, pass ends |
Both triplets come from this single pass. Each time the sum hit 0 we saved the trio and closed both pointers inward, until they crossed.
Pass i = 2, nums[i] = -1:
nums[2] (-1) equals nums[1] (-1), so the continue skips this whole pass. Without this skip we would rebuild [-1, -1, 2] and [-1, 0, 1] all over again.
Pass i = 3, nums[i] = 0, lower = 4, higher = 5:
| lower (val) | higher (val) | sum | vs 0 | Action |
|---|---|---|---|---|
| 4 (1) | 5 (2) | 3 | more | move higher left |
| lower=4, higher=4 | — | — | — | lower not < higher, pass ends |
The sum was too big, so higher moved left and the pointers met. Nothing found. Later i values have fewer than two numbers left, so the loops stop.
Legend: lower is the left pointer moving right, higher is the right pointer moving left, and sum adds the fixed nums[i] to both pointed values.
Let us go pass by pass and watch the two pointers steer.
Pass i=0, first number -4.
lower starts at index 1 and higher at index 5. We need the two pointed numbers to add up to 4.
The sum never reached zero because -4 is too negative for the numbers on its right. Moving lower right was the correct choice every time, since the sorted array grows in that direction.
Pass i=1, first number -1.
lower starts at index 2 and higher at index 5. Now the two pointers only need to add up to 1, and this pass finds both answers.
Each time the sum hit zero, we grabbed the triplet and closed in from both sides at once. There were no equal neighbours to skip here, so the plain inward step was enough.
Pass i=2, first number -1 again.
Before setting up any pointers, the code checks nums[2] against nums[1]. Both are -1, so the continue skips this entire pass.
This single skip is what keeps the plain list free of duplicates, so we never need a set here.
Pass i=3, first number 0.
lower starts at index 4 and higher at index 5.
This time the sum was too large, so we moved higher instead of lower. Nothing matched. Later i values have fewer than two numbers left, so the outer loop stops.
Across all four passes, every step did exactly one of three things: found a match and closed in, nudged lower right for a bigger sum, or nudged higher left for a smaller sum. The pointers only ever move toward each other, so no trio is checked twice and no extra memory is needed.
| 💡 Interview Insight Interviewers love the follow-up “why sort at all?” The clean answer: sorting gives the pointers a reliable direction and turns duplicate skipping into a one-line neighbour check. |
| Approach | How it searches | Extra memory | Speed feel |
|---|---|---|---|
| Brute force | Three loops, every trio | A set of triplets | Slow on big arrays |
| Sort + hashset | Two loops, notebook lookup | A seen set per pass | Much faster |
| Sort + two pointers | One loop, pointers close in | Just two indices | Fast and lean |
Tables are exact, but a sketch often lands faster. Here is the same two-pointer trace drawn by hand.

All three give the same answer. They just pay different prices.
| Approach | Time | Space | Note |
|---|---|---|---|
| Brute force | O(n³) | O(n) | Simple, but three loops make it slow |
| Sort + hashset | O(n²) | O(n) | Fast, needs a seen set each pass |
| Sort + two pointers | O(n²) | O(1) extra | Fast, lean, the expected answer |
The two faster versions share the same time class. What separates them is memory. A two-pointer sweep carries almost nothing, and its duplicate handling is two small skips.
In an interview, start with brute force, point out the wasted third loop, then tighten it into the two-pointer sweep and explain the two duplicate guards. That climb is the story interviewers want to hear.
| 💡 Interview Insight If pushed on the sort cost, note that sorting is O(n log n), which sits comfortably under the O(n²) main work. So sorting does not change the overall time class, and it buys you clean pointers and easy duplicate skips. |
A few small traps catch beginners on 3Sum. Keep them in mind.
Run those last two cases through your code before you call it done. They catch more bugs than any ordinary input will.
A: Sorting does two jobs. It puts equal numbers next to each other so duplicate triplets are easy to skip with a neighbour check, and it gives the two pointers a reliable direction, since values to the right are larger and values to the left are smaller.
A: After sorting, skip a first number that equals the previous one, and after saving a match, slide each pointer past any equal neighbours before stepping inward. These two guards keep every triplet unique without needing a hash set.
A: The brute-force triple loop runs in O(n cubed). Both the hash-set version and the two-pointer version run in O(n squared), with sorting adding an O(n log n) cost that sits comfortably under the main work.
A: Once you fix the first number, you only need two other numbers that add up to the negative of that fixed value. That smaller hunt is exactly a two-sum, which the two pointers or a hash set can solve quickly.
3Sum in Java looks scary at first, with its triplets and its no-repeat rule. But once you sort the array, most of the fear melts away.
Our six-number trace showed the payoff clearly. Brute force ground through every trio. The two-pointer sweep fixed one number and let two pointers close in, finding both triplets in a couple of clean steps.
So take the pattern, not just the answer. Sort when order helps you. Fix one number to shrink a hard problem into an easier one. Then let two pointers do the walking.
That habit turns a slow cube of work into something fast and tidy. It will do the same for many array problems waiting further down the list.