我需要编写一个C / C ++函数,它可以快速检查字符串是否以~1000个预定义后缀中的一个结尾。具体来说,字符串是主机名,我需要检查它是否属于几百个预定义的二级域名之一。
此函数将被调用很多,因此需要尽可能高效地编写。只要结果很快,就会发生任何事情。
后缀集在编译时预先确定,不会改变。
我正在考虑实现Rabin-Karp的变体或编写一个工具,该工具将生成具有嵌套ifs和开关的函数,这些函数将根据特定的后缀集进行定制。由于有问题的应用程序是64位加速比较,我可以存储长度最多为8个字节的后缀作为常量排序数组,并在其中进行二进制搜索。
还有其他合理的选择吗?
答案 0 :(得分:11)
如果后缀不包含任何扩展/规则(如正则表达式),则可以按相反顺序构建Trie后缀,然后根据该字符串匹配字符串。例如
suffixes:
foo
bar
bao
reverse order suffix trie:
o
-a-b (matches bao)
-o-f (matches foo)
r-a-b (matches bar)
然后可以使用它们来匹配您的字符串:
"mystringfoo" -> reverse -> "oofgnirtsym" -> trie match -> foo suffix
答案 1 :(得分:4)
您提到您只关注二级域名,因此即使不知道匹配域的精确集合,您也可以提取输入字符串的相关部分。
然后只需使用哈希表。以不会发生碰撞的方式对其进行尺寸标注,因此您不需要铲斗;查找将完全是O(1)。对于小散列类型(例如32位),您需要检查字符串是否真正匹配。对于64位散列,另一个域与表中的一个散列冲突的概率已经很低(10 ^ -17),你可以使用它。
答案 2 :(得分:3)
我会反转所有后缀字符串,构建它们的前缀树,然后测试你的IP字符串的反向。
答案 3 :(得分:2)
我认为构建自己的自动机将是最有效的方式..它是您的第二种解决方案,根据该解决方案,从一组有限的后缀开始,它会生成一个适合该后缀的自动机。
我认为你可以轻松地使用flex来做这件事,注意以特殊的方式扭转输入或处理事实,你只是寻找后缀(只是为了有效的事情)..
顺便使用 Rabin-Karp 方法也很有效,因为后缀很短。您可以使用所需的所有后缀来填充哈希集,然后
答案 4 :(得分:0)
只需创建一组26x26的域阵列。例如thisArray [0] [0]将是以'aa'结尾的域名,thisArray [0] [1]是以'ab'结尾的所有域名等等......
一旦你有了这个,只需在你的数组中搜索thisArray [主机名的第二个字符] [主机名的最后一个字符]来获取可能的域名。如果在那个阶段不止一个,那就强迫其余部分。
答案 5 :(得分:0)
我认为解决方案应该根据输入字符串的类型而有很大差异。如果字符串是某种可以从末尾迭代的字符串类(例如stl字符串),那么它们比以NULL结尾的C字符串容易得多。
向后迭代字符串(不要进行反向复制 - 使用某种向后迭代器)。构建一个Trie,其中每个节点由两个64位字,一个模式和一个位掩码组成。然后在每个级别一次检查8个字符。如果要匹配少于8个字符,则使用掩码 - 例如拒绝“* .org”会给出一个32位的掩码。掩码也用作终止标准。
构造一个与单个传递上的字符串匹配的NDFA。这样你就不必首先迭代到最后但可以在一次传递中使用它。 NDFA可以转换为DFA,这可能会使实施更有效。 NDFA的构建和转换为DFA可能都非常复杂,您必须为它编写工具。
答案 6 :(得分:0)
经过一番研究和考虑,我决定采用trie /有限状态机方法。
从使用TRIE向后的最后一个字符开始解析字符串,只要到目前为止解析的后缀部分可以对应多个后缀。在某些时候,我们要么点击其中一个可能后缀的第一个字符,这意味着我们有一个匹配,命中一个死胡同,这意味着没有更多可能的匹配或进入只有一个后缀候选的情况。在这种情况下,我们只是比较后缀的剩余部分。
由于trie查找是恒定时间,因此最差情况复杂度为o(最大后缀长度)。事实证明这个功能非常快。在2.8Ghz Core i5上,它可以检查每秒33,000,000个字符串,以获得2K可能的后缀。 2K后缀总计18千字节,扩展到320kb trie / state machine table。我想我可以更有效地存储它,但这个解决方案目前看起来效果还不错。
由于后缀列表太大,我不想手工编写所有代码,所以我最终编写了C#应用程序,为后缀检查功能生成C代码:
public static uint GetFourBytes(string s, int index)
{
byte[] bytes = new byte[4] { 0, 0, 0, 0};
int len = Math.Min(s.Length - index, 4);
Encoding.ASCII.GetBytes(s, index, len, bytes, 0);
return BitConverter.ToUInt32(bytes, 0);
}
public static string ReverseString(string s)
{
char[] chars = s.ToCharArray();
Array.Reverse(chars);
return new string(chars);
}
static StringBuilder trieArray = new StringBuilder();
static int trieArraySize = 0;
static void Main(string[] args)
{
// read all non-empty lines from input file
var suffixes = File
.ReadAllLines(@"suffixes.txt")
.Where(l => !string.IsNullOrEmpty(l));
var reversedSuffixes = suffixes
.Select(s => ReverseString(s));
int start = CreateTrieNode(reversedSuffixes, "");
string outFName = @"checkStringSuffix.debug.h";
if (args.Length != 0 && args[0] == "--release")
{
outFName = @"checkStringSuffix.h";
}
using (StreamWriter wrt = new StreamWriter(outFName))
{
wrt.WriteLine(
"#pragma once\n\n" +
"#define TRIE_NONE -1000000\n"+
"#define TRIE_DONE -2000000\n\n"
);
wrt.WriteLine("const int trieArray[] = {{{0}\n}};", trieArray);
wrt.WriteLine(
"inline bool checkSingleSuffix(const char* str, const char* curr, const int* trie) {\n"+
" int len = trie[0];\n"+
" if (curr - str < len) return false;\n"+
" const char* cmp = (const char*)(trie + 1);\n"+
" while (len-- > 0) {\n"+
" if (*--curr != *cmp++) return false;\n"+
" }\n"+
" return true;\n"+
"}\n\n"+
"bool checkStringSuffix(const char* str, int len) {\n" +
" if (len < " + suffixes.Select(s => s.Length).Min().ToString() + ") return false;\n" +
" const char* curr = (str + len - 1);\n"+
" int currTrie = " + start.ToString() + ";\n"+
" while (curr >= str) {\n" +
" assert(*curr >= 0x20 && *curr <= 0x7f);\n" +
" currTrie = trieArray[currTrie + *curr - 0x20];\n" +
" if (currTrie < 0) {\n" +
" if (currTrie == TRIE_NONE) return false;\n" +
" if (currTrie == TRIE_DONE) return true;\n" +
" return checkSingleSuffix(str, curr, trieArray - currTrie - 1);\n" +
" }\n"+
" --curr;\n"+
" }\n" +
" return false;\n"+
"}\n"
);
}
}
private static int CreateTrieNode(IEnumerable<string> suffixes, string prefix)
{
int retVal = trieArraySize;
if (suffixes.Count() == 1)
{
string theSuffix = suffixes.Single();
trieArray.AppendFormat("\n\t/* {1} - {2} */ {0}, ", theSuffix.Length, trieArraySize, prefix);
++trieArraySize;
for (int i = 0; i < theSuffix.Length; i += 4)
{
trieArray.AppendFormat("0x{0:X}, ", GetFourBytes(theSuffix, i));
++trieArraySize;
}
retVal = -(retVal + 1);
}
else
{
var groupByFirstChar =
from s in suffixes
let first = s[0]
let remainder = s.Substring(1)
group remainder by first;
string[] trieIndexes = new string[0x60];
for (int i = 0; i < trieIndexes.Length; ++i)
{
trieIndexes[i] = "TRIE_NONE";
}
foreach (var g in groupByFirstChar)
{
if (g.Any(s => s == string.Empty))
{
trieIndexes[g.Key - 0x20] = "TRIE_DONE";
continue;
}
trieIndexes[g.Key - 0x20] = CreateTrieNode(g, g.Key + prefix).ToString();
}
trieArray.AppendFormat("\n\t/* {1} - {2} */ {0},", string.Join(", ", trieIndexes), trieArraySize, prefix);
retVal = trieArraySize;
trieArraySize += 0x60;
}
return retVal;
}
因此它会生成如下代码:
inline bool checkSingleSuffix(const char* str, const char* curr, const int* trie) {
int len = trie[0];
if (curr - str < len) return false;
const char* cmp = (const char*)(trie + 1);
while (len-- > 0) {
if (*--curr != *cmp++) return false;
}
return true;
}
bool checkStringSuffix(const char* str, int len) {
if (len < 5) return false;
const char* curr = (str + len - 1);
int currTrie = 81921;
while (curr >= str) {
assert(*curr >= 0x20 && *curr <= 0x7f);
currTrie = trieArray[currTrie + *curr - 0x20];
if (currTrie < 0) {
if (currTrie == TRIE_NONE) return false;
if (currTrie == TRIE_DONE) return true;
return checkSingleSuffix(str, curr, trieArray - currTrie - 1);
}
--curr;
}
return false;
}
因为我在checkSingleSuffix中的特定数据集len从未超过9,所以我尝试用switch(len)和硬编码的比较例程替换比较循环,这些例程一次比较多达8个字节的数据,但它没有'无论如何都会影响整体表现。
感谢所有贡献他们想法的人!