有效地检查数百个可能后缀之一的字符串

时间:2010-04-04 17:09:44

标签: c++ algorithm url string 64-bit

我需要编写一个C / C ++函数,它可以快速检查字符串是否以~1000个预定义后缀中的一个结尾。具体来说,字符串是主机名,我需要检查它是否属于几百个预定义的二级域名之一。

此函数将被调用很多,因此需要尽可能高效地编写。只要结果很快,就会发生任何事情。

后缀集在编译时预先确定,不会改变。

我正在考虑实现Rabin-Karp的变体或编写一个工具,该工具将生成具有嵌套ifs和开关的函数,这些函数将根据特定的后缀集进行定制。由于有问题的应用程序是64位加速比较,我可以存储长度最多为8个字节的后缀作为常量排序数组,并在其中进行二进制搜索。

还有其他合理的选择吗?

7 个答案:

答案 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位的掩码。掩码也用作终止标准。

C字符串

构造一个与单个传递上的字符串匹配的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个字节的数据,但它没有'无论如何都会影响整体表现。

感谢所有贡献他们想法的人!