TreeMap sortuj według wartości


137

Chcę napisać komparator, który pozwoli mi posortować TreeMap według wartości zamiast domyślnego naturalnego porządku.

Próbowałem czegoś takiego, ale nie mogę dowiedzieć się, co poszło nie tak:

import java.util.*;

class treeMap {
    public static void main(String[] args) {
        System.out.println("the main");
        byValue cmp = new byValue();
        Map<String, Integer> map = new TreeMap<String, Integer>(cmp);
        map.put("de",10);
        map.put("ab", 20);
        map.put("a",5);

        for (Map.Entry<String,Integer> pair: map.entrySet()) {
            System.out.println(pair.getKey()+":"+pair.getValue());
        }
    }
}

class byValue implements Comparator<Map.Entry<String,Integer>> {
    public int compare(Map.Entry<String,Integer> e1, Map.Entry<String,Integer> e2) {
        if (e1.getValue() < e2.getValue()){
            return 1;
        } else if (e1.getValue() == e2.getValue()) {
            return 0;
        } else {
            return -1;
        }
    }
}

Myślę, że pytam: czy mogę Map.Entryprzejść do komparatora?




Odpowiedzi:


179

Nie możesz mieć TreeMapsamego sortowania według wartości, ponieważ jest to sprzeczne ze SortedMapspecyfikacją:

MapŻe dostarcza ponadto całkowite uporządkowanie na jej klucze .

Jednak korzystając z kolekcji zewnętrznej, zawsze możesz sortować w Map.entrySet()dowolny sposób, według kluczy, wartości lub nawet ich kombinacji (!!).

Oto ogólna metoda, która zwraca SortedSetz Map.Entry, biorąc pod uwagę Map, której wartościami są Comparable:

static <K,V extends Comparable<? super V>>
SortedSet<Map.Entry<K,V>> entriesSortedByValues(Map<K,V> map) {
    SortedSet<Map.Entry<K,V>> sortedEntries = new TreeSet<Map.Entry<K,V>>(
        new Comparator<Map.Entry<K,V>>() {
            @Override public int compare(Map.Entry<K,V> e1, Map.Entry<K,V> e2) {
                int res = e1.getValue().compareTo(e2.getValue());
                return res != 0 ? res : 1;
            }
        }
    );
    sortedEntries.addAll(map.entrySet());
    return sortedEntries;
}

Teraz możesz wykonać następujące czynności:

    Map<String,Integer> map = new TreeMap<String,Integer>();
    map.put("A", 3);
    map.put("B", 2);
    map.put("C", 1);   

    System.out.println(map);
    // prints "{A=3, B=2, C=1}"
    System.out.println(entriesSortedByValues(map));
    // prints "[C=1, B=2, A=3]"

Zauważ, że dziwne rzeczy mogą się wydarzyć, jeśli spróbujesz zmodyfikować SortedSetsamą siebie lub Map.Entrywnętrze, ponieważ nie jest to już „widok” oryginalnej mapy, jak entrySet()jest.

Ogólnie rzecz biorąc, potrzeba sortowania wpisów mapy według jej wartości jest nietypowa.


Uwaga dotycząca ==dlaInteger

Twój oryginalny komparator porównuje Integerużycie ==. Jest to prawie zawsze błędne, ponieważ ==z Integeroperandami jest równość odniesienia, a nie równość wartości.

    System.out.println(new Integer(0) == new Integer(0)); // prints "false"!!!

Powiązane pytania


5
jeśli dodasz map.put ("D", 2); wynik to nadal „[C = 1, B = 2, A = 3]”, a nie „[C = 1, B = 2, D = 2, A = 3]”
Igor Milla

3
Poprawka: int res = e2.getValue (). CompareTo (e1.getValue ()); return res! = 0? res: 1;
beshkenadze

new Integer (0) == new Integer (0) Porównujesz odniesienia do obiektów i tworzysz dwa obiekty! Wyjście jest absolutnie poprawne i przewidywalne.
Yuriy Chernyshov

2
„Ogólnie rzecz biorąc, potrzeba sortowania wpisów mapy według wartości jest nietypowa” Jestem tu początkującym, ale czuję, że powinno to być bardzo powszechne? Na przykład, jeśli chcę pobrać 3 najstarsze osoby na mapie {"ana" = 37, "peter" = 43, "john" = 4, "mary" = 15, "Matt" = 78}
fartagaintuxedo

@igormilla Jest prosty powód, dla którego Twój kod działa: Autoboxing używa Integer.valueOf. A to ma pamięć podręczną instancji od -128 do 127!
żartowniś

75

odpowiedź polygenelubricants jest prawie idealna. Ma jednak jeden ważny błąd. Nie będzie obsługiwać wpisów mapy, w których wartości są takie same.

Ten kod: ...

Map<String, Integer> nonSortedMap = new HashMap<String, Integer>();
nonSortedMap.put("ape", 1);
nonSortedMap.put("pig", 3);
nonSortedMap.put("cow", 1);
nonSortedMap.put("frog", 2);

for (Entry<String, Integer> entry  : entriesSortedByValues(nonSortedMap)) {
    System.out.println(entry.getKey()+":"+entry.getValue());
}

Wynikałoby:

ape:1
frog:2
pig:3

Zwróć uwagę, jak nasza krowa zniknęła, gdy miała wspólną wartość „1” z naszą małpą: O!

Ta modyfikacja kodu rozwiązuje ten problem:

static <K,V extends Comparable<? super V>> SortedSet<Map.Entry<K,V>> entriesSortedByValues(Map<K,V> map) {
        SortedSet<Map.Entry<K,V>> sortedEntries = new TreeSet<Map.Entry<K,V>>(
            new Comparator<Map.Entry<K,V>>() {
                @Override public int compare(Map.Entry<K,V> e1, Map.Entry<K,V> e2) {
                    int res = e1.getValue().compareTo(e2.getValue());
                    return res != 0 ? res : 1; // Special fix to preserve items with equal values
                }
            }
        );
        sortedEntries.addAll(map.entrySet());
        return sortedEntries;
    }

2
-1. AFASIK żadna Setimplementacja nie zawiera elementu więcej niż raz. Właśnie naruszyłeś to ograniczenie. Z SortedSetinterfejsu API: Zwróć uwagę, że kolejność obsługiwana przez posortowany zestaw musi być zgodna z równymi . Rozwiązaniem byłoby przejście na Listimplementację.
dacwe

4
@dacwe równe wartości, a nie klucze
bellum

1
@bellum: Mówimy Settutaj o s. Ponieważ Comparatornarusza Setumowę Set.remove, Set.containsetc nie działa ! Sprawdź ten przykład w ideone .
dacwe,

3
Uwaga, jeśli przełączysz się int res= e1.getValue().compareTo(e2.getValue());na int res= e2.getValue().compareTo(e1.getValue());, masz porządek malejący wartości zamiast rosnąco.
tugcem

6
Wolałbym raczej wrócić, res != 0 ? res : e1.getKey().compareTo(e2.getKey())aby zachować kolejność kluczy z równymi wartościami.
Marco Lackovic

46

W Javie 8:

LinkedHashMap<Integer, String> sortedMap = 
    map.entrySet().stream().
    sorted(Entry.comparingByValue()).
    collect(Collectors.toMap(Entry::getKey, Entry::getValue,
                             (e1, e2) -> e1, LinkedHashMap::new));

17

A TreeMapjest zawsze sortowane według kluczy, wszystko inne jest niemożliwe. A Comparatorjedynie pozwala kontrolować sposób sortowania kluczy.

Jeśli chcesz posortowane wartości, musisz wyodrębnić je do a Listi posortować.


10

Nie można tego zrobić za pomocą a Comparator, ponieważ zawsze otrzyma klucz mapy do porównania. TreeMapmożna sortować tylko według klucza.


2
jeśli TreeMap przekaże klucz tylko do komparatora, czy byłoby to wykonalne, gdybym utworzył odniesienie do TreeMap w konstruktorze komparatora, a następnie używając klucza do uzyskania wartości, coś takiego (nie wiem, jak przekazać przez odniesienie): class byValue implementuje Comparator {TreeMap theTree; public byValue (TreeMap theTree) {this.theTree = theTree; } public int compare (String k1, String k2) {// użyj metody getKey TreeMap do wartości}}
vito huang

1
@vito: nie, ponieważ zwykle jednego z dwóch kluczy nie ma jeszcze na mapie i nie można uzyskać jego wartości.
Joachim Sauer

Czy to nie jest przykład robienia tego w komparatorze TreeMap? (chociaż, jak wspomniano powyżej, to łamie specyfikację, SortedMapktóra określa sortowanie według kluczy) beginnersbook.com/2014/07/…
Marcus

@Marcus: Comparatorużywa istniejącej mapy, aby pobrać wartości do sortowania. Innymi słowy, nie może sortować dowolnych wartości wstawionych TreeMappóźniej, tylko wartości, które są już w oryginalnej Mapie.
Joachim Sauer

5

Odpowiedź Olofa jest dobra, ale potrzebuje jeszcze jednej rzeczy, zanim będzie idealna. W komentarzach pod swoją odpowiedzią dacwe (poprawnie) wskazuje, że jego implementacja narusza kontrakt Porównaj / Równość dla zestawów. Jeśli spróbujesz wywołać zawiera lub usunąć wpis, który jest wyraźnie w zestawie, zestaw nie rozpozna go z powodu kodu, który pozwala na umieszczenie w zestawie wpisów o równych wartościach. Aby to naprawić, musimy przetestować równość między kluczami:

static <K,V extends Comparable<? super V>> SortedSet<Map.Entry<K,V>> entriesSortedByValues(Map<K,V> map) {
    SortedSet<Map.Entry<K,V>> sortedEntries = new TreeSet<Map.Entry<K,V>>(
        new Comparator<Map.Entry<K,V>>() {
            @Override public int compare(Map.Entry<K,V> e1, Map.Entry<K,V> e2) {
                int res = e1.getValue().compareTo(e2.getValue());
                if (e1.getKey().equals(e2.getKey())) {
                    return res; // Code will now handle equality properly
                } else {
                    return res != 0 ? res : 1; // While still adding all entries
                }
            }
        }
    );
    sortedEntries.addAll(map.entrySet());
    return sortedEntries;
}

Należy zauważyć, że kolejność utrzymywana przez posortowany zestaw (niezależnie od tego, czy podano jawny komparator) musi być zgodna z równymi, jeśli posortowany zestaw ma poprawnie zaimplementować interfejs Set ... interfejs Set jest zdefiniowany w kategoriach operacji equals , ale posortowany zestaw wykonuje wszystkie porównania elementów przy użyciu metody compareTo (lub porównaj), więc dwa elementy uznane za równe przez tę metodę są, z punktu widzenia posortowanego zestawu, równe . " ( http://docs.oracle.com/javase/6/docs/api/java/util/SortedSet.html )

Ponieważ początkowo przeoczyliśmy równość, aby wymusić na zestawie dodanie wpisów o równej wartości, teraz musimy przetestować równość w kluczach, aby zestaw faktycznie zwrócił wpis, którego szukasz. To trochę bałaganiarskie i zdecydowanie nie tak, jak zestawy miały być używane - ale to działa.


3

Wiem, że ten post zawiera konkretną prośbę o sortowanie TreeMap według wartości, ale dla tych z nas, którzy tak naprawdę nie dbają o implementację, ale chcą rozwiązania, które utrzymuje kolekcję posortowaną po dodaniu elementów, byłbym wdzięczny za opinie na temat tego opartego na TreeSet rozwiązanie. Po pierwsze, elementy nie są łatwo pobierane za pomocą klucza, ale w przypadku użycia, który miałem pod ręką (znalezienie n kluczy o najniższych wartościach), nie było to wymagane.

  TreeSet<Map.Entry<Integer, Double>> set = new TreeSet<>(new Comparator<Map.Entry<Integer, Double>>()
  {
    @Override
    public int compare(Map.Entry<Integer, Double> o1, Map.Entry<Integer, Double> o2)
    {
      int valueComparison = o1.getValue().compareTo(o2.getValue());
      return valueComparison == 0 ? o1.getKey().compareTo(o2.getKey()) : valueComparison;
    }
  });
  int key = 5;
  double value = 1.0;
  set.add(new AbstractMap.SimpleEntry<>(key, value));

2

Wiele osób słyszy rady, aby używać List i ja też wolę z niej korzystać

tutaj są dwie metody, które musisz posortować wpisy mapy według ich wartości.

    static final Comparator<Entry<?, Double>> DOUBLE_VALUE_COMPARATOR = 
        new Comparator<Entry<?, Double>>() {
            @Override
            public int compare(Entry<?, Double> o1, Entry<?, Double> o2) {
                return o1.getValue().compareTo(o2.getValue());
            }
        };

        static final List<Entry<?, Double>> sortHashMapByDoubleValue(HashMap temp)
        {
            Set<Entry<?, Double>> entryOfMap = temp.entrySet();

            List<Entry<?, Double>> entries = new ArrayList<Entry<?, Double>>(entryOfMap);
            Collections.sort(entries, DOUBLE_VALUE_COMPARATOR);
            return entries;
        }
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.