Back to Stream: filter
Q17Question 17 of 19

Find duplicate characters in a string using groupingBy + counting and filtering count > 1

Key Concepts
  • Two-phase pattern: count characters with groupingBy + counting, then filter entries with count greater than 1
  • Use LinkedHashMap to preserve first-seen order of duplicate characters
  • chars() returns IntStream of char values as int; cast to char for the grouping key
  • counting() returns Long, so compare against the long literal 1L
  • Decide case sensitivity and whether non-letters are counted before building the frequency map
  • This count-then-filter pattern generalizes to any 'appears more than N times' query

Definition

Count each character with groupingBy and counting, then filter entries whose count exceeds one to find the characters that appear more than once.
DIAGRAM
- **Phase 1**: `chars().mapToObj(c -> (char) c).collect(groupingBy(c -> c, LinkedHashMap::new, counting()))` builds a frequency map.
- **Phase 2**: `entrySet().stream().filter(e -> e.getValue() > 1).map(Entry::getKey)` extracts duplicate characters.
- **LinkedHashMap** preserves first-appearance order so duplicates come out in reading order.
- **counting()** returns Long values, so the comparison uses the long literal.
- **Generalizable** to any 'more than N' threshold by changing the filter condition.

Priya at the National Bank of India tallies each character in a reference code, then lists only the characters that showed up two or more times. A repetitive code like SAV2SAV would flag S, A, and V as duplicates.

Understanding the Concepts

What does this question ask?

The interviewer wants you to find characters that appear more than once in a string. This tests the count-then-filter pattern with groupingBy, counting, and entry filtering.

How do you find duplicate characters?

JAVA
package com.bank.core;

import java.util.*;
import java.util.stream.Collectors;

public class DuplicateCharacters {
    public static void main(String[] args) {
        String accountRef = "NATIONAL BANK";

        Map<Character, Long> frequency = accountRef.chars()
            .mapToObj(c -> Character.toLowerCase((char) c))
            .filter(Character::isLetter)
            .collect(Collectors.groupingBy(
                c -> c,
                LinkedHashMap::new,
                Collectors.counting()));

        List<Character> duplicates = frequency.entrySet().stream()
            .filter(e -> e.getValue() > 1)
            .map(Map.Entry::getKey)
            .toList();

        System.out.println("Frequency: " + frequency);
        System.out.println("Duplicates: " + duplicates);
    }
}
TEXT
Frequency: {n=3, a=3, t=1, i=1, o=1, l=1, b=1, k=1}
Duplicates: [n, a]

Why use LinkedHashMap instead of HashMap?

HashMap has no ordering guarantee, so the duplicates would appear in an unpredictable order. LinkedHashMap preserves insertion order, which matches the first-appearance order of each character in the string.

JAVA
// HashMap: duplicates could be [a, n] or [n, a]
Map<Character, Long> unordered = accountRef.chars()
    .mapToObj(c -> (char) c)
    .collect(Collectors.groupingBy(c -> c, Collectors.counting()));

// LinkedHashMap: duplicates are always in first-seen order
Map<Character, Long> ordered = accountRef.chars()
    .mapToObj(c -> (char) c)
    .collect(Collectors.groupingBy(
        c -> c, LinkedHashMap::new, Collectors.counting()));

How do you handle case sensitivity?

Decide before counting whether 'A' and 'a' are the same character.

JAVA
// Case-insensitive: lowercase before grouping
Map<Character, Long> caseInsensitive = accountRef.chars()
    .mapToObj(c -> Character.toLowerCase((char) c))
    .filter(Character::isLetter)
    .collect(Collectors.groupingBy(c -> c, Collectors.counting()));

// Case-sensitive: no lowercasing
Map<Character, Long> caseSensitive = accountRef.chars()
    .mapToObj(c -> (char) c)
    .filter(Character::isLetter)
    .collect(Collectors.groupingBy(c -> c, Collectors.counting()));

How is this different from finding duplicate integers?

DIAGRAM
| Aspect | Duplicate characters | Duplicate integers |
|---|---|---|
| Source | String.chars() | List.stream() |
| Classifier | c -> c (Character) | n -> n (Integer) |
| Pre-processing | Case normalization, filter non-letters | None typically |
| Result | Set of characters | Set of integers |

The pattern is identical: count, then filter for count greater than one.

What are common mistakes?

Mistake 1: forgetting to filter non-letter characters.

JAVA
// WRONG: spaces counted as duplicates
String text = "NATIONAL BANK";
Map<Character, Long> wrong = text.chars()
    .mapToObj(c -> (char) c)
    .collect(Collectors.groupingBy(c -> c, Collectors.counting()));
// Space ' ' appears with count 1, but digits/punctuation could inflate counts

// CORRECT: filter letters first
Map<Character, Long> correct = text.chars()
    .mapToObj(c -> Character.toLowerCase((char) c))
    .filter(Character::isLetter)
    .collect(Collectors.groupingBy(c -> c, Collectors.counting()));

Mistake 2: comparing Long with == instead of > for the threshold.

JAVA
// WRONG: == 2 finds only characters appearing exactly twice
.filter(e -> e.getValue() == 2)

// CORRECT: > 1 finds all characters appearing more than once
.filter(e -> e.getValue() > 1)

What about finding the count of each duplicate?

JAVA
Map<Character, Long> duplicatesWithCount = frequency.entrySet().stream()
    .filter(e -> e.getValue() > 1)
    .collect(Collectors.toMap(
        Map.Entry::getKey,
        Map.Entry::getValue,
        (a, b) -> a,
        LinkedHashMap::new));

duplicatesWithCount.forEach((ch, count) ->
    System.out.println("'" + ch + "' appears " + count + " times"));
TEXT
'n' appears 3 times
'a' appears 3 times

What are common interview follow-up questions?

  • "How do you find the first non-repeating character?" Filter for count == 1 and take findFirst from the LinkedHashMap.
  • "How do you find characters appearing exactly N times?" Change the filter to e.getValue() == n.
  • "What if the string contains Unicode characters beyond BMP?" Use codePoints() instead of chars() and group by Integer code points.

Visual Overview

DIAGRAM
  String: "NATIONAL BANK"
       │
       │  .chars() → IntStream
       │  .mapToObj(toLowerCase)
       │  .filter(isLetter)
       ▼
  Phase 1: groupingBy + counting
  ┌──────────────────────────┐
  │  n → 3                   │
  │  a → 3                   │
  │  t → 1                   │
  │  i → 1                   │
  │  o → 1                   │
  │  l → 1                   │
  │  b → 1                   │
  │  k → 1                   │
  └──────────────────────────┘
       │
  Phase 2: filter(count > 1)
       ▼
  Duplicates: [n, a]

Key Takeaway

Finding duplicate characters uses the two-phase count-then-filter pattern. First, count each character with groupingBy and counting into a LinkedHashMap for first-seen order. Then stream the entries, filter to those whose count is greater than one, and collect the keys.

The LinkedHashMap is the correctness detail that gives duplicates in the order they first appeared in the string, which reads naturally. At the National Bank of India, Rajesh used this in a code-strength rule to discourage repetitive, weak reference codes that reuse the same characters.

The senior point is that this count-then-filter shape is completely general. It answers any "appears more than N times" question, not just duplicates, and it applies to any collection of elements, not only characters. The two decisions to make before counting are case sensitivity and whether non-letters should participate.

Remember one sentence: count characters with groupingBy and counting into a LinkedHashMap, then filter entries where count exceeds one to find duplicates in first-seen order.

---

Share
Use ← → arrow keys to navigate