1. Overview

A common programming problem is counting the occurrences or frequencies of distinct elements in a list. It can be helpful, for example, when we want to know the highest or lowest occurrence or a specific occurrence of one or more elements.

In this tutorial, we’ll look at some common solutions to counting occurrences in an array.

2. Count Occurrences

We need to understand this problem’s constraints to approach a solution.

2.1. Constraints

First, we must understand whether we count:

  • occurrences of objects
  • occurrences of primitives

If we’re dealing with numbers, we need to know the range of values we want to count. This might be a small fixed range of values, or it could be the entire numeric range, with values appearing sparsely.

2.2. How to Approach a Solution

With primitives such as int or char, we can use a fixed-size array of counters to store the frequencies of each value. This works but has limitations due to the maximum size of the counting array that can be in memory. Furthermore, extending this to objects wouldn’t work.

Using maps is a more adaptable solution to the problem.

3. Using a Counters Array

Let’s use a counters array for positive integers in a fixed range.

3.1. Count Positive Integers in a Fixed Range

So, let’s say we have values 0…(n-1) and want to know their occurrences:

static int[] countOccurrencesWithCounter(int[] elements, int n) {
    int[] counter = new int[n];

    for (int element : elements) {
        counter[element]++;
    }

    return counter;
}

The algorithm is straightforward and loops over the array while incrementing the counter’s position of a specific element.

Let’s look at the algorithm complexity:

  • Time complexity: O(n) for accessing the array
  • Space complexity: O(n) depending on the size of the input array

Let’s look at a unit test where we find the occurrence of the number 3 in the first ten numbers:

int[] counter = countOccurrencesWithCounter(new int[] { 2, 3, 1, 1, 3, 4, 5, 6, 7, 8 }, 10);
assertEquals(2, counter[3]);

Another interesting application of counters is for characters in a string. For example, we can look at counting frequencies in a string permutation.

3.2. Other Use Cases and Limitations

Although an array’s maximum size is quite large, it’s usually not a good practice to use it for frequencies unless we know it’s a finite set we are counting.

It wouldn’t be easy to use it for a sparse range of values. This applies, for example, to fractional numbers, where finding a suitable range to store the decimals would be difficult.

For negative numbers, we can use an offset and store the negative in the counter. For example, if we have a k offset representing the [-k, k] values range, we can create a counter array:

int[] counter = new int[(k * 2) + 1];

Then, we can store an occurrence at the value + k position.

This approach has limitations due to the range of values that might not fit the actual values for which we want to store the frequencies. Moreover, we can’t use this data structure to count object occurrences.

4. Use Maps

Maps are more appropriate for counting occurrences. Furthermore, the size of a map is limited only by the JVM memory available, making it suitable for storing a large number of entries.

Like a counter, we increment the frequency, but this time, it’s related to a specific map key.  A map allows us to work with objects. Therefore, we can use generics to create a map with a generic key:

static <T> Map<T, Integer> countOccurrencesWithMap(T[] elements) {

    Map<T, Integer> counter = new HashMap<>();

    for (T element : elements) {
        counter.merge(element, 1, Integer::sum);
    }

    return counter;
}

Let’s look at the algorithm complexity:

  • Time complexity: O(n) for accessing the array
  • Space complexity: O(m) where m is the number of distinct values within the original array

Let’s look at a test to find occurrences for integers. With maps, we can also search for a negative integer:

Map<Integer, Integer> counter = countOccurrencesWithMap(new Integer[] { 2, 3, 1, -1, 3, 4, 5, 6, 7, 8, -1 });
assertEquals(2, counter.get(-1));

Likewise, we can count string occurrences:

Map<String, Integer> counter = countOccurrencesWithMap(new String[] { "apple", "orange", "banana", "apple" });
assertEquals(2, counter.get("apple"));

We could also look at Guava Multiset to store frequencies relative to specific keys.

5. Use Java 8 Streams

From Java 8, we can use streams to collect the count of the occurrences grouped by the distinct elements. It works just like the previous example with maps. However, using streams allows us to use functional programming and take advantage of parallel execution when possible.

Let’s look at the case where we count occurrences of integers:

static <T> Map<T, Long> countOccurrencesWithStream(T[] elements) {

    return Arrays.stream(elements)
      .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
}

Notably, when we use arrays, we must first convert to a stream.

The algorithm complexity would be similar to using maps:

  • Time complexity: O(n) for accessing the array
  • Space complexity: O(m) where m is the number of distinct values of the array

The advantage of using streams might be related to the speed of execution. However, we still need to iterate over all the input elements and use space to create a map of occurrences.

Let’s look at a test for integers:

Map<Integer, Long> counter = countOccurrencesWithStream(new int[] { 2, 3, 1, -1, 3, 4, 5, 6, 7, 8, -1 });
assertEquals(2, counter.get(-1));

Likewise, we look at a test for strings:

Map<String, Long> counter = countOccurrencesWithStream(new String[] { "apple", "orange", "banana", "apple" });
assertEquals(2, counter.get("apple"));

6. Conclusion

In this article, we saw solutions for counting occurrences in an array. The most adaptable solution is to use a map, simple or created with a stream. However, if we have primitive integers in a fixed range, we can use counters.

As always, the code presented in this article is available over on GitHub.