【问题标题】:Finding if a string contains any string in a collection查找字符串是否包含集合中的任何字符串
【发布时间】:2014-10-07 00:59:51
【问题描述】:

我正在尝试提高我拥有的 Java 函数的性能,该函数用于确定给定的搜索字符串是否包含集合中 >0 的字符串。这看起来像是过早的优化,但该函数被称为 A LOT,因此任何加速都会非常有益。

目前的代码如下所示:

public static boolean containsAny(String searchString, List<String> searchCollection) {
    int size = searchCollection.size();
    for (int i = 0; i < size; i++) {
        String stringInCollection = searchCollection.get(i);
        if (!Util.isNullOrEmpty(stringInCollection)) {
            // This is a performance optimization of contains.
            if (searchString.indexOf(stringInCollection, 0) > -1) {
                return true;
            }
        }
    }
    return false;
}

该列表通常包含大约 30 个元素,并且在每次调用之间多次重复使用相同的集合。

上面的代码是一个非常简单的线性搜索。我认为除非我们更改数据结构以使其优于 O(n),否则它不会有显着改善。是否有任何数据结构可以让我做到这一点?

【问题讨论】:

  • 选择另一个数据结构来存储你的字符串。例如,Map&lt;Character, List&lt;String&gt;&gt; 的键是 abecedarium 的字母,List&lt;String&gt; 包含以用作键的字符开头的单词的排序列表。或使用trie
  • 它们主要用于那个,是的。我不确定它们的适用性。这让我想到了Longest common substring problem,它可以用后缀树有效地解决。它不是现货,但可以使用。
  • 你的琴弦有多大?
  • 查看Aho Corasick。你必须构造一个状态机,但之后搜索很快。
  • 我已经对当前建议进行了一些性能测量,您当前的代码显示出比我提出的更好的性能,甚至比 @Joop 基于正则表达式模式的更好(即使模式被缓存)。

标签: java string data-structures collections


【解决方案1】:

使用 Aho-Corasick 算法可以显着加快速度。

您可以使用 O(集合中所有字符串的总长度)时间和空间为集合构建 Aho-Corasick 自动机。然后可以通过遍历这个自动机来检查集合中的一个字符串是否是给定字符串 S 在 O(S.length) 时间内的子字符串。

【讨论】:

    【解决方案2】:
    // Make a regex pattern (once only):
    StringBuilder pattern = new StringBuilder();
    for (String sought : searchCollection) {
        if (!Util.isNullOrEmpty(sought)) {
            if (pattern.length() != 0) {
                pattern.append('|');
            }
            pattern.append(Pattern.quote(sought));
        }
    }
    final Pattern PATTERN = Pattern.compile("(" + pattern + ")");
    

    这会创建一个替代模式,例如"(abc|def|ghi)"。您可以考虑不区分大小写的搜索。

    而在函数containsAny

    Matcher m = PATTERN.matcher(searchString);
    return m.find();
    

    Regex 编译比较聪明。这相当于使用搜索词集合的搜索树:"agent" and "agitator" to ("ag", ("ent", "itator"))

    【讨论】:

    • 谢谢!我会试试看效果如何。
    • 但如果搜索字符串本身包含“|”符号,则可能导致错误的结果。
    • @SivaKumar Pattern.quote(sought) 应该返回 sought 并转义所有正则表达式特殊字符。
    • @JoopEggen 谢谢。例如 (abc|def|ghi) ,我想搜索 bc| ,它将返回true。但是没有一个字符串包含 bc| ,现在它会成为问题。
    • @SivaKumar 你的意思是在“abc|”上找到(你遗漏了'a')将在寻求 abc 时产生 true。对于整个字词搜索,搜索字符串可以使用边界标记:"\\b(abc|def|ghi)\\b"
    【解决方案3】:

    这是一个 CPU 密集型操作,不会长时间运行或在 I/O 上阻塞。如果您使用的是 Java 8,则可以使用并行流进行并行处理,如下所示。该方法已更改为使用 Collection 而不是 List 以使其更加灵活。

    public static boolean containsAny(final String searchString,
            final Collection<String> searchCollection) {
        return searchCollection.stream().parallel()
                .anyMatch(x -> searchString.indexOf(x) > -1);
    }
    

    此外,不应使用List,而应使用Set 作为基础数据结构,以便消除重复条目(如果有)。

    【讨论】:

      【解决方案4】:

      我相信最适合这种情况的数据结构是Suffix Tree。对于大小为n 的字符串,构建树需要Theta(n),并在其中搜索长度为m 的子字符串需要O(m)

      这是非常适合(并且打算)搜索字符串的数据结构之一。这是一种很常见的数据结构,网上有很多实现。

      【讨论】:

        【解决方案5】:

        使用 Aho Corasick 算法,您可以在大约 2/3 的时间内完成搜索。

        @user2040251 等人(包括我自己)接受的答案建议使用 Aho Corasick 算法。

        从您的 cmets 中,我可以看出您不是在寻找通用解决方案,而是在特定用例中表现良好的解决方案。

        @Vlad 创建了一个可能的测试套件来对一些建议的解决方案进行基准测试。

        @Marco13 对http://ahocorasick.org/ 的 Java 实现执行的测试表明您的初始实现更快。

        您的 cmets 提供了有关您尝试解决的问题的重要附加详细信息:

        • 大约 30 个要搜索的字符串
        • 要查找的字符串长度为 10 - 40 个字符。
        • 要搜索的字符串长度通常约为 100 个字符。
        • 您正在搜索的字符串是文件路径。

        I made a couple of quick modifications to @Vlad's gist to better match the specifics of the problem you described.

        我之前曾评论说,其他人测试过的 Aho-Corasick 实现正在寻找所有潜在的匹配项。找到第一个匹配项后返回的方法应该快得多。 为了看看我的直觉是否正确,我created a branch of Robert Bor's java Aho-Corasick implementation。 该分支现已合并到 Aho-Corasick!

        • 在 4337 毫秒内完成 100000 个包含任何内容(平均 0 毫秒)
        • 在 41153 毫秒内完成 100000 个 containsAnyWithRegex(平均 0 毫秒)
        • 在 23624 毫秒内完成 100000 个 containsAnyWithOffset(平均 0 毫秒)
        • 在 7956 毫秒内完成 100000 个 containsAnyAhoCorasickDotOrg(平均 0 毫秒)
        • 在 5351 毫秒内完成 100000 个 containsAnyAhoCorasickDotOrgMatches(平均 0 毫秒)
        • 在 2948 毫秒内完成 100000 个包含AnyAhoCorasickDYoo(平均 0 毫秒)
        • 在 7052 毫秒内完成 100000 个包含AnyHospool(平均 0 毫秒)
        • 在 5397 毫秒内完成 100000 个 containsAnyRaita(平均 0 毫秒)
        • 在 8285 毫秒内完成 100000 个 containsAnyJava8StreamParallel(平均 0 毫秒)

        我还实现了一个在自己的线程中执行每个搜索的方法。这种实现很糟糕,并且执行速度慢了大约 10 倍。

        更新:自从我最初的测试以来,我遇到了An even faster Aho-Corasick implementation.

        我包含了@GladwinB 建议的 Java 8 并行流实现的基准测试以及两个 com.eaio.stringsearch 实现。

        可能仍有收获。例如,本文描述了 Aho-Corasick 的一组匹配变体,听起来适合您的问题。Towards Faster String Matching for Intrusion Detection

        【讨论】:

          【解决方案6】:

          比较一下这种倒置优化的版本:

            public static boolean containsAny(String searchString, List<String> searchCollection) {
              for (int offset = 0; offset < searchString.length(); offset++) {
                for (String sought: searchCollection) {
                  int remainder = searchString.length() - offset;
                  if (remainder >= sought.length && searchString.startsWith(sought, offset)) {
                    return true;
                  }
                }
              }
              return false;
            }
          

          注意startsWith 与offset 的用法。

          【讨论】:

            【解决方案7】:

            你可以试试这个解决方案吗:

                final String[] searchList = searchCollection.toArray(new String[0]);
                Arrays.sort(searchList, new Comparator<String>() {
                    @Override
                    public int compare(final String o1, final String o2) {
                        if (o1 == null && o2 == null) {
                            return 0;
                        }
                        if (o1 == null || o1.isEmpty()) {
                            return 1;
                        }
                        if (o2 == null || o2.isEmpty()) {
                            return -1;
                        }
                        return o1.compareTo(o2);
                    }
                });
                final int result = Arrays.binarySearch(searchList, searchString);
                return result >= 0 ? true : false;
            

            【讨论】:

            • 这是假设字符串列表已排序,但在任何地方都没有说明?如果列表未排序,则在将其传递给此函数之前对其进行排序是有意义的,因此它只会排序一次,而不是每次调用函数时。除此之外,我同意这个解决方案。
            • 这个解决方案不能解决问题中的问题。它仅适用于 searchString 和列表中的字符串的完全匹配,但问题询问子字符串。
            【解决方案8】:

            正如许多其他人所回答的那样,通常有更好的数据结构用于存储和搜索字符串。您的问题是您的列表只有 30 个条目。使用更复杂的数据结构和算法所增加的开销很容易超过您从中获得的收益。

            别误会,你的瓶颈是 indexOf 线。看起来它占了处理的 95%。但是如果其他数据结构没有帮助(我尝试了一个现成的 Aho-Corasick Trie,它的速度是原来的两倍),这里有一些东西需要检查......

            关于使用 indexOf 而不是 contains 的评论是有问题的。在我的测试中。我看到每秒大约 150 万次“包含”查找,而 indexOf 只有大约 70 万次查找。如果你有相同的结果,那将使你的速度翻倍。

            改变

            // This is a performance optimization of contains.
            if (searchString.indexOf(stringInCollection, 0) > -1) {
            

            [返回]到

            if (searchString.contains(stringInCollection)) {
            

            如果您有兴趣,我测试的 trie 在这里:http://ahocorasick.org/,代码非常简单。我看到的问题是在找到第一场比赛后没有提前退出的功能。它解析整个字符串并找到所有匹配项。对于没有匹配项的情况(830K/秒),它比 indexOf() 快,但仍然比 contains() 慢。

            显然http://ahocorasick.org/ 不见了。

            非常相似的代码(可能相同)可以在https://github.com/robert-bor/aho-corasick找到

            【讨论】:

            • 链接失效了,很遗憾。请问可以修吗?
            • 性能提升很大程度上取决于数据。例如,如果字符串足够长,那么加速比与理想世界中子字符串的数量(大约 30 个)成正比。但是,例如的内部循环Aho-Corasick 算法比contains 更复杂,因此可能会慢几倍,但不会慢到 30 倍。对长字符串的解决方案进行基准测试以查看性能差异会非常有趣。
            【解决方案9】:

            您可以使用 HashSet 数据结构。但是哈希集不允许重复。例如,您不能在 HashSet 中出现两次字符串“foo”。

            从好的方面来说,复杂度应该是 O(1)。

            http://docs.oracle.com/javase/7/docs/api/java/util/HashSet.html

            【讨论】:

            • 这与问题无关
            【解决方案10】:

            @Yrlec 从您的评论中可以将 searchCollection 视为常量而没有太多修改,您可以对 arraylist 进行排序并缓存它,或者您可以实现自定义 List 类,该类存储对添加到的已排序元素的引用它。

            这样做的原因是,如果您对 searchCollection 进行了排序,那么您可以使用 String 的 compareTo 方法并减少迭代次数,从而在一定程度上提高您的方法性能。

            public static boolean containsAny(String searchString, List<String> searchCollectionSorted) {
                int size = searchCollectionSorted.size();
                for (int i = 0; i < size; i++) {
                    String stringInCollection = searchCollectionSorted.get(i);
                    if (!Util.isNullOrEmpty(stringInCollection)) {
                        if (stringInCollection.compareToIgnoreCase(searchString) > 0) {
                            if (searchString.startsWith(stringInCollection) {
                                return true;
                            } else {
                                // No point of iterating if we reach here as the searchstring is greater and hence iterations are saved improving performance
                                break;
                            }
                        }
                    }
                }
                return false;
            }
            

            【讨论】:

            • 这仅在searchString 以任何stringInCollection 开头时有效,但如果它在中间包含stringInCollection 则无效。
            【解决方案11】:

            TreeSet、HashSet 或 PrefixTree 都是很好的解决方案。 如果您需要搜索集合中是否存在给定前缀(复杂度 O(length(S)),则应该首选 PrefixTree,否则使用 HashSet。 http://docs.oracle.com/javase/7/docs/api/java/util/HashSet.html

            【讨论】:

              猜你喜欢
              • 2019-05-19
              • 1970-01-01
              • 2015-09-08
              • 2013-12-04
              • 2023-03-27
              • 2018-08-27
              • 2012-11-15
              • 2020-12-01
              相关资源
              最近更新 更多