0

I want to collect the associated keys of List of maximum numbers from Map<String,Integer> in Java 8+

For example:

final Map<String, Integer> map = new HashMap<>();
map.put("first", 50);
map.put("second", 10);
map.put("third", 50);

here I want to return the List of keys associated with maximum values.

For the above example, expected output is [first,third]. Because these 2 keys having the same max value.

I tried with below approach, but able to get single max key only.

final String maxKey = map.entrySet()
    .stream()
    .max(Map.Entry.comparingByValue())
    .map(Map.Entry::getKey)
    .orElse(null);

final List<String> keysInDescending = map.entrySet()
    .stream()
    .sorted(Map.Entry.<String,Integer>comparingByValue().reversed())
    .map(Map.Entry::getKey)
    .collect(Collectors.toList());

System.out.println(maxKey); // third
System.out.println(keysInDescending); //[third, first, second]

But my expected output is [first, third]. How can we achieve it in Java 8+ versions?

Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
Remo
  • 534
  • 7
  • 26
  • 2
    Yet another variation of [How to force max to return ALL maximum values in a Java Stream?](https://stackoverflow.com/q/29334404/2711488) Using e.g. `int max = Collections.max(map.values()); List keys = map.entrySet() .filter(e -> e.getValue() == max) .map(Map.Entry::getKey) .toList();` isn’t that bad. It doesn’t have to be crammed into a single Stream operation. – Holger Nov 11 '22 at 11:57

3 Answers3

6

Group by the values, giving a Map<Integer, List<String>>, then take the value of the biggest key

List<String> maxKeys = map.entrySet()
        .stream()
        .collect(groupingBy(Map.Entry::getValue, mapping(Map.Entry::getKey, toList())))
        .entrySet()
        .stream()
        .max(Map.Entry.<Integer, List<String>>comparingByKey())
        .orElseThrow().getValue();

That iterates twice, but the second time the map is smallest


Best performant solution still is one-iteration for-loop

List<String> maxKeys = new ArrayList<>();
int maxValue = Integer.MIN_VALUE;

for (Map.Entry<String, Integer> e : map.entrySet()) {
    if (e.getValue() < maxValue) continue;
    if (e.getValue() > maxValue) maxKeys.clear();
    maxValue = e.getValue();
    maxKeys.add(e.getKey());
}
azro
  • 53,056
  • 7
  • 34
  • 70
3

Slight unclearness; take max value first.

    final Integer maxValue = map.values()
        .stream()
        .max(Function.identity())
        .map(Map.Entry::getKey)
        .orElse(Integer.MIN_VALUE);

    final List<String> keysInDescending = map.entrySet()
        .stream()
        .filter(e -> maxValue.equals(e.getValue()))
        .map(e -> e.getKey())
        //.sorted(Comparator.reverseOrder())
        .collect(Collectors.toList());

Sorting probably is not needed.

Joop Eggen
  • 107,315
  • 7
  • 83
  • 138
  • Why the sorting of the strings? – Ricola Nov 11 '22 at 12:43
  • @Ricola you are right, the original question sorted reversed probably intended for the values during an earlier attempt. Out-commented it, should one want the keys sorted, which for a HashMap would be nice. – Joop Eggen Nov 11 '22 at 12:48
2

Easy solution with streams

Step 1 : get the maxValue

final Integer maxValue = map.values()
    .stream()
    .max(Comparator.naturalOrder())
    .orElse(null);

or more simply:

// But be aware of NoSuchElementException if the the map is empty.
final Integer maxValue = Collections.max(map.values());

Step 2 : get keys associated with this max value:

List<String> result = map.entrySet()
    .stream()
    .filter(e -> e.getValue().equals(maxValue))
    .map(Map.Entry::getKey)
    .collect(Collectors.toList());

Only one pass

OP asked if we can do it one pass. I really don't think it's necessary as you can't do better than O(n) and it's likely that a one pass solution could be slower (as you need to keep a list of the keys that you might not need).

With a for loop

Quite easy and clear

ArrayList<String> result = new ArrayList<>();
int max = Integer.MIN_VALUE;

for (Map.Entry<String, Integer> entry : map.entrySet()) {
    Integer value = entry.getValue();
    if(value < max) continue;
    if(value > max){
        max = value;
        result = new ArrayList<>();
    } 
    // this applies in both > max and == max cases
    result.add(entry.getKey());
}

With streams

If we absolutely want to use streams, we can actually convert the code above to streams and we can use collect. But I really wouldn't suggest this approach as it makes everything more complicated. Also since the accumulator needs to keep both the keys and the max value, you need to create a private class to hold the data. So this is just for the sake of the exercise/brain teaser.

List<String> result = map.entrySet()
        .stream()
        .collect(MaxData::supply, MaxData::accumulate, MaxData::combine)
        .keys;

And the accumulator class:

 public static class MaxData {
        int max;
        List<String> keys;

        private MaxData(int max, List<String> keys) {
            this.max = max;
            this.keys = keys;
        }

        public static MaxData supply() {
            return new MaxData(Integer.MIN_VALUE, null);
        }

        public void accumulate(Map.Entry<String, Integer> entry) {
            Integer entryValue = entry.getValue();
            if (entryValue > max) {
                max = entryValue;
                keys = new ArrayList<>();
                keys.add(entry.getKey());
            } else if (entryValue == max) {
                keys.add(entry.getKey());
            }
            // last case is entryValue < max, we keep the current max
        }

        public void combine(MaxData other) {
            if (other.max > max){
               max = other.max;
               keys = other.keys;
            } else if(other.max == max) {
                keys.addAll(other.keys);
            }
            // last case is entryValue < max, we keep the current max
        }
    }
Ricola
  • 2,621
  • 12
  • 22
  • is any option is there to collect in step 1 itself? instead of iterating map again – Remo Nov 11 '22 at 11:37
  • 2
    Don’t use mutable data, like an `ArrayList` you are adding to, with `reduce`. That’s what `collect` is for. – Holger Nov 11 '22 at 12:06
  • @Holger I am not entirely sure this case was not respecting the specifications of reduce, but I edited it to use `collect` instead. I wonder if there is a way to create only one class that implements `Collector`. The only way I see it is one class implementing `Collector` and one other to implement the accumulation type. – Ricola Nov 11 '22 at 12:46
  • 1
    I don’t know what you mean. You don’t implement the `Collector` interface but pass three method references. These methods don’t have to be in another class. Of course, you need a container type to hold the current maximum and the list. But what is the other type? – Holger Nov 11 '22 at 13:05
  • I am not doing it here, but I _could_ pass `.collect(new MyCustomCollector())` and implement these three functions (+ `finisher()`) in `MyCustomCollector` class. But the interface is `interface Collector` with `A` being the container class, so I still need an extra class. My question was if there could be a way to implement only one class doing the accumulation and pass it to `.collect`. If it's not clear enough, I'll create a separate question with more details. – Ricola Nov 11 '22 at 13:12
  • 1
    There is no requirement for `A` to be a different class. But implementing the `Collector` interface is unusual. There is no point in doing so, as you can always use [`Collector.of(…)`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/stream/Collector.html#of(java.util.function.Supplier,java.util.function.BiConsumer,java.util.function.BinaryOperator,java.util.stream.Collector.Characteristics...)) to create a collector out of the three or four functions and there is no semantic beyond these functions and the characteristics that a custom implementation class could introduce – Holger Nov 11 '22 at 13:20
  • 1
    See https://ideone.com/W5ESkU for how you can implement it with a minimum of declarations. Though technically, it still creates another class for the temporary container. If you have an existing class capable of holding a mutable integer and a list, you’d not need it. – Holger Nov 11 '22 at 13:44
  • I asked https://stackoverflow.com/q/74403784/7424948 – Ricola Nov 11 '22 at 14:16