Frequency counting and deduplication
Frequency maps and deduplication with HashMap, HashSet, arrays, streams, custom equality and seen-set interview patterns.
On this page
🧮 What are frequency counting and deduplication?
Frequency counting records how many times each value appears. Deduplication keeps or detects each value only once. They are sibling patterns: one stores a counter per key, the other stores membership per key, like a bouncer who either clicks a tally counter or checks a guest list.
For this lesson, we'll focus on the two most reusable hash-based patterns: counting with HashMap and arrays, then deduplicating with HashSet, LinkedHashSet, TreeSet, streams and field-based seen sets. Interviewers ask these because the code is short, but the details reveal whether you understand equality, ordering, overflow, tie-breaking and the difference between "same object" and "same key".
Below the line (out of scope)
- The bucket mechanics behind
HashMapandHashSet: How HashMap works and How HashSet, LinkedHashSet and TreeSet work - Top-K after counting, including heap tie-breakers: Top-K with a heap
- Grouping full objects rather than counting them: Grouping and partitioning
- The overview map of all seven patterns: Collections in action: the 7 patterns at a glance
The 30-second answer
For counting, I use a map from value to count and update it in one pass: merge(key, 1, Integer::sum), put(key, getOrDefault(key, 0) + 1), or compute. Collectors.groupingBy(..., counting()) is concise and returns Long counts. For lowercase English letters, an int[26] beats a HashMap<Character, Integer> because it is 26 contiguous 4-byte counters with no hashing, boxing or per-entry node objects. For deduplication, I use HashSet when order does not matter, LinkedHashSet when first-seen order matters, TreeSet when sorted unique output matters, and a separate seen-key set when uniqueness is by one field.
What you’ll learn
- Build frequency maps with
merge,getOrDefault,computeand stream collectors - Choose between
HashMap,int[]andMap<K, Long>for counting - Solve anagram, first-unique and most-frequent-value problems
- Deduplicate with
HashSet,LinkedHashSet,TreeSetanddistinct() - Deduplicate custom objects using the
equalsandhashCodecontract - Deduplicate by a field with a seen set or
Collectors.toMap - Explain complexity, tie-breaking, overflow and common mistakes in interviews
01Picture it first: a tally sheet and a guest list
Picture a batch of order IDs coming across your desk. If the question is "how many times did each ID appear?", you add marks to a tally sheet. If the question is "have I seen this ID before?", you check a guest list and add it the first time.
The two diagrams below show the same stream producing different state. Counting stores a number; dedup stores existence.
A frequency map is built step by step from api, db, api, cache, api with api ending at 3, db at 1 and cache at 1
A seen set accepts e1 and e2, rejects the second e1, then accepts e3 while preserving the idea of first-seen membership
Notice the shape of the data. In both patterns the key is the value you care about: a word, user ID, event ID, character or field extracted from an object. The difference is the value stored next to it, either a count or nothing more than membership.
02Frequency counting under the hood
Try it yourself first
Before reading on, count api, db, api, cache, api by hand. Which keys are created, and which existing key is updated twice?
Here's how I'd write the loop in production when a plain integer count is enough:
static Map<String, Integer> countServices(List<String> services) {
Map<String, Integer> frequency = new HashMap<>();
for (String service : services) {
frequency.merge(service, 1, Integer::sum);
}
return frequency;
}
Notice three things:
mergestores1when the key is absent or mapped tonull.- For an existing non-null count, it calls
Integer::sumwith the old count and1. - The map contains only distinct services, so the extra space is O(u), not O(n), where u is unique services.
The same loop with getOrDefault is a little more explicit:
static Map<String, Integer> countWithGetOrDefault(List<String> words) {
Map<String, Integer> frequency = new HashMap<>();
for (String word : words) {
int next = frequency.getOrDefault(word, 0) + 1;
frequency.put(word, next);
}
return frequency;
}
getOrDefault does not insert the default. It only gives you a value to compute with, then put writes the new count. I like this version when teaching because it shows the read, increment and write steps.
compute is useful when the update logic depends on the old value:
static Map<String, Integer> countWithCompute(List<String> words) {
Map<String, Integer> frequency = new HashMap<>();
for (String word : words) {
frequency.compute(word, (key, old) -> old == null ? 1 : old + 1);
}
return frequency;
}
For simple counting, merge is usually cleaner. compute shines when you need to remove a key by returning null, cap a value, or use more involved update logic. In concurrent code, the same method names exist on ConcurrentHashMap, but the thread-safety story is a separate lesson.
Premium lesson
Keep reading with the Java Pass
The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.
- Deduplication under the hood
- Classic interview problems solved
- Complexity and applications
- Common mistakes
- How to say it in the interview
From ₹199 · one-time payment · No auto-renewal