Find Duplicate Characters in a String in Java (With Counts)

Find duplicate characters in a string in Java with a map, streams, an int array or sorting, plus case, emoji, first unique character and complexity.

Character counts for the string howtodoinjava, with o counted three times and a twice

To find duplicate characters in a string in Java, we count how often each character occurs, for example in a Map<Character, Long>, and keep the characters whose count is greater than one. The same counts give us the unique characters, the first non-repeated character and the number of times any character repeats.

Finding duplicate characters is a common String interview question, and it also shows up in real code, for example in a password rule that rejects too many repeats of one character, or in a coupon code generator that avoids codes such as AAB7.

The following example counts every character of howtodoinjava with a stream and prints the repeated ones.

String input = "howtodoinjava";
Map<Character, Long> counts = input.chars()
        .mapToObj(c -> (char) c)
        .collect(Collectors.groupingBy(c -> c, LinkedHashMap::new, Collectors.counting()));
                                        // {h=1, o=3, w=1, t=1, d=1, i=1, n=1, j=1, a=2, v=1}
List<Character> duplicates = counts.entrySet().stream()
        .filter(e -> e.getValue() > 1)
        .map(Map.Entry::getKey)
        .toList();                      // [o, a]

The letter o appears three times and a twice, and the LinkedHashMap lists them in the order they first appear. We build the same counts with a loop, an int array and sorting, handle case, spaces and emoji, find the first non-repeated character and compare the cost of each approach.

1. Counting Characters with a Map

A character map stores each distinct character as a key and its number of occurrences as the value. We read the string once, and for every character we either add it with a count of one or increase its count. After the loop, a count above one marks a duplicate and a count of one marks a unique character.

Character counts for the string howtodoinjava, with o counted three times and a twice
One pass over the string fills the map, and every count above one is a duplicate character

The method countChars() builds that map with Map.merge(), which stores 1 for a new key and adds 1 to the existing count otherwise. It returns an empty map for null or empty input instead of throwing.

static Map<Character, Integer> countChars(String input) {
    Map<Character, Integer> counts = new LinkedHashMap<>();
    if (input == null || input.isEmpty()) {
        return counts;
    }
    for (char c : input.toCharArray()) {
        counts.merge(c, 1, Integer::sum);
    }
    return counts;
}

From the map, we filter the entries by count to get the duplicates, the duplicates with their counts, or the characters that appear once.

Map<Character, Integer> bag = countChars("howtodoinjava");
List<Character> duplicateChars = bag.entrySet().stream()
        .filter(e -> e.getValue() > 1).map(Map.Entry::getKey).toList();    // [o, a]
Map<Character, Integer> duplicatesWithCount = new LinkedHashMap<>(bag);
duplicatesWithCount.values().removeIf(count -> count == 1);
String withCount = duplicatesWithCount.toString();                         // "{o=3, a=2}"
List<Character> uniqueChars = bag.entrySet().stream()
        .filter(e -> e.getValue() == 1).map(Map.Entry::getKey).toList();   // [h, w, t, d, i, n, j, v]

We use LinkedHashMap on purpose. A HashMap works too, but its iteration order depends on the hash codes, so the printed duplicates can come out in a different order than they appear in the text.

2. Using chars() and groupingBy() in a Stream

The method String.chars() returns an IntStream of the char values, which we turn into Character objects and group with Collectors.groupingBy(), as the intro example does. For a quick count of a single character, a filter is enough.

String input = "howtodoinjava";
long oCount = input.chars().filter(c -> c == 'o').count();             // 3
String repeated = input.chars()
        .mapToObj(c -> (char) c)
        .collect(Collectors.groupingBy(c -> c, LinkedHashMap::new, Collectors.counting()))
        .entrySet().stream()
        .filter(e -> e.getValue() > 1)
        .map(e -> e.getKey() + "=" + e.getValue())
        .collect(Collectors.joining(", "));                            // "o=3, a=2"

The stream version reads well in one chain, but it boxes every char into a Character. For long texts in a hot loop, the plain merge() loop or the array in the next section creates less garbage.

3. Counting with an int Array Instead of a Map

When the input contains only ASCII characters, we can count in an int[128] array, where the index is the character code. The array needs no boxing and no hashing, and its size stays the same for any input length, which is the usual answer to “find duplicates without a collection”.

String input = "howtodoinjava";
int[] counts = new int[128];
for (char c : input.toCharArray()) {
    if (c < 128) {
        counts++;
    }
}
StringBuilder dups = new StringBuilder();
for (char c = 0; c < 128; c++) {
    if (counts > 1) {
        dups.append(c);
    }
}
String asciiDups = dups.toString();     // "ao"

The result comes out in alphabetical order, because we read the array by index. The if check skips characters above code 127, so an accented letter does not cause an ArrayIndexOutOfBoundsException. For full Unicode text, the map is the right structure.

4. Sorting the Characters First

Another approach sorts the characters, so equal characters end up next to each other, and compares each character with its neighbor. It needs no map, but sorting costs O(n log n) time. We append a character only when it differs from the last one we added, so a character that appears three times is still reported once.

String input = "howtodoinjava";
char[] chars = input.toCharArray();
Arrays.sort(chars);                     // aadhijnoootvw
StringBuilder found = new StringBuilder();
for (int i = 1; i < chars.length; i++) {
    boolean repeat = chars[i] == chars[i - 1];
    boolean alreadyAdded = !found.isEmpty() && found.charAt(found.length() - 1) == chars[i];
    if (repeat && !alreadyAdded) {
        found.append(chars[i]);
    }
}
String sortedDups = found.toString();   // "ao"

Many online solutions use two nested loops instead and print a character on every repeat, so ooo is printed twice. The alreadyAdded check avoids that bug.

5. Ignoring Letter Case and Spaces

In a sentence, we often want J and j to count as the same letter, and spaces or punctuation not to count at all. We convert each character with Character.toLowerCase() and skip everything that is not a letter.

String title = "Java Jump Start";
Map<Character, Long> letters = title.chars()
        .filter(Character::isLetter)
        .mapToObj(c -> Character.toLowerCase((char) c))
        .collect(Collectors.groupingBy(c -> c, LinkedHashMap::new, Collectors.counting()));
List<Character> repeatedLetters = letters.entrySet().stream()
        .filter(e -> e.getValue() > 1).map(Map.Entry::getKey).toList();   // [j, a, t]

Without the filter, the two spaces would show up as a duplicate, and without the lower-case step, J would not match the j in “Jump”.

6. Counting Emoji and Other Code Points

A Java char is a UTF-16 code unit, and characters outside the Basic Multilingual Plane, such as most emoji, take two char values called a surrogate pair. Counting with chars() splits each emoji into two halves, so the counts no longer match what the user sees. The method codePoints() returns whole Unicode code points instead.

String reactions = "\uD83D\uDE00\uD83D\uDE00\uD83D\uDC4D";       // grinning, grinning, thumbs up
Map<String, Long> byCodePoint = reactions.codePoints()
        .mapToObj(Character::toString)
        .collect(Collectors.groupingBy(s -> s, LinkedHashMap::new, Collectors.counting()));
int distinctEmoji = byCodePoint.size();                        // 2
long grinningCount = byCodePoint.get("\uD83D\uDE00");          // 2
long charKeys = reactions.chars().distinct().count();          // 3

The method Character.toString(int), added in Java 11, turns a code point back into a String. The last line shows the problem with chars(), which sees three distinct values, namely one shared high surrogate and two different low surrogates.

7. Finding the First Non-Repeated Character

The first non-repeated character is the first key with a count of one in an insertion-ordered map. Interviewers ask for it right after the duplicate question, and the same countChars() method from section 1 answers it.

static Optional<Character> firstUnique(String input) {
    return countChars(input).entrySet().stream()
            .filter(e -> e.getValue() == 1)
            .map(Map.Entry::getKey)
            .findFirst();
}
Optional<Character> first = firstUnique("howtodoinjava");      // Optional[h]
Optional<Character> second = firstUnique("aabbc");              // Optional
Optional<Character> none = firstUnique("aabb");                 // Optional.empty

8. Checking a Password Rule with Character Counts

Say a sign-up form rejects passwords in which any single character appears more than three times, because such passwords are simple to guess. The rule needs the highest count in the map, which Collections.max() returns from the map values.

static boolean tooManyRepeats(String password, int limit) {
    Map<Character, Integer> counts = countChars(password);
    return !counts.isEmpty() && Collections.max(counts.values()) > limit;
}
boolean weak = tooManyRepeats("aaaa1234", 3);                  // true
boolean ok = tooManyRepeats("abca1234", 3);                    // false
boolean blank = tooManyRepeats("", 3);                         // false

The isEmpty() check matters, because Collections.max() throws a NoSuchElementException for an empty collection.

9. Map, Array or Sort: Picking a Technique

With n characters in the string and k distinct characters, the approaches differ mainly in extra memory and in the order of the result.

ApproachTimeExtra memoryResult order
LinkedHashMap with merge()O(n)O(k)First appearance
Stream with groupingBy()O(n)O(k), plus boxingFirst appearance
int[128] array (ASCII only)O(n)Fixed 128 countersAlphabetical
Sort and compare neighborsO(n log n)O(n) for the char[] copyAlphabetical
Two nested loopsO(n2)NoneFirst appearance

For most code, the map with merge() or the stream with groupingBy() is the right choice, because it handles any character and keeps the original order. The array is worth it only for ASCII input in performance-sensitive code.

10. Find Duplicate Characters in a String FAQs

The follow-up tasks change one constraint at a time, such as no collections, mixed case, removal instead of counting, or the running time.

10.1. How do we find duplicate characters without using collections?

We count in an int[128] array for ASCII text, or an int[26] array indexed by c – ‘a’ for lowercase letters, as shown in section 3. Sorting the char[] and comparing neighbors also works without a collection.

10.2. How do we remove duplicate characters from a string?

We keep the first occurrence of each character with distinct() on the code points and build a new string.

String once = "howtodoinjava".codePoints().distinct()
        .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
        .toString();                    // "howtdinjav"

10.3. Does Character.toLowerCase() work for every language?

Yes, for single characters. The method converts one character at a time and covers most cases, such as J to j. For whole words in languages with special rules, such as Turkish, we lower-case the string with toLowerCase(Locale) before counting.

10.4. What is the time complexity of finding duplicate characters?

The map and array solutions run in O(n), because they read every character once. Sorting takes O(n log n), and the nested-loop solution takes O(n2).

11. Conclusion

Finding duplicate characters comes down to counting. A LinkedHashMap filled with merge() or a stream with groupingBy() gives every count in the original order, and the duplicates, the unique characters and the first non-repeated character are each one filter away.

An int array is a fast option for ASCII text, while codePoints() keeps emoji whole. The same idea works for words, as shown in finding duplicate words in a string, and more String tasks are listed in the Java String guide.

12. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. I tried your getCharBag method in a kata at code wars. It’s called “Duplicate Encoder”, and your way of creating the map is wrong. It creates fewer values, and doesn’t count properly. You can try it for yourself.

    • Hi, Thanks for the feedback. Although, I did not understand what the issue in the function is, I have still updated the method with more robust code. I will request you to share what exactly is wrong in the function and what exactly is “Duplicate Encoder” problem.

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.