Topics › Basics › Learn Basic Hashing
Counting Frequencies of Array Elements
Java source ↗ Edit this article ↗
Jump to code:
Problem
Given an array of N integers, report how many times each distinct value occurs. Print each value together with its count, in the order in which the values first appear in the array.
The classic version of this problem (the one with an elegant O(1) extra-space answer) adds the constraint that every element lies in [1, N]. We handle the general case with hashing and then show that special-case trick, because it reappears in several “find the missing/duplicate number” problems.
Constraints: 1 ≤ N ≤ 10^5, values fit in a 32-bit signed integer. The methods return a Map<Integer, Integer> (value to count) whose iteration order is the order of first appearance, so the tests can compare both the counts and the order.
Example 1
arr = [10, 5, 10, 15, 10, 5]
answer = 10 -> 3, 5 -> 2, 15 -> 1 // 10 appears three times, 5 twice, 15 once
Example 2
arr = [2, 3, 2, 3, 5, 1, 4]
answer = 2 -> 2, 3 -> 2, 5 -> 1, 1 -> 1, 4 -> 1 // values are in [1, 7], the special case
Example 3
arr = [1, 1, 1, 1]
answer = 1 -> 4 // a single distinct value
Approach 1: Brute force with a visited array
Intuition. For each element, scan the rest of the array and count copies. To avoid reporting the same value again when we reach its second copy, mark every copy as visited while counting; later, skip visited positions. The order of first appearance is preserved for free because the outer loop runs left to right.
Algorithm.
- Create
visited[0..n-1], allfalse, and an empty ordered map. - For each
inot yet visited: setcount = 1; for everyj > iwitharr[j] == arr[i], markvisited[j]and incrementcount. - Record
arr[i] -> count.
static Map<Integer, Integer> bruteForce(int[] arr) {
int n = arr.length;
boolean[] visited = new boolean[n];
Map<Integer, Integer> freq = new LinkedHashMap<>(); // insertion order = order of first appearance
for (int i = 0; i < n; i++) {
if (visited[i]) continue; // already counted as a copy of an earlier element
int count = 1;
for (int j = i + 1; j < n; j++) {
if (arr[j] == arr[i]) {
visited[j] = true;
count++;
}
}
freq.put(arr[i], count);
}
return freq;
}
Complexity. Time O(n^2) in the worst case (all elements distinct: every outer iteration scans the whole remainder). Space O(n) for the visited flags, plus the output.
Approach 2: Counting array
Intuition. If the values are non-negative and not too large, let the value be an index: count[v]++ for every element, then every count is available in one read. This is array hashing. A second pass over the original array, inserting each value into the result the first time it is encountered, restores the order of first appearance.
Algorithm.
- Find
maxValueand allocatecount[0..maxValue]. - For each
vinarr,count[v]++. - For each
vinarragain: ifvis not yet in the result, recordv -> count[v].
static Map<Integer, Integer> countingArray(int[] arr) {
int maxValue = 0;
for (int v : arr) maxValue = Math.max(maxValue, v);
int[] count = new int[maxValue + 1];
for (int v : arr) count[v]++; // one pass to count
Map<Integer, Integer> freq = new LinkedHashMap<>();
for (int v : arr) {
if (!freq.containsKey(v)) freq.put(v, count[v]); // report each value once, at its first appearance
}
return freq;
}
Complexity. Time O(n + maxValue). Space O(maxValue) for the counting array. This is the fastest option when maxValue is at most around 10^6; it fails outright for negative values and wastes memory when values are large and sparse.
Approach 3: Hash map
Intuition. A HashMap gives the same “value in, count out” interface without reserving a slot for every possible value, so it handles negative, huge and sparse keys. merge(v, 1, Integer::sum) inserts 1 for a new key and adds 1 to an existing count. Using LinkedHashMap makes the iteration order equal to insertion order, which is exactly the order of first appearance, so no second pass is needed.
Algorithm.
- Create an empty
LinkedHashMap. - For each
vinarr,freq.merge(v, 1, Integer::sum). - Return the map.
static Map<Integer, Integer> hashMap(int[] arr) {
Map<Integer, Integer> freq = new LinkedHashMap<>();
for (int v : arr) freq.merge(v, 1, Integer::sum); // absent -> 1, present -> old + 1
return freq;
}
Dry run on arr = [10, 5, 10, 15, 10, 5]:
| v | action | map |
|---|---|---|
| 10 | absent, insert 1 | {10: 1} |
| 5 | absent, insert 1 | {10: 1, 5: 1} |
| 10 | present, 1 + 1 | {10: 2, 5: 1} |
| 15 | absent, insert 1 | {10: 2, 5: 1, 15: 1} |
| 10 | present, 2 + 1 | {10: 3, 5: 1, 15: 1} |
| 5 | present, 1 + 1 | {10: 3, 5: 2, 15: 1} |
Complexity. Time O(n) on average, since each merge is O(1) on average. Space O(d) for d distinct values. This is the general-purpose answer.
Approach 4: In place, when every value is in [1, N]
Intuition. If all values lie in [1, n], the array has exactly one slot per possible value, so the array can be its own counting table. The obstacle is that each slot already holds an element we still need to read. The trick is to store two numbers in one slot: keep the original (shifted to [0, n - 1]) in the low part and add n once for every occurrence of the slot’s value. Since the original is always less than n, slot % n recovers the original and slot / n recovers the count. No extra array is needed.
Algorithm.
- Subtract
1from every element so values become indices in[0, n - 1]. - For each
i, letv = arr[i] % n(the original shifted value ati, even ifarr[i]has already been increased) and doarr[v] += n. - The count of the original value
xisarr[x - 1] / n. Report values in first-appearance order by walking the untouched input.
static Map<Integer, Integer> inPlace(int[] input) {
int[] arr = input.clone(); // keep the caller's array intact
int n = arr.length;
for (int i = 0; i < n; i++) arr[i]--; // shift values to [0, n - 1] so they are valid indices
for (int i = 0; i < n; i++) arr[arr[i] % n] += n; // arr[i] % n is the original value here; add n at its slot
Map<Integer, Integer> freq = new LinkedHashMap<>();
for (int i = 0; i < n; i++) {
int value = input[i];
if (!freq.containsKey(value)) freq.put(value, arr[value - 1] / n); // slot v - 1 holds n * count(v) + junk < n
}
return freq;
}
Dry run on arr = [2, 3, 2, 3, 5, 1, 4], n = 7. After the shift the array is [1, 2, 1, 2, 4, 0, 3]. Processing each index: i = 0 adds 7 to slot 1; i = 1 reads 9 % 7 = 2, adds 7 to slot 2; i = 2 reads 8 % 7 = 1, adds 7 to slot 1; i = 3 reads 2, adds 7 to slot 2; i = 4 adds 7 to slot 4; i = 5 adds 7 to slot 0; i = 6 adds 7 to slot 3. Final array: [8, 16, 15, 9, 11, 0, 3]. Counts: value 2 is 16 / 7 = 2, value 3 is 15 / 7 = 2, value 5 is 11 / 7 = 1, value 1 is 8 / 7 = 1, value 4 is 9 / 7 = 1, value 6 is 0 / 7 = 0 and value 7 is 3 / 7 = 0 (they are absent).
Complexity. Time O(n). Extra space O(1) (the clone and the result map exist only so the method is side-effect free and testable; the trick itself uses no additional memory).
Edge cases and pitfalls
- Empty array. All four approaches return an empty map. The in-place version must not execute
arr[i] % nwithn = 0; because the loops do not run, it does not. - Negative values. The counting array and the in-place trick both index by value and break. Use the hash map.
- Large values.
countingArrayon[10^9, 10^9, 7]tries to allocate a billion ints. Check the value range before choosing it. - Overflow in the in-place trick. A slot can reach
n × count + n - 1 ≤ n^2, which exceedsintwhenn > 46,340. For largernapply the same idea to along[]copy, or use the hash map. - Order of reporting.
HashMapiterates in an arbitrary order; useLinkedHashMap(or a second pass over the input) when the statement wants first-appearance order, andTreeMapwhen it wants sorted order. - Boxing cost.
Map<Integer, Integer>boxes every key; for10^5elements it is fine, but when values are small non-negative integers theint[]counting array is several times faster.
Related problems
- Basic Hashing explains the pre-store and fetch idea behind Approaches 2 and 3 and shows how a hash table works internally.
- Highest Occurring Element in an Array is this problem followed by one more pass to pick the maximum count.
- Majority Element-I asks for the element with count greater than
n / 2. - Find the repeating and missing number reuses the “values in [1, n] index the array itself” trick from Approach 4.
- Two Sum is the other introductory use of a hash map: looking up complements instead of counts.