【发布时间】:2012-08-21 12:29:13
【问题描述】:
我目前正在研究正则表达式的问题,当与某个输入匹配时,该问题最终会在指数时间内运行,例如,(a*)* 和 (a|a)* 在匹配字符串时可能会出现“catastrophic backtracking” aaaaab - 对于匹配字符串中的每个额外的“a”,尝试匹配字符串所需的时间加倍。仅当引擎使用回溯/NFA 方法尝试在失败之前尝试树中所有可能的分支时才会出现这种情况,例如 PCRE 中使用的方法。
我的问题是,为什么(a?)* 没有漏洞?根据我对回溯的理解,字符串“aaaab”中应该发生的事情本质上就是(a|a)* 发生的事情。如果我们使用标准的 Thomspson NFA 构造来构造 NFA,那么对于发生的每个 epsilon 转换,引擎肯定必须继续采用它们并以与两个 a 的情况相同的方式回溯?例如(省略一些步骤以及 @ 替换 epsilon 的地方):
"aaaa" 匹配,但不能匹配 'b',失败(回溯)
"aaaa@" 匹配,'b' 失败(回溯)
"aaa@a" 匹配,'b' 失败(回溯)
"aaa@a@" 匹配,'b' 失败(回溯)
...
"@a@a@a@a@" 匹配,'b' 失败(回溯)
尝试所有可能的 epsilons 和 a 组合,肯定会导致路由的指数级爆炸?
从 NFA 中删除 epsilon 转换是有意义的,但我相信这会从 (a*)* 模式中删除所有非确定性。不过这绝对是脆弱的,所以我不完全确定发生了什么!
非常感谢您!
编辑: Qtax 已经指出,当使用传统回溯遍历 NFA 时,epsilons 不能仍然存在,否则(@)* 将尝试永远匹配。那么,什么样的 NFA 实现可能会导致 (a*)* 和 (a|a)* 呈指数增长,而 (a?)* 不是这样呢?这确实是问题的症结所在。
【问题讨论】:
-
PCRE 和 Perl 正则表达式引擎在许多方面进行了优化。您的所有示例都将很快失败,而不会发生灾难性的回溯,即使是在大字符串上也是如此。
-
我知道这一点,也许一个更好的例子是 Java 的正则表达式引擎,它不会很快失败。 PCRE 和 Perl 的早期版本也遇到了同样的问题,我的问题可以应用到他们身上!但是,在那些似乎容易受到
(a*)*等模式影响的引擎(即未优化的引擎)上,(a?)*似乎没问题 - 为什么?! -
优化。量化的零宽度表达式(没有副作用)是没用的,如果按照您的描述使用它(尝试匹配
a,然后每次重复都为空字符串),那么正则表达式永远不会失败,因为您可以无限地重复()*。 -
关于
()*的一个好点。我发现很难理解现有实现如何优化以阻止这种情况发生。一个非常幼稚的实现,通过 Thompson 的构造(e-moves 和所有)简单地构造 NFA,然后尝试回溯它永远不会因为你提到的()*原因而终止,那么他们做了什么导致(a?)*到成功,但(a*)*失败?它不能像完全删除电子动作那么简单,因为我认为这会导致(a*)*具有确定性 -
如果您想要一个正则表达式引擎如何处理这些情况的示例,您可以查看Perl's regex debug 输出,它向您展示了表达式编译的内容和匹配的每一步:
^(a*)*$和@987654324 @。示例命令(win):perl -Mre=debug -e "'aaab' =~ /^(a?)*$/"
标签: regex theory pcre backtracking finite-automata