TreeMap classificar por valor

137

Quero escrever um comparador que permita classificar um TreeMap por valor, em vez da ordem natural padrão.

Eu tentei algo assim, mas não consigo descobrir o que deu errado:

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

Acho que o que estou perguntando é: Posso passar um Map.Entrypasse para o comparador?

vito huang
fonte
talvez isso ajude você a stackoverflow.com/questions/109383/…
Inv3r53
Veja também: stackoverflow.com/questions/30425836/…
Paul Jackson

Respostas:

179

Você não pode TreeMapclassificar os valores, pois isso desafia a SortedMapespecificação:

A Mapque fornece ainda um total de pedidos em suas chaves .

No entanto, usando uma coleção externa, você sempre pode classificar da maneira Map.entrySet()que desejar, por chaves, valores ou até mesmo uma combinação (!!) das duas.

Aqui está um método genérico que retorna um SortedSetde Map.Entry, dados a Mapcujos valores são 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;
}

Agora você pode fazer o seguinte:

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

Observe que coisas descoladas acontecerão se você tentar modificar o SortedSetpróprio, ou o Map.Entryinterior, porque essa não é mais uma "visualização" do mapa original entrySet().

De um modo geral, a necessidade de classificar as entradas de um mapa por seus valores é atípica.


Nota ==paraInteger

Seu comparador original compara Integerusando ==. Isso quase sempre está errado, já que ==com Integeroperandos é uma igualdade de referência, não uma igualdade de valor.

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

Perguntas relacionadas

poligenelubricants
fonte
5
se você adicionar map.put ("D", 2); o resultado ainda é "[C = 1, B = 2, A = 3]" e não "[C = 1, B = 2, D = 2, A = 3]"
Igor Milla
3
Solução: int res = e2.getValue (). CompareTo (e1.getValue ()); retornar res! = 0? res: 1;
beshkenadze
novo Inteiro (0) == novo Inteiro (0) Você compara uma referência a um objeto e cria dois objetos! A saída é absolutamente correta e previsível.
Yuriy Chernyshov
2
"De um modo geral, a necessidade de classificar as entradas de um mapa por seus valores é atípica" Sou iniciante aqui, mas acho que deve ser uma coisa muito comum de se fazer? Por exemplo, se eu quiser recuperar as 3 pessoas mais velhas em um mapa {"ana" = 37, "peter" = 43, "john" = 4, "mary" = 15, "Matt" = 78}
fartagaintuxedo
@igormilla Há uma razão simples para o seu código funcionar: a caixa automática usa Integer.valueOf. E isso tem um cache de instâncias para -128 a 127!
Jokster
75

resposta de poligenelubricants é quase perfeita. Ele tem um bug importante. Ele não manipula entradas de mapa onde os valores são os mesmos.

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

Saída:

ape:1
frog:2
pig:3

Observe como nossa vaca desapareceu ao compartilhar o valor "1" com o nosso macaco: O!

Esta modificação do código resolve esse 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
fonte
2
-1. AFASIK nenhuma Setimplementação contém um elemento mais de uma vez. Você acabou de violar essa restrição. A partir da SortedSetAPI: Note que a ordem mantida por um conjunto classificado deve ser consistente com iguais ... . A solução seria mudar para uma Listimplementação.
dacwe
4
@dacwe valores iguais, não chaves
bellum
1
@bellum: Estamos falando de Sets aqui. Desde que Comparatorviola o Setcontrato Set.remove, Set.containsetc não funciona ! Veja este exemplo em ideone .
dacwe
3
Apenas uma nota, se você mudar int res= e1.getValue().compareTo(e2.getValue());para int res= e2.getValue().compareTo(e1.getValue());, você tem valores ordem decrescente em vez de ascendente.
Tugcem
6
Prefiro voltar res != 0 ? res : e1.getKey().compareTo(e2.getKey())a preservar a ordem das chaves com valores iguais.
Marco Lackovic
45

No 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
fonte
17

A TreeMapé sempre classificado pelas teclas, qualquer outra coisa é impossível. A Comparatorapenas permite controlar como as chaves são classificadas.

Se você deseja que os valores classificados, você deve extraí-los Liste ordená- los .

Michael Borgwardt
fonte
10

Isso não pode ser feito usando a Comparator, pois ele sempre compara a chave do mapa. TreeMapsó pode classificar pela chave.

Joachim Sauer
fonte
2
se o TreeMap passasse apenas a chave para o Comparator, seria possível se eu fizesse uma referência do TreeMap no construtor do comparador e, em seguida, usando a chave para obter o valor, algo assim (não sei como passar por referência): class byValue implementa o comparador {TreeMap theTree; public byValue (ÁrvoreMapa da Árvore) {this.theTree = theTree; } public int compare (String k1, String k2) {// use o método getKey do TreeMap para o valor}}
vito huang
1
@vito: não, porque geralmente uma das duas chaves ainda não estará no mapa e você não poderá obter seu valor.
Joachim Sauer
Não é este um exemplo de como fazer isso no Comparador do TreeMap? (apesar de, tal como mencionado acima, isto faz quebrar a especificação para SortedMapque especifica a classificação por teclas) beginnersbook.com/2014/07/...
Marcus
@ Marcus: que Comparatorusa um mapa existente para obter os valores por ordem. Em outras palavras, ele não pode classificar valores arbitrários colocados TreeMapposteriormente, apenas valores que já estão no mapa original.
Joachim Sauer
5

A resposta de Olof é boa, mas precisa de mais uma coisa antes de ser perfeita. Nos comentários abaixo de sua resposta, dacwe (corretamente) ressalta que sua implementação viola o contrato Compare / Igual a Conjuntos. Se você tentar chamar a opção contém ou remover uma entrada que esteja claramente no conjunto, o conjunto não o reconhecerá devido ao código que permite que entradas com valores iguais sejam colocadas no conjunto. Portanto, para corrigir isso, precisamos testar a igualdade entre as chaves:

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

"Observe que a ordem mantida por um conjunto classificado (se um comparador explícito é ou não fornecido) deve ser consistente com iguais se o conjunto classificado deve implementar corretamente a interface Set ... a interface Set é definida em termos da operação igual , mas um conjunto classificado executa todas as comparações de elementos usando seu método compareTo (ou compare), portanto, dois elementos que são considerados iguais por esse método são, do ponto de vista do conjunto classificado, iguais . " ( http://docs.oracle.com/javase/6/docs/api/java/util/SortedSet.html )

Como originalmente ignoramos a igualdade para forçar o conjunto a adicionar entradas com valores iguais, agora precisamos testar a igualdade nas chaves para que o conjunto realmente retorne a entrada que você está procurando. Isso é meio confuso e definitivamente não é como os aparelhos deveriam ser usados ​​- mas funciona.

Halogênio
fonte
3

Eu sei que este post pede especificamente a classificação de um TreeMap por valores, mas para aqueles que realmente não se importam com a implementação, mas desejam uma solução que mantenha a coleção classificada à medida que os elementos forem adicionados, eu gostaria de receber feedback sobre esse TreeSet baseado em solução. Por um lado, os elementos não são facilmente recuperados por chave, mas para o caso de uso que eu tinha em mãos (localizando as n chaves com os valores mais baixos), isso não era um 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
fonte
2

Muitas pessoas ouvem conselhos para usar a Lista e eu prefiro usá-la também

Aqui estão dois métodos para ordenar as entradas do mapa de acordo com seus 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
fonte