【问题标题】:Efficient string matching algorithm高效的字符串匹配算法
【发布时间】:2009-03-05 00:28:09
【问题描述】:

我正在尝试构建一个高效的字符串匹配算法。这将在大容量环境中执行,因此性能至关重要。

这是我的要求:

  • 给定一个域名,即 www.example.com,确定它是否与条目列表中的一个“匹配”。
  • 条目可能是绝对匹配,即 www.example.com。
  • 条目可能包含通配符,例如 *.example.com。
  • 通配符条目从最定义的级别开始匹配。例如,*.example.com 将匹配 www.example.com、example.com 和 sub.www.example.com。
  • 未嵌入通配符条目,即 sub.*.example.com 不会成为条目。

语言/环境:C# (.Net Framework 3.5)

我考虑过将条目(和域查找)拆分为数组,颠倒顺序,然后遍历数组。虽然准确,但感觉很慢。

我考虑过正则表达式,但担心将条目列表准确地表示为正则表达式。

我的问题:根据上面列出的描述,查找一个以域名形式存在的字符串是否与字符串列表中的任何一个匹配的有效方法是什么?

【问题讨论】:

  • 问题是? / 顺便说一句,我会使用正则表达式,只需确保将表达式编译一次(而不是一次又一次地计算)。
  • “感觉慢”是什么意思?你真的测量过什么吗?
  • 您希望搜索列表中有多少项目?所有这些项目都会在内存中吗?您是否考虑过使用数据库?
  • @Ed:慢是相对的。我正在尝试确定是否有更有效的方法使用字符串算法。
  • 搜索列表将被加载到内存中。它相对不重要——成千上万的条目。

标签: c# algorithm


【解决方案1】:

如果您想自己动手,我会将条目存储在树结构中。请参阅my answer to another SO question 了解拼写检查程序,了解我的意思。

而不是用“。”标记结构。字符,我只会将每个条目视为一个完整的字符串。无论如何,任何标记化的实现仍然需要对完整的字符集进行字符串匹配,因此您不妨一次性完成所有操作。

这与常规拼写检查树之间的唯一区别是:

  1. 匹配需要反向进行
  2. 您必须考虑通配符

要解决第 2 点,您只需在测试结束时检查“*”字符。

一个简单的例子:

参赛作品:

*.fark.com
www.cnn.com

树:

m -> o -> c -> . -> k -> r -> a -> f -> . -> *
                \
                 -> n -> n -> c -> . -> w -> w -> w

检查 www.blog.fark.com 将涉及跟踪树直到第一个 "*"。因为遍历以"*" 结束,所以存在匹配项。

检查 www.cern.com 将在 n,n,c,...的第二个“n”处失败。

检查 dev.www.cnn.com 也会失败,因为遍历以 "*" 以外的字符结束。

【讨论】:

  • 这是一个很好的方法,假设“*”只能出现在末尾(这在这里看起来很合理)。如果将 trie 扁平化为某个深度(例如 8 个字符)的字符串数组,您可能会获得加速,因为在这个深度,trie 非常稀疏,并且跟踪每个字符的指针很慢。
  • 好建议。跟随开销的指针会减慢触摸速度。我想知道最佳阵列深度是多少?像往常一样,我们需要进行一些分析,看看我们的解决方案是否真的有帮助:)
  • 这是最好的内存猪。 (我实际上已经实施过一次)。使用虚拟内存,我也不太确定性能。
  • 毫无疑问会占用大量内存。最简单的方法将使用大约 50 到 100 倍于原始列表的内存。我想你会在前一千个条目中查看大约 15-20 MB。
  • 与任何设计决策一样,内存成本必须与性能改进进行权衡,并且两者都必须进行衡量。我在这里描述了我能想到的最快方法,因为 OP 在询问性能。
【解决方案2】:

我会使用正则表达式,只需确保将表达式编译一次(而不是一次又一次地计算)。

【讨论】:

  • 我认为,即使使用预编译,正则表达式对于这种简单性质的匹配也会非常过分且效率低下。再说一次,我认为大多数幼稚的方法也会非常低效,所以也许这是最好的简单解决方案。 +1
【解决方案3】:

您不需要正则表达式 .. 只需反转所有字符串, 摆脱'*',并放置一个标志以指示部分匹配 直到这一点过去。

不知何故,trie 或后缀 trie 看起来最合适。

如果域列表在编译时已知,您可以查看 在 '.' 标记化并使用多台 gperf 生成的机器。

链接: 谷歌搜索 http://marknelson.us/1996/08/01/suffix-trees/

【讨论】:

    【解决方案4】:

    我会使用树结构来存储规则,其中每个树节点是/包含一个字典。

    构造树使得“com”、“net”等是顶级条目,“example”在下一级,依此类推。您需要一个特殊标志来说明该节点是通配符。

    要执行查找,按句点拆分字符串,然后向后迭代,根据输入导航树。

    这似乎与您所说的您考虑的相似,但假设规则不会更改每次运行,使用缓存的基于字典的树将比数组列表更快。

    此外,我敢打赌这种方法会比 RegEx 更快。

    【讨论】:

    • 我正要说几乎完全一样的话,但我向下滚动,它就在那里! +1
    【解决方案5】:

    对于您认为有效的输入,您似乎有一套明确定义的规则 - 您可以考虑为此使用手写的LL parser。这样的解析器相对容易编写和优化。通常你会让解析器输出一个描述输入的树结构 - 我会使用这个树作为匹配例程的输入,该例程执行将树与条目列表匹配的工作,使用上面描述的规则。

    这是article on recursive descent parsers

    【讨论】:

    • “您可能会考虑为此使用手写的 LL 解析器”——您不认为这可能有点矫枉过正吗?
    • 如果他对正则表达式死心,则不会。
    • 此外 - 正则表达式引擎只是一个 DFA - 构建您自己的 DFA 以考虑您想要的规则可以更快,前提是您知道自己在做什么。
    【解决方案6】:

    假设规则如你所说:文字或以*开头。

    Java:

    public static boolean matches(String candidate, List<String> rules) {
        for(String rule : rules) {
            if (rule.startsWith("*")) {
                rule = rule.substring(2);
            }
            if (candidate.endsWith(rule)) {
                return true;
            }
        }
        return false;
    }
    

    这取决于您拥有的规则数量。

    编辑:

    这里要清楚一点。

    当我说“对规则进行排序”时,我的意思是从规则字符中创建一棵树。

    然后您使用匹配字符串尝试遍历树(即,如果我有一个 xyz 字符串,我从 x 字符开始,看看它是否有一个 y 分支,然后是一个 z 子节点)。

    对于“通配符”,我将使用相同的概念,但将其“向后”填充,并与匹配候选者的后面一起走。

    如果你有很多(很多很多)规则,我会对规则进行排序。

    对于非通配符匹配,您迭代每个字符以缩小可能的规则(即,如果它以“w”开头,那么您使用“w”规则等)

    如果它是通配符匹配,您会执行完全相同的操作,但您会根据“反向规则”列表进行操作,只需将字符串末尾与规则末尾匹配即可。

    【讨论】:

      【解决方案7】:

      我会尝试结合使用trieslongest-prefix matching(用于IP 网络的路由)。如果空间是一个问题,Directed Acyclic Word Graphs 可能比尝试更合适。

      【讨论】:

        【解决方案8】:

        我将建议树结构方法的替代方法。使用 Burrows-Wheeler 变换创建域列表的压缩索引。有关该技术的完整说明,请参阅http://www.ddj.com/architect/184405504?pgno=1

        【讨论】:

          【解决方案9】:

          看看RegExLib

          【讨论】:

            【解决方案10】:

            不确定你对拆分和迭代的想法是什么,但似乎不会很慢:

            如您所说,向上和反向拆分域。存储本质上可以是一棵树。使用哈希表来存储 TLD。例如,键是“com”,值是该 TLD 下的子域的哈希表,反复迭代。

            【讨论】:

              【解决方案11】:

              鉴于您的要求,我认为您正在考虑从字符串末尾 (TLD) 到主机名的工作。您可以使用正则表达式,但由于您并没有真正使用正则表达式的任何功能,我不明白您为什么要承担它们的成本。如果您反转字符串,则更明显的是您实际上只是在寻找前缀匹配('*.example.com' 变为:“'moc.elpmaxe' 是我输入字符串的开头吗?),这当然不会'不需要像正则表达式那样笨拙的东西。

              您使用什么结构来存储条目列表在很大程度上取决于列表的大小以及它的更改频率……对于一个庞大的稳定列表,树/特里树可能是性能最高的;一个经常变化的列表需要一个易于初始化/更新的结构,等等。如果没有更多信息,我不会推荐任何一种结构。

              【讨论】:

                【解决方案12】:

                我想我很想用另一个问题来回答你的问题:你在做什么,你认为你的瓶颈是一些字符串匹配,超出了简单的字符串比较?肯定还有其他东西在您的性能分析中列在更高的位置吗?

                我会首先使用明显的字符串比较测试,这在 90% 的情况下都是正确的,如果它们失败,则回退到正则表达式

                【讨论】:

                • 我们的字符串比较测试在第一次通过时远未接近 90%。
                【解决方案13】:

                如果只是匹配字符串,那么您应该查看 trie 数据结构和算法。较早的答案表明,如果您的所有通配符一开始都是一个通配符,则可以使用一些特定的算法。然而,处理通用通配符的要求意味着,为了快速执行,您将需要生成一个状态机。

                这就是正则表达式库为您所做的:“预编译”正则表达式 == 生成状态机;这使得运行时的实际匹配速度更快。如果没有特别的优化工作,您不太可能获得比这更好的性能。

                如果您想自己动手,我可以说专门为多个通配符编写自己的状态机生成器应该具有教育意义。在这种情况下,您需要阅读他们在正则表达式库中使用的算法类型...

                【讨论】:

                  【解决方案14】:

                  研究 KMP (Knuth-Morris-Pratt) 或 BM (Boyer-Moore) 算法。这些使您可以比线性时间更快地搜索字符串,但需要进行一些预处理。正如其他人所指出的那样,删除前导星号当然至关重要。

                  这些信息的一个来源是:

                  KMP:http://www-igm.univ-mlv.fr/~lecroq/string/node8.html

                  BM:http://www-igm.univ-mlv.fr/~lecroq/string/node14.html

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 2012-07-24
                    • 1970-01-01
                    • 2012-09-14
                    • 1970-01-01
                    • 2017-10-11
                    • 2023-03-08
                    相关资源
                    最近更新 更多