TreeMap сортировка по значению

137

Я хочу написать компаратор, который позволит мне сортировать TreeMap по значению вместо естественного упорядочения по умолчанию.

Я пробовал что-то вроде этого, но не могу узнать, что пошло не так:

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

Я думаю, что я спрашиваю: могу ли я Map.Entryпередать в компаратор?

Вито Хуан
источник
возможно, это поможет вам stackoverflow.com/questions/109383/…
Inv3r53
Смотрите также: stackoverflow.com/questions/30425836/…
Пол Джексон,

Ответы:

179

Вы не можете TreeMapсамостоятельно сортировать значения, так как это не поддается SortedMapспецификации:

А Map, кроме того, обеспечивает полный порядок своих ключей .

Однако, используя внешнюю коллекцию, вы всегда можете отсортировать по Map.entrySet()своему усмотрению, либо по ключам, значениям или даже по комбинации (!!) двух.

Вот общий метод , который возвращает SortedSetиз Map.Entry, учитывая , Mapчьи ценности 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;
}

Теперь вы можете сделать следующее:

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

Обратите внимание, что прикольные вещи произойдут, если вы попытаетесь изменить либо SortedSetсам, либо Map.Entryвнутри, потому что это больше не «вид» оригинальной карты, как entrySet()есть.

Вообще говоря, необходимость сортировать записи карты по ее значениям нетипична.


Обратите внимание на ==дляInteger

Ваш оригинальный компаратор сравнивает, Integerиспользуя ==. Это почти всегда неправильно, так как ==с Integerоперандами является ссылочное равенство, а не равенство значений.

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

Смежные вопросы

polygenelubricants
источник
5
если вы добавите map.put ("D", 2); результат по-прежнему "[C = 1, B = 2, A = 3]", а не "[C = 1, B = 2, D = 2, A = 3]"
Игорь Милла,
3
Исправлено: int res = e2.getValue (). CompareTo (e1.getValue ()); вернуть res! = 0? разрешение: 1;
Бешкенадзе
new Integer (0) == new Integer (0) Вы сравниваете ссылки на объекты и создаете два объекта! Результат абсолютно правильный и предсказуемый.
Юрий Чернышов
2
«Вообще говоря, необходимость сортировать записи карты по ее значениям нетипична». Я новичок здесь, но я чувствую, что это должно быть очень распространенным делом? Например, если я хочу получить 3 самых старых человека на карте {"ana" = 37, "peter" = 43, "john" = 4, "mary" = 15, "Matt" = 78}
fartagaintuxedo
@igormilla Существует простая причина, почему ваш код работает: Autoboxing использует Integer.valueOf. И это имеет кэш экземпляров для -128 до 127!
Jokster
75

ответ на полигенные смазки почти идеален. Хотя есть одна важная ошибка. Он не будет обрабатывать записи карты, где значения одинаковы.

Этот код: ...

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

Будет вывод:

ape:1
frog:2
pig:3

Обратите внимание, как исчезла наша корова, когда она поделилась значением «1» с нашей обезьяной: O!

Эта модификация кода решает эту проблему:

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 нет Setреализации содержат элемент более одного раза. Вы только что нарушили это ограничение. Из SortedSetAPI: Обратите внимание , что порядок поддерживается отсортированным набор должен быть согласован с равными ... . Решение было бы перейти к Listреализации.
dacwe 12.12.12
4
@ Dacwe равные значения, а не ключи
Беллум
1
@bellum: Мы говорим о Setс здесь. Так как Comparatorнарушает Setдоговор Set.removeи Set.containsт. Д. Не работает ! Проверьте этот пример на ideone .
dacwe
3
Просто обратите внимание, если вы переключитесь int res= e1.getValue().compareTo(e2.getValue());в int res= e2.getValue().compareTo(e1.getValue());, у вас будет порядок убывания значений вместо возрастания.
Tugcem
6
Я бы предпочел вернуться, res != 0 ? res : e1.getKey().compareTo(e2.getKey())чтобы сохранить порядок ключей с одинаковыми значениями.
Марко Лацкович
46

В 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

TreeMapБудет всегда отсортирован по клавишам, все остальное невозможно. ComparatorПросто позволяет контролировать , как ключи сортируются.

Если вы хотите отсортированные значения, вы должны извлечь их в Listи отсортировать.

Майкл Боргвардт
источник
10

Это не может быть сделано с помощью a Comparator, так как он всегда получит ключ карты для сравнения. TreeMapсортировать можно только по ключу.

Йоахим Зауэр
источник
2
если TreeMap будет передавать ключ только в Comparator, будет ли это возможно, если я сделаю ссылку на TreeMap в конструкторе компаратора, а затем использую ключ для получения значения, что-то вроде этого (не уверен, как передать по ссылке): class byValue реализует Comparator {TreeMap theTree; public byValue (TreeMap theTree) {this.theTree = theTree; } public int сравнивать (String k1, String k2) {// использовать метод getKey TreeMap для значения}}
vito huang
1
@vito: нет, потому что обычно один из двух ключей еще не будет на карте, и вы не сможете получить его значение.
Иоахим Зауэр
Разве это не пример того, как это делается в компараторе TreeMap? (хотя, как упоминалось выше, это нарушает спецификацию, для SortedMapкоторой указана сортировка по ключам) beginnersbook.com/2014/07/…
Marcus
@Marcus: Comparatorиспользует существующую карту для сортировки значений. Другими словами, он не может сортировать произвольные значения, введенные TreeMapпозже, только значения, которые уже есть в исходной карте.
Иоахим Зауэр
5

Ответ Олофа хорош, но для совершенства нужна еще одна вещь. В комментариях под своим ответом dacwe (правильно) указывает, что его реализация нарушает контракт Сравнения / Равного для Наборов. Если вы попытаетесь вызвать, содержит или удаляет запись, которая явно находится в наборе, набор не распознает ее из-за кода, позволяющего размещать записи с равными значениями в наборе. Итак, чтобы это исправить, нам нужно проверить равенство ключей:

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

«Обратите внимание, что порядок, поддерживаемый отсортированным набором (независимо от того, предоставлен или нет явный компаратор), должен соответствовать равенствам, если отсортированный набор должен правильно реализовывать интерфейс Set ... интерфейс Set определяется в терминах операции equals , но отсортированный набор выполняет все сравнения элементов, используя свой метод CompareTo (или сравнение), поэтому два элемента, которые считаются равными с помощью этого метода, с точки зрения отсортированного набора равны ". ( http://docs.oracle.com/javase/6/docs/api/java/util/SortedSet.html )

Поскольку мы изначально упустили равенство, чтобы заставить набор добавлять записи с равными значениями, теперь мы должны проверить равенство в ключах, чтобы набор фактически возвращал искомую запись. Это немного грязно и определенно не так, как наборы были предназначены для использования - но это работает.

галоген
источник
3

Я знаю, что этот пост специально просит сортировать TreeMap по значениям, но для тех из нас, кто на самом деле не заботится о реализации, но хочет решение, которое сохраняет сортировку коллекции по мере добавления элементов, я был бы признателен за отзыв об этом на основе TreeSet решение. С одной стороны, элементы нелегко получить по ключу, но для имеющегося у меня варианта использования (поиск n ключей с наименьшими значениями) это не было требованием.

  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

Многие люди слышат совет использовать List, и я предпочитаю использовать его

Вот два метода, которые вам нужно отсортировать записи карты в соответствии с их значениями.

    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;
        }
зина
источник