Introduction 

Finding duplicate numbers in an array is one of the most frequently asked questions in Java coding interviews, precisely because it offers such a clear, teachable progression from a naive O(n²) brute-force solution to an optimal O(n) approach using a HashSet — exactly the kind of algorithmic optimization story interviewers love to walk through with candidates.

This guide covers the brute-force nested-loop approach, a sorting-based method, the optimal HashSet approach, a HashMap-based variant that also counts how many times each duplicate appears, and a modern Streams-based alternative — giving you a complete toolkit for this classic problem along with a clear understanding of each approach's time and space trade-offs.


Understanding the Problem: Detecting vs Counting Duplicates 

There are two related but distinct versions of this problem worth distinguishing upfront:

Advertisement
  • Simply identifying which values are duplicated (regardless of how many times).
  • Counting exactly how many times each duplicate value appears.

Both are covered in this guide, since real interview questions and practical use cases ask for either version depending on context.


Method 1: Brute Force Using Nested Loops

The most straightforward approach compares every element against every other element.

 
public class DuplicatesBruteForce {
    public static void main(String[] args) {
        int[] arr = {4, 3, 6, 2, 8, 3, 6, 9};

        System.out.println("Duplicate numbers:");
        for (int i = 0; i < arr.length; i++) {
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[i] == arr[j]) {
                    System.out.print(arr[i] + " ");
                    break;
                }
            }
        }
    }
}
 

How this works

The outer loop picks each element, and the inner loop checks every element that comes after it (starting from i + 1, avoiding redundant comparisons and self-comparison) for a match. The break statement prevents printing the same duplicate value multiple times if it appears more than twice.

Output:

 
Duplicate numbers:
3 6
 

The problem: this approach runs in O(n²) time, since every element is compared against nearly every other element — for an array of 10,000 elements, that's roughly 100 million comparisons, clearly impractical at scale.


Method 2: Using Sorting

Sorting the array first brings duplicate values next to each other, allowing a single linear pass to detect them.

 
import java.util.Arrays;

public class DuplicatesSorting {
    public static void main(String[] args) {
        int[] arr = {4, 3, 6, 2, 8, 3, 6, 9};
        Arrays.sort(arr);

        System.out.println("Duplicate numbers:");
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] == arr[i - 1]) {
                System.out.print(arr[i] + " ");
            }
        }
    }
}
 

How this works

After sorting, the array becomes:

 
{2, 3, 3, 4, 6, 6, 8, 9}
 

Any duplicate values become adjacent to each other. The loop simply checks whether each element equals the one immediately before it, printing it if so.

Output:

 
Duplicate numbers:
3 6
 

Time complexity: Arrays.sort() runs in O(n log n), and the subsequent linear scan is O(n) — giving an overall O(n log n) time complexity, a meaningful improvement over brute force's O(n²), though this approach does modify the original array's order (a consideration if the original ordering needs to be preserved).


Method 3: Using a HashSet (Optimal Approach) 

The most efficient standard approach uses a HashSet to track values already seen, achieving O(n) time complexity.

 
import java.util.HashSet;

public class DuplicatesHashSet {
    public static void main(String[] args) {
        int[] arr = {4, 3, 6, 2, 8, 3, 6, 9};
        HashSet<Integer> seen = new HashSet<>();
        HashSet<Integer> duplicates = new HashSet<>();

        for (int num : arr) {
            if (!seen.add(num)) {
                duplicates.add(num);
            }
        }

        System.out.println("Duplicate numbers: " + duplicates);
    }
}
 

How this works

seen.add(num) attempts to add each number to the seen set, returning false if that value was already present (since HashSet doesn't allow duplicates).

When add() returns false, we know we've encountered a repeat, so it's added to a separate duplicates set (itself a HashSet, automatically avoiding printing the same duplicate value more than once even if it repeats three or more times).

Output:

 
Duplicate numbers: [3, 6]
 

Why this is optimal

HashSet operations (add(), contains()) run in O(1) average time, making the entire algorithm O(n) overall — a single pass through the array, with constant-time work per element.

This is generally the expected, ideal answer in technical interviews for this exact problem.

 

Method 4: Using a HashMap to Count Occurrences 

When you need to know not just which values are duplicated, but how many times each one appears, a HashMap tracking counts is the right tool.

 
import java.util.HashMap;

public class DuplicatesHashMapCount {
    public static void main(String[] args) {
        int[] arr = {4, 3, 6, 2, 8, 3, 6, 3, 9};
        HashMap<Integer, Integer> countMap = new HashMap<>();

        for (int num : arr) {
            countMap.put(num, countMap.getOrDefault(num, 0) + 1);
        }

        System.out.println("Occurrence counts:");
        for (var entry : countMap.entrySet()) {
            if (entry.getValue() > 1) {
                System.out.println(entry.getKey() + " appears " + entry.getValue() + " times");
            }
        }
    }
}
 

How this works

countMap.getOrDefault(num, 0) + 1 retrieves the current count for a number (defaulting to 0 if it's the first occurrence), increments it by one, and stores the updated count back into the map — building a complete frequency table in a single pass, from which we then filter and print only the entries with a count greater than 1.

Output:

 
Occurrence counts:
3 appears 3 times
6 appears 2 times
 

Method 5: Using Java Streams 

For a modern, declarative approach, Java Streams (combined with Collectors.groupingBy()) can express the counting logic concisely.

 
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;

public class DuplicatesStreams {
    public static void main(String[] args) {
        Integer[] arr = {4, 3, 6, 2, 8, 3, 6, 3, 9};

        Map<Integer, Long> countMap = Arrays.stream(arr)
                .collect(Collectors.groupingBy(num -> num, Collectors.counting()));

        countMap.entrySet().stream()
                .filter(entry -> entry.getValue() > 1)
                .forEach(entry -> System.out.println(
                        entry.getKey() + " appears " + entry.getValue() + " times"));
    }
}
 

How this works

Collectors.groupingBy(num -> num, Collectors.counting()) groups identical numbers together and counts occurrences within each group, producing a Map<Integer, Long> mapping each number to its frequency — functionally equivalent to Method 4's HashMap logic, expressed in a more declarative style.

Note: The array must use the Integer wrapper type (not primitive int) for Arrays.stream() to work with Collectors.groupingBy() directly.

Output:

 
3 appears 3 times
6 appears 2 times
 

How Java Handles This Internally (Memory Concept) 

  • Method 1: No additional data structure is needed beyond the original array — all comparisons happen directly on the existing int[] in place, making it the most memory-efficient (O(1) extra space) despite being the slowest in time.
  • Method 2: Arrays.sort() typically sorts primitive arrays in place (using a dual-pivot quicksort variant for primitives), so no significant extra heap allocation occurs beyond the sorting algorithm's internal recursion.
  • Methods 3 and 4: HashSet and HashMap are both heap-allocated data structures, internally using a hash table structure to achieve average O(1) lookup and insertion — this is the space trade-off (O(n) extra memory) for achieving O(n) time complexity.

Real-Life Analogy: Spotting Repeat Guests at a Party Check-In 

Imagine checking guests into a party by name, keeping a running list of everyone who's already checked in. Each time a new guest arrives, you check whether their name is already on your list — if it is, they're a "duplicate" (perhaps someone trying to check in twice, or a name mix-up worth flagging), and if not, you add them to the list and let them in normally.

This is exactly the HashSet approach: maintaining a running "seen" record and flagging anything that's already there, all in a single pass through the arriving guests, rather than repeatedly cross-checking the entire guest list against itself for every single arrival (the much slower brute-force equivalent).


Comparison Table of All Methods

Method Time Complexity Space Complexity Best Used When
Brute Force O(n²) O(1) Very small arrays, memory-constrained contexts
Sorting O(n log n) O(1) to O(log n) (sort overhead) When modifying array order is acceptable
HashSet O(n) O(n) Standard, optimal, interview-expected solution
HashMap with Counts O(n) O(n) Needing occurrence counts, not just detection
Streams O(n) O(n) Modern, declarative-style codebases

Best Practices 

  • Use the HashSet approach (Method 3) as your default for simply identifying duplicate values — it's the optimal, interview-expected O(n) solution.
  • Use the HashMap approach (Method 4) specifically when you need occurrence counts, not just a list of which values are duplicated.
  • Consider the sorting approach (Method 2) only when you're already sorting the array for other reasons, or when the O(1) extra space (excluding sort overhead) genuinely matters more than avoiding the O(n log n) time cost.
  • Avoid the brute-force approach (Method 1) in production code for anything beyond very small arrays, given its O(n²) time complexity.
  • Use Java Streams (Method 5) when your codebase already favors a declarative style, remembering that it requires the Integer wrapper type rather than primitive int[].
 

Common Mistakes Beginners Make

  • Defaulting to the brute-force approach without recognizing the significant performance improvement available through HashSet-based detection.
  • Using a single HashSet to both track "seen" values and store duplicates, mixing up the two responsibilities and often producing incorrect or confusing results — Method 3's approach of using two separate sets cleanly avoids this.
  • Forgetting getOrDefault() when building a HashMap-based frequency count, instead writing more verbose (and error-prone) manual null-checking logic.
  • Modifying the original array order via sorting without realizing this side effect, when the original order needed to be preserved for later use.
  • Attempting to use Arrays.stream() directly with a primitive int[] for Streams-based grouping, not realizing Collectors.groupingBy() requires the Integer wrapper type.

Expert Tips for Interviews

A strong, complete interview answer sounds like this:

"The brute-force approach compares every pair of elements, giving O(n²) time. A better approach sorts the array first, bringing duplicates adjacent to each other, achieving O(n log n) time with a single linear scan afterward. The optimal solution uses a HashSet, adding each element and checking if it was already present — since HashSet operations are O(1) on average, this achieves O(n) time overall, at the cost of O(n) additional space. If I also needed to know how many times each duplicate appears, I'd use a HashMap to build a frequency count instead of just a HashSet."

Walking through all three complexity tiers (brute force, sorting, HashSet) in order, explicitly stating each one's time complexity, demonstrates the kind of structured algorithmic thinking interviewers specifically want to see in this classic question.


Pros and Cons 

Brute Force

Pros

  • ✅ No extra memory needed

Cons

  • ❌ O(n²) — impractical for large arrays

Sorting

Pros

  • ✅ O(n log n), no significant extra memory

Cons

  • ❌ Modifies the original array's order

HashSet

Pros

  • ✅ O(n) time — optimal for detection

Cons

  • ❌ O(n) extra memory

HashMap with Counts

Pros

  • ✅ Provides occurrence counts, not just detection

Cons

  • ❌ O(n) extra memory, same as HashSet

Frequently Asked Questions

1. What is the most efficient way to find duplicate numbers in an array in Java?

Using a HashSet to track values already seen achieves O(n) time complexity — the optimal standard solution for this problem.

2. How do I find duplicates without using extra memory?

Sort the array first (which can typically be done in place with O(1) extra space beyond the sort's internal overhead), then scan through it checking for adjacent equal elements.

3. What is the time complexity of the brute-force approach for finding duplicates?

O(n²), since it compares every element against every other element using nested loops.

4. How do I count how many times each duplicate appears, not just detect them?

Use a HashMap to build a frequency count for each element, then filter for entries where the count exceeds 1.

5. Does sorting the array affect the original array's order?

Yes. Sorting rearranges the array's elements into ascending order, which may not be desirable if the original ordering needs to be preserved for other purposes.

6. Can I find duplicates using Java Streams?

Yes. Use Collectors.groupingBy() combined with Collectors.counting() to build a frequency map, then filter for entries with a count greater than 1. This requires the Integer wrapper type rather than a primitive int[].

7. What is the space complexity of the HashSet-based approach?

O(n), since in the worst case (no duplicates at all), every element needs to be stored in the HashSet.

8. Is finding duplicates in an array a common interview question?

Yes. It is extremely common because it clearly demonstrates the progression from brute-force thinking to optimal algorithmic solutions using appropriate data structures.

9. How do I find duplicates in an array of Strings instead of integers?

The same HashSet or HashMap-based approaches work identically for String arrays, since both collections support any object type.

10. What's the difference between using a HashSet and a HashMap for this problem?

A HashSet only tells you whether a value has been seen before, making it ideal for simple duplicate detection. A HashMap additionally tracks how many times each value occurs, making it useful when occurrence counts are required.

11. Can duplicates be found in a single pass through the array?

Yes. Both the HashSet and HashMap approaches process the array in a single O(n) pass, unlike brute force or sorting-based methods.

12. Does this approach work for arrays containing negative numbers?

Yes. HashSet, HashMap, and sorting-based approaches work the same way for positive numbers, negative numbers, and zero.