【问题标题】:Efficiently check string for one of several hundred possible suffixes有效地检查数百个可能后缀之一的字符串
【发布时间】:2011-02-04 05:37:47
【问题描述】:

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

这个函数会被调用很多,所以它需要尽可能高效地编写。 Bitwise hacks 等等,只要结果很快。

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

我正在考虑实现 Rabin-Karp 的变体,或者编写一个工具来生成一个带有嵌套 if 和 switch 的函数,这些函数可以针对特定的后缀集进行定制。由于所讨论的应用程序是 64 位的以加快比较速度,因此我可以将最长 8 个字节的后缀存储为 const 排序数组并在其中进行二进制搜索。

还有其他合理的选择吗?

【问题讨论】:

  • 字符串是否会作为字符串类传递给函数,您可以在 O(1) 时间内获得名称,还是必须搜索以找到 NULL 终止符的 C 字符串?
  • 反转字符串和后缀,然后解决“快速启动”问题。我打赌你可以生成自己的有限状态机——正则表达式会做的事情……我发誓——生成严格的有限状态机代码是要走的路。这不可能更接近 FSM 的域 - 您要么接受输入字符串,要么不接受。你可以把它弄紧,也许需要人工干预。
  • 哦,还有一件事:从一个幼稚的实现开始,对它进行单元测试以致死,然后破解部分以提高效率 - 对其进行相同的测试。给它大约 1000 个好的字符串和大约 20000 个随机字符串,然后随机改变大约 1000 个字符串——所以再生成 20k 个。 TDD 是你的朋友。
  • 不只是 TDD 的正确性 - 您可以重复使用这些样本来测试您所做的任何更改的性能。
  • 所以......还有一个问题 - 你能忍受稍慢的初始启动时间吗?换句话说,检查函数应该是硬编码的,还是可以使用像 Trie 这样的动态结构?

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


【解决方案1】:

如果后缀不包含任何扩展/规则(如正则表达式),您可以按相反的顺序构建后缀的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

【讨论】:

    【解决方案2】:

    您提到您只查看二级域名,因此即使不知道匹配域的精确集合,您也可以提取输入字符串的相关部分。

    然后简单地使用哈希表。以没有碰撞的方式对其进行尺寸标注,因此您不需要存储桶;查找将完全是 O(1)。对于小型散列类型(例如 32 位),您需要检查字符串是否真的匹配。对于 64 位散列,另一个域与表中的一个散列发生冲突的概率已经非常低(10^-17 阶),您可能可以忍受它。

    【讨论】:

      【解决方案3】:

      我会反转所有的后缀字符串,构建它们的前缀树,然后测试你的 IP 字符串的反转。

      【讨论】:

        【解决方案4】:

        我认为构建您自己的自动机将是最有效的方式。这是您的第二种解决方案,根据该解决方案,它从一组有限的后缀开始,生成一个适合该后缀的自动机。

        我认为您可以轻松地使用flex 来做到这一点,注意反转输入或以特殊方式处理您正在寻找后缀的事实(只是为了提高效率)..

        顺便说一句,使用 Rabin-Karp 方法也会很有效,因为您的后缀会很短。您可以使用所需的所有后缀来拟合哈希集,然后

        • 取一个字符串
        • 取后缀
        • 计算后缀的哈希
        • 检查后缀是否在表中

        【讨论】:

          【解决方案5】:

          只需创建一组 26x26 的域数组。例如thisArray[0][0] 是以 'aa' 结尾的域,thisArray[0][1] 是以'ab' 结尾的所有域……

          一旦你有了它,只需在你的数组中搜索 thisArray[主机名的第二个字符][主机名的最后一个字符]即可获取可能的域。如果在那个阶段有多于一个,那么就暴力破解其余的。

          【讨论】:

            【解决方案6】:

            我认为解决方案应该根据输入字符串的类型有很大不同。如果字符串是某种可以从末尾迭代的字符串类(例如 stl 字符串),则比以 NULL 结尾的 C 字符串要容易得多。

            字符串类

            向后迭代字符串(不要进行反向复制 - 使用某种向后迭代器)。构建一个 Trie,其中每个节点由两个 64 位字、一个模式和一个位掩码组成。然后在每个级别一次检查 8 个字符。如果您想匹配少于 8 个字符,则使用掩码 - 例如deny "*.org" 会给出一个设置了 32 位的掩码。掩码也用作终止条件。

            C 字符串

            构造一个 NDFA 以在单次遍历字符串时匹配字符串。这样,您不必先迭代到最后,而是可以一次使用它。可以将 NDFA 转换为 DFA,这可能会使实现更加高效。 NDFA 的构建和向 DFA 的转换都可能非常复杂,以至于您必须为其编写工具。

            【讨论】:

              【解决方案7】:

              经过一些研究和考虑,我决定采用 trie/有限状态机方法。

              只要到目前为止解析的后缀部分可以对应多个后缀,就使用 TRIE 从最后一个字符开始向后解析字符串。在某些时候,我们要么点击一个可能的后缀的第一个字符,这意味着我们有一个匹配,要么遇到死胡同,这意味着没有更多可能的匹配,或者陷入只有一个后缀候选的情况。在这种情况下,我们只是比较后缀的其余部分。

              由于 trie 查找是恒定时间,因此最坏情况的复杂度为 o(最大后缀长度)。事实证明,该功能非常快。在 2.8Ghz Core i5 上,它每秒可以检查 33,000,000 个字符串以查找 2K 可能的后缀。 2K 后缀总计 18 KB,扩展为 320kb 的 trie/状态机表。我想我本可以更有效地存储它,但这个解决方案目前似乎足够好用。

              由于后缀列表太大,我不想全部手动编码,所以我最终编写了 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 个字节的数据,但它没有无论哪种方式都不会影响整体性能。

              感谢所有贡献自己想法的人!

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2019-09-12
                • 1970-01-01
                • 1970-01-01
                • 2011-12-16
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2017-10-30
                相关资源
                最近更新 更多