TreeMap ordina per valore


137

Voglio scrivere un comparatore che mi permetta di ordinare una TreeMap in base al valore anziché all'ordinamento naturale predefinito.

Ho provato qualcosa del genere, ma non riesco a scoprire cosa è andato storto:

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

Immagino che cosa sto chiedendo è: posso ottenere un Map.Entrypassaggio al comparatore?




Risposte:


179

Non puoi avere lo TreeMapstesso ordinamento sui valori, poiché ciò sfida le SortedMapspecifiche:

A Mapche fornisce inoltre un ordinamento totale sulle sue chiavi .

Tuttavia, utilizzando una raccolta esterna, è sempre possibile ordinare Map.entrySet()come desiderato, in base a chiavi, valori o persino una combinazione (!!) dei due.

Ecco un metodo generico che restituisce un SortedSetdi Map.Entry, dato un i Mapcui valori sono 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;
}

Ora puoi fare quanto segue:

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

Nota che succederà qualcosa di strano se provi a modificare il SortedSetsé stesso o l' Map.Entryinterno, perché questa non è più una "vista" della mappa originale come entrySet()è.

In generale, la necessità di ordinare le voci di una mappa in base ai suoi valori è atipica.


Nota ==perInteger

Il tuo comparatore originale confronta Integerusando ==. Questo è quasi sempre sbagliato, poiché ==con gli Integeroperandi è un'uguaglianza di riferimento, non un'uguaglianza di valore.

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

Domande correlate


5
se aggiungi map.put ("D", 2); il risultato è ancora "[C = 1, B = 2, A = 3]" e non "[C = 1, B = 2, D = 2, A = 3]"
Igor Milla

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

new Integer (0) == new Integer (0) Confronti i riferimenti a un oggetto e crei due oggetti! L'output è assolutamente corretto e prevedibile.
Yuriy Chernyshov,

2
"In generale, la necessità di ordinare le voci di una mappa in base ai suoi valori è atipica" Sono un principiante qui ma sento che dovrebbe essere una cosa molto comune da fare? Ad esempio, se voglio recuperare le 3 persone più anziane in una mappa {"ana" = 37, "peter" = 43, "john" = 4, "mary" = 15, "Matt" = 78}
fartagaintuxedo

@igormilla C'è un semplice motivo per cui il tuo codice funziona: Autoboxing utilizza Integer.valueOf. E questo ha una cache di istanze da -128 a 127!
burlone

75

la risposta dei poligenelubrificanti è quasi perfetta. Ha un bug importante però. Non gestirà le voci della mappa in cui i valori sono gli stessi.

Questo codice: ...

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

Uscita:

ape:1
frog:2
pig:3

Nota come la nostra mucca è scomparsa mentre condivideva il valore "1" con la nostra scimmia: O!

Questa modifica del codice risolve questo problema:

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 nessuna Setimplementazione contiene un elemento più di una volta. Hai appena violato quel vincolo. Dal SortedSetAPI: Si noti che l'ordinamento gestito da un insieme ordinato deve essere coerente con uguali ... . La soluzione sarebbe passare a Listun'implementazione.
dacwe,

4
@dacwe valori uguali non chiavi
bellum

1
@bellum: stiamo parlando di Sets qui. Dal momento che Comparatorviola il Setcontratto Set.remove, Set.containsecc. Non funziona ! Dai un'occhiata a questo esempio su ideone .
dacwe,

3
Solo una nota, se si passa int res= e1.getValue().compareTo(e2.getValue());a int res= e2.getValue().compareTo(e1.getValue());, si ha un ordine di valori decrescente invece di crescente.
tugcem,

6
Preferirei tornare res != 0 ? res : e1.getKey().compareTo(e2.getKey())per preservare l'ordine delle chiavi con valori uguali.
Marco Lackovic,

46

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

17

A TreeMapviene sempre ordinato in base alle chiavi, qualsiasi altra cosa è impossibile. A Comparatorconsente semplicemente di controllare il modo in cui le chiavi sono ordinate.

Se vuoi i valori ordinati, devi estrarli in a Liste ordinarli.


10

Questo non può essere fatto usando un Comparator, poiché otterrà sempre la chiave della mappa da confrontare. TreeMappuò solo ordinare in base alla chiave.


2
se TreeMap passerà la chiave solo a Comparator, sarebbe possibile se io facessi un riferimento a TreeMap nel costruttore del comparatore, quindi usando la chiave per ottenere il valore, qualcosa del genere (non so come passare per riferimento): class byValue implementa Comparator {TreeMap theTree; public byValue (TreeMap theTree) {this.theTree = theTree; } public int compare (String k1, String k2) {// usa il metodo getKey di TreeMap per il valore}}
vito huang

1
@vito: no, perché di solito una delle due chiavi non sarà ancora nella mappa e non sarai in grado di ottenere il suo valore.
Joachim Sauer,

Non è questo un esempio di come farlo all'interno del comparatore di TreeMap? (anche se, come detto sopra, questo non infrange le specifiche per le SortedMapquali si specifica l'ordinamento per chiavi) beginnersbook.com/2014/07/…
Marcus

@Marcus: che Comparatorutilizza una mappa esistente per ottenere i valori da ordinare. In altre parole, non può ordinare i valori arbitrari inseriti in TreeMapseguito, solo i valori che sono già nella Mappa originale.
Joachim Sauer,

5

La risposta di Olof è buona, ma ha bisogno di un'altra cosa prima che sia perfetta. Nei commenti sotto la sua risposta, dacwe (correttamente) sottolinea che la sua implementazione viola il contratto Compare / Equals per i Set. Se si tenta di chiamare contiene o rimuove una voce che è chiaramente nel set, il set non lo riconoscerà a causa del codice che consente di inserire voci con valori uguali nel set. Quindi, per risolvere questo problema, dobbiamo verificare l'uguaglianza tra le chiavi:

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

"Si noti che l'ordinamento gestito da un set ordinato (indipendentemente dal fatto che sia fornito o meno un comparatore esplicito) deve essere coerente con uguale se il set ordinato deve implementare correttamente l'interfaccia Set ... l'interfaccia Set viene definita in termini di operazione uguale , ma un set ordinato esegue tutti i confronti di elementi usando il suo metodo compareTo (o compare), quindi due elementi che sono considerati uguali da questo metodo sono, dal punto di vista del set ordinato, uguali . " ( http://docs.oracle.com/javase/6/docs/api/java/util/SortedSet.html )

Dato che inizialmente abbiamo trascurato l'uguaglianza per forzare l'insieme ad aggiungere voci di uguale valore, ora dobbiamo testare l'uguaglianza nelle chiavi affinché l'insieme restituisca effettivamente la voce che stai cercando. Questo è un po 'disordinato e sicuramente non è il modo in cui i set dovevano essere usati - ma funziona.


3

So che questo post richiede specificamente di ordinare una TreeMap in base ai valori, ma per quelli di noi che non si interessano molto all'implementazione ma vogliono una soluzione che mantenga la raccolta ordinata quando vengono aggiunti elementi, apprezzerei il feedback su questo TreeSet-based soluzione. Per uno, gli elementi non sono facilmente recuperabili dalla chiave, ma per il caso d'uso che avevo a portata di mano (trovare le chiavi n con i valori più bassi), questo non era un requisito.

  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

Molte persone sentono il consiglio di usare List e preferisco anche usarlo

qui ci sono due metodi necessari per ordinare le voci della mappa in base ai loro valori.

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