Сортировать карту <Key, Value> по значениям - PullRequest
1514 голосов
/ 21 сентября 2008

Я относительно новичок в Java, и часто обнаруживаю, что мне нужно отсортировать Map<Key, Value> по значениям.

Поскольку значения не являются уникальными, я обнаруживаю, что преобразую keySet в array и сортирую этот массив по , сортирует массив с помощью пользовательского компаратора , который сортирует по значение, связанное с ключом.

Есть ли более простой способ?

Ответы [ 49 ]

843 голосов
/ 06 апреля 2010

Вот общая версия:

public class MapUtil {
    public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) {
        List<Entry<K, V>> list = new ArrayList<>(map.entrySet());
        list.sort(Entry.comparingByValue());

        Map<K, V> result = new LinkedHashMap<>();
        for (Entry<K, V> entry : list) {
            result.put(entry.getKey(), entry.getValue());
        }

        return result;
    }
}
408 голосов
/ 16 августа 2009

Важное примечание:

Этот код может сломаться несколькими способами. Если вы намереваетесь использовать предоставленный код, обязательно прочитайте также комментарии, чтобы знать о последствиях. Например, значения больше не могут быть получены по их ключу. (get всегда возвращает null.)


Кажется, намного проще, чем все вышесказанное. Используйте TreeMap следующим образом:

public class Testing {
    public static void main(String[] args) {
        HashMap<String, Double> map = new HashMap<String, Double>();
        ValueComparator bvc = new ValueComparator(map);
        TreeMap<String, Double> sorted_map = new TreeMap<String, Double>(bvc);

        map.put("A", 99.5);
        map.put("B", 67.4);
        map.put("C", 67.4);
        map.put("D", 67.3);

        System.out.println("unsorted map: " + map);
        sorted_map.putAll(map);
        System.out.println("results: " + sorted_map);
    }
}

class ValueComparator implements Comparator<String> {
    Map<String, Double> base;

    public ValueComparator(Map<String, Double> base) {
        this.base = base;
    }

    // Note: this comparator imposes orderings that are inconsistent with
    // equals.
    public int compare(String a, String b) {
        if (base.get(a) >= base.get(b)) {
            return -1;
        } else {
            return 1;
        } // returning 0 would merge keys
    }
}

Выход:

unsorted map: {D=67.3, A=99.5, B=67.4, C=67.4}
results: {D=67.3, B=67.4, C=67.4, A=99.5}
283 голосов
/ 24 мая 2014

Java 8 предлагает новый ответ: преобразовать записи в поток и использовать комбинаторы сравнения из Map.Entry:

Stream<Map.Entry<K,V>> sorted =
    map.entrySet().stream()
       .sorted(Map.Entry.comparingByValue());

Это позволит вам использовать записи, отсортированные в порядке возрастания значений. Если вам нужно нисходящее значение, просто поменяйте местами компаратор:

Stream<Map.Entry<K,V>> sorted =
    map.entrySet().stream()
       .sorted(Collections.reverseOrder(Map.Entry.comparingByValue()));

Если значения не сопоставимы, вы можете передать явный компаратор:

Stream<Map.Entry<K,V>> sorted =
    map.entrySet().stream()
       .sorted(Map.Entry.comparingByValue(comparator));

Затем вы можете перейти к использованию других потоковых операций для получения данных. Например, если вы хотите топ-10 на новой карте:

Map<K,V> topTen =
    map.entrySet().stream()
       .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
       .limit(10)
       .collect(Collectors.toMap(
          Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));

Или выведите на System.out:

map.entrySet().stream()
   .sorted(Map.Entry.comparingByValue())
   .forEach(System.out::println);
209 голосов
/ 06 августа 2010

Три однострочных ответа ...

Я бы использовал Google Collections Гуава , чтобы сделать это - если ваши значения Comparable, то вы можете использовать

valueComparator = Ordering.natural().onResultOf(Functions.forMap(map))

, которая создаст функцию (объект) для карты [которая принимает любую из клавиш в качестве входных данных, возвращает соответствующее значение], а затем применяет естественное (сопоставимое) упорядочение к ним [значениям].

Если они несопоставимы, то вам нужно будет что-то сделать в соответствии с

valueComparator = Ordering.from(comparator).onResultOf(Functions.forMap(map)) 

Они могут применяться к TreeMap (как Ordering расширяется Comparator) или LinkedHashMap после некоторой сортировки

NB : если вы собираетесь использовать TreeMap, помните, что если сравнение == 0, то элемент уже находится в списке (что произойдет, если у вас есть несколько значений, которые сравнивают одно и то же ). Чтобы облегчить это, вы можете добавить свой ключ к компаратору следующим образом (при условии, что ваши ключи и значения Comparable):

valueComparator = Ordering.natural().onResultOf(Functions.forMap(map)).compound(Ordering.natural())

= Применить естественное упорядочение к значению, отображаемому ключом, и соединить его с естественным упорядочением ключа

Обратите внимание, что это все равно не будет работать, если ваши ключи сравниваются с 0, но этого должно быть достаточно для большинства comparable элементов (так как hashCode, equals и compareTo часто синхронизируются ...)

См. Ordering.onResultOf () и Functions.forMap () .

Осуществление

Так что теперь, когда у нас есть компаратор, который делает то, что мы хотим, нам нужно получить от него результат.

map = ImmutableSortedMap.copyOf(myOriginalMap, valueComparator);

Теперь это, скорее всего, будет работать, но:

  1. необходимо сделать с полной картой
  2. Не пытайтесь сравнивать выше на TreeMap; нет смысла пытаться сравнивать вставленный ключ, когда он не имеет значения, до тех пор, пока после ввода не произойдет, т.е. он сломается очень быстро

Точка 1 для меня немного нарушает условия сделки; Коллекции Google невероятно ленивы (и это хорошо: вы можете выполнять практически все операции за одно мгновение; настоящая работа выполняется, когда вы начинаете использовать результат), и для этого необходимо скопировать карту целом !

"Полный" ответ / Live отсортированная карта по значениям

Не волнуйтесь, хотя; если вы были достаточно одержимы сортировкой «живой» карты таким образом, вы могли бы решить не одну, а обе (!) из вышеперечисленных проблем с помощью чего-то сумасшедшего, например:

Примечание. Это значительно изменилось в июне 2012 года - предыдущий код никогда не работал: требуется внутренний HashMap для поиска значений без создания бесконечного цикла между TreeMap.get() -> compare() и * 1073. * -> get()

import static org.junit.Assert.assertEquals;

import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

import com.google.common.base.Functions;
import com.google.common.collect.Ordering;

class ValueComparableMap<K extends Comparable<K>,V> extends TreeMap<K,V> {
    //A map for doing lookups on the keys for comparison so we don't get infinite loops
    private final Map<K, V> valueMap;

    ValueComparableMap(final Ordering<? super V> partialValueOrdering) {
        this(partialValueOrdering, new HashMap<K,V>());
    }

    private ValueComparableMap(Ordering<? super V> partialValueOrdering,
            HashMap<K, V> valueMap) {
        super(partialValueOrdering //Apply the value ordering
                .onResultOf(Functions.forMap(valueMap)) //On the result of getting the value for the key from the map
                .compound(Ordering.natural())); //as well as ensuring that the keys don't get clobbered
        this.valueMap = valueMap;
    }

    public V put(K k, V v) {
        if (valueMap.containsKey(k)){
            //remove the key in the sorted set before adding the key again
            remove(k);
        }
        valueMap.put(k,v); //To get "real" unsorted values for the comparator
        return super.put(k, v); //Put it in value order
    }

    public static void main(String[] args){
        TreeMap<String, Integer> map = new ValueComparableMap<String, Integer>(Ordering.natural());
        map.put("a", 5);
        map.put("b", 1);
        map.put("c", 3);
        assertEquals("b",map.firstKey());
        assertEquals("a",map.lastKey());
        map.put("d",0);
        assertEquals("d",map.firstKey());
        //ensure it's still a map (by overwriting a key, but with a new value) 
        map.put("d", 2);
        assertEquals("b", map.firstKey());
        //Ensure multiple values do not clobber keys
        map.put("e", 2);
        assertEquals(5, map.size());
        assertEquals(2, (int) map.get("e"));
        assertEquals(2, (int) map.get("d"));
    }
 }

Когда мы помещаем, мы гарантируем, что хеш-карта имеет значение для компаратора, а затем помещаем его в TreeSet для сортировки. Но перед этим мы проверяем хэш-карту, чтобы увидеть, что ключ на самом деле не является дубликатом. Кроме того, созданный нами компаратор также будет включать ключ, чтобы дублированные значения не удаляли неповторяющиеся ключи (из-за сравнения ==). Эти 2 пункта жизненно важны для обеспечения соблюдения контракта на карту; если вы думаете, что не хотите этого, то вы почти полностью изменили карту (до Map<V,K>).

Конструктор должен называться

 new ValueComparableMap(Ordering.natural());
 //or
 new ValueComparableMap(Ordering.from(comparator));
182 голосов
/ 21 сентября 2008

С http://www.programmersheaven.com/download/49349/download.aspx

private static <K, V> Map<K, V> sortByValue(Map<K, V> map) {
    List<Entry<K, V>> list = new LinkedList<>(map.entrySet());
    Collections.sort(list, new Comparator<Object>() {
        @SuppressWarnings("unchecked")
        public int compare(Object o1, Object o2) {
            return ((Comparable<V>) ((Map.Entry<K, V>) (o1)).getValue()).compareTo(((Map.Entry<K, V>) (o2)).getValue());
        }
    });

    Map<K, V> result = new LinkedHashMap<>();
    for (Iterator<Entry<K, V>> it = list.iterator(); it.hasNext();) {
        Map.Entry<K, V> entry = (Map.Entry<K, V>) it.next();
        result.put(entry.getKey(), entry.getValue());
    }

    return result;
}
48 голосов
/ 02 марта 2014

В Java 8 вы можете использовать streams api , чтобы сделать это значительно менее многословно:

Map<K, V> sortedMap = map.entrySet().stream()
                         .sorted(Entry.comparingByValue())
                         .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
31 голосов
/ 21 сентября 2008

Сортировка ключей требует, чтобы Comparator просматривал каждое значение для каждого сравнения. Более масштабируемое решение будет использовать entrySet напрямую, так как тогда значение будет сразу доступно для каждого сравнения (хотя я не подкреплял это цифрами).

Вот общая версия такой вещи:

public static <K, V extends Comparable<? super V>> List<K> getKeysSortedByValue(Map<K, V> map) {
    final int size = map.size();
    final List<Map.Entry<K, V>> list = new ArrayList<Map.Entry<K, V>>(size);
    list.addAll(map.entrySet());
    final ValueComparator<V> cmp = new ValueComparator<V>();
    Collections.sort(list, cmp);
    final List<K> keys = new ArrayList<K>(size);
    for (int i = 0; i < size; i++) {
        keys.set(i, list.get(i).getKey());
    }
    return keys;
}

private static final class ValueComparator<V extends Comparable<? super V>>
                                     implements Comparator<Map.Entry<?, V>> {
    public int compare(Map.Entry<?, V> o1, Map.Entry<?, V> o2) {
        return o1.getValue().compareTo(o2.getValue());
    }
}

Существуют способы уменьшить вращение памяти для вышеуказанного решения. Первый созданный ArrayList может, например, использоваться повторно в качестве возвращаемого значения; это потребовало бы подавления некоторых общих предупреждений, но это могло бы стоить того, чтобы повторно использовать библиотечный код. Кроме того, Comparator не нужно перераспределять при каждом вызове.

Вот более эффективная, хотя и менее привлекательная версия:

public static <K, V extends Comparable<? super V>> List<K> getKeysSortedByValue2(Map<K, V> map) {
    final int size = map.size();
    final List reusedList = new ArrayList(size);
    final List<Map.Entry<K, V>> meView = reusedList;
    meView.addAll(map.entrySet());
    Collections.sort(meView, SINGLE);
    final List<K> keyView = reusedList;
    for (int i = 0; i < size; i++) {
        keyView.set(i, meView.get(i).getKey());
    }
    return keyView;
}

private static final Comparator SINGLE = new ValueComparator();

Наконец, если вам нужен постоянный доступ к отсортированной информации (а не просто сортировать ее время от времени), вы можете использовать дополнительную мультикарту. Дайте мне знать, если вам нужно больше деталей ...

26 голосов
/ 21 сентября 2008

Библиотека commons-collection содержит решение под названием TreeBidiMap . Или вы можете взглянуть на API Google Collections. Он имеет TreeMultimap , который вы можете использовать.

И если вы не хотите использовать эти фреймворки ... они поставляются с исходным кодом.

25 голосов
/ 21 января 2010

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

Вот решение, которое, я думаю, подходит лучше:

public static <K, V extends Comparable<V>> Map<K, V> sortByValues(final Map<K, V> map) {
    Comparator<K> valueComparator =  new Comparator<K>() {
        public int compare(K k1, K k2) {
            int compare = map.get(k2).compareTo(map.get(k1));
            if (compare == 0) return 1;
            else return compare;
        }
    };
    Map<K, V> sortedByValues = new TreeMap<K, V>(valueComparator);
    sortedByValues.putAll(map);
    return sortedByValues;
}

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

17 голосов
/ 20 ноября 2013

Для достижения этой цели с новыми функциями в Java 8:

import static java.util.Map.Entry.comparingByValue;
import static java.util.stream.Collectors.toList;

<K, V> List<Entry<K, V>> sort(Map<K, V> map, Comparator<? super V> comparator) {
    return map.entrySet().stream().sorted(comparingByValue(comparator)).collect(toList());
}

Записи упорядочены по их значениям с использованием данного компаратора. В качестве альтернативы, если ваши значения взаимно сопоставимы, явный компаратор не требуется:

<K, V extends Comparable<? super V>> List<Entry<K, V>> sort(Map<K, V> map) {
    return map.entrySet().stream().sorted(comparingByValue()).collect(toList());
}

Возвращенный список является снимком данной карты во время вызова этого метода, поэтому ни один из них не отразит последующие изменения другого. Для живого итеративного просмотра карты:

<K, V extends Comparable<? super V>> Iterable<Entry<K, V>> sort(Map<K, V> map) {
    return () -> map.entrySet().stream().sorted(comparingByValue()).iterator();
}

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

...