Die drei häufigsten Zeichen ausgeben (mit Streams)

Vielleicht hat @Landei eine Idee …

Es soll die Häufigkeit jedes Zeichen gezählt werden, und die drei häufigsten Zeichen sollen ausgegeben werden. Dabei soll die Reihenfolge gleich oft vorkommender Zeichen erhalten bleiben.

Das Ganze mit Streams.

Mein erster Versuch:

new ArrayList<>("abracadabra".chars()
        .boxed()
        .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
        .entrySet()).stream()
        .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
        .limit(3)
        .forEach(me -> System.out.println((char) (int) me.getKey() + " " + me.getValue()));

Die Ausgabe ist: a 5 r 2 b 2 Problem: b kommt im String vor r vor, in der Ausgabe aber nicht … also ist die Ausgabe falsch.

Zweiter Versuch:

new ArrayList<>("abracadabra".chars()
        .boxed()
        .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
        .entrySet()).stream()
        .sorted(Map.Entry.comparingByKey()) // vorgeschaltet...
        .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
        .limit(3)
        .forEach(me -> System.out.println((char) (int) me.getKey() + " " + me.getValue()));

Jetzt ist die Ausgabe zwar richtig (a 5 b 2 r 2), aber nur für diese Eingabe. Würde sich die Eingabe ändern, zum Beispiel arbacadabra, dann stimmt die Ausgabe nicht mehr.

Hat jemand eine Idee? Für StackOverflow ist diese Frage zu speziell.

Bin leider noch nicht weiter …

"abracadabra".chars()
        .boxed()
        .reduce(new LinkedHashMap<Integer, Long>(), (a, b) -> {
            a.put(b, a.getOrDefault(b, 0L) + 1L);
            return a;
        }, (a, b) -> {
            a.putAll(b);
            return a;
        })
        .entrySet()
        .stream()
        .reduce(new TreeSet<Map.Entry<Integer, Long>>((a, b) -> Long.compare(b.getValue(), a.getValue())), (a, b) -> {
            a.add(b);
            return a;
        }, (a, b) -> {
            a.addAll(b);
            return b;
        })
        .stream()
        .limit(3)
        .forEach(me -> System.out.println((char) (int) me.getKey() + " " + me.getValue()));

Die Ausgabe ist nun: a 5 b 2 c 1 … das heißt, doppelt vorkommende Zeichen mit gleicher Anzahl werden nicht mehr mitgezählt (hier käme das r auch zweimal vor).

Jetzt hat es geklappt: :wink:

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;

public class Zeichen {
    public static void printMostOccurring(final String s) {
        s.chars()
                .boxed()
                .reduce(new LinkedHashMap<Integer, Long>(), (a, b) -> {
                    a.put(b, a.getOrDefault(b, 0L) + 1L);
                    return a;
                }, (a, b) -> {
                    a.putAll(b);
                    return a;
                })
                .entrySet()
                .stream()
                .reduce(new ArrayList<Map.Entry<Integer, Long>>(), (a, b) -> {
                    a.add(b);
                    return a;
                }, (a, b) -> {
                    a.addAll(b);
                    return b;
                })
                .stream()
                .sorted((a, b) -> Long.compare(b.getValue(), a.getValue()))
                .limit(3)
                .forEach(me -> System.out.println((char) (int) me.getKey() + " " + me.getValue()));
        System.out.println();
    }

    public static void main(final String[] args) {
        printMostOccurring("the quick brown fox jumped over the lazy dog");
        printMostOccurring("abracadabra");
        printMostOccurring("arbacadabra");
    }
}

Der Trick war … Eine LinkedHashMap zum Zählen zu verwenden und eine ArrayList zum Sortieren.

Dieses Thema kann weg. Doppelpostings und ich habe auf meine eigene Frage geantwortet. Beides unschön.