TreeMap ordenar por valor

137

Quiero escribir un comparador que me permita ordenar un TreeMap por valor en lugar del orden natural predeterminado.

Intenté algo como esto, pero no puedo descubrir qué salió mal:

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

Supongo que lo que estoy preguntando es: ¿puedo Map.Entrypasarle al comparador?

vito huang
fuente
tal vez esto te ayudará a stackoverflow.com/questions/109383/…
Inv3r53
Ver también: stackoverflow.com/questions/30425836/…
Paul Jackson

Respuestas:

179

No puede TreeMapordenar los valores por sí mismo, ya que eso desafía la SortedMapespecificación:

A Mapque además proporciona un orden total en sus teclas .

Sin embargo, utilizando una colección externa, siempre puede ordenar Map.entrySet()como lo desee, ya sea por claves, valores o incluso una combinación (!!) de los dos.

Aquí hay un método genérico que devuelve un SortedSetde Map.Entry, dado un Mapcuyos valores son 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;
}

Ahora puedes hacer lo siguiente:

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

Tenga en cuenta que sucederán cosas funky si intenta modificar el SortedSetpropio o el Map.Entryinterior, porque esto ya no es una "vista" del mapa original como entrySet()es.

En términos generales, la necesidad de ordenar las entradas de un mapa por sus valores es atípica.


Nota sobre ==paraInteger

Su comparador original se compara Integerusando ==. Esto casi siempre está mal, ya que ==con los Integeroperandos es una igualdad de referencia, no una igualdad de valores.

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

Preguntas relacionadas

poligenelubricantes
fuente
55
si agrega map.put ("D", 2); el resultado sigue siendo "[C = 1, B = 2, A = 3]" y no "[C = 1, B = 2, D = 2, A = 3]"
Igor Milla
3
Corrección: int res = e2.getValue (). CompareTo (e1.getValue ()); return res! = 0? res: 1;
beshkenadze
nuevo entero (0) == nuevo entero (0) ¡Usted compara las referencias a un objeto y crea dos objetos! La salida es absolutamente correcta y predecible.
Yuriy Chernyshov
2
"En términos generales, la necesidad de ordenar las entradas de un mapa por sus valores es atípica" ¿Soy un principiante aquí pero siento que debería ser algo muy común? Por ejemplo, si quiero recuperar a las 3 personas más viejas en un mapa {"ana" = 37, "peter" = 43, "john" = 4, "mary" = 15, "Matt" = 78}
fartagaintuxedo
@igormilla Hay una razón simple por la cual su código funciona: Autoboxing usa Integer.valueOf. ¡Y eso tiene un caché de instancias para -128 a 127!
bromista
75

La respuesta de polygenelubricants es casi perfecta. Sin embargo, tiene un error importante. No manejará las entradas del mapa donde los valores son los mismos.

Este código: ...

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

Daría salida:

ape:1
frog:2
pig:3

Observe cómo nuestra vaca desapareció al compartir el valor "1" con nuestro mono: ¡O!

Esta modificación del código resuelve ese 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;
    }
Olof Larsson
fuente
2
-1. AFASIK ninguna Setimplementación contiene un elemento más de una vez. Acabas de violar esa restricción. A partir de la SortedSetAPI: Tenga en cuenta que el orden mantenido por un conjunto ordenado debe ser consistente con los iguales ... . La solución sería cambiar a una Listimplementación.
dacwe
44
@dacwe valores iguales no claves
bellum
1
@bellum: Estamos hablando de Sets aquí. ¡Ya que Comparatorviola el Setcontrato Set.remove, Set.containsetc. no funciona ! Mira este ejemplo en ideone .
dacwe
3
Solo una nota, si cambia int res= e1.getValue().compareTo(e2.getValue());a int res= e2.getValue().compareTo(e1.getValue());, tiene un orden de valores descendente en lugar de ascendente.
Tugcem
66
Prefiero volver res != 0 ? res : e1.getKey().compareTo(e2.getKey())a preservar el orden de las teclas con valores iguales.
Marco Lackovic
46

En 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
fuente
17

A siempreTreeMap está ordenado por las teclas, cualquier otra cosa es imposible. A simplemente le permite controlar cómo se ordenan las claves.Comparator

Si desea los valores ordenados, debe extraerlos en Listay ordenarlos.

Michael Borgwardt
fuente
10

Esto no se puede hacer usando a Comparator, ya que siempre obtendrá la clave del mapa para comparar. TreeMapsolo se puede ordenar por clave.

Joachim Sauer
fuente
2
si TreeMap solo pasará la clave a Comparator, ¿sería factible si hago una referencia de TreeMap en el constructor del comparador, y luego uso la clave para obtener el valor, algo como esto (no estoy seguro de cómo pasar por referencia): class byValue implementa Comparator {TreeMap theTree; public byValue (TreeMap theTree) {this.theTree = theTree; } public int compare (String k1, String k2) {// usa el método getKey de TreeMap con el valor}}
vito huang
1
@vito: no, porque generalmente una de las dos claves aún no estará en el mapa y no podrás obtener su valor.
Joachim Sauer
¿No es este un ejemplo de hacer esto dentro del Comparador de TreeMap? (aunque, como se mencionó anteriormente, esto rompe la especificación para la SortedMapcual se especifica la clasificación por claves) beginnersbook.com/2014/07/…
Marcus
@Marcus: que Comparatorusa un mapa existente para obtener los valores para ordenar. En otras palabras, no puede ordenar los valores arbitrarios puestos TreeMapmás adelante, solo los valores que ya están en el Mapa original.
Joachim Sauer
5

La respuesta de Olof es buena, pero necesita una cosa más antes de que sea perfecta. En los comentarios debajo de su respuesta, dacwe (correctamente) señala que su implementación viola el contrato Comparar / Igual para Conjuntos. Si intenta llamar a las entradas que contiene o elimina en una entrada que está claramente en el conjunto, el conjunto no lo reconocerá debido al código que permite colocar entradas con valores iguales en el conjunto. Entonces, para solucionar esto, necesitamos probar la igualdad entre las teclas:

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

"Tenga en cuenta que la ordenación mantenida por un conjunto ordenado (se proporcione o no un comparador explícito) debe ser coherente con iguales si el conjunto ordenado implementa correctamente la interfaz Establecer ... la interfaz Establecer se define en términos de la operación igual , pero un conjunto ordenado realiza todas las comparaciones de elementos utilizando su método compareTo (o compare), por lo que dos elementos que se consideran iguales por este método son, desde el punto de vista del conjunto ordenado, iguales ". ( http://docs.oracle.com/javase/6/docs/api/java/util/SortedSet.html )

Como originalmente pasamos por alto la igualdad para forzar al conjunto a agregar entradas de igual valor, ahora tenemos que probar la igualdad en las claves para que el conjunto realmente devuelva la entrada que está buscando. Esto es un poco desordenado y definitivamente no es cómo se pretendía usar los conjuntos, pero funciona.

Halógeno
fuente
3

Sé que esta publicación solicita específicamente ordenar un TreeMap por valores, pero para aquellos de nosotros que realmente no nos importa la implementación, pero queremos una solución que mantenga la colección ordenada a medida que se agregan elementos, agradecería sus comentarios sobre este basado en TreeSet solución. Por un lado, los elementos no se recuperan fácilmente por clave, pero para el caso de uso que tenía a mano (encontrar las n claves con los valores más bajos), esto no 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));
Paul Jackson
fuente
2

Mucha gente escucha consejos para usar List y prefiero usarlo también

Aquí hay dos métodos que necesita para ordenar las entradas del Mapa de acuerdo con sus valores.

    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
fuente