【问题标题】:Fastest way to check if a List<String> contains a unique String检查 List<String> 是否包含唯一字符串的最快方法
【发布时间】:2011-03-19 11:08:28
【问题描述】:

基本上我有大约 1,000,000 个字符串,对于每个请求,我必须检查一个字符串是否属于列表。

我担心性能,那么最好的方法是什么? ArrayList?哈希?

【问题讨论】:

  • 一个很好的练习是尝试两个不同的列表/集合/地图,然后看看你是否可以通过阅读集合的 java 文档来弄清楚为什么你得到不同的时间:)
  • 为了确定自己做对了,请学会使用分析器。挂在最低处的是 JDK 中的 jvisualvm。

标签: java string performance list contains


【解决方案1】:

最好的办法是使用HashSet 并通过contains() 方法检查集合中是否存在字符串。 HashSet 是为通过使用对象方法hashCode()equals() 快速访问而构建的。 HashSet 的 Javadoc 声明:

此类为基本操作(添加、删除、包含和大小)提供恒定的时间性能,

HashSet stores objects in hash bucketshashCode 方法返回的值将确定对象存储在哪个桶中。这样,HashSet 的相等数量检查必须通过@987654331 执行@方法被简化为同一个哈希桶中的其他对象。

要有效地使用 HashSet 和 HashMap,您必须遵守 in the javadoc 概述的 equalshashCode 合同。在java.lang.String 的情况下,已经实现了这些方法来执行此操作。

【讨论】:

  • 还有什么?它有 O(1) 用于添加和包含。
  • 感谢@Andreas_D,我添加了 Javadoc 中的引文,说明它具有恒定的时间性能。
  • 当数以百万计的字符串不再适合主内存时,有趣的部分就出现了。
【解决方案2】:

一般来说,HashSet 会为您提供更好的性能,因为它不必像 ArrayList 那样查看每个元素并进行比较,但通常最多比较几个元素,其中哈希码相等。

但是,对于 1M 的字符串,hashSet 的性能可能仍然不是最优的。大量缓存未命中会减慢搜索集合的速度。如果所有字符串的可能性相同,那么这是不可避免的。但是,如果某些字符串比其他字符串更频繁地被请求,那么您可以将公共字符串放入一个小的 hashSet 中,并在检查较大的集合之前先检查它。小哈希集的大小应适合缓存(例如,最多几百 K)。对小哈希集的命中将非常快,而对较大哈希集的命中则以受内存带宽限制的速度进行。

【讨论】:

  • +1:尽管我突然想到,由于字符串是单独分配的,因此特定哈希图中总共有多少可能并不特别相关,因为搜索只会命中他们中的一小部分。更相关的可能是字符串本身中 char 数组的实际分配模式,Java 程序员无论如何都对其进行零控制(这是一件好事)。
  • @Software Monkey - 目的是通过将最常搜索的字符串放在它自己的地图中,该地图的命中率会很高。带有频繁使用字符串的较小哈希映射将比较大映射具有更高的缓存命中率,因为每个缓存行将在映射支持数组中对应于几个经常使用的字符串。当然,正如您所说,这对字符串本身的分配没有帮助。如果这是一个问题,那么首先分配最常见的字符串可能会更好地使用缓存,因为 VM 可能会从堆的同一区域进行分配。
【解决方案3】:

在继续之前,请考虑一下:您为什么担心性能?多久调用一次此检查?

至于可能的解决方案:

  • 如果列表已经排序,那么您可以使用java.util.Collections.binarySearch,它提供与java.util.TreeSet 相同的性能特征。

  • 否则,您可以使用 java.util.HashSet 作为 O(1) 的性能特征。请注意,计算尚未计算的字符串的哈希码是一个 O(m) 操作,其中 m=string.length()。还要记住,哈希表只有在达到给定的负载因子之前才能正常工作,即哈希表将使用比普通列表更多的内存。 HashSet 使用的默认加载因子是 0.75,这意味着内部 1e6 对象的 HashSet 将使用具有 1.3e6 条目的数组。

  • 如果 HashSet 不适合您(例如,因为有很多哈希冲突、因为内存紧张或因为有很多插入),那么请考虑使用 Trie。在 Trie 中查找的最坏情况复杂度为 O(m),其中 m=string.length()。 Trie 还有一些可能对您有用的额外好处:例如,它可以为您提供搜索字符串的 最接近。但请记住,最好的代码是没有代码,所以只有在收益超过成本的情况下才推出自己的 Trie 实现。

  • 如果您需要更复杂的查询,请考虑使用数据库,例如匹配子字符串或正则表达式。

【讨论】:

  • -1:他担心性能,因为他 (a) 拥有庞大的数据集,并且 (b) 任何称职的 1/2 方式的体面程序员都应该始终考虑算法的性能特征或数据结构适合任务。
【解决方案4】:

我会使用Set,在大多数情况下HashSet 很好。

【讨论】:

  • krock 的答案在将 OP 推向最佳解决方案方面稍好一些:TreeSet 具有 O(log2(N)) 性能,而 HashSet 理想情况下具有 O(1)。
  • @Carl,假设 equals 和 hashCode() 都是 O(1),即不考虑字符串长度。
【解决方案5】:

有了这么多的字符串,我立刻想到了Trie。它适用于更有限的字符集(例如字母)和/或当许多字符串的开头重叠时。

【讨论】:

    【解决方案6】:

    这里的练习是我的结果。

    private static final int TEST_CYCLES = 4000;
    private static final long RAND_ELEMENT_COUNT = 1000000l;
    private static final int RAND_STR_LEN = 20;
    //Mean time
    /*
    Array list:18.55425
    Array list not contains:17.113
    Hash set:5.0E-4
    Hash set not contains:7.5E-4
    */
    

    我相信数字不言自明。哈希集的查找时间要快得多。

    【讨论】:

      【解决方案7】:

      也许您的情况不需要它,但我认为提及一些节省空间的概率算法很有用。例如Bloom filter

      【讨论】:

        【解决方案8】:

        如果您有如此大量的字符串,最好的机会是使用数据库。寻找 MySQL。

        【讨论】:

        • 总的来说我同意你的看法,但他担心查找性能——这不会增加很多开销吗?
        • 增加了网络延迟,但您可以使用 SQL 的全部功能。另一个考虑因素是内存——一百万个 32 个字符的字符串,每个字符串意味着约 64MB 的 RAM。这是典型的 CPU 与内存的权衡。我会对其进行基准测试并查看。
        • @Rup:当然。还有很多出错的机会。如果数据适合内存(而且必须,因为他们已经把它塞进去了),那么就应该在内存中寻找它。
        • @duffymo:对于存在性的直接测试,您在数据库服务器中所做的任何事情都无法达到哈希中contains() 的性能。
        • @Carl Smotricz&Rup:我不知道。所以谢谢你们的cmets。
        【解决方案9】:

        不仅适用于 String,您还可以将 Set 用于任何需要独特项目的情况。

        如果项目的类型是原始的或包装的,你可能不在乎。但如果是类,则必须重写两个方法:

        1. hashCode()
        2. 等于()

        【讨论】:

          【解决方案10】:

          有时您想检查一个对象是否在列表/集合中,同时您希望对列表/集合进行排序。如果您还想在不使用枚举或迭代器的情况下轻松检索对象,您可以考虑同时使用ArrayList&lt;String&gt;HashMap&lt;String, Integer&gt;。该列表由地图支持。

          我最近做的一些工作的例子:

          public class NodeKey<K> implements Serializable, Cloneable{
          private static final long serialVersionUID = -634779076519943311L;
          
          private NodeKey<K> parent;
          private List<K> children = new ArrayList<K>();
          private Map<K, Integer> childrenToListMap = new HashMap<K, Integer>();
          
          public NodeKey() {}
          
          public NodeKey(Collection<? extends K> c){
              List<K> childHierarchy = new ArrayList<K>(c);
              K childLevel0 = childHierarchy.remove(0);
          
              if(!childrenToListMap.containsKey(childLevel0)){
                  children.add(childLevel0);
                  childrenToListMap.put(childLevel0, children.size()-1);
              }
          
              ...
          

          在这种情况下,参数K 将是您的String。 map(childrenToMapList)存储Strings作为key插入到列表(children)中,map值是列表中的索引位置。

          列表和映射的原因是您可以检索列表的索引值,而无需对 HashSet&lt;String&gt; 进行迭代。

          【讨论】:

            猜你喜欢
            • 2016-05-05
            • 2013-03-14
            • 2014-10-14
            • 1970-01-01
            • 1970-01-01
            • 2021-10-12
            • 1970-01-01
            • 2016-09-16
            • 2011-07-14
            相关资源
            最近更新 更多