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.