Contains Duplicate problem in DSA: Brute Force, Sorting, and the HashSet Trick

  • Last Updated: July 12, 2026
  • By: javahandson
  • Series
img

Contains Duplicate problem in DSA: Brute Force, Sorting, and the HashSet Trick

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.

1. Introduction

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.

1.1 What This Article Covers

Here is the road map for the rest of the guide.

  • A plain statement of the problem, plus the detail beginners usually overthink.
  • Two small ideas, a hash set and sorted adjacency, that unlock every fast solution.
  • Three approaches, each with pseudocode, code, a dry run, and its real cost.
  • A side-by-side table, and a short guide on which one to reach for.
  • Two follow-up questions interviewers like to bolt on afterwards.
  • The mistakes that cost people the offer, and a small program that ties it together.
  • Ten interview questions with short, quotable answers.

2. Understanding the Problem

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.

2.1 The Rules in Plain Words

Every version of this question comes down to the same handful of rules.

  • You receive an array of integers, for example [1, 2, 3, 1].
  • Your job is to answer true when any value appears at least twice.
  • False comes back only when every single value is unique.
  • An empty array or a one-element array has no duplicate, so both answer false.
  • Values may repeat any number of times, and two occurrences already settle it.

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.

2.2 A First Example

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.

2.3 What the Answer Is Not

Beginners tend to solve a harder problem than the one on the page. Guard against that.

  • You do not return the duplicate value itself.
  • Positions of the repeats never appear in the answer.
  • Counting how many times a value repeats is wasted work here.
  • The original order of the array does not need to survive, unless the caller says otherwise.

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.

3. Concepts You Need Here

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.

3.1 A Hash Set

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.

3.2 Sorting for Adjacency

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.

3.3 Why Position Does Not Matter Here

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.

4. Approach 1: Brute Force

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.

4.1 Pseudocode

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 found

The 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.

4.2 Java Code

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
    }
}

4.3 Why This Works, Step by Step

Let us walk through the logic slowly. Each part has one clear job.

  • The outer index marks the first number in a pair.
  • Our inner index marks a number that sits after it, so every later value gets tested.
  • Inside the loop we compare the two values, and a match means a repeat.
  • Spotting a match ends everything immediately, because one duplicate settles the question.
  • Finishing both loops with no match means every value was unique, so the answer is false.

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.

4.4 Dry Run of the Brute Force

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.

4.5 Time and Space Cost

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.

5. Approach 2: Sorting First

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.

5.1 Pseudocode

sort(nums)
for i from 1 to n-1:
    if nums[i] == nums[i-1]:
        return true
return false

Notice 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.

5.2 Java Code

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
    }
}

5.3 Reading the Code

  • First we sort the array, which reorders the values from smallest to largest.
  • Then the loop walks from the second position onward, comparing each value with the one before it.
  • Since duplicates now sit side by side, a single neighbour check is enough to catch them.
  • Reaching the end with no match proves every value was unique.

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.

5.4 Dry Run of the Sorting Approach

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.

5.5 Time and Space Cost

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).

6. Approach 3: The Hash Set Trick

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.

6.1 Pseudocode

seen = empty set
for each num in nums:
    if num is in seen:
        return true
    add num to seen
return false

Read 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.

6.2 Java Code

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
    }
}

6.3 Reading the Code Line by Line

This version does the most thinking per line, so let us break it down gently.

  • We create an empty set called seen, which holds every value we have looked at so far.
  • The loop runs once through the array, and no inner loop appears anywhere, which is the whole point.
  • For each number we ask the set whether it already holds that value, and a yes ends the method.
  • Otherwise the number joins the set, and we continue to the next one.
  • Falling out of the loop means nothing repeated, so the answer is false.

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.

6.4 A Shorter Version

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.

6.5 Time and Space Cost

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.

7. Dry Run of the Hash Set Approach

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.

7.1 The Same Dry Run on Paper

Contains Duplicate problem in DSA hash set dry run

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.

7.2 A Run With No Duplicates

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.

8. Comparing the Three Approaches

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.

8.1 Which One Should You Pick?

Context decides, and here is a short guide.

  • Reach for the hash set by default, because O(n) time is the best available here.
  • Switch to sorting when memory is tight and rearranging the input is acceptable.
  • Keep brute force for tiny arrays, or as your opening move on a whiteboard.
  • Copy the array before sorting whenever the caller still depends on the original order.

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.

9. Two Common Follow-Ups

Solve the base problem quickly and a follow-up usually arrives. These two show up most often, and both reuse ideas you already have.

9.1 Duplicates Within a Distance

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.

9.2 When the Values Fit a Small Range

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.

10. Common Mistakes and Pitfalls

A handful of small traps catch beginners on this problem. Read them once and you will dodge most of them.

  • Adding to the set before checking it. That reports a duplicate on every value, and it is the single most common bug here.
  • Starting the neighbour loop at 0 in the sorting approach. Position 0 has no previous value, so the comparison reads past the front of the array.
  • Forgetting that sorting rewrites the caller’s array. Copy it first whenever the original order still matters.
  • Starting the inner brute-force loop at 0 instead of i plus 1. Every value then matches itself, and the answer is always true.
  • Quoting the neighbour scan as the cost of the sorting approach. The sort dominates, so the honest answer is O(n log n).
  • Claiming a hash set is O(1) in the worst case. Constant time is the average, and heavy collisions can push it higher.
  • Hunting for the duplicate’s position. The question asks for true or false, and nothing more.

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.

11. Practical Walkthrough: A Duplicate Checker

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.

11.1 The Program

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.

11.2 What the Output Tells You

Three inputs, three different shapes of answer. That is the point of the exercise.

  • An array with a repeat names the guilty value, which makes the result easy to trust.
  • Unique values produce a plain confirmation rather than a bare false.
  • The one-element array never reaches the loop, because the guard catches it first.

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.

12. Interview Questions

Q: What is the Contains Duplicate problem in DSA?

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.

Q: What is the fastest way to solve Contains Duplicate?

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.

Q: How does the sorting approach work?

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.

Q: What is the time complexity of the brute force solution?

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.

Q: In the set solution, why check before adding?

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.

Q: Is a hash set really constant time?

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.

Q: Which approach should I use when memory is tight?

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.

Q: Does an empty array contain a duplicate?

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.

Q: How do I find duplicates that sit within k positions of each other?

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).

Q: Which edge cases should I test before I say I am done?

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.

13. Conclusion

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.

14. Further Reading

Leave a Comment