How do I print the top 5 most occurring numbers in an array?

Viewed 87

I'm supposed to "Write a method that prints out the top 5 most commonly occurring numbers in an array along with the count of occurrences for each" I have a method that prints out the occurrence of each entry in an array and I created another method to print the top 5 occurrences in the array. I'm able to get the values to display correctly in descending order but I'm having trouble getting the keys to do the same. For Example, my array is [7, 8, 5, 3, 9, 3, 9, 4, 7, 3, 6, 9, 3, 2, 9, 6, 5, 7, 6, 3]. For the top5 method my my output is "2 occurs: 5 times 3 occurs: 4 times 4 occurs: 3 times 5 occurs: 3 times 6 occurs: 2 times" when it should be "3 occurs: 5 times 9 occurs: 4 times 6 occurs: 3 times 7 occurs: 3 times 5 occurs: 2 times" How can I fix my method to get the expected output?

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Random;

public class HW4 {

    public static void main(String[] args) {
        int[] a = getRandom(20, 1, 10);
        System.out.println(Arrays.toString(a));
        System.out.println();
        occurrence(a);
        System.out.println();
        top5(a);

    }

    public static int[] getRandom(int n, int low, int high) {

        long seed = 0;
        Random random = new Random(seed);
        random.setSeed(seed);

        int[] result = new int[n];

        for (int i = 0; i < n; i++)

            result[i] = random.nextInt(high - low) + low;

        return result;

    }

    public static void occurrence(int[] x) {
        HashMap<Integer, Integer> occurrences = new HashMap<>();
        for (int key : x) {
            if (occurrences.containsKey(key)) {
                occurrences.put(key, occurrences.get(key) + 1);
            } else {
                occurrences.put(key, 1);
            }
        }
        for (int key : occurrences.keySet()) {
            System.out.println(key + " occurs: " + occurrences.get(key) + " times");
        }
    }

    public static void top5(int[] arr) {
        HashMap<Integer, Integer> lookup = new HashMap<Integer, Integer>();
        for (int key : arr) {
            if (lookup.containsKey(key)) {
                lookup.put(key, lookup.get(key) + 1);
            } else {
                lookup.put(key, 1);
            }
        }
        ArrayList<Integer> keys = new ArrayList<>(lookup.keySet());
        ArrayList<Integer> values = new ArrayList<>(lookup.values());
        for (int i = 0; i < 5; i++) {
            Collections.sort(values, Collections.reverseOrder());
            System.out.println(keys.get(i) + " occurs: " + values.get(i) + " times");
        }
    }
}
5 Answers
public static void top5(int[] arr) {
    IntStream.of(arr).boxed()
            .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
            .entrySet().stream()
            .sorted((e1, e2) -> Long.compare(e2.getValue() , e1.getValue()))
            .limit(5)
            .forEach(e -> System.out.printf("%d occurs %d times%n", e.getKey(), e.getValue()));
}

The first two lines create the histogram, that is a Map which maps each value to its number of occurrences. The next line gets the stream of entries which will be sorted in the following line by their value (that is the number of occurences) in reverse order. You want only 5 elements, so the result is limited to 5 and in the last line the results are output nicely formatted.

List of Entries + Sorting

The simplest approach would be to calculate the frequencies of each number in the array using a HashMap, as you've done.

Then dump all the map-entries into a list and sort the list by value. We need a Comparator to perform this sorting. And for that, we can use Java 8 method Map.Entry.comparingByValue() to obtain a comparator.

And as a final step, print the first 5 elements.

public static void top5(int[] arr) {
    Map<Integer, Integer> lookup = new HashMap<>();
    for (int key : arr) {
        int value = lookup.getOrDefault(key, 0); // or lookup.merge(key, 1, Integer::sum); instead of these two lines
        lookup.put(key, value + 1);
    }
    
    List<Map.Entry<Integer, Integer>> sortedEntries = new ArrayList<>(lookup.entrySet());
    
    sortedEntries.sort(Map.Entry.<Integer, Integer>comparingByValue().reversed());
    
    for (int i = 0; i < 5; i++) {
        System.out.println(
            sortedEntries.get(i).getKey() + " occurs: " + 
            sortedEntries.get(i).getValue() + " times"
        );
    }
}

PriorityQueue

Instead of sorting the whole data, a more performant approach would be to use a PriorityQueue.

The first step remains the same - generate a map of frequencies.

The next step is to create a PriorityQueue which would store map-entries. For that, we would also need a comparator.

Then iterate over the entry-set, trying to offer each entry to a queue. If the size of the queue is equal to 5 and the value of the next entry is greater than the value of the lowest value in the queue (which would be the value of root-element of the PriorityQueue), then the root-element of the queue needs to be removed. The next entry should be added to the queue whenever there's a free space for it (if the queue size is less than 5).

Here's how it might be implemented:

public static void top5(int[] arr) {
    Map<Integer, Integer> lookup = new HashMap<>();
    for (int key : arr) {
        int value = lookup.getOrDefault(key, 0); // or lookup.merge(key, 1, Integer::sum); instead of these two lines
        lookup.put(key, value + 1);
    }
    
    Queue<Map.Entry<Integer, Integer>> queue = new PriorityQueue<>(
        Map.Entry.comparingByValue()
    );
    
    for (Map.Entry<Integer, Integer> entry: lookup.entrySet()) {
        if (queue.size() == 5 && queue.peek().getValue() < entry.getValue()) queue.poll();
        if (queue.size() < 5) queue.offer(entry)
    }
    
    while (!queue.isEmpty()) {
        Map.Entry<Integer, Integer> next = queue.poll();
        
        System.out.println(
            next.getKey() + " occurs: " +
            next.getValue() + " times"
        );
    }
}

If you're looking for a method using Hashmap, you could do that

public static void top5(int[] arr) {
        LinkedHashMap<Integer, Integer> lookup = new LinkedHashMap<>();
        for (int key : arr) {
            if (lookup.containsKey(key)) {
                lookup.put(key, lookup.get(key) + 1);
            } else {
                lookup.put(key, 1);
            }
        }
        Map<Integer, Integer> result = lookup.entrySet() /* sorts the linked_map by value */
                .stream()
                .sorted(Map.Entry.comparingByValue())
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        Map.Entry::getValue,
                        (oldValue, newValue) -> oldValue, LinkedHashMap::new));

        List<Integer> key_in_order
                = new ArrayList<Integer>(result.keySet());  // convert to list

        /* reverse the key because it was backward compared to what you want */
        Collections.reverse(key_in_order);

        for (int i = 0; i < 5; i++) {
            System.out.println("key : "+String.valueOf(key_in_order.get(i)) + " value : " + result.get(key_in_order.get(i)) );
        }
    }

As for often with java, the result requires very big function lol There's probably a more efficient way but that's what I've come up with personally

Once you have created occurrences map,

  1. you can create one max heap of the keys. You can pass comparator in max heap to compare based on values from occurrences map.
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a,b)->occurrences.get(b)-occurrences.get(a))
  1. after this you can just poll from maxHeap for 5 times
for(int i=0;i<5;i++){
  if(!maxHap.isEmpty()){
    System.out.println(maxHeap.poll());
  }
}

You can update your top5 function as below:

public static void top5(Map<Integer, Integer> occurrences) {
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> occurrences.get(b) - occurrences.get(a));
        maxHeap.addAll(occurrences.keySet());
        System.out.println("top 5");
        for (int i = 0; i < 5; i++) {
            if (!maxHeap.isEmpty()) {
                System.out.println(maxHeap.peek() + "  occurs: " + occurrences.get(maxHeap.poll()) + " times");
            }
        }
    }

In this answer I will assume that you have at least five numbers:

Integer top5[] = new Integer[5];
int length = 0;
int minimumIndex = 0;
for (Integer key : occurrences.keySet) {
    if (length < 5) { //we take the first five items whatever they are
        top5[length++] = key; //we assign key to top5[length] and increment length
        if (length === 5) { //The elements are filled, getting the index of the minimum
            minimumIndex = 0;
            for (int index = 1; index < 5; index++) {
                if (occurrences.get(top5[minimumIndex]).intValue() > occurrences.get(top5[index]).intValue()) minimumIndex = index;
            }
        }
    }
    } else { //buildup completed, need to compare all elements
        if (occurrences.get(key).intValue() > occurrences.get(top5[minimumIndex]).intValue()) {
            //We found a larger item than the fifth largest so far
            top5[minimumIndex] = key; //we replace it
            //we find the smallest number now
            minimumIndex = 0;
            for (int index = 1; index < 5; index++) {
                if (occurrences.get(top5[minimumIndex]).intValue() > occurrences.get(top5[index]).intValue()) minimumIndex = index;
            }
        }
    }
}
top5 = Arrays.sort(top5, Collections.reverseOrder());

If this answer seems to be too complicated, then you can separate the computation of minimumIndex into a separate method and use it twice. Otherwise, it should be easy to apply, especially since there are comments added as well.

Related