正则表达式中的(a?)*是否具有指数增长?

12
我正在研究一些正则表达式的问题,当对某个输入进行匹配时,它们可能会以指数级的时间运行。例如,(a*)*(a|a)*在与字符串aaaaab匹配时可能会出现'灾难性回溯'的情况——对于匹配字符串中每一个额外的'a',尝试匹配该字符串所需的时间就加倍。只有引擎使用回溯/NFA方法在失败之前尝试树中所有可能的分支,例如PCRE所使用的方法,才会出现这种情况。
我的问题是,为什么(a?)*不会出现这种漏洞?根据我对回溯的理解,在字符串"aaaab"中应该发生的基本上就是(a|a)*中发生的情况。如果我们使用标准的Thomspson NFA构造来构建NFA,则对于每个ε转换,引擎都必须继续采取它们并以与两个a的情况相同的方式进行回溯吗?例如(省略了一些步骤,其中@替换ε):
"aaaa"匹配,但不能匹配'b',失败(回溯)
"aaaa@"匹配,'b'失败(回溯)
"aaa@a"匹配,'b'失败(回溯)
"aaa@a@"匹配,'b'失败(回溯)
...
"@a@a@a@a@"匹配,'b'失败(回溯)
尝试所有可能的ε和a的组合,导致路线指数级扩大。
从NFA中删除ε转换似乎是有道理的,但我认为这会使(a*)*模式中所有的非确定性都消失。然而,这种模式肯定是脆弱的,所以我不太确定发生了什么!
非常感谢您提前的帮助!

编辑:Qtax 指出,在使用传统回溯时,NFA 遍历时仍然不能出现 epsilon,否则 (@)* 将会一直尝试匹配。那么,有哪种 NFA 实现可能导致 (a*)*(a|a)* 呈指数增长,而 (a?)* 则不是呢?这才是问题的核心。


PCRE和Perl正则表达式引擎在许多方面进行了优化。即使在处理大字符串时,所有示例也会迅速失败而无需进行灾难性回溯。 - Qtax
我知道这一点,也许更好的例子是Java的正则表达式引擎,它不会快速失败。早期版本的PCRE和Perl也遇到了同样的问题,我的问题也可以应用于它们!然而,在那些容易受到(a*)*(即未经优化的引擎)等模式影响的引擎上,(a?)*似乎没问题 - 为什么?! - Jarmex
1
优化。没有副作用的量化零宽表达式是无用的,如果按照您描述的方式使用它(尝试匹配a然后对每个重复匹配空字符串),那么正则表达式永远不会失败,因为您可以无限地重复()* - Qtax
()*的一个好处是,我发现很难理解现有实现如何进行优化以防止这种情况发生。通过Thompson构造(包括e-moves)简单地构建NFA,然后尝试通过它进行回溯的非常天真的实现,永远不会因你提到的()*原因而终止,那么他们做了什么导致(a?)*成功,但(a*)*失败?它不能简单地删除所有e-moves,因为我认为这会导致(a*)*变得确定。 - Jarmex
如果你想看一个正则表达式引擎如何处理这些情况的例子,可以查看Perl的正则表达式调试输出,它展示了表达式编译和每个匹配步骤:^(a*)*$^(a?)*$。例如命令(win):perl -Mre=debug -e "'aaab' =~ /^(a?)*$/" - Qtax
1个回答

4

好的,经过一番调查,我最终发现这是由于在NFA实现中使用了“屏障”。简单来说,在NFA中的战略点(例如在a*的NFA构造中'a'转换后立即进入节点),会放置屏障。它们要求匹配已经从上次击中该屏障时进行了进展。这可以防止NFA陷入匹配无限数量的epsilons的情况,并允许其终止。

换句话说,不可能仅通过匹配e-moves从一个屏障到达同一屏障-如果发生这种情况,则路线被放弃并从先前点进行回溯。这也具有副作用,即(a?)*不容易出现指数级的爆炸,因为a?第二次无法匹配null。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接