TreeMap nach Wert sortieren

137

Ich möchte einen Komparator schreiben, mit dem ich eine TreeMap anstelle der natürlichen Standardreihenfolge nach Wert sortieren kann.

Ich habe so etwas versucht, kann aber nicht herausfinden, was schief gelaufen ist:

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

Ich denke, was ich frage ist: Kann ich eine Map.EntryÜbergabe an den Komparator bekommen?

Vito Huang
quelle
Vielleicht hilft Ihnen dies bei stackoverflow.com/questions/109383/…
Inv3r53
mögliches Duplikat von Wie sortiere ich eine Baumkarte nach ihren Werten?
Nandeesh
Siehe auch: stackoverflow.com/questions/30425836/…
Paul Jackson

Antworten:

179

Sie können TreeMapdie Werte nicht selbst sortieren lassen, da dies der SortedMapSpezifikation widerspricht :

A Map, das ferner eine Gesamtbestellung seiner Schlüssel liefert .

Mit einer externen Sammlung können Sie jedoch jederzeit Map.entrySet()nach Schlüsseln, Werten oder sogar einer Kombination (!!) der beiden sortieren, wie Sie möchten.

Hier ist eine generische Methode, die a SortedSetvon zurückgibt Map.Entry, vorausgesetzt, Mapderen Werte sind 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;
}

Jetzt können Sie Folgendes tun:

    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]"

Beachten Sie, dass funky Dinge passieren, wenn Sie versuchen, entweder das SortedSetSelbst oder das Innere zu ändern Map.Entry, da dies keine "Ansicht" der ursprünglichen Karte mehr ist, wie sie entrySet()ist.

Im Allgemeinen ist die Notwendigkeit, die Einträge einer Karte nach ihren Werten zu sortieren, untypisch.


Hinweis ==fürInteger

Ihr ursprünglicher Komparator vergleicht Integermit ==. Dies ist fast immer falsch, da es sich ==bei IntegerOperanden um eine Referenzgleichheit handelt, nicht um eine Wertgleichheit.

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

Verwandte Fragen

Polygenschmierstoffe
quelle
5
wenn Sie map.put hinzufügen ("D", 2); das Ergebnis ist immer noch "[C = 1, B = 2, A = 3]" und nicht "[C = 1, B = 2, D = 2, A = 3]"
Igor Milla
3
Fix: int res = e2.getValue (). CompareTo (e1.getValue ()); return res! = 0? res: 1;
beshkenadze
new Integer (0) == new Integer (0) Sie vergleichen Verweise auf ein Objekt und erstellen zwei Objekte! Die Ausgabe ist absolut korrekt und vorhersehbar.
Yuriy Chernyshov
2
"Im Allgemeinen ist die Notwendigkeit, die Einträge einer Karte nach ihren Werten zu sortieren, untypisch." Ich bin hier ein Anfänger, aber ich denke, es sollte eine sehr häufige Sache sein? Zum Beispiel, wenn ich die 3 ältesten Personen in einer Karte abrufen möchte {"ana" = 37, "peter" = 43, "john" = 4, "mary" = 15, "Matt" = 78}
furagaintuxedo
@igormilla Es gibt einen einfachen Grund, warum Ihr Code funktioniert: Autoboxing verwendet Integer.valueOf. Und das hat einen Cache von Instanzen für -128 bis 127!
Jokster
75

Die Antwort auf Polygenschmierstoffe ist nahezu perfekt. Es hat jedoch einen wichtigen Fehler. Karteneinträge mit gleichen Werten werden nicht verarbeitet.

Dieser Code: ...

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

Würde ausgeben:

ape:1
frog:2
pig:3

Beachten Sie, wie unsere Kuh verschwand, als sie den Wert "1" mit unserem Affen teilte: O!

Diese Änderung des Codes löst dieses 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;
    }
Olof Larsson
quelle
2
-1. AFASIK keine SetImplementierung enthält ein Element mehr als einmal. Sie haben gerade diese Einschränkung verletzt. Über die SortedSetAPI: Beachten Sie, dass die Reihenfolge, die von einem sortierten Satz verwaltet wird , mit gleich .... übereinstimmen muss . Die Lösung wäre, zu einer ListImplementierung zu wechseln .
Dacwe
4
@ Dacwe gleiche Werte nicht Schlüssel
Bellum
1
@bellum: Wir reden hier über Sets. Da das Comparatorgegen den SetVertrag verstößt Set.remove, funktioniertSet.contains etc nicht ! Überprüfen Sie dieses Beispiel bei ideone .
Dacwe
3
Nur eine Notiz, wenn Sie wechseln int res= e1.getValue().compareTo(e2.getValue());in int res= e2.getValue().compareTo(e1.getValue());, haben Sie eine absteigende Werte statt aufsteigend sortiert.
Tugcem
6
Ich würde lieber zurückkehren res != 0 ? res : e1.getKey().compareTo(e2.getKey()), um die Reihenfolge der Schlüssel mit gleichen Werten beizubehalten.
Marco Lackovic
45

In Java 8:

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

A TreeMapwird immer nach den Schlüsseln sortiert, alles andere ist unmöglich. Mit A Comparatorkönnen Sie lediglich steuern, wie die Schlüssel sortiert werden.

Wenn Sie die sortierten Werte möchten, müssen Sie sie in a extrahieren Listund sortieren.

Michael Borgwardt
quelle
10

Dies kann nicht mit a erfolgen Comparator, da immer der Schlüssel der Karte zum Vergleichen angezeigt wird. TreeMapkann nur nach dem Schlüssel sortieren.

Joachim Sauer
quelle
2
Wenn die TreeMap nur den Schlüssel an Comparator übergibt, wäre es möglich, wenn ich im Konstruktor des Komparators eine Referenz auf die TreeMap mache und dann den Schlüssel verwende, um den Wert zu erhalten, ungefähr so ​​(nicht sicher, wie ich als Referenz übergeben soll): class byValue implementiert Comparator {TreeMap theTree; public byValue (TreeMap theTree) {this.theTree = theTree; } public int compare (String k1, String k2) {// benutze die getKey-Methode von TreeMap für den Wert}}
vito huang
1
@vito: nein, da normalerweise einer der beiden Schlüssel noch nicht in der Karte enthalten ist und Sie seinen Wert nicht erhalten können.
Joachim Sauer
Ist dies nicht ein Beispiel dafür im Komparator von TreeMap? (obwohl, wie oben erwähnt, dies die Spezifikation verletzt, für SortedMapdie die Sortierung nach Schlüsseln festgelegt ist) beginnersbook.com/2014/07/…
Marcus
@Marcus: Comparatorverwendet eine vorhandene Karte, um die zu sortierenden Werte abzurufen . Mit anderen Worten, es können keine beliebigen Werte sortiert werden, die TreeMapspäter eingegeben werden, sondern nur Werte, die bereits in der ursprünglichen Karte enthalten sind.
Joachim Sauer
5

Olof Antwort ist gut, aber es braucht eine weitere Sache , bevor es ist perfekt. In den Kommentaren unter seiner Antwort weist dacwe (korrekt) darauf hin, dass seine Implementierung gegen den Compare / Equals-Vertrag für Sets verstößt. Wenn Sie versuchen, Einträge aufzurufen oder zu entfernen, die eindeutig im Satz enthalten sind, erkennt der Satz dies nicht, da Einträge mit gleichen Werten in den Satz eingefügt werden können. Um dies zu beheben, müssen wir die Gleichheit zwischen den Schlüsseln testen:

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

"Beachten Sie, dass die Reihenfolge, die von einer sortierten Menge beibehalten wird (unabhängig davon, ob ein expliziter Komparator bereitgestellt wird oder nicht), mit gleich übereinstimmen muss, wenn die sortierte Menge die Set-Schnittstelle korrekt implementieren soll ... Die Set-Schnittstelle wird in Bezug auf die Gleichheitsoperation definiert , aber ein Element führt alle vergleiche unter Verwendung seines compareTo Menge sortierte (oder zu vergleichen) Verfahren, so dass zwei Elemente, die vom Standpunkt des sortierten Satzes gehalten werden, gleich nach dieser Methode sind, gleich .“ ( http://docs.oracle.com/javase/6/docs/api/java/util/SortedSet.html )

Da wir ursprünglich die Gleichheit übersehen haben, um die Menge zu zwingen, gleichwertige Einträge hinzuzufügen, müssen wir jetzt die Gleichheit in den Schlüsseln testen, damit die Menge tatsächlich den gewünschten Eintrag zurückgibt. Das ist ein bisschen chaotisch und definitiv nicht so, wie Sets verwendet werden sollten - aber es funktioniert.

Halogen
quelle
3

Ich weiß, dass in diesem Beitrag speziell nach dem Sortieren einer TreeMap nach Werten gefragt wird, aber für diejenigen von uns, die sich nicht wirklich für die Implementierung interessieren, aber eine Lösung wünschen, die die Sammlung beim Hinzufügen von Elementen sortiert, würde ich mich über Feedback zu diesem TreeSet-basierten freuen Lösung. Zum einen lassen sich Elemente nicht einfach per Schlüssel abrufen, aber für den Anwendungsfall, den ich zur Hand hatte (die n Schlüssel mit den niedrigsten Werten zu finden), war dies keine Voraussetzung.

  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));
Paul Jackson
quelle
2

Viele Leute hören Ratschläge, List zu verwenden, und ich bevorzuge es auch

Hier sind zwei Methoden, mit denen Sie die Einträge der Karte nach ihren Werten sortieren können.

    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;
        }
Zina
quelle