我有种子字符串列表,大约100个预定义字符串。所有字符串仅包含ASCII字符。
std::list<std::wstring> seeds{ L"google", L"yahoo", L"stackoverflow"};
我的应用程序不断收到很多可以包含任何字符的字符串。我需要检查每个收到的行并确定它是否包含任何种子。比较必须不区分大小写。
我需要尽可能快的算法来测试收到的字符串。
现在我的应用程序使用了这个算法:
std::wstring testedStr;
for (auto & seed : seeds)
{
if (boost::icontains(testedStr, seed))
{
return true;
}
}
return false;
效果很好,但我不确定这是最有效的方法。
如何实现该算法以获得更好的性能?
这是一款Windows应用。应用会收到有效的std::wstring
字符串。
更新
为此,我实施了Aho-Corasick算法。如果有人可以查看我的代码那就太棒了 - 我对这些算法没有太大的经验。链接到实施:gist.github.com
答案 0 :(得分:56)
如果存在有限数量的匹配字符串,这意味着您可以构造一个树,从根到叶读取,类似的字符串将占用类似的分支。
这也称为trie, or Radix Tree。
例如,我们可能会有字符串cat, coach, con, conch
以及dark, dad, dank, do
。他们的特里可能看起来像这样:
搜索树中的一个单词将从根开始搜索树。使它成为叶子将对应于与种子的匹配。无论如何,字符串中的每个字符都应与其中一个孩子匹配。如果没有,您可以终止搜索(例如,您不会考虑以“g”开头的任何单词或以“cu”开头的任何单词)。
有各种算法用于构建树以及搜索它以及动态修改它,但我想我会给出解决方案的概念性概述而不是特定的,因为我不知道它的最佳算法。
从概念上讲,您可能用于搜索树的算法与 radix sort 背后的想法有关,该算法是字符串中的字符在给定的字符串中可能采用的固定数量的类别或值的值时间点。
这可让您针对word-list检查一个字。既然你正在寻找这个单词列表作为输入字符串的子字符串,那么它将会比这更多。
编辑正如其他答案所提到的,用于字符串匹配的Aho-Corasick算法是一种用于执行字符串匹配的复杂算法,包含一个带有附加链接的trie,用于通过树获取“快捷方式”并伴随着不同的搜索模式。 (作为一个有趣的说明,Alfred Aho也是流行编译器教科书的贡献者,编译器:原理,技术和工具以及算法教科书,计算机的设计和分析算法。他也是贝尔实验室的前成员.Margaret J. Corasick似乎没有太多关于她自己的公开信息。)
答案 1 :(得分:50)
它构建trie / automaton,其中一些顶点标记为终点,这意味着字符串有种子。
它内置于O(sum of dictionary word lengths)
,并在O(test string length)
优点:
如果它是ASCII(非ASCII字符无论如何都不匹配),你可以通过降低每个符号使它不区分大小写
答案 2 :(得分:16)
你应该尝试一个预先存在的正则表达式实用程序,它可能比你的手动滚动算法慢,但正则表达式是关于匹配多种可能性,所以它可能已经比hashmap或简单比较快几倍所有字符串。我相信正则表达式实现可能已经使用了RiaD提到的Aho-Corasick算法,所以基本上你可以随意使用经过良好测试和快速实现。
如果你有C ++ 11,你已经有了一个标准的正则表达式库
#include <string>
#include <regex>
int main(){
std::regex self_regex("google|yahoo|stackoverflow");
regex_match(input_string ,self_regex);
}
我希望这能产生最好的最小匹配树,所以我希望它真的很快(而且可靠!)
答案 3 :(得分:10)
更快的方法之一是使用后缀树https://en.wikipedia.org/wiki/Suffix_tree,但这种方法有很大的缺点 - 难以构建数据结构。此算法允许从线性复杂度https://en.m.wikipedia.org/wiki/Ukkonen%27s_algorithm
中的字符串构建树答案 4 :(得分:6)
编辑:正如Matthieu M.指出的,OP询问字符串是否包含关键字。我的答案仅在字符串等于关键字时才有效,或者如果您可以拆分字符串,例如由空间角色。
特别是对于大量可能的候选人并且在编译时使用perfect hash function和gperf之类的工具了解它们是值得一试的。主要原则是,您使用种子生成一个生成器,并生成一个包含散列函数的函数,该函数不会对所有种子值发生冲突。在运行时,您为函数提供一个字符串,它计算哈希值,然后检查它是否是唯一可能与哈希值对应的候选项。
运行时成本是对字符串进行哈希处理,然后与唯一可能的候选项进行比较(种子大小为O(1),字符串长度为O(1))。
要使比较大小写不敏感,只需在种子和字符串上使用tolower。
答案 5 :(得分:3)
因为弦的数量不大(~100),所以可以使用下一个算法:
int checks[N];
校验和数组。std::map<int, std::vector<std::wstring>>
,其中int
是字符串的校验和,向量应包含所有字符串校验和。
为每个长度(最多N)创建此类映射的数组,也可以在编译时完成。checks
整数,并且对于它们中的每一个(从1到N的数字)移除(XK)字符的值,其中K是整数的数字checks
数组。因此,对于存储在checks
数组中的所有长度,您将始终具有正确的校验和。
之后在地图上搜索确实存在具有这种对的字符串(长度和校验和),如果存在 - 比较它。它应该给出假阳性结果(当校验和和长度相等但短语不是时)非常罕见。
所以,让我们说R是大字符串的长度。然后循环它将需要O(R)。
每一步你将用“+”小数(char值)执行N次操作,N次操作用“ - ”小数(char值),这是非常快的。每个步骤都必须在checks
数组中搜索计数器,即O(1),因为它是一个内存块。
此外,每个步骤都必须在地图数组中找到地图,也就是O(1),因为它也是一个内存块。 在内部地图中,您将必须搜索具有正确校验和的字符串(F),其中F是地图的大小,并且它通常不包含2-3个字符串,因此我们通常可以假装它也是O (1)。
你也可以检查一下,如果没有相同校验和的字符串(只有100个单词才有可能发生),你可以完全丢弃地图,存储对而不是地图。
所以,最后应该给O(R),O很小。
这种计算checksum
的方法可以改变,但它非常简单而且非常快,并且具有非常罕见的假阳性反应。
答案 6 :(得分:3)
作为DarioOO答案的变体,通过为字符串编码lex parser,您可以更快地实现正则表达式匹配。虽然通常与yacc一起使用,但这是lex自身完成工作的情况,而lex解析器通常非常有效。
如果所有字符串都很长,这种方法可能会失败,因为Aho-Corasick,Commentz-Walter或Rabin-Karp之类的算法可能会提供重大改进,我怀疑lex实现使用任何这样的算法。
如果您必须能够在不重新配置的情况下配置字符串,这种方法会更难,但由于flex是开源的,您可能会蚕食其代码。