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通过比较器吗?




Answers:


179

您不能TreeMap对值本身进行排序,因为这违反了SortedMap规范:

一个Map是还提供了一个总体排序的键

但是,使用外部集合,您始终Map.entrySet()可以根据需要按键,值或什至两者的组合(!!)进行排序。

这是一个通用方法,如果的值为,则返回的SortedSetof :Map.EntryMapComparable

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

相关问题


5
如果添加map.put(“ D”,2); 结果仍然是“ [C = 1,B = 2,A = 3]”,而不是“ [C = 1,B = 2,D = 2,A = 3]”
Igor Milla

3
修复:int res = e2.getValue()。compareTo(e1.getValue()); 返回res!= 0?res:1;
beshkenadze 2012年

new Integer(0)== new Integer(0)您确实比较了对一个对象的引用,并且您确实创建了两个对象!输出是绝对正确和可预测的。
Yuriy Chernyshov 2014年

2
“通常来说,按地图的值对地图项进行排序的需求是不典型的”我是这里的初学者,但是我觉得应该做的很普遍吗?例如,如果我想在地图{“ ana” = 37,“ peter” = 43,“ john” = 4,“ mary” = 15,“ Matt” = 78}中检索3个最老的人
fartagaintuxedo

@igormilla代码运行的原因很简单:自动装箱使用Integer.valueOf。并且具有-128到127的实例缓存!
jokster

75

polygenelubricants的答案几乎是完美的。它有一个重要的错误。它不会处理值相同的映射条目。

这段代码:...

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中:请注意,排序集维护的排序必须与equals ... 一致。解决方案是更改为List实现。
dacwe 2012年

4
@dacwe等于值而不是键
bellum

1
@bellum:我们在Set这里谈论s。由于Comparator违反Set合同Set.removeSet.contains等等不起作用在ideone上检查此示例
dacwe 2013年

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())以保留具有相等值的键的顺序。
Marco Lackovic

45

在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

不能使用来完成此操作Comparator,因为它将始终获取要比较的地图关键字TreeMap只能按键排序。


2
如果TreeMap仅将密钥传递给Comparator,如果我在比较器的构造函数中对TreeMap进行引用,则可行,然后使用该密钥获取值,类似这样(不确定如何通过引用传递):class byValue实现Comparator {TreeMap theTree; public byValue(TreeMap theTree){this.theTree = theTree; } public int compare(String k1,String k2){//使用TreeMap的getKey方法为该值}}
vito huang 2010年

1
@vito:不,因为通常两个键之一不会出现在地图上,因此您将无法获得其值。
约阿希姆·绍尔

这不是在TreeMap的比较器中执行此操作的示例吗?(即使,如上所述,这样做打破说明书SortedMap指定排序由键)beginnersbook.com/2014/07/...
马库斯

@Marcus:Comparator使用现有地图获取要排序的值。换句话说,它不能对TreeMap后来放入的任意值进行排序,只能对原始Map中已经存在的值进行排序。
约阿希姆·绍尔

5

Olof的回答很好,但是还需要做件事才能使它变得完美。在他的回答下面的评论中,dacwe(正确)指出他的实现违反了Sets的Compare / Equals合同。如果您尝试对包含在集合中的条目进行包含或删除操作,则该集合将无法识别该条目,因为该代码允许将具有相等值的条目放置在集合中。因此,为了解决此问题,我们需要测试键之间的相等性:

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接口,则排序后的集合(无论是否提供了显式比较器)所维护的顺序必须与equals一致。Set接口是根据equals操作定义的,但是排序后的集合使用其compareTo(或compare)方法执行所有元素比较,因此,从排序后的集合的角度来看,此方法认为相等的两个元素都等于。” (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的建议,我也更喜欢使用它

这是您需要根据其值对Map条目进行排序的两种方法。

    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;
        }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.