在 Java 中按值映射自动排序

Posted

技术标签:

【中文标题】在 Java 中按值映射自动排序【英文标题】:Automatically sorted by values map in Java 【发布时间】:2011-11-19 21:23:10 【问题描述】:

我需要有一个 自动 在 Java 中按值排序的映射 - 以便在我添加新的键值对或更新现有的键值对,甚至删除一些条目。

还请记住,这张地图将会非常大(大小为 100 的数千,甚至是 10 的数百万条目)。

所以基本上我正在寻找以下功能:

假设我们有一个实现上述功能的“SortedByValuesMap”类 我们有以下代码:

SortedByValuesMap<String,Long> sorted_map = new SortedByValuesMap<String, Long>();
sorted_map.put("apples", 4);
sorted_map.put("oranges", 2);
sorted_map.put("bananas", 1);
sorted_map.put("lemons", 3);
sorted_map.put("bananas", 6);

for (String key : sorted_map.keySet()) 
  System.out.println(key + ":" + sorted_map.get(key));

输出应该是:

bananas:6
apples:4
lemons:3
oranges:2

尤其是对我来说真正重要的是能够获得带有 任何时候的最低值 - 使用如下命令:

smallestItem = sorted_map.lastEntry();

这应该给我“橙子”条目

编辑:我是 Java 新手,所以请详细说明您的答案 - 谢谢

EDIT2:这可能会有所帮助:我正在使用它来计算巨大文本文件中的单词(对于那些熟悉的人:特别是 n-gram)。所以我需要建立一个地图,其中键是单词,值是这些单词的频率。但是,由于限制(如 RAM),我只想保留 X 最常用的词——但你不能事先知道哪些是最常用的词。因此,我认为它可能起作用的方式(作为近似值)是开始计算单词,当地图达到上限(如 1 百万个条目)时,将删除最不频繁的条目以保持地图的大小100 万。

【问题讨论】:

数以百万计的条目?为什么不使用数据库呢? 如果有两个具有相同最小值的键怎么办? lastEntry() 的预期行为应该是什么? (例如,limes 的另一个条目 -> 2 在地图中) @Kru:数据库会让它变得非常慢 如果这只是英语,那么您高估了单词的数量,尤其是常用的单词。 @Dave Newton 你是对的 - 我提到了一些词,以免让不熟悉 n-gram 的人感到困惑,这正是我实际计算的。 N-grams,尤其是随着 N 的增加,可以变得非常多样化。可能的组合呈指数增长。 【参考方案1】:

保留2个数据结构:

单词字典 -> 计数。只需使用普通的HashMap&lt;String, Long&gt;

一个用于跟踪顺序的“数组”,这样list[count] 包含具有该计数的单词Set&lt;String&gt;

我写这个好像它是一个数组作为符号方便。事实上,您可能不知道出现次数的上限,因此您需要一个可调整大小的数据结构。使用Map&lt;Long, Set&lt;String&gt;&gt; 实现。或者,如果这占用了太多内存,请使用ArrayList&lt;Set&lt;String&gt;&gt;(您必须测试count == size() - 1,如果是,请使用add() 而不是set(count + 1))。

增加单词的出现次数(伪代码):

// assumes data structures are in instance variables dict and arr
public void tally(final String word)

    final long count = this.dict.get(word) or 0 if absent;
    this.dict.put(word, count + 1);
    // move word up one place in arr
    this.arr[count].remove(word);   // This is why we use a Set: for fast deletion here.
    this.arr[count + 1].add(word);

按顺序迭代单词(伪代码):

for(int count = 0; count < arr.size; count++)
    for(final String word : this.arr[count])
        process(word, count);

【讨论】:

【参考方案2】:

如果 Long 值不同,如何使用附加索引或仅使用 TreeMap&lt;Long, TreeSet&lt;String&gt;&gt;TreeMap&lt;Long, String&gt;

你也可以写Heap。

【讨论】:

长值不明显。两个不同的条目可能具有相同的 Long 值 - Long 值实际上代表频率 所以你可以使用TreeMap&lt;Long, TreeSet&lt;String&gt;&gt; 这可能行得通,但我担心它会使时间加倍,因为我们将地图操作加倍 - 就我而言,我有数百万个条目可能会产生巨大的影响 没那么多。只是常数因子会小幅上升。您还可以创建一些配对类,例如 Map.Entry&lt;K,V&gt; 并使用 TreeSet&lt;Pair&lt;Long, String&gt;&gt; 可以,但您可以同时保留TreeMap&lt;Long,TreeSet&lt;String&gt;&gt;Map&lt;String,Long&gt;。我想在java中没有提供两种技巧的单一数据结构。在 SQL 表中,您希望在两列上有索引,所以我想您在 java 中还需要 2 个“索引”。【参考方案3】:

Guava BiMap解决办法:

//Prepare original data
BiMap<String, Integer> biMap = HashBiMap.create();
biMap.put("apples" , 4);
biMap.put("oranges", 2);
biMap.put("bananas", 1);
biMap.put("lemons" , 3);
biMap.put("bananas", 6);

//Create a desc order SortedMap
SortedMap<Integer, String> sortedMap = new TreeMap<Integer, String>(new Comparator<Integer>()
    @Override public int compare(Integer o1, Integer o2) 
      return o2-o1;
);

//Put inversed map
sortedMap.putAll(biMap.inverse());
for (Map.Entry<Integer, String> e: sortedMap.entrySet()) 
      System.out.println(e);

System.out.println(sortedMap.lastKey()); 

【讨论】:

OP 说这些值不是唯一的,所以 BiMap 不起作用。【参考方案4】:

试试http://paaloliver.wordpress.com/2006/01/24/sorting-maps-in-java/ 上发布的解决方案。您也可以灵活地进行升序或降序排序。

这就是他们所说的

import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

public class MapValueSort 

    /** inner class to do soring of the map **/
    private static class ValueComparer implements Comparator<String> 
        private Map<String, String>  _data = null;
        public ValueComparer (Map<String, String> data)
            super();
            _data = data;
        

         public int compare(String o1, String o2) 
             String e1 = (String) _data.get(o1);
             String e2 = (String) _data.get(o2);
             return e1.compareTo(e2);
         
    

    public static void main(String[] args)

        Map<String, String> unsortedData = new HashMap<String, String>();
        unsortedData.put("2", "DEF");
        unsortedData.put("1", "ABC");
        unsortedData.put("4", "ZXY");
        unsortedData.put("3", "BCD");


        SortedMap<String, String> sortedData = new TreeMap<String, String>(new MapValueSort.ValueComparer(unsortedData));

        printMap(unsortedData);

        sortedData.putAll(unsortedData);
        System.out.println();
        printMap(sortedData);
    

    private static void printMap(Map<String, String> data) 
        for (Iterator<String> iter = data.keySet().iterator(); iter.hasNext();) 
            String key = (String) iter.next();
            System.out.println("Value/key:"+data.get(key)+"/"+key);
        
    


输出

Value/key:BCD/3
Value/key:DEF/2
Value/key:ABC/1
Value/key:ZXY/4

Value/key:ABC/1
Value/key:BCD/3
Value/key:DEF/2
Value/key:ZXY/4

【讨论】:

【参考方案5】:

我发现需要一个类似的结构来保存按关联值排序的对象列表。根据此线程中 Mechanical snail 的建议,我编写了此类地图的基本实现。随意使用。

import java.util.*;

/**
 * A map where @link #keySet() and @link #entrySet() return sets ordered
 * with ascending associated values with respect to the the comparator provided
 * at constuction. The order of two or more keys with identical values is not
 * defined.
 * <p>
 * Several contracts of the Map interface are not satisfied by this minimal
 * implementation.
 */
public class ValueSortedMap<K, V> extends HashMap<K, V> 
    protected Map<V, Collection<K>> valueToKeysMap;

    public ValueSortedMap() 
        this((Comparator<? super V>) null);
    

    public ValueSortedMap(Comparator<? super V> valueComparator) 
        this.valueToKeysMap = new TreeMap<V, Collection<K>>(valueComparator);
    

    public boolean containsValue(Object o) 
        return valueToKeysMap.containsKey(o);
    

    public V put(K k, V v) 
        V oldV = null;
        if (containsKey(k)) 
            oldV = get(k);
            valueToKeysMap.get(oldV).remove(k);
        
        super.put(k, v);
        if (!valueToKeysMap.containsKey(v)) 
            Collection<K> keys = new ArrayList<K>();
            keys.add(k);
            valueToKeysMap.put(v, keys);
         else 
            valueToKeysMap.get(v).add(k);
        
        return oldV;
    

    public void putAll(Map<? extends K, ? extends V> m) 
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            put(e.getKey(), e.getValue());
    

    public V remove(Object k) 
        V oldV = null;
        if (containsKey(k)) 
            oldV = get(k);
            super.remove(k);
            valueToKeysMap.get(oldV).remove(k);
        
        return oldV;
    

    public void clear() 
        super.clear();
        valueToKeysMap.clear();
    

    public Set<K> keySet() 
        LinkedHashSet<K> ret = new LinkedHashSet<K>(size());
        for (V v : valueToKeysMap.keySet()) 
            Collection<K> keys = valueToKeysMap.get(v);
            ret.addAll(keys);
        
        return ret;
    

    public Set<Map.Entry<K, V>> entrySet() 
        LinkedHashSet<Map.Entry<K, V>> ret = new LinkedHashSet<Map.Entry<K, V>>(size());
        for (Collection<K> keys : valueToKeysMap.values()) 
            for (final K k : keys) 
                final V v = get(k);
                ret.add(new Map.Entry<K,V>() 
                    public K getKey() 
                        return k;
                    

                    public V getValue() 
                        return v;
                    

                    public V setValue(V v) 
                        throw new UnsupportedOperationException();
                    
                );
            
        
        return ret;
    

此实现不遵守 Map 接口的所有约定,例如在实际映射中返回的键集和条目集中反映值更改和删除,但这样的解决方案将有点大,包含在类似的论坛中这。也许我会研究一个并通过 github 或类似的东西提供它。

【讨论】:

【参考方案6】:

更新:您不能按值对地图进行排序,抱歉。

您可以使用 SortedMap 实现,如 TreeMapComparator 按值定义顺序(而不是默认 - 按键)。

或者,更好的是,您可以将元素放入带有预定义比较器的PriorityQueue 值。与 TreeMap 相比,它应该更快并且占用更少的内存。

【讨论】:

你能提供一个例子来说明如何做到这一点吗? 我不认为你可以使用优先队列,因为文档说迭代器不能保证以任何特定的顺序遍历队列。 @Timothy Jones:这就是为什么我建议使用 PriorityQueue 作为替代方案(如果可能的话)。我没有说清楚。感谢您指出。 如果我使用按值对项目进行排序的 TreeMap,那么通过 Key 访问项目也会很快吗? 为了能够按值对 TreeMap 进行排序,您的键也需要包含这些值。在这种情况下,您将很难通过键查找值...【参考方案7】:

你可以参考java.util.LinkedHashMap的实现。 基本思想是,使用内部链表来存储订单。以下是一些细节:

从 HashMap 扩展而来。在 HashMap 中,每个条目都有一个 key 和 value,这是基本的。您可以添加 next 和 prev 指针以按值顺序存储条目。以及用于获取第一个和最后一个条目的标头和尾指针。对于每次修改(添加、删除、更新),您可以添加自己的代码来更改列表顺序。它只不过是一个线性搜索和指针开关。

如果条目太多,添加/更新肯定会很慢,因为它是链表而不是数组。但是只要对列表进行排序,我相信有很多方法可以加快搜索速度。

所以这就是你得到的:一个在通过键检索条目时与 HashMap 具有相同速度的映射。按顺序存储条目的链表。

如果此解决方案满足您的要求,我们可以进一步讨论。


致 jtahlborn: 正如我所说,如果没有任何优化,它肯定很慢。由于我们现在谈论的是性能而不是 impl,所以可以做很多事情。

一种解决方案是使用树而不是链表,例如红黑树。然后迭代树而不是迭代地图。

关于最小值,比较容易。只需使用成员变量来存储最小值,添加或更新元素时​​,更新最小值。删除时,搜索最小的树(这非常快)

如果树过于复杂,也可以使用另一个列表/数组来标记列表中的一些位置。例如,每个可能有 100 个元素。然后在搜索时,只需先搜索位置列表,然后搜索真实列表。这个列表也需要维护,重新计算位置列表的修改次数是合理的,可能是100次。

【讨论】:

OP 表示使用可能包含数千万条目的集合。用这么多条目更新“排序”链表会非常慢。【参考方案8】:

如果您只需要“min”值,那么只需使用法线贴图并在修改时随时跟踪“min”值。

编辑:

所以,如果您真的需要价值排序并且想要使用开箱即用的解决方案,那么您基本上需要 2 个集合。一张法线贴图(例如 HashMap)和一张 SortedSet(例如 TreeSet>)。您可以通过 TreeSet 遍历有序元素,并使用 HashMap 按键查找频率。

显然,您总是可以自己编写一些类似于 LinkedHashMap 的代码,其中元素可以通过键定位并按顺序遍历,但这几乎完全是自定义代码(我怀疑任何特定的已经存在,但是我可能是错的)。

【讨论】:

因为在此过程中,有时我可能想删除具有最小值的项目。删除该项目后,我需要知道具有最小值的下一个项目。有点像最薄弱的环节。 为什么投反对票? @Timothy Jones 基本上把我的建议写成了选择的答案。

以上是关于在 Java 中按值映射自动排序的主要内容,如果未能解决你的问题,请参考以下文章

如何在熊猫中按值计数对列进行排序

在火花对 RDD 中按值排序

如何在mysql中按值排序

在python中按值对defaultdict进行排序

在 Lua 中按值关联排序表

在NodeJS中按值排序数组