Jak stworzyć mapę z odrębnymi wartościami z mapy (i użyć odpowiedniego klucza za pomocą BinaryOperator)?

13

Mam mapę Map<K, V>i moim celem jest usunięcie zduplikowanych wartości i Map<K, V>ponowne wygenerowanie tej samej struktury . W przypadku, gdy wartość duplikatów zostanie znaleziony, musi być wybrany jeden klawisz ( k) z dwóch przycisków ( k1i k1), które utrzymują te wartości, z tego powodu, przyjmijmy, że BinaryOperator<K>dając kod k1i k2jest dostępny.

Przykładowe dane wejściowe i wyjściowe:

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

Moja próba użycia Stream::collect(Supplier, BiConsumer, BiConsumer)jest nieco niezręczna i zawiera zmienne operacje, takie jak Map::puti Map::removektórych chciałbym uniknąć:

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

Czy istnieje rozwiązanie wykorzystujące odpowiednią kombinację Collectorsjednego Stream::collectpołączenia (np. Bez operacji zmiennych)?

Nikolas
źródło
2
Jakie są Twoje wskaźniki „ lepszy ” lub „ najlepszy ”? Czy trzeba to zrobić przez Streams?
Turing85
Jeśli ta sama wartość jest powiązana z 2 kluczami, jak wybrać, który klucz ma zostać zachowany?
Michael
Jakie są oczekiwane wyniki w twoim przypadku?
YCF_L
1
@ Turing85: Tak jak powiedziałem. Lepiej lub najlepiej byłoby bez wyraźnej modyfikowalnych metod wykorzystania map, takich jak Map::putlub Map::removewewnątrz Collector.
Nikolas
1
Warto na to spojrzeć BiMap. Prawdopodobnie duplikat Usuń zduplikowane wartości z HashMap w Javie
Naman

Odpowiedzi:

12

Możesz użyć 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));
}
MikeFHay
źródło
9

Spróbuj tego: Prostym sposobem jest odwrócenie klucza i wartości, a następnie użycie toMap()kolektora z funkcją scalania.

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.
źródło
2
Nie widzę, co mapkupuje operacja pośrednia . Wydaje się, że zamieniasz klucze i wartości, to wszystko jest jasne, ale o co chodzi, możesz to zrobić na etapie zbierania tak samo?
GPI
3
@GPI i Michael, to dlatego, że musi scalić klucze, więc odwróć pary scalą klucze. Brakuje zatem drugiej inwersji.
Jean-Baptiste Yunès
2
@HadiJ Nie! Inwersja była poprawna! ale drugi był potrzebny, aby wrócić. Scalanie służy do scalania kluczy, ale scalanie jest możliwe tylko dla wartości ...
Jean-Baptiste Yunès
@ Jean-BaptisteYunès Rozumiem potrzebę łączenia, ale dlaczego nie rozumiem od razu, dlaczego kodujesz swap(); collect(key, value, binOp);zamiast collect(value, key, binOp). Może naprawdę muszę spróbować tego w jshell?
GPI
2
Pozwoliłem sobie na użycie zmiennej lokalnej wprowadzonej w pytaniu we współdzielonym kodzie. Cofnij, na wypadek, gdyby sprzeczne było to z Twoją intencją podczas udzielania odpowiedzi.
Naman
4

Uważam, że rozwiązanie inne niż strumieniowe jest bardziej wyraziste:

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

To stosuje się Map.mergez twoją dwufunkcyjną redukcją i służy LinkedHashMapdo zachowania oryginalnej kolejności wpisów.

Federico Peralta Schaffner
źródło
2
Tak, doszedłem do tego (podobnego) rozwiązania. Jednak szukam podejścia Java-Stream , ponieważ jest to sposób bardziej deklaratywny. Mam +1
Nikolas
1

Znalazłem sposób korzystania tylko Collectorsbez potrzeby zbierania i dalszego przetwarzania zwróconej mapy. Chodzi o to:

  1. Grupuj Map<K, V>do Map<V, List<K>.

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

    {jabłko = [1, 5, 3], pomarańczowy = [4, 2]}

  2. Zmniejsz liczbę nowych klawiszy ( List<K>) do Kużywania 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
        )
    );

    {jabłko = 5, pomarańczowy = 4}

  3. Z Map<V, K>powrotem odwróć Map<K, V>strukturę do tyłu - co jest bezpieczne, ponieważ zarówno klucze, jak i wartości są gwarantowane jako odrębne.

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

    {5 = jabłko, 4 = pomarańczowy}

Ostateczny kod:

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
źródło
1

Kolejna próba uzyskania pożądanego rezultatu za pomocą „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));
vishesh chandra
źródło