Frequency and counting
Counting how often each value appears turns many “compare everything with everything” problems into a single pass. Use an array when values are small integers and a HashMap when they are not.
Learning goals
- Count occurrences with a frequency array or a HashMap
- Build a histogram and find the most frequent value
- Count characters in a string with a 26-slot array
- Find distinct and duplicated values in one pass
Prerequisites
Are two words anagrams? Comparing every letter with every other letter is slow. Counting the letters of each word and comparing the counts takes one pass.
Frequency arrays
When values are integers in a small range [0, k), allocate int[] count = new int[k] and do count[x]++ for each value. Every update is O(1), the whole pass is O(n + k), and no hashing is involved.
The animation counts the values, tracks how many are distinct, and shows how many values occur more than once, all in the same pass. Counters that turn red have more than one occurrence.
Worst
O(n + k)
Space
O(k)
Frequency array: count, distinct, duplicates
Interactive · edit the inputint[] arr · length 11
count[]
One counter per possible value 0..9. Indexing by value makes each update O(1).
1public static int[] frequency(int[] arr, int maxValue) {2int[] count = new int[maxValue + 1];3int distinct = 0, duplicated = 0;4for (int x : arr) {5if (count[x] == 0) distinct++; // first time we see x6else if (count[x] == 1) duplicated++; // second time: x is a duplicate7count[x]++;8}9return count;10}
Live state
maxValue
9
distinct
0
duplicated
0
Execution trace (dry run table)Show 24 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | maxValue=9 distinct=0 duplicated=0 | One counter per possible value 0..9. Indexing by value makes each update O(1). |
| 2 | L5 | x=3 count[x]=0 distinct=1 duplicated=0 | 3 has not been seen before: a new distinct value. |
| 3 | L7 | x=3 count[x]=1 distinct=1 duplicated=0 | count[3]++ → 1. |
| 4 | L5 | x=1 count[x]=0 distinct=2 duplicated=0 | 1 has not been seen before: a new distinct value. |
| 5 | L7 | x=1 count[x]=1 distinct=2 duplicated=0 | count[1]++ → 1. |
| 6 | L5 | x=4 count[x]=0 distinct=3 duplicated=0 | 4 has not been seen before: a new distinct value. |
| 7 | L7 | x=4 count[x]=1 distinct=3 duplicated=0 | count[4]++ → 1. |
| 8 | L6 | x=1 count[x]=1 distinct=3 duplicated=1 | 1 was seen once already, so it is now a duplicated value. |
| 9 | L7 | x=1 count[x]=2 distinct=3 duplicated=1 | count[1]++ → 2. |
| 10 | L5 | x=5 count[x]=0 distinct=4 duplicated=1 | 5 has not been seen before: a new distinct value. |
| 11 | L7 | x=5 count[x]=1 distinct=4 duplicated=1 | count[5]++ → 1. |
| 12 | L5 | x=9 count[x]=0 distinct=5 duplicated=1 | 9 has not been seen before: a new distinct value. |
| 13 | L7 | x=9 count[x]=1 distinct=5 duplicated=1 | count[9]++ → 1. |
| 14 | L5 | x=2 count[x]=0 distinct=6 duplicated=1 | 2 has not been seen before: a new distinct value. |
| 15 | L7 | x=2 count[x]=1 distinct=6 duplicated=1 | count[2]++ → 1. |
| 16 | L5 | x=6 count[x]=0 distinct=7 duplicated=1 | 6 has not been seen before: a new distinct value. |
| 17 | L7 | x=6 count[x]=1 distinct=7 duplicated=1 | count[6]++ → 1. |
| 18 | L6 | x=5 count[x]=1 distinct=7 duplicated=2 | 5 was seen once already, so it is now a duplicated value. |
| 19 | L7 | x=5 count[x]=2 distinct=7 duplicated=2 | count[5]++ → 2. |
| 20 | L6 | x=3 count[x]=1 distinct=7 duplicated=3 | 3 was seen once already, so it is now a duplicated value. |
| 21 | L7 | x=3 count[x]=2 distinct=7 duplicated=3 | count[3]++ → 2. |
| 22 | L6 | x=5 count[x]=2 distinct=7 duplicated=3 | 5 was already counted as duplicated; only its count grows. |
| 23 | L7 | x=5 count[x]=3 distinct=7 duplicated=3 | count[5]++ → 3. |
| 24 | L9 | distinct=7 duplicated=3 most frequent=5 | 7 distinct values, 3 values appearing more than once, most frequent is 5 (3 times). One pass: O(n + k) time. |
Frequency maps
For large, negative or non-integer keys (strings, objects), use a HashMap<K, Integer>. merge(key, 1, Integer::sum) increments in one call, and getOrDefault(key, 0) reads a count safely.
Expected O(1) per update, O(n) per pass. Iteration order of a HashMap is unspecified; use TreeMap if you need keys in sorted order, or LinkedHashMap to keep first-seen order.
1Map<String, Integer> freq = new HashMap<>();2for (String word : words) {3freq.merge(word, 1, Integer::sum);4}5int apples = freq.getOrDefault("apple", 0);67// one-liner with streams8Map<String, Long> counts = Arrays.stream(words)9.collect(Collectors.groupingBy(w -> w, Collectors.counting()));
Average
O(n)
Space
O(distinct keys)
Histograms
A histogram is a frequency table drawn as bars. It answers questions such as “which value is most common?”, “how many values fall in each bucket?” and “what is the mode?”.
To bucket continuous data, map each value to a bucket index first, for example (score / 10) for scores 0-100, then count the bucket indices.
1static int mode(int[] arr, int maxValue) {2int[] count = new int[maxValue + 1];3for (int x : arr) count[x]++;4int best = 0;5for (int v = 1; v <= maxValue; v++) {6if (count[v] > count[best]) best = v; // ties keep the smaller value7}8return best;9}
Character counts
For lowercase English letters, int[26] indexed by c - 'a' is the fastest counter. Two strings are anagrams exactly when their 26 counts match: increment for one string, decrement for the other, and check that everything is zero.
For general Unicode text, use a HashMap<Integer, Integer> over code points (str.codePoints()), because a char cannot represent every character.
1static boolean isAnagram(String s, String t) {2if (s.length() != t.length()) return false;3int[] count = new int[26];4for (int i = 0; i < s.length(); i++) {5count[s.charAt(i) - 'a']++;6count[t.charAt(i) - 'a']--;7}8for (int c : count) if (c != 0) return false;9return true;10}
Worst
O(n)
Space
O(1) (26 counters)
Distinct elements
The number of distinct elements is the number of non-zero counters, or simply the size of a HashSet built from the values. Increment a distinct counter the first time a value's count goes from 0 to 1.
Arrays.stream(arr).distinct().count() does the same with a hash set internally. Sorting and counting value changes works too, in O(n log n) with O(1) extra space.
1static int countDistinct(int[] arr) {2Set<Integer> seen = new HashSet<>();3for (int x : arr) seen.add(x);4return seen.size();5}
Counting duplicates
A value is duplicated when its count reaches 2. Detecting that moment (the count goes from 1 to 2) counts each duplicated value once, no matter how many copies it has. Counting every extra copy instead gives n - distinct.
To return early on the first duplicate, Set.add returns false when the value is already present.
1static boolean containsDuplicate(int[] arr) {2Set<Integer> seen = new HashSet<>();3for (int x : arr) {4if (!seen.add(x)) return true; // add() returns false if x was already there5}6return false;7}
Practice ladder
Now try it yourself
Easy
- Valid anagram
- First unique character in a string
- Contains duplicate
Medium
- Top K frequent elements
- Group anagrams
- Sort characters by frequency
Advanced
- Longest palindrome that can be built from given letters
- Find all duplicates in an array of 1..n using O(1) extra space
- Majority element in O(1) space (Boyer-Moore voting)
Key takeaways
- A frequency array gives O(1) updates for small integer keys; a HashMap handles any key.
- Character counts with int[26] solve anagram and permutation checks in O(n).
- Distinct and duplicate counts fall out of the same counting pass.