Contains Duplicate in Java DSA: From Brute Force to a One-Pass HashSet
-
Last Updated: July 29, 2026
-
By: javahandson
-
Series
Learn Java in a easy way
Solve Contains Duplicate in Java DSA three ways — brute force, sort, and a one-pass HashSet — with full step-by-step dry runs and complexity comparison.
Contains Duplicate in Java is one of those warm-up problems that hides a neat lesson. The question is tiny. You get an array, and you must say whether any value shows up more than once.
It sounds almost too easy. Return true if a number repeats, and false if every number is unique. That is the whole task.
But the fun part is how many ways you can solve it. We will build the answer in three steps, from slow to fast. First we brute force it with two loops. Then we sort the array and peek at neighbours. Finally we sweep once with a HashSet, which is the version interviewers want.
Every approach gets a full, step-by-step dry run. To keep it honest, we use a seven-number array that mixes positives and negatives: [4, -1, 2, 7, -1, 5, 3]. The value -1 sits at two spots, so this array has a duplicate.
We also trace a second array, [4, -1, 2, 7, 9, 5, 3], where every value is unique. That second run shows what happens when the answer is false and the loops go all the way to the end. Nothing is skipped in either run.
Let us pin down the rules before we touch any code.
For our first array the answer is true, because -1 sits at index 1 and again at index 4. Swap that second -1 for a 9, and you get [4, -1, 2, 7, 9, 5, 3], where every value is unique and the answer is false. We will trace both.
A duplicate is just the same value seen twice at two different spots. So the real job is remembering what you have already seen. The moment a value shows up again, you have your answer.
A HashSet is a bag that holds only unique values. Add a number, and it either goes in fresh or the set tells you it was already there.
Compare every number with every number after it. If any pair matches, you found a duplicate. It is slow, but it makes the goal crystal clear.
for i from 0 to n-1: // first number
for j from i+1 to n-1: // every number after i
if nums[i] == nums[j]:
return true // found a repeat
return false // no repeats at allpublic class ContainsDuplicateBrute {
public static boolean containsDuplicate(int[] nums) {
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[i] == nums[j]) {
return true;
}
}
}
return false;
}
public static void main(String[] args) {
int[] nums = { 4, -1, 2, 7, -1, 5, 3 };
System.out.println(containsDuplicate(nums)); // true
}
}Array: [4, -1, 2, 7, -1, 5, 3]. The i index holds the first number, and j scans every number after it. We trace each comparison until one pair matches. The i (val) column shows the index and its value, and the same for j (val).
| Step | i (val) | j (val) | nums[i] == nums[j] ? | Action |
|---|---|---|---|---|
| 1 | 0 (4) | 1 (-1) | 4 == -1 ? No | j moves right |
| 2 | 0 (4) | 2 (2) | 4 == 2 ? No | j moves right |
| 3 | 0 (4) | 3 (7) | 4 == 7 ? No | j moves right |
| 4 | 0 (4) | 4 (-1) | 4 == -1 ? No | j moves right |
| 5 | 0 (4) | 5 (5) | 4 == 5 ? No | j moves right |
| 6 | 0 (4) | 6 (3) | 4 == 3 ? No | i done, i moves right |
| 7 | 1 (-1) | 2 (2) | -1 == 2 ? No | j moves right |
| 8 | 1 (-1) | 3 (7) | -1 == 7 ? No | j moves right |
| 9 | 1 (-1) | 4 (-1) | -1 == -1 ? Yes | return true |
Let us walk each comparison and see what the loops are doing. The array is [4, -1, 2, 7, -1, 5, 3].
Steps 1 to 6: i is parked on 4. The outer loop fixes i at index 0, which holds 4. Now j sweeps every later slot to see if any equals 4.
So the number 4 was compared against all six numbers after it and matched none. That is one full inner sweep with no luck.
Steps 7 to 9: i moves to the first -1. Now i sits at index 1, which holds -1. The j scan restarts from index 2.
Notice the loop never touched index 5 or 6. Once the first -1 found its twin, the search was over. The outer loop also never moved past index 1.
Now the unique array [4, -1, 2, 7, 9, 5, 3]. Nothing repeats, so no pair ever matches. The loops must run all the way to the end and then return false. That is 21 comparisons in total, and we show every one.
| Step | i (val) | j (val) | nums[i] == nums[j] ? | Action |
|---|---|---|---|---|
| 1 | 0 (4) | 1 (-1) | 4 == -1 ? No | j moves right |
| 2 | 0 (4) | 2 (2) | 4 == 2 ? No | j moves right |
| 3 | 0 (4) | 3 (7) | 4 == 7 ? No | j moves right |
| 4 | 0 (4) | 4 (9) | 4 == 9 ? No | j moves right |
| 5 | 0 (4) | 5 (5) | 4 == 5 ? No | j moves right |
| 6 | 0 (4) | 6 (3) | 4 == 3 ? No | i moves right |
| 7 | 1 (-1) | 2 (2) | -1 == 2 ? No | j moves right |
| 8 | 1 (-1) | 3 (7) | -1 == 7 ? No | j moves right |
| 9 | 1 (-1) | 4 (9) | -1 == 9 ? No | j moves right |
| 10 | 1 (-1) | 5 (5) | -1 == 5 ? No | j moves right |
| 11 | 1 (-1) | 6 (3) | -1 == 3 ? No | i moves right |
| 12 | 2 (2) | 3 (7) | 2 == 7 ? No | j moves right |
| 13 | 2 (2) | 4 (9) | 2 == 9 ? No | j moves right |
| 14 | 2 (2) | 5 (5) | 2 == 5 ? No | j moves right |
| 15 | 2 (2) | 6 (3) | 2 == 3 ? No | i moves right |
| 16 | 3 (7) | 4 (9) | 7 == 9 ? No | j moves right |
| 17 | 3 (7) | 5 (5) | 7 == 5 ? No | j moves right |
| 18 | 3 (7) | 6 (3) | 7 == 3 ? No | i moves right |
| 19 | 4 (9) | 5 (5) | 9 == 5 ? No | j moves right |
| 20 | 4 (9) | 6 (3) | 9 == 3 ? No | i moves right |
| 21 | 5 (5) | 6 (3) | 5 == 3 ? No | loops end, return false |
Here nothing matches, so the loops grind through every pair. Watch how the block of comparisons shrinks as i moves right.
Each time i moves right, its inner scan gets one step shorter, because j only ever looks at numbers after i. After the very last pair fails, both loops finish and the method returns false. That climb from six comparisons down to one is exactly the O(n²) shape.
Three comparisons for four numbers feels fine. But for a few thousand numbers this crawls, which is why we improve it next.
Here is a smart trick. Sort the array, and any duplicates land right next to each other. Now you only need to compare each number with the one before it.
sort nums
for i from 1 to n-1:
if nums[i] == nums[i-1]: // same as the neighbour before
return true
return falseimport java.util.Arrays;
public class ContainsDuplicateSort {
public static boolean containsDuplicate(int[] nums) {
Arrays.sort(nums);
for (int i = 1; i < nums.length; i++) {
if (nums[i] == nums[i - 1]) {
return true;
}
}
return false;
}
public static void main(String[] args) {
int[] nums = { 4, -1, 2, 7, -1, 5, 3 };
System.out.println(containsDuplicate(nums)); // true
}
}Original array: [4, -1, 2, 7, -1, 5, 3]. First the sort rearranges it, then the loop scans neighbours. We show both stages fully.
Stage 1 – the sort. Arrays.sort turns [4, -1, 2, 7, -1, 5, 3] into [-1, -1, 2, 3, 4, 5, 7]. The two -1s were far apart before, at index 1 and index 4. Now they sit side by side at index 0 and index 1.
Stage 2 – the neighbour scan. The loop starts at index 1 and compares each value with its left neighbour, nums[i-1].
| Step | i | nums[i-1] | nums[i] | Equal ? | Action |
|---|---|---|---|---|---|
| 1 | 1 | -1 | -1 | Yes | return true |
The scan matched on its very first step, so the loop never reached index 2 or beyond. Sorting pulled the twin -1s to the front, so the check found them right away.
Stage 1: sorting does the heavy lifting. Before sorting, the duplicate -1s lived at index 1 and index 4, with 2 and 7 wedged between them. After Arrays.sort, the array becomes [-1, -1, 2, 3, 4, 5, 7]. Both -1s are now neighbours. This is the key move. Sorting turns a scattered search into a simple side-by-side check.
Stage 2, step 1: i at index 1. The loop begins at index 1 so that nums[i-1] is always safe to read. At i equals 1, the left neighbour nums[0] is -1 and the current value nums[1] is also -1. We compare them, they are equal, and the method returns true right away.
Because the match landed on the first comparison, the loop stopped there. The other five numbers were never even checked.
Now the unique array [4, -1, 2, 7, 9, 5, 3]. After sorting it becomes [-1, 2, 3, 4, 5, 7, 9]. Every neighbour pair is different, so the loop runs to the end and returns false. We trace all six comparisons.
| Step | i | nums[i-1] | nums[i] | Equal ? | Action |
|---|---|---|---|---|---|
| 1 | 1 | -1 | 2 | No | continue |
| 2 | 2 | 2 | 3 | No | continue |
| 3 | 3 | 3 | 4 | No | continue |
| 4 | 4 | 4 | 5 | No | continue |
| 5 | 5 | 5 | 7 | No | continue |
| 6 | 6 | 7 | 9 | No | loop ends, return false |
Here the sorted array is [-1, 2, 3, 4, 5, 7, 9], climbing steadily with no repeats. The loop checks each value against the one on its left.
Because a sorted array places any duplicate side by side, one clean left-to-right scan is enough to be sure. No match in any neighbour pair means no duplicate anywhere.
This beats brute force on large arrays. Still, sorting rearranges your data and costs that log factor, so the next version does better without touching order.
Walk the array once. Keep a HashSet of every value you have already seen. Before adding a number, ask the set whether it is already inside. If yes, that is your duplicate. This is the answer interviewers hope to see.
seen = empty hash set
for each value v in nums:
if v is already in seen:
return true // seen this one before
add v to seen
return false // finished with no repeatimport java.util.HashSet;
import java.util.Set;
public class ContainsDuplicateHashSet {
public static boolean containsDuplicate(int[] nums) {
Set<Integer> seen = new HashSet<>();
for (int v : nums) {
if (seen.contains(v)) {
return true;
}
seen.add(v);
}
return false;
}
public static void main(String[] args) {
int[] nums = { 4, -1, 2, 7, -1, 5, 3 };
System.out.println(containsDuplicate(nums)); // true
}
}Array: [4, -1, 2, 7, -1, 5, 3]. We trace every value in order. Each row shows the set before the check, whether the value was already inside, the action taken, and the set after. The set starts empty. HashSet does not keep any order, so we list values in the order they went in only to make the trace easy to read.
| Step | v (value) | seen before | v in seen ? | Action | seen after |
|---|---|---|---|---|---|
| 1 | 4 | { } | No | add 4 | { 4 } |
| 2 | -1 | { 4 } | No | add -1 | { 4, -1 } |
| 3 | 2 | { 4, -1 } | No | add 2 | { 4, -1, 2 } |
| 4 | 7 | { 4, -1, 2 } | No | add 7 | { 4, -1, 2, 7 } |
| 5 | -1 | { 4, -1, 2, 7 } | Yes (-1 seen) | return true | { 4, -1, 2, 7 } |
Let us go value by value and watch the set decide each step. The array is [4, -1, 2, 7, -1, 5, 3].
So the loop stopped at step 5 and never looked at 5 or 3. That second -1 found the first -1 already waiting in the set, which is the whole trick.
One timing rule makes this work. We always check the set before we add the current value. That is why a lonely value can never wrongly match itself, and why the repeat is caught only when its twin arrives.
Now the unique array [4, -1, 2, 7, 9, 5, 3]. No value repeats, so the check never hits, and every value gets added. The loop finishes all seven steps and returns false.
| Step | v (value) | seen before | v in seen ? | Action | seen after |
|---|---|---|---|---|---|
| 1 | 4 | { } | No | add 4 | { 4 } |
| 2 | -1 | { 4 } | No | add -1 | { 4, -1 } |
| 3 | 2 | { 4, -1 } | No | add 2 | { 4, -1, 2 } |
| 4 | 7 | { 4, -1, 2 } | No | add 7 | { 4, -1, 2, 7 } |
| 5 | 9 | { 4, -1, 2, 7 } | No | add 9 | { 4, -1, 2, 7, 9 } |
| 6 | 5 | { 4, -1, 2, 7, 9 } | No | add 5 | { 4, -1, 2, 7, 9, 5 } |
| 7 | 3 | { 4, -1, 2, 7, 9, 5 } | No | add 3 | { 4, -1, 2, 7, 9, 5, 3 } |
This time no value is ever a repeat, so each step just grows the set by one.
After the last value, the loop ends with no match found, so the method returns false. Notice the set ends with all seven values inside. When the answer is false, the HashSet approach touches every element exactly once, which is its O(n) behaviour.
| 💡 Interview Insight A classic follow-up is “can you do it without sorting?” The HashSet answer is exactly that. Say you trade a little memory for speed, then return true the instant a value repeats. That trade is the whole point. |
| Approach | How it searches | Extra memory | Speed feel |
|---|---|---|---|
| Brute force | Two loops, every pair | None | Slow on big arrays |
| Sort + neighbours | Sort, then one scan | None extra | Faster |
| One-pass HashSet | One loop, set lookup | A set of seen values | Fastest, one pass |
Tables are exact, but a sketch often lands faster. Here is the same HashSet true-case trace drawn by hand.

All three give the same answer. They just pay different prices.
| Approach | Time | Space | Note |
|---|---|---|---|
| Brute force | O(n²) | O(1) | Simple, but two loops make it slow |
| Sort + neighbours | O(n log n) | O(1) extra | Faster, but reorders your data |
| One-pass HashSet | O(n) | O(n) | Fastest, the expected answer |
The HashSet version wins on speed, at the cost of some memory for the set. The sort version saves memory but pays the log factor and shuffles your array. Brute force stays simple yet slow.
In an interview, start with brute force, point out the wasted second loop, then jump to the HashSet and explain the one-pass check. That climb is the story interviewers want to hear.
| 💡 Interview Insight If asked about the trade-off, keep it short. HashSet is O(n) time and O(n) space. Sorting is O(n log n) time but almost no extra space. Name both, and let the interviewer pick which cost matters more. |
A few small traps catch beginners on Contains Duplicate. Keep them in mind.
Run the empty and single-element cases through your code before you call it done. They catch more bugs than any ordinary input will.
A: A one-pass HashSet is fastest. You walk the array once, and for each value you check the set before adding it. If the value is already there, you return true. This runs in O(n) time with O(n) extra space.
A: Yes. Sort the array first, then compare each value with its left neighbour. Duplicates land next to each other after sorting. This uses O(1) extra space but costs O(n log n) time and reorders your data.
A: The check must come first so a value never matches itself. If you add before checking, every number would look like a duplicate. Checking first means the second copy of a value finds the first one waiting in the set.
A: It should return false. An empty array or an array with a single element has no pair of values, so no duplicate is possible. Always test these edge cases before submitting.
Contains Duplicate in Java looks trivial, and in a way it is. Yet it teaches a habit you will reuse everywhere.
Our seven-number traces showed the payoff clearly. Brute force compared pairs until it stumbled on a match, or ground through all 21 pairs when nothing repeated. Sorting pulled the twin -1s together first. The HashSet remembered what it had seen and caught the repeat in a single pass.
So take the pattern, not just the answer. When a problem asks “have I seen this before?”, reach for a set. That one idea unlocks a huge pile of array and string problems waiting further down the list.
javahandson.com | DSA Series | Arrays & Strings