查找字符串是否包含集合中的任何字符串

时间:2014-08-13 15:20:04

标签: java string data-structures collections

我试图提高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),否则我认为它不会得到显着改善。是否有任何数据结构可以让我这样做?

11 个答案:

答案 0 :(得分:13)

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

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

答案 1 :(得分:8)

// 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();

正则表达式编译相对聪明。它与使用您搜索的单词集合的搜索树相当:"agent" and "agitator" to ("ag", ("ent", "itator"))

答案 2 :(得分:8)

这是一项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作为基础数据结构,以便消除重复的条目(如果有的话)。

答案 3 :(得分:3)

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

来自@ user2040251(包括我自己)的接受答案提出了Aho Corasick算法。

从您的评论中我可以看到您不是在寻找一般解决方案,而是在特定用例中表现良好的解决方案。

@Vlad创建了一个可能的测试套件,以对一些提议的解决方案进

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

您的评论提供了有关您要解决的问题的重要详细信息:

  • 搜索
  • 大约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 Robert Bor's java Aho-Corasick implementation。 这个分支现在已合并到Aho-Corasick!

  • 已完成100000包含任何4337毫秒(平均0毫秒)
  • 在41153毫秒(平均0毫秒)
  • 完成100000包含AnyWithRegex
  • 完成100000 containsAnyWithOffset in 23624 ms(avg 0 ms)
  • 完成100000包含AnyAhoCorasickDotOrg 7956 ms(平均0毫秒)
  • 完成100000包含AnyAhoCorasickDotOrgMatches在5351毫秒(平均0毫秒)
  • 在2948毫秒内完成100000包含AnyAhoCorasickDYoo(平均0毫秒)
  • 完成100000包含在7052毫秒内的AnyHospool(平均0毫秒)
  • 已完成100000包含AnyRaita,时间为5397 ms(平均0毫秒)
  • 完成100000 containsAnyJava8StreamParallel 8285 ms(平均0毫秒)

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

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

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

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

答案 4 :(得分:3)

您可以尝试使用此解决方案:

    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;

答案 5 :(得分:2)

与此相比,这是一种倒置和优化的版本:

  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。

答案 6 :(得分:2)

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

这是非常适合(和打算)搜索字符串的数据结构之一。它是一种非常常见的数据结构,在线实现了许多实现。

答案 7 :(得分:2)

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

不要误解我的意思,你的瓶颈就是indexOf行。看起来它占了处理的95%。但是,如果其他数据结构没有帮助(我尝试过现成的Aho-Corasick Trie并且它的速度是它的两倍),这里有一个值得检查的东西......

关于使用indexOf而不是contains的注释是值得怀疑的。在我的测试中。我看到每秒大约有150万次查找&#34;包含&#34;而indexOf只有约700K。如果你有相同的结果,那么你的速度就会加倍。

更改

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

[返回]

if (searchString.contains(stringInCollection)) {

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

答案 8 :(得分:2)

@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;
}

答案 9 :(得分:2)

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

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

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

答案 10 :(得分:0)

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

相关问题