Wie erstelle ich eine Karte mit unterschiedlichen Werten aus einer Karte (und verwende den richtigen Schlüssel mit BinaryOperator)?

13

Ich habe eine Karte Map<K, V>und mein Ziel ist es, die doppelten Werte zu entfernen und dieselbe Struktur Map<K, V>erneut auszugeben . Falls der doppelte Wert gefunden wird, muss kaus den beiden Schlüsseln ( k1und k1), die diese Werte enthalten, ein Schlüssel ( ) ausgewählt werden. Aus diesem Grund wird davon ausgegangen, dass das BinaryOperator<K>Geben kvon k1und k2verfügbar ist.

Beispiel für Ein- und Ausgabe:

// Input
Map<Integer, String> map = new HashMap<>();
map.put(1, "apple");
map.put(5, "apple");
map.put(4, "orange");
map.put(3, "apple");
map.put(2, "orange");

// Output: {5=apple, 4=orange} // the key is the largest possible

Mein Versuch, etwas zu verwenden, Stream::collect(Supplier, BiConsumer, BiConsumer)ist etwas sehr ungeschickt und enthält veränderbare Operationen wie Map::putund, Map::removedie ich vermeiden möchte:

// // the key is the largest integer possible (following the example above)
final BinaryOperator<K> reducingKeysBinaryOperator = (k1, k2) -> k1 > k2 ? k1 : k2;

Map<K, V> distinctValuesMap = map.entrySet().stream().collect(
    HashMap::new,                                                              // A new map to return (supplier)
    (map, entry) -> {                                                          // Accumulator
        final K key = entry.getKey();
        final V value = entry.getValue();
        final Entry<K, V> editedEntry = Optional.of(map)                       // New edited Value
            .filter(HashMap::isEmpty)
            .map(m -> new SimpleEntry<>(key, value))                           // If a first entry, use it
            .orElseGet(() -> map.entrySet()                                    // otherwise check for a duplicate
                    .stream() 
                    .filter(e -> value.equals(e.getValue()))
                    .findFirst()
                    .map(e -> new SimpleEntry<>(                               // .. if found, replace
                            reducingKeysBinaryOperator.apply(e.getKey(), key), 
                            map.remove(e.getKey())))
                    .orElse(new SimpleEntry<>(key, value)));                   // .. or else leave
        map.put(editedEntry.getKey(), editedEntry.getValue());                 // put it to the map
    },
    (m1, m2) -> {}                                                             // Combiner
);

Gibt es eine Lösung mit einer geeigneten Kombination Collectorsinnerhalb eines Stream::collectAnrufs (z. B. ohne veränderbare Operationen)?

Nikolas
quelle
2
Was sind Ihre Metriken für " besser " oder "am besten "? Muss durch Streams gemacht werden?
Turing85
Wenn der gleiche Wert mit 2 Schlüsseln verknüpft ist, wie wählen Sie aus, welcher Schlüssel beibehalten wird?
Michael
Was ist die erwartete Ausgabe in Ihrem Fall?
YCF_L
1
@ Turing85: Wie gesagt. Das Bessere oder Beste wäre ohne explizite Verwendung veränderlicher Kartenmethoden wie Map::putoder Map::removeinnerhalb der Collector.
Nikolas
1
Ein Blick lohnt sich BiMap. Möglicherweise ein Duplikat von Doppelte Werte aus HashMap in Java entfernen
Naman

Antworten:

12

Sie können Collectors.toMap verwenden

private Map<Integer, String> deduplicateValues(Map<Integer, String> map) {
    Map<String, Integer> inverse = map.entrySet().stream().collect(toMap(
            Map.Entry::getValue,
            Map.Entry::getKey,
            Math::max) // take the highest key on duplicate values
    );

    return inverse.entrySet().stream().collect(toMap(Map.Entry::getValue, Map.Entry::getKey));
}
MikeFHay
quelle
9

Versuchen Sie Folgendes: Auf einfache Weise werden Schlüssel und Wert umgekehrt, und dann wird der toMap()Kollektor mit der Zusammenführungsfunktion verwendet.

map.entrySet().stream()
        .map(entry -> new AbstractMap.SimpleEntry<>(entry.getValue(), entry.getKey()))
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, reducingKeysBinaryOperator));

Map<K, V> output = map.entrySet().stream()
        .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey, reducingKeysBinaryOperator))
        .entrySet().stream()
        .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
Hadi J.
quelle
2
Ich sehe nicht, was die Zwischenoperation mapkauft. Sie scheinen Schlüssel und Werte zu tauschen, so viel ist klar, aber worum geht es, Sie könnten das trotzdem beim Sammeln tun?
GPI
3
@GPI und Michael, das liegt daran, dass er die Schlüssel zusammenführen muss. Wenn Sie also die Paare umkehren, werden die Schlüssel zusammengeführt. Was dann fehlt, ist die zweite Inversion.
Jean-Baptiste Yunès
2
@HadiJ Nein! Inversion war richtig! aber ein zweiter war notwendig, um zurück zu kommen. Das Zusammenführen wird zum Zusammenführen der Schlüssel verwendet, aber das Zusammenführen ist nur für Werte möglich ...
Jean-Baptiste Yunès
@ Jean-BaptisteYunès verstehe ich die Notwendigkeit , zu fusionieren, aber warum ich nicht bekommen sofort ist Code , warum Sie swap(); collect(key, value, binOp);statt collect(value, key, binOp). Vielleicht muss ich das wirklich mal ausprobieren?
GPI
2
Hat sich die Freiheit genommen, die in der Frage in dem von Ihnen gemeinsam genutzten Code eingeführte lokale Variable zu verwenden. Gehen Sie zurück, falls dies der Absicht widerspricht, während Sie die Antwort gegeben haben.
Naman
4

Ich finde die Nicht-Streams-Lösung ausdrucksvoller:

BinaryOperator<K> reducingKeysBinaryOperator = (k1, k2) -> k1 > k2 ? k1 : k2;

Map<V, K> reverse = new LinkedHashMap<>(map.size());
map.forEach((k, v) -> reverse.merge(v, k, reducingKeysBinaryOperator));

Map<K, V> result = new LinkedHashMap<>(reverse.size());
reverse.forEach((v, k) -> result.put(k, v));

Dies wird zusammen Map.mergemit Ihrer reduzierenden Bi-Funktion verwendet und dient LinkedHashMapdazu, die Reihenfolge der ursprünglichen Einträge beizubehalten.

Federico Peralta Schaffner
quelle
2
Ja, ich habe diese (ähnliche) Lösung abgeschlossen. Ich suche jedoch nach dem Java-Stream- Ansatz, da er viel aussagekräftiger ist. Habe meine +1
Nikolas
1

Ich habe eine Möglichkeit gefunden, Collectorsdie zurückgegebene Karte nur zu verwenden, ohne sie erneut sammeln und weiterverarbeiten zu müssen. Die Idee ist:

  1. Gruppieren Sie die Map<K, V>zu Map<V, List<K>.

    Map<K, V> distinctValuesMap = this.stream.collect(
        Collectors.collectingAndThen(
            Collectors.groupingBy(Entry::getValue),
            groupingDownstream 
        )
    );

    {Apfel = [1, 5, 3], Orange = [4, 2]}

  2. Reduzieren Sie die Verwendung der neuen Schlüssel ( List<K>) .KBinaryOperator<K>

    Function<Entry<V, List<Entry<K, V>>>, K> keyMapFunction = e -> e.getValue().stream()
        .map(Entry::getKey)
        .collect(Collectors.collectingAndThen(
            Collectors.reducing(reducingKeysBinaryOperator),
            Optional::get
        )
    );

    {Apfel = 5, Orange = 4}

  3. Kehren Sie den Map<V, K>Rücken wieder zur Map<K, V>Struktur um - was sicher ist, da sowohl Schlüssel als auch Werte als unterschiedlich garantiert sind.

    Function<Map<V, List<Entry<K,V>>>, Map<K, V>> groupingDownstream = m -> m.entrySet()
        .stream()
        .collect(Collectors.toMap(
            keyMapFunction,
            Entry::getKey
        )
    );

    {5 = Apfel, 4 = Orange}

Der endgültige Code:

final BinaryOperator<K> reducingKeysBinaryOperator = ...

final Map<K, V> distinctValuesMap = map.entrySet().stream().collect(
        Collectors.collectingAndThen(
            Collectors.groupingBy(Entry::getValue),
            m -> m.entrySet().stream().collect(
                Collectors.toMap(
                    e -> e.getValue().stream().map(Entry::getKey).collect(
                        Collectors.collectingAndThen(
                            Collectors.reducing(reducingKeysBinaryOperator),
                            Optional::get
                        )
                    ),
                    Entry::getKey
                )
            )
        )
    );
Nikolas
quelle
1

Ein weiterer Ansatz, um mit "Stream and Collectors.groupingBy" das gewünschte Ergebnis zu erzielen.

    map = map.entrySet().stream()
    .collect(Collectors.groupingBy(
            Entry::getValue,
            Collectors.maxBy(Comparator.comparing(Entry::getKey))
            )
    )
    .entrySet().stream()
    .collect(Collectors.toMap(
            k -> {
                return k.getValue().get().getKey();
            }, 
            Entry::getKey));
Vishesh Chandra
quelle