TreeMap trier par valeur

137

Je veux écrire un comparateur qui me permettra de trier un TreeMap par valeur au lieu de l'ordre naturel par défaut.

J'ai essayé quelque chose comme ça, mais je ne trouve pas ce qui ne va pas:

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

Je suppose que ce que je demande est: puis-je obtenir un Map.Entrypassage au comparateur?

vito huang
la source
peut-être que cela vous aidera à stackoverflow.com/questions/109383/…
Inv3r53
Voir aussi: stackoverflow.com/questions/30425836/…
Paul Jackson

Réponses:

179

Vous ne pouvez pas avoir le TreeMaptri lui - même sur les valeurs, car cela défie la SortedMapspécification:

Un Mapqui assure en outre une commande totale sur ses clés .

Cependant, en utilisant une collection externe, vous pouvez toujours trier Map.entrySet() comme vous le souhaitez, soit par clés, valeurs, ou même une combinaison (!!) des deux.

Voici une méthode générique qui retourne a SortedSetof Map.Entry, étant donné a Mapdont les valeurs sont 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;
}

Vous pouvez maintenant effectuer les opérations suivantes:

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

Notez que des trucs géniaux se produiront si vous essayez de modifier soit le SortedSetlui - même, soit l' Map.Entryintérieur, car ce n'est plus une "vue" de la carte originale comme l' entrySet()est.

D'une manière générale, la nécessité de trier les entrées d'une carte par ses valeurs est atypique.


Remarque sur ==pourInteger

Votre comparateur d'origine compare en Integerutilisant ==. C'est presque toujours faux, car ==avec les Integeropérandes est une égalité de référence, pas une égalité de valeur.

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

Questions connexes

lubrifiants polygènes
la source
5
si vous ajoutez map.put ("D", 2); le résultat est toujours "[C = 1, B = 2, A = 3]" et non "[C = 1, B = 2, D = 2, A = 3]"
Igor Milla
3
Correction: int res = e2.getValue (). CompareTo (e1.getValue ()); retourne res! = 0? res: 1;
beshkenadze
new Integer (0) == new Integer (0) Vous comparez une référence à un objet, et vous créez deux objets! Le résultat est absolument correct et prévisible.
Yuriy Chernyshov
2
"D'une manière générale, la nécessité de trier les entrées d'une carte par ses valeurs est atypique" Je suis un débutant ici mais j'ai l'impression que cela devrait être une chose très courante? Par exemple, si je veux récupérer les 3 personnes les plus âgées d'une carte {"ana" = 37, "peter" = 43, "john" = 4, "mary" = 15, "Matt" = 78}
fartagaintuxedo
@igormilla Il y a une raison simple pour laquelle votre code fonctionne: la boxe automatique utilise Integer.valueOf. Et cela a un cache d'instances pour -128 à 127!
jokster
75

La réponse des lubrifiants polygéniques est presque parfaite. Il a cependant un bug important. Il ne gérera pas les entrées de mappage dont les valeurs sont identiques.

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

Produirait:

ape:1
frog:2
pig:3

Remarquez comment notre vache a disparu en partageant la valeur «1» avec notre singe: O!

Cette modification du code résout ce problème:

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
la source
2
-1. AFASIK aucune Setimplémentation ne contient un élément plus d'une fois. Vous venez de violer cette contrainte. De l' SortedSetAPI: Notez que l'ordre maintenu par un ensemble triée doit être compatible avec égaux ... . La solution serait de passer à une Listimplémentation.
dacwe
4
@dacwe égales valeurs pas clés
bellum
1
@bellum: Nous parlons de Sets ici. Puisque le Comparatorviole le Setcontrat Set.remove, Set.containsetc. ne fonctionne pas ! Vérifiez cet exemple sur ideone .
dacwe
3
Juste une note, si vous passez int res= e1.getValue().compareTo(e2.getValue());à int res= e2.getValue().compareTo(e1.getValue());, vous avez un ordre des valeurs décroissant au lieu de croissant.
tugcem
6
Je reviendrai plutôt res != 0 ? res : e1.getKey().compareTo(e2.getKey())pour conserver l'ordre des clés avec des valeurs égales.
Marco Lackovic
46

Dans 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
la source
17

A TreeMapest toujours trié par les clés, tout le reste est impossible. A Comparatorvous permet simplement de contrôler comment les clés sont triées.

Si vous voulez les valeurs triées, vous devez les extraire dans un Listet les trier.

Michael Borgwardt
la source
10

Cela ne peut pas être fait en utilisant a Comparator, car il obtiendra toujours la clé de la carte à comparer. TreeMapne peut trier que par clé.

Joachim Sauer
la source
2
si le TreeMap ne passera la clé qu'au comparateur, serait-il possible si je fais une référence du TreeMap dans le constructeur du comparateur, puis en utilisant la clé pour obtenir la valeur, quelque chose comme ça (je ne sais pas comment passer par référence): class byValue implémente le comparateur {TreeMap theTree; public byValue (TreeMap theTree) {this.theTree = theTree; } public int compare (String k1, String k2) {// utilise la méthode getKey de TreeMap à la valeur}}
vito huang
1
@vito: non, car généralement l'une des deux clés ne sera pas encore dans la carte et vous ne pourrez pas obtenir sa valeur.
Joachim Sauer
N'est-ce pas un exemple de faire cela dans le comparateur de TreeMap? (même si, comme mentionné ci-dessus, cela enfreint la spécification pour SortedMaplaquelle spécifie le tri par clés) beginnersbook.com/2014/07/...
Marcus
@Marcus: qui Comparatorutilise une carte existante pour obtenir les valeurs à trier. En d'autres termes, il ne peut pas trier les valeurs arbitraires placées dans le TreeMapdernier, uniquement les valeurs qui sont déjà dans la carte d'origine.
Joachim Sauer
5

La réponse d'Olof est bonne, mais il lui faut encore une chose avant d'être parfaite. Dans les commentaires ci-dessous sa réponse, dacwe souligne (correctement) que son implémentation viole le contrat Compare / Equals pour les Sets. Si vous essayez d'appeler contient ou de supprimer une entrée qui est clairement dans l'ensemble, l'ensemble ne la reconnaîtra pas en raison du code qui permet aux entrées de valeurs égales d'être placées dans l'ensemble. Donc, pour résoudre ce problème, nous devons tester l'égalité entre les clés:

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

"Notez que l'ordre maintenu par un ensemble trié (qu'un comparateur explicite soit fourni ou non) doit être cohérent avec égal si l'ensemble trié doit implémenter correctement l'interface Set ... l'interface Set est définie en termes de l'opération d'égalité , mais un ensemble trié effectue toutes les comparaisons d'éléments en utilisant sa méthode compareTo (ou compare), de sorte que deux éléments jugés égaux par cette méthode sont, du point de vue de l'ensemble trié, égaux . " ( http://docs.oracle.com/javase/6/docs/api/java/util/SortedSet.html )

Comme nous avions à l'origine négligé l'égalité afin de forcer l'ensemble à ajouter des entrées de valeur égale, nous devons maintenant tester l'égalité dans les clés pour que l'ensemble renvoie réellement l'entrée que vous recherchez. C'est un peu compliqué et ce n'est certainement pas la façon dont les ensembles étaient censés être utilisés - mais cela fonctionne.

Halogène
la source
3

Je sais que cet article demande spécifiquement de trier un TreeMap par valeurs, mais pour ceux d'entre nous qui ne se soucient pas vraiment de la mise en œuvre mais qui veulent une solution qui garde la collection triée au fur et à mesure que les éléments sont ajoutés, j'apprécierais les commentaires sur ce TreeSet Solution. D'une part, les éléments ne sont pas facilement récupérés par clé, mais pour le cas d'utilisation que j'avais sous la main (trouver les n clés avec les valeurs les plus basses), ce n'était pas une exigence.

  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
la source
2

Beaucoup de gens entendent qu'il est conseillé d'utiliser List et je préfère l'utiliser également

voici deux méthodes dont vous avez besoin pour trier les entrées de la carte en fonction de leurs valeurs.

    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
la source