3Sum in Java DSA: From the Triple Loop to the Two-Pointer Sweep
-
Last Updated: July 26, 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 dry run. We trace the same six numbers through the best version, so you can watch the pointers close in and the triplets fall out.
Let us pin down the rules before touching code. A clear picture now saves you from messy bugs later.
For our array the answer is [[-1, -1, 2], [-1, 0, 1]]. Two trios, both summing to zero, and neither one repeated.
Notice the trap early. The array has two copies of -1, so a lazy solution could easily print the same triplet twice. Handling those duplicates cleanly is half the challenge.
The math here is easy. Pick three numbers, check if they sum to zero. A child could do it by hand for six numbers.
The pain is the no-repeat rule. Our array holds -1 twice, so the same-looking triplet can form from different slots.
Every approach below has to answer one question: how do I avoid printing a triplet I already have? That single worry shapes all three solutions.
Our six numbers were picked to be a little annoying, on purpose. That keeps the trace honest.
A tidy input like [-1, 0, 1] would hide all of this. A messy one shows you what the pointers really do.
| 💡 Interview Insight A very common opener is “how would you avoid duplicate triplets?” Mention two things: sort the array first, then skip over equal neighbours for each pointer. Naming both up front shows you already see the real difficulty. |
Three small ideas carry this whole problem. Each one shows up again in later array questions, so it is worth learning them well.
Sorting means we arrange the numbers from smallest to largest before we start. Our array becomes [-4, -1, -1, 0, 1, 2].
Why bother? Two big reasons, and both make the rest of the code easier.
Sorting costs a bit of time up front. That cost is small next to the mess it saves us later.
This is the trick that makes 3Sum click. We fix one number, then chase the other two with two pointers.
One pointer, called lo, starts just after the fixed number. The other, called hi, starts at the far right end.
Think of two people walking toward each other across a sorted shelf. One nudges right for more, the other nudges left for less, until they meet in the middle.
Here is the mental shortcut. Once you fix the first number, the rest is just a two-sum problem in disguise.
Say we fix -4. Now we only need two other numbers that add up to 4, because -4 plus 4 is 0. So the target flips to the negative of the fixed number.
So 3Sum is really “pick one number, then solve 2Sum on the rest.” That framing makes the code much less scary.
The first idea most people reach for is honest and blunt. Try every possible group of three numbers. Check each one to see if it sums to zero.
It works, but it is slow, and it needs a little extra care to dodge duplicate triplets. Still, it is a great warm-up, because it proves you understand what the answer even is.
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 [nums[a], nums[b], nums[c]] to result
return result as a listLet us walk this one slowly, because the three loops can look intimidating at first. The whole plan is really simple: try every trio, keep the ones that add to zero. Nothing clever, just patience.
Start with the very first line. We sort the array. We do this so that when we build a triplet, its three numbers always come out in increasing order. That small habit is what lets a set catch duplicates for us later, since [-1, -1, 2] will always be written the same way.
Now look at the three nested loops. Picture them as three fingers pointing at three different slots in the array. The outer finger a picks the first number. Its middle neighbour b picks the second. Then the inner finger c picks the third.
Why does b start after a, and c start after b? Because we never want to reuse the same slot twice, and we never want to look at the same trio in a different order. Forcing a is left of b is left of c means each group of three positions gets visited exactly once.
Inside the deepest loop sits the only real decision. We add the three chosen numbers together and ask one plain question: does this sum equal zero? If yes, we have found a triplet, so we drop it into result. If no, we do nothing and let the loops roll on to the next trio.
One detail deserves attention. We store answers in a set, not a plain list. A set refuses to hold the same triplet twice. So even though our array has two copies of -1 that could build [-1, 0, 1] in two ways, the set quietly keeps only one. That is our duplicate guard, and it costs us almost no thought.
When all three loops finish, every possible trio has been tried. The set now holds each zero-sum triplet exactly once, and we hand it back as a list. Slow, yes, but you can see at a glance that it cannot miss an answer.
import 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]]
}
}Each piece here maps straight to the pseudocode, so let us keep it quick.
The HashSet is doing the heavy lifting on duplicates here. Because the array is sorted, every equal triplet is written the exact same way, so the set treats them as one.
Let us trace the sorted array [-4, -1, -1, 0, 1, 2]. We will only show the trios that actually sum to zero, since the loop tries many that do not.
| a | b | c | nums[a] + nums[b] + nums[c] | Sum is 0? | Set after this step |
|---|---|---|---|---|---|
| -4 (i0) | -1 (i1) | … | -4 + -1 + … none reach 0 | No | { } |
| -1 (i1) | -1 (i2) | 2 (i5) | -1 + -1 + 2 = 0 | Yes | { [-1,-1,2] } |
| -1 (i1) | 0 (i3) | 1 (i4) | -1 + 0 + 1 = 0 | Yes | { [-1,-1,2], [-1,0,1] } |
| -1 (i2) | 0 (i3) | 1 (i4) | -1 + 0 + 1 = 0 | Yes | set already has it, kept once |
The loops try many more trios than these four rows. We only listed the ones that matter, so the duplicate story stays clear.
A handful of trios summed to zero, but only two are unique. Let us break that down.
Now watch the set do its job on that last row. The triplet [-1, 0, 1] was already inside, so the set simply ignored the copy.
Three nested loops mean we check roughly n times n times n groups. So the time grows fast as the array gets bigger.
For six numbers this is fine. For a few thousand it crawls, because the cube of a big number is enormous. That speed wall is exactly what we fix next.
The dry run showed the pain. That third loop is doing far too much work. We can drop it entirely by remembering the numbers we have already seen.
So we keep only two loops. The outer one fixes the first number. The inner one walks through the rest, and for each value it asks a hash set whether the exact partner it needs has already gone by.
This is the same 2Sum-with-a-hashset trick you may know, wrapped inside one outer loop. It trades the third loop for a little extra memory.
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]) // the partner we want
if need is in seen:
add sorted triplet [nums[i], need, nums[j]] to result
add nums[j] to seen
return result as a listThis version has one loop fewer than the brute force, so it runs quicker, but it asks you to hold a new idea in your head. That idea is the seen set: a little notebook where we jot down every number we have walked past on this pass. Let us go through it slowly.
First line, we sort again. Same reason as before. Sorting keeps equal numbers together and lets us write each triplet in a fixed order, which makes the outer result set catch duplicates.
Now the outer loop. Its job is to fix the first number of the triplet. Call that fixed number nums[i]. Think of it as saying, “OK, this number is definitely in my trio. Now who are its two partners?”
Right after we fix nums[i], we create a fresh, empty seen set. This is important: it is emptied for every new i. The notebook only remembers numbers from the current pass, not from earlier ones, so old values never leak in and cause wrong matches.
Then the inner loop walks j from just after i to the end. At each stop, it looks at nums[j] and does a tiny bit of arithmetic. We want all three numbers to add to zero. Two of them, nums[i] and nums[j], are already on the table. So the third one, the partner we still need, must be whatever makes the total zero. That value is need, and it equals minus the sum of the two we have.
Here is the part beginners often ask about: when do we write nums[j] into the notebook? Only after we finish checking it. That order matters. It means when we look for need, the notebook holds every number strictly before j, never j itself. So a number can never wrongly pair with itself.
Let us picture it on a tiny slice. Suppose nums[i] is -1, and as j moves we pass a 0. We write 0 in the notebook. Later j lands on 1. We compute need = -(-1 + 1) = 0. We check the notebook, and yes, 0 is there. So -1, 0 and 1 form a zero-sum triplet, and we save it.
When the inner loop ends, we have found every partner pair that works with this fixed nums[i]. The outer loop then slides to the next first number and repeats the whole hunt with a brand new notebook. After all passes, the result set holds each triplet once.
import 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)) {
List<Integer> t = Arrays.asList(nums[i], need, nums[j]);
result.add(t);
}
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]]
}
}The shape is close to the pseudocode, so let us focus on the why behind each line.
Compare this with brute force. We swapped the whole third loop for one hash-set lookup, which is close to instant.
Let us trace the pass where i fixes the first -1, since that pass finds both triplets. The sorted array is [-4, -1, -1, 0, 1, 2], and i sits at index 1.
| j | nums[j] | need = -(nums[i]+nums[j]) | need in seen? | Action | seen after |
|---|---|---|---|---|---|
| 2 | -1 | 2 | No | no match | { -1 } |
| 3 | 0 | 1 | No | no match | { -1, 0 } |
| 4 | 1 | 0 | Yes (0 seen) | save [-1, 0, 1] | { -1, 0, 1 } |
| 5 | 2 | -1 | Yes (-1 seen) | save [-1, -1, 2] | { -1, 0, 1, 2 } |
Two triplets came out of a single pass. The notebook seen did the matching, and the outer result set will keep each one just once.
Four inner steps, two triplets found. Let us look closely at how the notebook earned its keep.
Notice the timing rule paying off. Each number was written to seen only after we checked it, so nothing ever matched with itself.
| 💡 Interview Insight If asked why you empty the seen set for every new i, say this: it stops numbers from an earlier first-element pass from leaking in and forming false triplets. Small point, but it shows you really understand the scope of the set. |
We dropped a loop, so the time falls hard. The sort adds a small cost that hides under the bigger term.
This is already a big jump over brute force. But it still leans on hash sets and extra memory, and interviewers usually want the leaner two-pointer version next.
The hash-set version is fast, but it carries a notebook for every pass. On a sorted array we do not need that notebook at all.
Instead we fix the first number, then send two pointers inward from both ends of the rest. Because the array is sorted, the pointers always know which way to move. Too small a sum, move the left one right. Too big, move the right one left.
This is the answer interviewers hope to see. It runs in the same O(n²) time as the hash version, but it uses only a couple of index variables, no extra sets per pass.
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
lo = i + 1
hi = n - 1
while lo < hi:
sum = nums[i] + nums[lo] + nums[hi]
if sum < 0: lo = lo + 1
else if sum > 0: hi = hi - 1
else:
add [nums[i], nums[lo], nums[hi]] to result
move lo right past any equal values
move hi left past any equal values
lo = lo + 1
hi = hi - 1
return resultThis is the version worth truly understanding, so we will take it gently and cover every line. The good news: once the array is sorted, the logic is almost mechanical. There is no notebook to track, just two pointers and a running sum.
Line one, we sort. By now you know why. Sorted numbers let the two pointers trust the direction they move, and they let us skip duplicates by simply looking at the neighbour.
Now the outer loop, which fixes the first number nums[i]. Same core idea as before: lock in one number, then hunt for two partners that finish the zero sum. But look at the line right inside the loop, because it is the first duplicate guard.
That guard says: if i is past the start and nums[i] equals the number just before it, skip this whole pass with continue. Why? Because we already ran a full search using that exact value as the first number. Running it again would only rediscover the same triplets. So when we see a repeated first number, we quietly move on.
Next we set up the two pointers. lo starts one slot to the right of i, at the smallest of the remaining numbers. hi starts at the very last slot, the largest. They will now walk toward each other and never cross.
The while loop keeps going as long as lo is still left of hi. Inside it, we add the three numbers, the fixed one plus the two the pointers point at, and store that in sum. Everything now hinges on comparing sum to zero, and there are exactly three cases.
Why does moving lo right give a bigger sum, and moving hi left give a smaller one? Only because the array is sorted. To lo’s right sit larger numbers. To hi’s left sit smaller numbers. The sort is what makes this steering reliable, and it is the whole reason we sorted in the first place.
Now the trickiest bit, the duplicate skipping after a match. Once we save a triplet, we do not just nudge the pointers by one. We first slide lo rightward past any values equal to the one it just used, and slide hi leftward past any values equal to the one it just used. Only then do we take the final step inward.
Think of the two pointers as two hands closing a book. Every time they meet a matching pair, they clap once, record it, then keep closing until they touch. The equal-value skips just stop them from clapping twice for the same pair.
When lo and hi finally meet, this pass is done. The outer loop moves to the next distinct first number, and the whole closing motion repeats. After every first number has had its turn, result holds every unique triplet, and we return it.
import 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 lo = i + 1, hi = nums.length - 1;
while (lo < hi) {
int sum = nums[i] + nums[lo] + nums[hi];
if (sum < 0) {
lo++;
} else if (sum > 0) {
hi--;
} else {
result.add(Arrays.asList(nums[i], nums[lo], nums[hi]));
while (lo < hi && nums[lo] == nums[lo + 1]) lo++;
while (lo < hi && nums[hi] == nums[hi - 1]) hi--;
lo++;
hi--;
}
}
}
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]]
}
}This version does the most thinking in the fewest moving parts, so let us go gently.
Two lines quietly carry the whole duplicate story here.
Now we trace the pass that fixes the first -1, at i index 1. The sorted array is [-4, -1, -1, 0, 1, 2], so lo starts at index 2 and hi at index 5.
| lo | hi | nums[lo] | nums[hi] | sum with -1 | Action |
|---|---|---|---|---|---|
| 2 | 5 | -1 | 2 | -1 + -1 + 2 = 0 | save [-1,-1,2], move both in |
| 3 | 4 | 0 | 1 | -1 + 0 + 1 = 0 | save [-1,0,1], move both in |
| 4 | 3 | — | — | lo has passed hi | stop this pass |
Legend: lo is the left pointer moving right, hi is the right pointer moving left, and sum adds the fixed -1 to both pointed values.
Two neat steps found both triplets, then the pointers met and stopped. Let us look at each one.
Then the pointers crossed, and the while loop ended for this pass. There was no wasted checking after that.
There is a quiet promise in this loop. Every pass either finds a match and closes in, or nudges one pointer, so the two always march toward each other and never repeat work.
Same array, same two triplets, three very different amounts of work and memory.
| 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 |
The gap grows with size. At six numbers it barely shows. At a few thousand it is the difference between a snappy answer and a long wait.
| 💡 Interview Insight Interviewers love the follow-up, “why sort at all?” The clean answer: sorting gives the two pointers a reliable direction and makes duplicate skipping a one-line neighbour check. Without the sort, neither trick works. |
Tables are exact, but a sketch often lands faster. Here is the same two-pointer trace drawn by hand.

The sorted array sits along the top with its indices. Each step block shows where lo and hi point and the sum they make. The panel beside it shows whether we saved a triplet or moved a pointer.
Follow the colour cues as you read it.
Some people read pictures faster than tables. Use whichever version clicks for you, or sketch your own while you follow the code.
All three give the same correct answer. They just pay very different prices for it.
| 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 |
Notice the two faster versions share the same time class. The real separator is memory and cleanliness. The two-pointer sweep carries almost nothing, and its duplicate handling reads as two small skips.
In an interview, start with brute force to show you understand the goal. Then point out the wasted third loop. Finally tighten it into the sort-and-two-pointer sweep and explain the two duplicate guards.
That climb from slow to lean is exactly the story interviewers want. Jumping straight to the optimal answer skips the reasoning they came to watch.
| 💡 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.
javahandson.com | DSA Series | Arrays & Strings