Counting patterns
Hash maps as counters: the frequency map, majority element, grouping anagrams, first unique element, duplicate detection and subarray sums equal to k.
Learning goals
- Count occurrences with merge and read the counts back
- Use sorted-letter keys to group anagrams
- Combine prefix sums with a map to count subarrays with sum k
Prerequisites
Frequency map
The **frequency map** (Map<T, Integer>) is the most common hash-table pattern: loop once, counts.merge(x, 1, Integer::sum), and then ask questions of the counts. It replaces nested loops that would otherwise compare every pair (O(n²)) with a single O(n) pass.
When the keys are small dense integers or letters, an int[] count array is faster and smaller; use a map when keys are strings, large numbers or objects. Run the animation with “Count with merge”.
1static Map<Character, Integer> letterCounts(String s) {2Map<Character, Integer> counts = new HashMap<>();3for (char c : s.toCharArray()) counts.merge(c, 1, Integer::sum);4return counts;5}
getOrDefault, merge and computeIfAbsent
Interactive · edit the inputwords
map (shown in first-insertion order; HashMap order is not guaranteed)
An empty map from a word to how many times it has appeared.
1Map<String, Integer> counts = new HashMap<>();2for (String w : words) counts.put(w, counts.getOrDefault(w, 0) + 1); // option 0: a default for a missing key3for (String w : words) counts.merge(w, 1, Integer::sum); // option 1: insert 1, or combine with the old value4Map<Integer, List<String>> byLength = new HashMap<>();5for (String w : words) byLength.computeIfAbsent(w.length(), k -> new ArrayList<>()).add(w); // option 2: create the list on first use
Live state
words
6
Execution trace (dry run table)Show 14 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | words=6 | An empty map from a word to how many times it has appeared. |
| 2 | L2 | w=to getOrDefault=0 | getOrDefault("to", 0): the key is missing, so it returns the default 0. |
| 3 | L2 | w=to count=1 | put("to", 0 + 1): two map operations (a get and a put) for one update. |
| 4 | L2 | w=be getOrDefault=0 | getOrDefault("be", 0): the key is missing, so it returns the default 0. |
| 5 | L2 | w=be count=1 | put("be", 0 + 1): two map operations (a get and a put) for one update. |
| 6 | L2 | w=or getOrDefault=0 | getOrDefault("or", 0): the key is missing, so it returns the default 0. |
| 7 | L2 | w=or count=1 | put("or", 0 + 1): two map operations (a get and a put) for one update. |
| 8 | L2 | w=not getOrDefault=0 | getOrDefault("not", 0): the key is missing, so it returns the default 0. |
| 9 | L2 | w=not count=1 | put("not", 0 + 1): two map operations (a get and a put) for one update. |
| 10 | L2 | w=to getOrDefault=1 | getOrDefault("to", 0) returns the stored 1. |
| 11 | L2 | w=to count=2 | put("to", 1 + 1): two map operations (a get and a put) for one update. |
| 12 | L2 | w=be getOrDefault=1 | getOrDefault("be", 0) returns the stored 1. |
| 13 | L2 | w=be count=2 | put("be", 1 + 1): two map operations (a get and a put) for one update. |
| 14 | L2 | return={to=2, be=2, or=1, not=1} | Final map: {to=2, be=2, or=1, not=1}. |
Majority element
A majority element appears **more than n/2 times**. Count with a map and return as soon as a count passes n/2. At most one value can exceed half the array, so the first one that does is the answer.
Exactly half is not enough: in [1, 1, 2, 2] no value exceeds 2, so there is no majority. The Boyer-Moore voting algorithm finds the majority in O(1) space, but the map version is simpler and also handles variants such as “more than n/3”.
1static int majority(int[] nums) {2Map<Integer, Integer> counts = new HashMap<>();3for (int x : nums) {4int c = counts.merge(x, 1, Integer::sum);5if (c > nums.length / 2) return x; // strictly more than half of all elements6}7return -1; // no majority8}
Best
O(1)
Average
O(n)
Worst
O(n)
Space
O(n)
Majority element with a frequency map
Interactive · edit the inputint[] arr · length 7
counts
A majority element appears more than ⌊7/2⌋ = 3 times, so it needs at least 4.
1static int majority(int[] nums) {2Map<Integer, Integer> counts = new HashMap<>();3for (int x : nums) {4int c = counts.merge(x, 1, Integer::sum);5if (c > nums.length / 2) return x; // strictly more than half of all elements6}7return -1; // no majority8}
Live state
n
7
needed
> 3
Execution trace (dry run table)Show 9 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=7 needed=> 3 | A majority element appears more than ⌊7/2⌋ = 3 times, so it needs at least 4. |
| 2 | L5 | x=2 count=1 | 2 has been seen 1 time; not above 3 yet. |
| 3 | L5 | x=2 count=2 | 2 has been seen 2 times; not above 3 yet. |
| 4 | L5 | x=1 count=1 | 1 has been seen 1 time; not above 3 yet. |
| 5 | L5 | x=1 count=2 | 1 has been seen 2 times; not above 3 yet. |
| 6 | L5 | x=2 count=3 | 2 has been seen 3 times; not above 3 yet. |
| 7 | L5 | x=3 count=1 | 3 has been seen 1 time; not above 3 yet. |
| 8 | L5 | x=2 count=4 | 2 has now been seen 4 times, more than half of the 7 elements. |
| 9 | L5 | return=2 | Majority element 2: found after 7 of 7 elements, no second pass needed. |
Group anagrams
Anagrams share exactly the same letters, so sorting the letters of a word gives a **canonical key**: eat, tea and ate all become aet. Put each word into the list stored under its key. The values of the map are the groups.
Sorting a word of length k costs O(k log k), so n words cost O(n · k log k). If words only use a–z, a 26-count signature is an alternative key that avoids the sort. A LinkedHashMap keeps groups in first-seen order, which makes the output reproducible: [[eat, tea, ate], [tan, nat], [bat]] for the classic input.
1static List<List<String>> groupAnagrams(String[] words) {2Map<String, List<String>> groups = new LinkedHashMap<>();3for (String w : words) {4char[] cs = w.toCharArray(); Arrays.sort(cs);5groups.computeIfAbsent(new String(cs), k -> new ArrayList<>()).add(w); // anagrams share the sorted key6}7return new ArrayList<>(groups.values());8}
Group anagrams by sorted key
Interactive · edit the inputwords
map: sorted key → group
Two words are anagrams exactly when their letters sorted give the same string, so the sorted word is a perfect hash key for the group.
1static List<List<String>> groupAnagrams(String[] words) {2Map<String, List<String>> groups = new LinkedHashMap<>();3for (String w : words) {4char[] cs = w.toCharArray(); Arrays.sort(cs);5groups.computeIfAbsent(new String(cs), k -> new ArrayList<>()).add(w); // anagrams share the sorted key6}7return new ArrayList<>(groups.values());8}
Live state
words
6
Execution trace (dry run table)Show 14 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | words=6 | Two words are anagrams exactly when their letters sorted give the same string, so the sorted word is a perfect hash key for the group. |
| 2 | L4 | word=eat key=aet | Sort the letters of "eat": key "aet". |
| 3 | L5 | word=eat key=aet group size=1 | Key "aet" is new: start a group with "eat". |
| 4 | L4 | word=tea key=aet | Sort the letters of "tea": key "aet". |
| 5 | L5 | word=tea key=aet group size=2 | Key "aet" exists: "tea" joins that group. |
| 6 | L4 | word=tan key=ant | Sort the letters of "tan": key "ant". |
| 7 | L5 | word=tan key=ant group size=1 | Key "ant" is new: start a group with "tan". |
| 8 | L4 | word=ate key=aet | Sort the letters of "ate": key "aet". |
| 9 | L5 | word=ate key=aet group size=3 | Key "aet" exists: "ate" joins that group. |
| 10 | L4 | word=nat key=ant | Sort the letters of "nat": key "ant". |
| 11 | L5 | word=nat key=ant group size=2 | Key "ant" exists: "nat" joins that group. |
| 12 | L4 | word=bat key=abt | Sort the letters of "bat": key "abt". |
| 13 | L5 | word=bat key=abt group size=1 | Key "abt" is new: start a group with "bat". |
| 14 | L7 | groups=3 return=[[eat, tea, ate], [tan, nat], [bat]] | 3 groups: [[eat, tea, ate], [tan, nat], [bat]]. Cost: O(n · k log k) for n words of length k. |
First unique
To find the first element that appears exactly once, count everything in a first pass, then scan the array again in order and return the first index whose count is 1. Two passes are needed because you cannot know an element is unique until you have seen the whole array.
The array order, not the map order, decides which unique element comes first, which is why the second pass walks the array and not the map.
1static int firstUnique(int[] a) {2Map<Integer, Integer> count = new HashMap<>();3for (int x : a) count.merge(x, 1, Integer::sum); // pass 1: count every value4for (int i = 0; i < a.length; i++)5if (count.get(a[i]) == 1) return i; // pass 2: first value with count 16return -1;7}
First unique element with two passes
Interactive · edit the inputint[] arr · length 7
counts
Pass 1 counts each value; pass 2 walks the array again in order and stops at the first value whose count is 1.
1static int firstUnique(int[] a) {2Map<Integer, Integer> count = new HashMap<>();3for (int x : a) count.merge(x, 1, Integer::sum); // pass 1: count every value4for (int i = 0; i < a.length; i++)5if (count.get(a[i]) == 1) return i; // pass 2: first value with count 16return -1;7}
Live state
n
7
Execution trace (dry run table)Show 13 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=7 | Pass 1 counts each value; pass 2 walks the array again in order and stops at the first value whose count is 1. |
| 2 | L3 | x=4 count=1 | Count 4: now 1. |
| 3 | L3 | x=2 count=1 | Count 2: now 1. |
| 4 | L3 | x=4 count=2 | Count 4: now 2. |
| 5 | L3 | x=1 count=1 | Count 1: now 1. |
| 6 | L3 | x=2 count=2 | Count 2: now 2. |
| 7 | L3 | x=3 count=1 | Count 3: now 1. |
| 8 | L3 | x=3 count=2 | Count 3: now 2. |
| 9 | L5 | i=0 x=4 count=2 | 4 appears 2 times: skip it. |
| 10 | L5 | i=1 x=2 count=2 | 2 appears 2 times: skip it. |
| 11 | L5 | i=2 x=4 count=2 | 4 appears 2 times: skip it. |
| 12 | L5 | i=3 x=1 count=1 | 1 appears once: it is the first unique element. |
| 13 | L5 | return=3 | Return index 3 (value 1). |
Duplicate detection
To detect a duplicate, remember what you have seen. Set.add returns false when the value was already present, so the check and the insertion happen in one expression. The scan stops at the first duplicate, so the work done is proportional to the position of that duplicate, not to n.
Alternatives: sorting first (O(n log n), no extra space beyond the sort) and, for values in a small range, a boolean array. The hash set is the O(n) general answer.
1static boolean containsDuplicate(int[] a) {2Set<Integer> seen = new HashSet<>();3for (int x : a)4if (!seen.add(x)) return true; // add returns false when x was already in the set5return false;6}
Duplicate detection with a HashSet
Interactive · edit the inputint[] arr · length 6
seen (HashSet)
An empty set. add(x) returns false when x is already stored, which answers 'seen before?' and records x in one O(1) step.
1static boolean containsDuplicate(int[] a) {2Set<Integer> seen = new HashSet<>();3for (int x : a)4if (!seen.add(x)) return true; // add returns false when x was already in the set5return false;6}
Live state
n
6
Execution trace (dry run table)Show 6 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=6 | An empty set. add(x) returns false when x is already stored, which answers 'seen before?' and records x in one O(1) step. |
| 2 | L4 | x=3 add returns=true | 3 is new: add stores it and returns true. |
| 3 | L4 | x=1 add returns=true | 1 is new: add stores it and returns true. |
| 4 | L4 | x=4 add returns=true | 4 is new: add stores it and returns true. |
| 5 | L4 | x=1 add returns=false | 1 is already in the set: add returns false, so this is a duplicate. |
| 6 | L4 | return=true | Duplicate 1 found at index 3. Return true after scanning 4 of 6 elements. |
Subarray sum equals k
Count the contiguous subarrays whose sum is k. With prefix sums, the sum of a[i..j] is prefix[j+1] - prefix[i], so a subarray ending at j has sum k exactly when an earlier prefix equals current - k. Keep a map from prefix sum to how many times it has occurred, and add map[current - k] to the answer at every step.
Seed the map with {0: 1} so subarrays that start at index 0 are counted. This handles negative numbers and zeros, which break the sliding window technique. The animation marks every element that ends a matching subarray.
Best
O(n)
Average
O(n)
Worst
O(n)
Space
O(n)
Count subarrays with sum k (prefix sums + hash map)
Interactive · edit the inputint[] arr · length 6
seen: prefix → times
A subarray a[l..r] has sum k exactly when prefix[r + 1] - prefix[l] = k. Remember how often each prefix has occurred.
1static int countSubarraysWithSum(int[] a, int k) {2Map<Integer, Integer> seen = new HashMap<>();3seen.put(0, 1); // the empty prefix4int prefix = 0, count = 0;5for (int x : a) {6prefix += x;7count += seen.getOrDefault(prefix - k, 0); // earlier prefixes that leave sum k8seen.merge(prefix, 1, Integer::sum);9}10return count;11}
Live state
prefix
0
count
0
k
3
Execution trace (dry run table)Show 14 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | prefix=0 count=0 k=3 | A subarray a[l..r] has sum k exactly when prefix[r + 1] - prefix[l] = k. Remember how often each prefix has occurred. |
| 2 | L7 | i=0 prefix=1 need=-2 hits=0 count=0 | Nothing ending at 0 has prefix -2 before it: no new match. |
| 3 | L8 | i=0 prefix=1 count=0 | Record prefix 1 (now seen 1 time). |
| 4 | L7 | i=1 prefix=3 need=0 hits=1 count=1 | prefix 3 - k 3 = 0 was seen 1 time, so 1 subarray ending at 1 sum to 3. Count = 1. |
| 5 | L8 | i=1 prefix=3 count=1 | Record prefix 3 (now seen 1 time). |
| 6 | L7 | i=2 prefix=6 need=3 hits=1 count=2 | prefix 6 - k 3 = 3 was seen 1 time, so 1 subarray ending at 2 sum to 3. Count = 2. |
| 7 | L8 | i=2 prefix=6 count=2 | Record prefix 6 (now seen 1 time). |
| 8 | L7 | i=3 prefix=3 need=0 hits=1 count=3 | prefix 3 - k 3 = 0 was seen 1 time, so 1 subarray ending at 3 sum to 3. Count = 3. |
| 9 | L8 | i=3 prefix=3 count=3 | Record prefix 3 (now seen 2 times). |
| 10 | L7 | i=4 prefix=4 need=1 hits=1 count=4 | prefix 4 - k 3 = 1 was seen 1 time, so 1 subarray ending at 4 sum to 3. Count = 4. |
| 11 | L8 | i=4 prefix=4 count=4 | Record prefix 4 (now seen 1 time). |
| 12 | L7 | i=5 prefix=5 need=2 hits=0 count=4 | Nothing ending at 5 has prefix 2 before it: no new match. |
| 13 | L8 | i=5 prefix=5 count=4 | Record prefix 5 (now seen 1 time). |
| 14 | L10 | return=4 | 4 subarrays sum to 3. One pass, O(n) time and space; negative numbers are fine, unlike a sliding window. |
Practice ladder
Now try it yourself
Easy
- Majority Element
- First Unique Character in a String
- Valid Anagram
Medium
- Group Anagrams
- Subarray Sum Equals K
- Top K Frequent Elements
Advanced
- Contiguous Array
- Count Number of Nice Subarrays
Key takeaways
- A frequency map turns nested loops into one pass: merge(x, 1, Integer::sum).
- A canonical key such as sorted letters groups equivalent items.
- Prefix sum plus a map counts subarrays with sum k in O(n), even with negatives.