按照值对 Map<Key, Value> 进行排序

1881
我需要根据值对一个Map进行排序。
由于值不是唯一的,我发现自己需要将keySet转换为数组,并通过使用自定义比较器对该数组进行排序,以便根据与键关联的值进行排序。
有没有更简单的方法?

32
地图的目的不是为了排序,而是为了快速访问。对象相等的值会违反地图的约束条件。使用entry set,例如 List<Map.Entry<...>> list =new LinkedList(map.entrySet())Collections.sort .... 进行排序。 - Hannes
2
一个可能出现这种情况的案例是当我们尝试在Java中使用计数器(Map<Object,Integer>)时。按出现次数排序将成为常见操作。像Python这样的语言具有内置的计数器数据结构。对于Java中的另一种实现方式,此处提供了一个示例。 - demongolem
14
有很多使用排序映射的情况,这就是为什么在jdk中有TreeMap和ConcurrentSkipListMap的原因。 - alobodzk
8
TreeMap和ConcurrentSkipListMap会根据键进行排序。问题是如何按值排序。 - Peter
3
根据您的使用情况,保留一个重复的TreeMap,将值映射到键可能是合理的。例如,您的常规map可能为"a"->5,"b"->7。而您的“排序”map可以有5->“a”,7->“b”。您只需在不同的地方使用适当的map,并努力始终同时修改这两个map。虽然存在许多警告和假设,但对于某些情况而言,与所有依赖于主动排序您的值的答案相比,这可能是一种简单而有效的答案。 - rococo
显示剩余2条评论
65个回答

1009

以下是通用版本:

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

15
感到高兴这对您有所帮助。LinkedHashMap对于解决方案非常重要,因为它提供可预测的迭代顺序。 - Carter Page
4
@buzz3791 没错。这在任何排序算法中都是如此。在排序过程中更改结构中节点的值会导致不可预测(并且几乎总是不好的)结果。 - Carter Page
3
@Sheagorath 我在 Android 上尝试了一下,它也可以正常工作。考虑到您正在使用 Java 6 版本,这不是特定于平台的问题。您是否已经在您的值对象中正确实现了 Comparable 接口呢? - saiyancoder
7
Java 8 版本应该使用 forEachOrdered 而不是 forEach,因为 forEach 的文档中注明:“此操作的行为明确是不确定的。” - rob
1
完全抄袭了这个,但在评论中给@CarterPage署名(反正它将成为一个开源项目)。非常感谢。 - Nathan Beach
显示剩余21条评论

485

Java 8 提供了一个新的解决方案:将 Map 的 entry 转换成一个流,并使用来自 Map.Entry 的比较器组合器:

Java 8提供了一种新的解决方法:将Map的条目转换为流,并使用来自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));

上面显示的LinkedHashMap按照插入的顺序迭代条目。

或者打印到System.out:

map.entrySet().stream()
   .sorted(Map.Entry.comparingByValue())
   .forEach(System.out::println);

14
它将并行工作,但您可能会发现合并地图以组合部分结果的成本太高,因此并行版本的性能可能不如您所希望的那样好。但是它确实可以工作并产生正确的答案。 - Brian Goetz
排序完成后,我们该如何从 Stream<> 中提取回映射? - CyberMew
2
在 top10 的例子中,难道不需要使用 compareByValue 吗? - Leo
1
制作前十部分错误,您需要按此处所述添加两个参数:https://dev59.com/b2sz5IYBdhLWcg3wFUC_#19671853 - Steven
1
@Benj,从提取前10个方面来看,它是可行的,但生成的映射将不再有序。 - OrangeDog
显示剩余7条评论

431

重要说明:

这段代码可能会出现多种问题。如果您打算使用提供的代码,请务必仔细阅读评论,以了解其影响。例如,无法通过键检索值(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}

18
不再需要。在这个问题的代码中,为什么要将值强制转换为Double类型?难道不应该只是return ((Comparable)base.get(a)).compareTo(((Comparable)base.get(b)))吗? - Stephen
12
@Stephen:不行。在这种情况下,所有键值相等的键都会被删除(equals和按引用比较之间的区别)。此外:即使是这段代码也存在以下序列的问题:map.put("A","1d");map.put("B","1d");map.put("C",67d);map.put("D",99.5d); - steffen
43
Treemap所使用的比较器与equals方法不一致(请参见sortMap javadox)。这意味着从Treemap中检索项目将无法正常工作。sorted_map.get("A")将返回null。这意味着此处使用的Treemap是有问题的。 - mR_fr0g
91
如果不太清楚的话,这个解决方案可能无法实现您的要求,如果有多个键映射到相同的值,则只有一个键会出现在排序后的结果中。请注意。 - Maxy-B
64
Louis Wasserman(是的,Google Guava团队的成员之一)实际上非常不喜欢这个答案:“如果你对它稍微有点怀疑,它会以几种非常令人困惑的方式崩溃。如果支持映射发生变化,它就会崩溃。如果多个键映射到同一个值,它也会崩溃。如果你在支持映射中调用get方法,但该键不存在,它也会崩溃。如果你进行任何导致在地图中查找不存在的键的操作--例如Map.equals调用、containsKey等--它会以非常奇怪的堆栈跟踪方式崩溃。” https://plus.google.com/102216152814616302326/posts/bEQLDK712MJ - haylem
显示剩余11条评论

214
我会使用Guava来完成这个任务——如果你的值是可比较的,那么你可以使用它。
valueComparator = Ordering.natural().onResultOf(Functions.forMap(map))

这将创建一个函数(对象)用于地图[以任何键作为输入,返回相应的值],然后对它们[值]应用自然(可比较)排序。

如果它们不可比较,则需要做类似以下的操作:

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

这些可以应用于 TreeMap(因为 Ordering 扩展了 Comparator),或在排序后应用于 LinkedHashMap。请注意,如果您使用 TreeMap,则应该记住,如果比较结果等于 0,则该项已经在列表中(如果您有多个值进行比较相同,则会发生这种情况)。为缓解此问题,您可以像这样将键添加到比较器中(假设您的键和值是可比较的):(提示:请保留 "" 和 "" 和 html 标签)
valueComparator = Ordering.natural().onResultOf(Functions.forMap(map)).compound(Ordering.natural())

= 将自然排序应用于键映射的值,并将其与键的自然排序结合起来

请注意,如果您的键与0进行比较,则仍无法工作,但这对于大多数可比较项(例如hashCodeequalscompareTo通常是同步的...)应该足够。

请参见Ordering.onResultOf()Functions.forMap()

实现

现在我们有了一个按照我们想要的方式执行比较的比较器,我们需要从中获取结果。

map = ImmutableSortedMap.copyOf(myOriginalMap, valueComparator);

现在这个方法很可能可以工作,但是:

  1. 需要使用完整的完成地图
  2. 不要在TreeMap上尝试上面的比较器;当插入的键没有值时,尝试比较它是没有意义的,因为在put之后才会有值,即它会非常快地崩溃

对我来说,第1点有点成为瓶颈;Google集合非常懒惰(这很好:你几乎可以在瞬间完成几乎所有操作;真正的工作是在你开始使用结果时完成的),这需要复制一个完整的地图!

“完整”答案/按值排序的实时映射

不过别担心;如果你非常着迷于拥有这种方式排序的“实时”映射,你可以使用以下疯狂的东西解决上述两个问题中的一个或两个问题!

注意:这在2012年6月发生了重大变化——以前的代码永远无法工作:需要内部HashMap来查找值,而不会在TreeMap.get() -> compare()compare() -> 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"));
    }
 }

当我们进行put操作时,我们确保哈希映射具有比较器的值,然后将其放入TreeSet进行排序。但在此之前,我们检查哈希映射以查看键实际上不是重复的。此外,我们创建的比较器还将包括键,以便重复值不会删除非重复键(由于==比较)。
这两个项目对于确保地图合同得到保持至关重要;如果您认为您不想要这样做,那么您几乎已经到了完全颠倒地图的程度(到 Map<V,K>)。
构造函数需要被称为:
 new ValueComparableMap(Ordering.natural());
 //or
 new ValueComparableMap(Ordering.from(comparator));

如果原始映射中存在重复的值,ImmutableSortedMap.copyOf会抛出IllegalArgumentException异常。 - lbalazscs
1
谢谢您的帮助,我在一个项目中使用了您的解决方案。不过我认为put方法有问题:要像一个map一样运行,它需要返回与键先前相关联的值(如果存在),但是这个put方法不会这样做。我的解决方案是,如果存在,则返回已删除的值。 - alex
这个问题是否可以通过一开始使用super.put()而不是valueMap来简单解决呢?只是想想,还没有尝试过... - Christian Hujer
对象 valueComparator = Ordering.natural().onResultOf(Functions.forMap(map)).compound(Ordering.natural()); 导致错误,无法解析 .compound(Ordering<Comparable>)。 - Ryan Leach
感谢您提供的解决方案,我不得不添加以下代码以克服在调用containsKey时抛出的异常:@Override public boolean containsKey(Object key) { return valueMap.containsKey(key); } - has981
显示剩余5条评论

189

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

16
需要排序的列表是“new LinkedList”?哎呀。幸好Collections.sort()会先将列表转储到数组中,以避免这种错误(但是,将ArrayList转储到数组中应该比将LinkedList转储到数组中更快)。 - Dimitris Andreou
6
@gg.kaspersky 我并不是在说“对链表进行排序是不好的”,而是链表本身不是一个好的选择,在是否排序的情况下都是如此。使用一个 ArrayList 会更好,如果大小恰好为 map.size(),那就更好了。另请参阅 http://code.google.com/p/memory-measurer/wiki/ElementCostInDataStructuresArrayList 每个元素的平均成本:5字节 LinkedList 每个元素的平均成本:24字节。对于大小恰好的 ArrayList,平均成本将是 4 字节。也就是说,LinkedList 占用的内存是 ArrayList 的 六倍。这只是一种浪费。 - Dimitris Andreou
抱歉,我没有导入java.util.comparator。 - Rishi Dua
2
以上数值已按升序排序。如何按降序排序? - ram
1
将o1和o2替换为降序排序。 - Soheil
显示剩余4条评论

91

使用Java 8,你可以使用流API以更简洁的方式来实现:

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

如何按相反的顺序进行排序? - Vlad Holubiev
@VladGolubev comparing(Entry::getValue).reversed() 的意思是“按值进行比较并倒序排列”。 - assylias
8
找到了一个解决方案 - Collections.reverseOrder(comparing(Entry::getValue))。该代码会按照 Entry 的值进行排序,并返回一个逆序的比较器。 - Vlad Holubiev
1
我认为我在那里看到了一个错别字——“toMap”不应该被称为“Collectors.toMap()”,对吧? - Jake Stokes
2
@JakeStokes 或者使用静态导入 :-) - assylias
7
按条目值进行反向排序的更好方法是:Entry.comparingByValue(Comparator.reverseOrder()) - Gediminas Rimsa

32

排序键需要比较器查找每个比较的值。更可扩展的解决方案是直接使用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();

最后,如果您需要持续访问排序的信息(而不只是偶尔进行排序),您可以使用额外的多重映射。如果您需要更多详细信息,请告诉我...


如果返回List<Map.Entry<K,V>>,第二个版本可以更加简洁。这样做还可以更容易地迭代并获取键和值,而无需对映射执行大量的额外获取操作。这一切都是在假设您对此代码不安全的线程感到满意的情况下。如果支持映射或排序列表在多线程环境中共享,则所有赌注都将失效。 - Mike Miller

25

commons-collections库包含一个名为 TreeBidiMap 的解决方案。或者,您可以查看Google Collections API。它有一个名为 TreeMultimap 的工具,您也可以使用它。

如果您不想使用这些框架...它们提供了源代码。


你不必使用commons-collection。Java自带java.util.TreeMap。 - yoliho
2
是的,但是TreeMap在对map条目的_value_部分进行排序时远不如灵活。 - p3t0r
10
BidiMap的问题在于它添加了一个1:1关系约束,将键和值联系起来以使关系可逆(即键和值都需要唯一)。这意味着您无法使用BidiMap来存储诸如单词计数对象之类的内容,因为许多单词将具有相同的计数。 - Doug

25

我看了给出的答案,但很多答案比必要的复杂或在键值相同时移除地图元素。

这里是我认为更适合的解决方案:

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

请注意,该地图是按照从高到低排序的。


7
问题:如果您希望稍后使用返回的映射表,例如检查它是否包含特定元素,则始终会得到false,因为使用了自定义比较器!解决方案:将最后一行替换为: return new LinkedHashMap(sortedByValues); - Erel Segal-Halevi
这对我来说看起来是一个干净的解决方案,除了@ErelSegalHalevi指出的事实,检查Map中是否存在值将不可能,因为您指定了比较器。map.put("1", "One"); map.put("2", "Two"); map.put("3", "Three"); map.put("4", "Four"); map.put("5", "Five");如果在sortByValues()函数中返回新对象,如return new TreeMap<K, V>(sortedByValues); 将解决这个问题。谢谢Abhi。 - abhi
基本上与user157196和Carter Page的答案差不多。Carter Page的包含了LinkedHashMap修复。 - Kirby
解决方案的第4行应为int compare = map.get(k1).compareTo(map.get(k2)); 如果您需要升序。 - Leo

22

给定地图

   Map<String, Integer> wordCounts = new HashMap<>();
    wordCounts.put("USA", 100);
    wordCounts.put("jobs", 200);
    wordCounts.put("software", 50);
    wordCounts.put("technology", 70);
    wordCounts.put("opportunity", 200);

根据值升序排序地图

Map<String,Integer>  sortedMap =  wordCounts.entrySet().
                                                stream().
                                                sorted(Map.Entry.comparingByValue()).
        collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
    System.out.println(sortedMap);
    

按值从大到小对地图进行排序

Map<String,Integer>  sortedMapReverseOrder =  wordCounts.entrySet().
            stream().
            sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())).
            collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
    System.out.println(sortedMapReverseOrder);

输出:

{软件=50,技术=70,美国=100,工作=200,机会=200}

{工作=200,机会=200,美国=100,技术=70,软件=50}


它可以运行,但我不明白元素顺序在 HashMap 中如何起作用? - Ali Tou
1
如果你指的是纯HashMap,那么顺序并不重要。但在LinkedHashMap中,插入顺序被保留。 - WesternGun

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接