【问题标题】:Better data structure for a multi map of a hash map哈希映射的多映射的更好数据结构
【发布时间】:2013-06-16 08:43:11
【问题描述】:

事实证明,我选择的数据结构设计执行起来非常笨拙,因此我希望您能针对我正在尝试的内容提出一个更自然的数据结构,而不是询问您的专家意见如何执行它做,如下。我正在读取数据行。每列都是一个变量(Animal、Color、Crop、... - 其中有 45 个)。每行数据都有该列变量的值 - 您事先不知道值或行数。

Animal  Color   Crop    ...
-------------------------------------
cat     red     oat
cat     blue        hay
dog     blue        oat
bat     blue        corn
cat     red     corn
dog     gray        corn
...     ...     ...

当我读完后,它应该捕获每个变量、变量所取的每个值以及该变量取该值的次数,如下所示:

Animal [cat, 3][dog,2][bat, 1]...
Color [blue, 3][red,2][gray,1]...
Crop [corn,3][oat, 2][hay,1]...
...

我尝试了几种方法,我得到的最接近的方法是使用 GUAVA 哈希映射的多映射,如下所示:

Map<String, Integer> eqCnts = new HashMap<String, Integer>();
Multimap<String, Map> ed3Dcnt = HashMultimap.create();
for (int i = 0; i + 1 < header.length; i++) {
    System.out.format("Got a variable of %s\n", tmpStrKey = header[i]);
    ed3Dcnt.put(tmpStrKey, new HashMap<String, Integer>());
 }

似乎我已经完全创建了我想要的东西,但是使用起来非常尴尬和乏味,而且它的行为方式也很神秘(一方面,即使“ed3Dcnt.put()”插入了一个HashMap,对应的“.get()”不返回一个HashMap,而是一个Collection,这会产生一系列全新的问题。)请注意,我想对值的结果进行排序,从最高到最低,但我认为我可以轻松做到这一点。

那么,如果您愿意,请您对数据结构设计的更好选择提出建议?如果没有明显更好的设计选择,我该如何使用 .get() 返回的 Collection,而我想要的只是放入该插槽的单个 HashMap?

非常感谢 - Ed

【问题讨论】:

    标签: java data-structures


    【解决方案1】:

    您可以通过将 Map&lt;String, Integer&gt; 替换为 Multiset 来消除一些奇怪之处。

    A multiset (or a bag) 是一个允许重复元素的集合 - 并对它们进行计数。你又扔了一个苹果、一个梨和一个苹果。它记得它有两个苹果和一个梨。基本上,这就是您在刚刚使用的Map&lt;String, Integer&gt; 下所想象的。

    Multiset<String> eqCounts = HashMultiset.create();
    

    对应的“.get()”不返回一个HashMap,而是一个 收藏

    这是因为您使用了通用的“Multimap”接口。文档说:

    但是,您很少直接使用 Multimap 界面;更经常 您将使用ListMultimapSetMultimap,将键映射到列表或 分别设置。


    所以,坚持你的原始设计:

    • 每一列都是一个Multiset&lt;String&gt;,它将存储和计算您的值。
    • 您将有一个Map&lt;String, Multiset&lt;String&gt;&gt;(键是标题,值是列),您将在其中放置这样的列:

      Map<String, Multiset<String>> columns = Maps.newHashMap();
      for (int i = 0; i < headers.length; i++) {
          System.out.format("Got a variable of %s\n", headers[i]);
          columns.put(headers[i], HashMultiset.<String>create());
      }
      

    读取一行并将值放在它们所属的位置:

    String[] values = line.split(" ");
    for (int i = 0; i < headers.length; i++) {
        columns.get(headers[i]).add(values[i]);
    }
    

    说了这么多,你可以看到外部的HashMap 有点多余,整个事情仍然可以改进(尽管我认为它已经足够好了)。要进一步改进它,您可以尝试以下方法:

    1. 使用Multiset 的数组而不是HashMap。毕竟,您事先知道列数。
    2. 如果您不喜欢创建通用数组,请使用List.
    3. 也许是最好的:像这样创建一个类Column

      private static class Column {
          private final String header;
          private final Multiset<String> values;
      
          private Column(String header) {
              this.header = header;
              this.values = HashMultiset.create();
          }
      }
      

      不要使用String[] 作为标题,使用Map&lt;String, Multiset&lt;String&gt;&gt; 作为它们的值,而是使用Column[]。您可以创建此数组来代替创建 headers 数组。

    【讨论】:

      【解决方案2】:

      在我看来,最合适的是:

      HashMap<String, HashMap<String, Integer>> map= new HashMap<String, HashMap<String, Integer>>();
      

      现在,添加 header 内部地图:

      for (int i = 0; i + 1 < header.length; i++) {
          System.out.format("Got a variable of %s\n", tmpStrKey = header[i]);
          map.put(tmpStrKey, new HashMap<String, Integer>());
      }
      

      并且在内部映射中增加一个值:

      //we are in some for loop
      for ( ... ) {
          String columnKey = "animal"; //lets say we are here in the for loop
          for ( ... ) {
              String columnValue = "cat"; //assume we are here
              HashMap<String, Integer> innerMap = map.get(columnKey);
      
              //increment occurence
              Integer count = innerMap.get(columnValue);
              if (count == null) {
                  count = 0;
              }
              innerMap.put(columnValue, ++count);
          }
      }
      

      【讨论】:

      • 非常感谢,很好的回答,而且布局也很清楚。这对我有用。也非常感谢 @slanec 和 herman 分别对 MultiSet 和基数映射概念的两个非常有启发性的贡献(对于 HashMap 接口的新手错误感到抱歉)。
      • 请注意,HashMap 不是接口,它是 Map 的子类之一。我建议您对地图类型和值类型都使用 Map 接口:Map&lt;String, Map&lt;String, Integer&gt; map = new HashMap&lt;String, Map&lt;String, Integer&gt;&gt;()。不必引用 Map 的具体子类型。
      【解决方案3】:

      1) 多重地图中的地图通常称为基数地图。为了从一组值创建基数映射,我通常使用来自 Apache Commons Collections 的CollectionUtils.getCardinalityMap,尽管这不是通用的,因此您需要一个不安全(但已知是安全的)强制转换。如果您想使用 Guava 构建地图,我认为您应该首先将变量的值放在 Set&lt;String&gt; 中(以获取一组唯一值),然后对每个值使用 Iterables.frequency() 以获取计数。 (编辑:或者更简单:使用ImmutableMultiset.copyOf(collection) 将基数映射作为Multiset)无论如何,生成的基数映射是Map&lt;String, Integer,例如您已经在使用的。

      2) 我不明白您为什么需要 Multimap。毕竟你想将每个变量映射到一个基数映射,所以我会使用Map&lt;String, Map&lt;String, Integer&gt;&gt;。 编辑:或使用Map&lt;String, Multiset&lt;String&gt;&gt;,如果您决定使用多重集作为您的基数映射。

      【讨论】:

      • Apache Commons Collections 还有一个Bag,它是Multiset 的一个实现(类似于Guava 的Multiset ...本质上是一个计数Map&lt;K, Integer&gt;
      • 我认为你是使用 Multiset/Bag 还是 cardinality map 取决于你之后是否还需要完整的集合。例如。如果您正在读取数百万行,您可以拥有一个包含数百万个元素的 Multiset/Bag(尽管它可能会被优化为不保留多个引用,而只保留一个计数)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-07-27
      • 1970-01-01
      • 1970-01-01
      • 2010-09-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多