Contains Duplicate problem in DSA: Brute Force, Sorting, and the HashSet Trick
-
Last Updated: July 12, 2026
-
By: javahandson
-
Series
Learn Java in a easy way
Learn the Contains Duplicate problem in DSA the easy way. We compare brute force, sorting, and the fast hash set trick, with dry runs for all three and ten interview questions at the end. As a developer I will be using Java code but pseudo-codes, explanation and dry runs are language neutral and you can understand the concept easily. So don’t skip it because language is just a syntax.
The Contains Duplicate problem in DSA is one of those warm-up questions you meet early in any array series. It looks almost too simple to bother with. Yet it quietly teaches you how a hash set works, and that lesson shows up again and again in much harder problems.
Here is the task in plain words. Somebody hands you an array of numbers. You answer one yes-or-no question: does any value appear more than once? A single repeat makes the answer true. Every value unique makes it false.
That is the entire problem. No positions to report, no counts to return, no ordering to preserve. Just true or false.
We build up slowly. First comes the obvious brute force, because a correct slow answer beats a clever wrong one. Then sorting gives us a smarter middle ground. Finally the hash set gets us to a single pass, which is the answer most interviewers are waiting for.
Here is the road map for the rest of the guide.
Let us lock down the rules before writing any code. A clear problem statement saves you from silly bugs later, and it takes about thirty seconds.
Every version of this question comes down to the same handful of rules.
[1, 2, 3, 1].Worth knowing about the original version of this question: the array can hold up to about 100,000 numbers, and each value sits somewhere between minus one billion and plus one billion. Those limits rule out a few tempting shortcuts, and we come back to that in the follow-ups.
Take [1, 2, 3, 1]. The value 1 sits at position 0 and again at position 3, so the answer is true. You can stop reading the array the moment you notice that.
Now take [1, 2, 3, 4]. Nothing repeats, so the answer is false. Notice the difference in effort. Proving a duplicate exists can finish early, but proving no duplicate exists means looking at every value.
That asymmetry matters more than it looks. Every approach below can quit early on a true answer, and none of them can quit early on a false one.
Beginners tend to solve a harder problem than the one on the page. Guard against that.
Keep the goal small and the code stays small with it. Hunting for indices on this question is the most common way beginners talk themselves into a mess.
| 💡 Interview Insight Interviewers love to ask what you would do if the array were huge and could not fit in memory. Mentioning that a hash set trades memory for speed, and that sorting avoids that extra memory, shows you understand the trade-off before they even push on it. |
Before the code, let us name the ideas that carry this whole problem. Both are small, and you will lean on them in many array questions later.
A hash set stores unique values and answers one question quickly: is this value already inside? On average that check costs constant time, no matter how many values the set already holds.
Think of a guest list at a door. Somebody walks up, you glance at the list, and you either wave them through or spot their name already there. You never re-read the whole list from the top.
Almost every language ships one. The names differ, and the idea does not. Because the check is constant on average, a hash set turns a repeated search into a single question.
One catch is worth remembering. That constant cost is an average, not a guarantee. When many values collide into the same bucket, lookups degrade, and a badly behaved input can push the cost up.
The second idea is simpler still. Sort the array, and equal values land right next to each other. A duplicate is then never far away, because it is always the neighbour.
So a wide search collapses into a narrow one. Instead of comparing every value against every other value, you compare each value against exactly one: the value just before it.
Sorting costs time, of course. Yet it buys something valuable in exchange, which is a solution that needs almost no extra memory.
Sorting rearranges the array. Doesn’t that break the problem? Not at all, and the reason is worth saying out loud.
The question asks whether a repeated value exists anywhere. Moving values around cannot create a duplicate that was not there, and it cannot hide one that was. The multiset of values stays identical.
Compare that with a problem like Two Sum, where the answer is a pair of indices. Sorting there destroys the very thing you must report. Knowing when you may reorder freely, and when you may not, is a genuinely useful instinct.
The first idea most people reach for is direct. Compare every number with every other number. Any two that match prove a duplicate exists.
There is no shame in starting here. It mirrors the problem statement exactly, which makes it very hard to get wrong, and it gives you a working answer on the whiteboard while you think about speed.
for i from 0 to n-1:
for j from i+1 to n-1:
if nums[i] == nums[j]:
return true
return false // no duplicate foundThe outer loop picks one number. The inner loop checks it against every later number. Starting the inner index at i plus 1 does two jobs at once: it stops a number matching itself, and it stops us testing the same pair twice.
public 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 = { 1, 2, 3, 1 };
System.out.println(containsDuplicate(nums)); // Output: true
}
}Let us walk through the logic slowly. Each part has one clear job.
This code is easy to read and hard to get wrong. That is its real strength. Its weakness is speed, which we fix in the next two sections.
Trace nums = [1, 2, 3, 1] and follow the pairs in the order the loops produce them.
| Step | i | j | Compare | Match? |
|---|---|---|---|---|
| 1 | 0 | 1 | 1 vs 2 | No |
| 2 | 0 | 2 | 1 vs 3 | No |
| 3 | 0 | 3 | 1 vs 1 | Yes, return true |
Three comparisons and we are done. The early exit fires on the very first row where the values agree, so the loops never reach i equal to 1.
Now imagine the unlucky input [1, 2, 3, 4]. No pair ever matches, so all six comparisons run before the answer comes back false. That worst case is the number to keep in your head.
Brute force checks every pair of positions. For an array of size n that comes to n multiplied by (n minus 1), then halved, which grows like n squared. So the time cost is O(n squared).
Space stays at O(1). A pair of loop counters covers everything, and nothing gets copied.
Put real numbers on it. At 1,000 values you get roughly half a million comparisons, which finishes instantly. At 100,000 values you get about five billion, and the program crawls. That gap is the whole reason to keep reading.
Here is a neat middle ground. Sort the array, and any duplicate values sit shoulder to shoulder. Then you only need to check each number against its neighbour.
sort(nums)
for i from 1 to n-1:
if nums[i] == nums[i-1]:
return true
return falseNotice the loop starts at 1, not 0. Position 0 has nothing before it, so a comparison there would read past the front of the array.
import 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 = { 1, 2, 3, 1 };
System.out.println(containsDuplicate(nums)); // Output: true
}
}One caveat deserves attention. Sorting rewrites the array you were handed. When the caller still needs the original order, copy the array first and sort the copy. That copy costs O(n) extra memory, which is worth knowing before you promise anyone a low-memory solution.
Start with nums = [3, 1, 4, 1, 5]. Sorting turns it into [1, 1, 3, 4, 5], and the two 1s that began three positions apart are now neighbours.
| i | nums[i-1] | nums[i] | Equal? | Action |
|---|---|---|---|---|
| 1 | 1 | 1 | Yes | Duplicate found, return true |
One comparison after the sort, and the answer arrives. That looks wonderfully cheap until you remember what came before it.
The sort itself already touched every value and moved most of them. So the cheap-looking loop rides on top of the expensive part, and the sort dominates the total cost. Never quote the loop alone when an interviewer asks about complexity.
Sorting runs in O(n log n) time, and the neighbour scan adds only O(n). Adding those gives O(n log n) overall, which beats brute force comfortably on large inputs.
Memory depends on what you sort. Sorting the array in place keeps the extra cost to about O(log n), which is just the sort’s own bookkeeping. Sorting a defensive copy instead pushes that to O(n).
Now for the version interviewers really want. The idea is to remember every number as you walk the array once. Before storing a number, you ask one simple question: have I seen this value already?
A yes means you found a duplicate and can stop right there. Otherwise you record the value and move on. Hash sets fit perfectly here, because they answer that membership question in constant time on average.
seen = empty set
for each num in nums:
if num is in seen:
return true
add num to seen
return falseRead the loop body as check first, store second. Flipping those two lines quietly breaks the whole thing, and we come back to why in a moment.
import java.util.HashSet;
import java.util.Set;
public class ContainsDuplicateHashSet {
public static boolean containsDuplicate(int[] nums) {
Set<Integer> seen = new HashSet<>();
for (int num : nums) {
if (seen.contains(num)) {
return true;
}
seen.add(num);
}
return false;
}
public static void main(String[] args) {
int[] nums = { 1, 2, 3, 1 };
System.out.println(containsDuplicate(nums)); // Output: true
}
}This version does the most thinking per line, so let us break it down gently.
Why check before adding rather than the other way round? Add first and the set already holds the current number, so the very next check reports a duplicate that does not exist. Every value would look like a repeat.
| 💡 Interview Insight A common follow-up asks why we check before we add, instead of adding first. If you add first, the set already holds the current number, so the very next check would wrongly report a duplicate. Checking first keeps the logic correct. |
Most set implementations tell you something useful when you add a value: whether that value was new. You can lean on that and collapse the check and the store into one line.
import java.util.HashSet;
import java.util.Set;
public class ContainsDuplicateShort {
public static boolean containsDuplicate(int[] nums) {
Set<Integer> seen = new HashSet<>();
for (int num : nums) {
if (!seen.add(num)) { // add returns false when the value already exists
return true;
}
}
return false;
}
public static void main(String[] args) {
int[] nums = { 1, 2, 3, 4 };
System.out.println(containsDuplicate(nums)); // Output: false
}
}Same cost, half the lines, and one fewer lookup per value. Both versions are perfectly acceptable in an interview, so pick whichever you can explain confidently under pressure.
One sweep touches each value a single time, and each membership question costs constant time on average. Multiply those and the total lands at O(n).
Memory is where you pay. An array with no duplicates ends up with every value stored, so the worst case is O(n) extra space. That is the trade: you buy speed with memory.
Watching the logic run by hand makes it stick. Let us trace nums = [1, 2, 3, 1] and follow the set as it grows.
| Step | num | Is num in the set? | Set before | Action |
|---|---|---|---|---|
| 1 | 1 | No | { } (empty) | Add 1 to the set |
| 2 | 2 | No | { 1 } | Add 2 to the set |
| 3 | 3 | No | { 1, 2 } | Add 3 to the set |
| 4 | 1 | Yes | { 1, 2, 3 } | Duplicate! Return true |
Look at step four closely. We reach the second 1, and the set already holds a 1 from step one. So the membership check answers yes, and everything stops right there.
Compare the effort with brute force on the same input. Brute force needed three comparisons, and this needed four steps, which sounds worse. Stretch the array to 100,000 values, though, and the gap becomes enormous. Small inputs hide complexity differences, so never judge an approach by a four-element example.

The sketch above shows the same trace. The array sits on top, the growing set sits on the right, and the arrow shows how the second 1 finds its earlier twin. Many people find this picture easier than the table, so use whichever clicks for you.
Now trace nums = [4, 7, 2], where nothing repeats. Every membership check answers no, so every value joins the set.
| Step | num | Set before | Set after |
|---|---|---|---|
| 1 | 4 | { } | { 4 } |
| 2 | 7 | { 4 } | { 4, 7 } |
| 3 | 2 | { 4, 7 } | { 4, 7, 2 } |
The loop finishes and the method answers false. Two details are worth noticing here.
First, the set ends up holding all three values, which is that O(n) memory cost showing up in miniature. Second, no early exit was possible, because proving nothing repeats means inspecting everything. A clean array is always the expensive case.
All three approaches give the correct answer. They simply charge different prices for it.
| Point | Brute Force | Sorting | Hash Set |
|---|---|---|---|
| Time | O(n squared) | O(n log n) | O(n) average |
| Extra space | O(1) | O(log n) in place | O(n) |
| Changes the input | No | Yes | No |
| Early exit possible | Yes | Only after sorting | Yes |
| Work at 100,000 values | About 5 billion steps | About 1.7 million steps | 100,000 steps |
| Ease of writing | Very easy | Easy | Easy |
| Good for interviews | As a starting point | As the memory-tight answer | As the final answer |
Read the early-exit row twice. Brute force and the hash set can both stop the instant they find a repeat, while sorting must finish sorting before it learns anything at all. On an array whose first two values match, brute force actually wins on wall-clock time.
Context decides, and here is a short guide.
In an interview, walk that path out loud. Mention brute force to show you understand the problem, offer sorting as a smarter option, then land on the hash set for the best time complexity. That journey from slow to fast is the part interviewers actually grade.
| 💡 Interview Insight If the interviewer says memory is tight, pivot to the sorting approach. It needs no extra set and still beats brute force. Naming that trade out loud signals real judgment, not just memorised Big-O. |
Solve the base problem quickly and a follow-up usually arrives. These two show up most often, and both reuse ideas you already have.
A popular twist adds a limit. Now a duplicate only counts when the two equal values sit at most k positions apart. Far-apart repeats no longer qualify.
The fix is a sliding window. Keep a set of only the last k values, and drop anything that falls out of range as the window moves forward.
import java.util.HashSet;
import java.util.Set;
public class DuplicateWithinK {
public static boolean containsNearbyDuplicate(int[] nums, int k) {
Set<Integer> window = new HashSet<>();
for (int i = 0; i < nums.length; i++) {
if (i > k) {
window.remove(nums[i - k - 1]); // slide the window forward
}
if (!window.add(nums[i])) {
return true;
}
}
return false;
}
public static void main(String[] args) {
System.out.println(containsNearbyDuplicate(new int[] { 1, 2, 3, 1 }, 3)); // Output: true
System.out.println(containsNearbyDuplicate(new int[] { 1, 2, 3, 1 }, 2)); // Output: false
}
}Look at the two sample calls. With k equal to 3 the two 1s are close enough, so the answer is true. Tighten k to 2 and those same values sit too far apart, so the answer flips to false.
Cost stays at O(n) time, and memory drops to O(k) rather than O(n). Shrinking the window shrinks the memory bill, which is a nice thing to point out.
Sometimes you get extra information: every value lands between 0 and some modest limit. That changes the game, because you can swap the set for a plain array of flags.
public class DuplicateSmallRange {
// Assumes every value satisfies 0 <= value < limit
public static boolean containsDuplicate(int[] nums, int limit) {
boolean[] seen = new boolean[limit];
for (int num : nums) {
if (seen[num]) {
return true;
}
seen[num] = true;
}
return false;
}
public static void main(String[] args) {
int[] nums = { 3, 0, 2, 3 };
System.out.println(containsDuplicate(nums, 5)); // Output: true
}
}Same shape as before, with an array standing in for the set. Indexing is direct, so lookups cost constant time in the worst case rather than on average.
Two warnings come with this trick. It only works when values are non-negative and bounded, and the memory cost tracks the range instead of the array length. On the original problem, where values reach a billion in both directions, the flag array would be absurd.
A handful of small traps catch beginners on this problem. Read them once and you will dodge most of them.
Negative numbers and zeros need no special handling, by the way. Equality does not care about sign, so they behave like any other value in all three approaches.
Let us tie everything into one small program. Rather than printing a bare boolean, it reports the result in a sentence a human can read.
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class DuplicateChecker {
public static void report(int[] nums) {
if (nums == null || nums.length < 2) {
System.out.println("Fewer than two values, so nothing can repeat.");
return;
}
Set<Integer> seen = new HashSet<>();
for (int num : nums) {
if (!seen.add(num)) {
System.out.println(Arrays.toString(nums)
+ " has a duplicate: " + num);
return;
}
}
System.out.println(Arrays.toString(nums) + " holds only unique values.");
}
public static void main(String[] args) {
report(new int[] { 1, 2, 3, 1 });
report(new int[] { 1, 2, 3, 4 });
report(new int[] { 9 });
}
}[1, 2, 3, 1] has a duplicate: 1 [1, 2, 3, 4] holds only unique values. Fewer than two values, so nothing can repeat.
Three inputs, three different shapes of answer. That is the point of the exercise.
Notice how little changed from the plain single-pass version. The scan is identical. We only added a guard at the top and friendlier messages at the bottom.
One small bonus falls out of the shorter version. Since we return the moment a repeat appears, the value in hand is the duplicate itself, so reporting it costs nothing extra. Real code often wants that detail even when the puzzle does not.
A: Contains Duplicate asks you to answer true when any value in an array appears more than once, and false when every value is unique. The result is a boolean, never an index or a count.
A: The hash set approach wins. You walk the array once and ask whether each value already sits in the set before storing it. That gives O(n) time, because each membership check is constant on average.
A: Sorting places equal values next to each other, so you only compare each value with its previous neighbour. Total cost is O(n log n) time, and the sort rewrites the original array order.
A: Two nested loops give O(n squared) time and O(1) space. Simple and correct, yet it slows down badly once the array grows past a few thousand values.
A: Adding first puts the current value into the set, so the very next check reports a duplicate that does not exist. Checking before storing keeps the logic correct.
A: On average, yes. Heavy collisions can push individual lookups higher, so the honest phrasing in an interview is constant time on average, not guaranteed constant time.
A: Sorting fits best. It needs no extra set, sorts in place, and still runs in O(n log n) time. Just confirm first that rearranging the caller’s array is acceptable.
A: No. An empty array and a one-element array both answer false, and all three approaches handle those cases naturally without any special check.
A: Slide a window of the last k values through the array, keeping them in a set and dropping anything that falls out of range. That keeps O(n) time while cutting memory to O(k).
A: Try an empty array, a single value, an array with no repeats, one where the repeat sits at the very end, and one holding negative numbers. Those five inputs catch nearly every bug here.
Let us wrap up what we covered. The Contains Duplicate problem in DSA looks like a warm-up, and it is. Yet the hash set trick hiding inside it is a real skill you will reuse constantly.
We started with brute force, comparing every pair. Correct, honest, and slow at O(n squared). Its dry run showed both faces of the approach: three comparisons on a lucky input, six on an unlucky one.
Sorting came next and pulled the cost down to O(n log n). Equal values become neighbours, so one narrow scan replaces the wide search. The price is a rearranged array and a sort you cannot skip.
Then the hash set landed us at a single pass. Remember what you have seen, ask before you store, and stop the moment a value repeats. That is O(n) time, paid for with O(n) memory.
Along the way we handled the two usual follow-ups. Limiting duplicates to k positions apart turns the set into a sliding window. Bounded values let a simple flag array replace the set entirely.
So take the pattern, not just the answer. Walk once, remember what you have already met, and ask the cheapest possible question about each new item. Once that habit feels natural, a whole family of array and string problems opens up.