Come produrre una mappa con valori distinti da una mappa (e usare la chiave giusta usando BinaryOperator)?


13

Ho una mappa Map<K, V>e il mio obiettivo è quello di rimuovere i valori duplicati e restituire la stessa struttura Map<K, V>. Nel caso in cui venga trovato il valore duplicato, deve essere selezionata una chiave ( k) tra le due chiavi ( k1e k1) che contengono questi valori, per questo motivo, assumono il BinaryOperator<K>dare kda k1ed k2è disponibile.

Esempio di input e output:

// 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

Il mio tentativo di utilizzo Stream::collect(Supplier, BiConsumer, BiConsumer)è un po ' goffo e contiene operazioni mutabili come Map::pute Map::removeche vorrei evitare:

// // 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
);

Esiste una soluzione che utilizza una combinazione appropriata Collectorsall'interno di una Stream::collectchiamata (ad esempio senza operazioni mutabili)?


2
Qual è la tua metrica per " migliore " o " migliore "? Deve essere fatto attraverso Streams?
Turing85,

Se lo stesso valore è associato a 2 chiavi, come si sceglie quale chiave viene conservata?
Michael

Quali sono i risultati previsti nel tuo caso?
YCF_L

1
@ Turing85: come ho detto. Il meglio o il meglio sarebbe senza un uso esplicito di metodi di mappa mutabili come Map::puto Map::removeall'interno di Collector.
Nikolas

1
Vale la pena dare un'occhiata BiMap. Forse un duplicato di Rimuovi valori duplicati da HashMap in Java
Naman

Risposte:


12

Puoi usare Collectors.toMap

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));
}

9

Prova questo: il modo semplice è invertire la chiave e il valore, quindi utilizzare il toMap()raccoglitore con la funzione di unione.

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));

2
Non riesco a vedere cosa mapcompra l'operazione intermedia . Sembri scambiare chiavi e valori, questo è molto chiaro, ma qual è il punto, potresti farlo allo stesso modo nella fase di raccolta?
GPI

3
@GPI e Michael, questo perché deve unire le chiavi, quindi invertire le coppie uniranno le chiavi. Ciò che manca è quindi la seconda inversione.
Jean-Baptiste Yunès

2
@HadiJ No! L'inversione era corretta! ma un secondo era necessario per tornare indietro. L'unione viene utilizzata per unire le chiavi, ma l'unione è possibile solo per i valori ...
Jean-Baptiste Yunès

@ Jean-BaptisteYunès Capisco la necessità di unire, ma il motivo per cui non capisco immediatamente è il motivo per cui swap(); collect(key, value, binOp);invece di codice collect(value, key, binOp). Forse dovrei provare questo in un guscio per davvero?
GPI

2
Ha preso la libertà di usare la variabile locale introdotta nella domanda nel codice condiviso da te. Ripristina nel caso in cui sia in conflitto l'intenzione mentre stavi facendo la risposta.
Naman

4

Trovo la soluzione non stream più espressiva:

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));

Questo si usa Map.mergecon la tua doppia funzione di riduzione e usa LinkedHashMapper preservare l'ordine delle voci originali.


2
Sì, ho concluso questa (simile) soluzione. Tuttavia, sto cercando l' approccio java-stream , poiché è il modo più dichiarativo. Ho il mio +1
Nikolas il

1

Ho trovato un modo di utilizzare solo Collectorssenza la necessità di raccogliere e elaborare nuovamente la mappa restituita. L'idea è:

  1. Raggruppa il Map<K, V>to Map<V, List<K>.

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

    {apple = [1, 5, 3], orange = [4, 2]}

  2. Ridurre i nuovi tasti ( List<K>) Kall'utilizzo BinaryOperator<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
        )
    );

    {mela = 5, arancia = 4}

  3. Invertire la Map<V, K>schiena alla Map<K, V>struttura di nuovo, il che è sicuro poiché sia ​​le chiavi che i valori sono garantiti come distinti.

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

    {5 = mela, 4 = arancia}

Il codice finale:

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
                )
            )
        )
    );

1

Un altro approccio per ottenere il risultato desiderato con "Stream and Collectors.groupingBy".

    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));
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.