现代正则表达式引擎中有一些功能,使您能够匹配在没有该功能的情况下无法匹配的语言。例如,以下使用反向引用的正则表达式匹配由重复的单词组成的所有字符串的语言:(.+)\1
。这种语言不是正则的,不能被不使用反向引用的正则表达式匹配。
环视是否也影响正则表达式可以匹配的语言?即,是否有任何语言可以使用环视进行匹配而无法以其他方式进行匹配?如果是这样,那么对于所有类型的环视(负面或正面前瞻或后顾),还是仅适用于其中一些类型呢?
现代正则表达式引擎中有一些功能,使您能够匹配在没有该功能的情况下无法匹配的语言。例如,以下使用反向引用的正则表达式匹配由重复的单词组成的所有字符串的语言:(.+)\1
。这种语言不是正则的,不能被不使用反向引用的正则表达式匹配。
环视是否也影响正则表达式可以匹配的语言?即,是否有任何语言可以使用环视进行匹配而无法以其他方式进行匹配?如果是这样,那么对于所有类型的环视(负面或正面前瞻或后顾),还是仅适用于其中一些类型呢?
q(?!u)=q($|[^u])
。
其次,正则表达式不仅仅用于匹配表达式,它们还可以从字符串中消耗字符 - 或者至少我们喜欢这样认为。例如在Python中,我关心.start()和.end(),因此当然会使用:
>>> re.search('q($|[^u])', 'Iraq!').end()
5
>>> re.search('q(?!u)', 'Iraq!').end()
4
^([^a]|(?=..b))*$
,这相当于一个有7个状态的DFSA,所有状态都接受:A - (a) -> B - (a) -> C --- (a) --------> D
Λ | \ |
| (not a) \ (b)
| | \ |
| v \ v
(b) E - (a) -> F \-(not(a)--> G
| <- (b) - / |
| | |
| (not a) |
| | |
| v |
\--------- H <-------------------(b)-----/
仅表示状态A的正则表达式如下:
^(a([^a](ab)*[^a]|a(ab|[^a])*b)b)*$
u(?!v)w
转换为uw
和uv
来实现这一点,但我不相信存在一种算法可以普遍地做到这一点。相反,您可以在epsilon转换处将前瞻或neg(前瞻)附加到原始DFA上。细节有些棘手,但我认为它有效。 - Josh Haberman^([^a]|a(?=..b))*$
。换句话说,所有字符都允许出现,但每个 "a" 必须在三个字符后跟一个 "b"。我不认为您可以通过联合将此简化为两个正则表达式 A 和 B 进行组合。我认为您必须将积极的前瞻部分作为 NFA 构造的一部分。 - Josh Habermana(?=..b)
是空语言,因为 a ∩ a..b = ϵ
。所以如果我们遵循你的推理线路,r = ϵ
和 ([^a]|a(?=..b))*
等价于 ([^a]|ϵ)*
或者只是 [^a]*
。但这显然是错误的,因为 aaab
匹配原始正则表达式但不匹配所谓的等价表达式。 - Josh Haberman正如其他答案所说,回顾先行断言并没有为正则表达式增加任何额外的功能。
我认为我们可以通过以下方式来展示:
一个鹅卵石2-NFA(参见引用该文件的介绍部分)。
鹅卵石2NFA不涉及嵌套的前瞻,但是,我们可以使用多鹅卵石2NFAs的变体(见下面的章节)。
介绍
2-NFA是一种非确定性有限状态自动机,具有在其输入上向左或向右移动的能力。
单个鹅卵石机器是指该机器可以在输入带上放置一个鹅卵石(即使用一个鹅卵石标记特定的输入符号),并根据当前输入位置是否存在鹅卵石执行可能不同的转换。
众所周知,一个鹅卵石2-NFA与常规DFA具有相同的功能。
非嵌套前瞻
基本思路如下:
2NFA允许我们通过在输入带上向前或向后移动来回溯(或“前溯”)。因此,对于前瞻,我们可以执行前瞻正则表达式的匹配,然后回溯我们已经消耗掉的内容,以匹配前瞻表达式。为了确切地知道何时停止回溯,我们使用鹅卵石!在进入前瞻DFA之前,我们将鹅卵石丢弃以标记需要停止回溯的位置。
因此,在运行我们的字符串通过卵石2NFA之后,我们知道是否匹配了前瞻表达式,剩余的输入(即需要消耗的内容)正好是匹配剩余所需的内容。为了处理嵌套的向前看,我们可以使用这里定义的k-pebble 2NFA的受限版本:Two-Way and Multi-Pebble Automata及其Logics的复杂性结果(请参见Definition 4.1和Theorem 4.2)。
通常情况下,2个鹅卵石自动机可以接受非正则集合,但有以下限制:k个鹅卵石自动机可以被证明是正则的(上述论文中的定理4.2)。如果鹅卵石是P_1、P_2、...、P_K:u(?=v)(?=w)(?=x)z
,可以吗? - Francis Davey我同意其他帖子的观点,即回溯是正则表达式的一部分(这意味着它不会为正则表达式添加任何基本功能),但我有一个比我看到的其他观点更简单的论据。
我将通过提供DFA构造来展示反向引用是正则的。如果一个语言有一个可以识别它的DFA,则该语言是正则的。请注意,Perl实际上并没有在内部使用DFA(有关详细信息,请参见本文:http://swtch.com/~rsc/regexp/regexp1.html),但是为了证明目的,我们构建了一个DFA。
构建正则表达式的DFA的传统方法是首先使用Thompson算法构建NFA。给定两个正则表达式片段r1
和r2
,Thompson算法提供了连接(r1r2
)、交替(r1|r2
)和重复(r1*
)正则表达式的构造方法。这样,您可以逐位地构建一个NFA,该NFA识别原始的正则表达式。有关更多详细信息,请参见上面的文章。
为了展示正向和负向前瞻是正则的,我将提供一个用于正则表达式u
与正向或负向前瞻相连的构造:(?=v)
或(?!v)
。只有连接需要特殊处理;通常的交替和重复构造可以很好地工作。
u(?=v)
和u(?!v)
的构造如下:
换句话说,将u
的现有NFA的每个终态连接到一个接受状态和一个NFA for v
,但进行修改,如下所示。函数f(v)
定义为:
aa(v)
作用于 NFA v
,将每个接受状态变为“反接受状态”。反接受状态是指对于给定字符串s
,如果任何经过 NFA 的路径以这个状态作为终点,则匹配失败,即使另一个经过v
的路径以接受状态作为终点也无济于事。loop(v)
作用于 NFA v
,在任何接受状态上添加自我转移。换句话说,一旦路径导致到达接受状态,那么该路径无论后面输入什么都可以永久停留在接受状态。f(v) = aa(loop(v))
。f(v) = aa(neg(v))
。为了提供一个直观的例子来说明为什么这样做有效,我将使用正则表达式(b|a(?:.b))+
,这是我在 Francis 的证明评论中提出的正则表达式的稍微简化版本。如果我们将我的构造方法与传统的 Thompson 构造结合起来,就会得到:
e
代表 epsilon 转移(在不消耗任何输入的情况下进行转移),反接受状态用 X
标记。在图的左半部分,你可以看到 (a|b)+
的表示方式:任何一个a
或b
都会使得图进入接受状态,但也允许回到开始状态以便我们能够再次匹配。但是请注意,每次我们匹配一个a
时,我们也会进入图的右半部分,在那里我们处于反接受状态,直到我们匹配到“any”后面跟着一个b
。
这不是传统的NFA,因为传统的NFA没有反接收状态。然而,我们可以使用传统的NFA->DFA算法将其转换为传统的DFA。该算法的工作方式与通常相同,我们通过使我们的DFA状态对应于可能处于的NFA状态的子集来模拟NFA的多个运行。唯一的区别是我们稍微修改了决定DFA状态是否为接受(终止)状态的规则。在传统算法中,如果任何 NFA状态是接受状态,则DFA状态为接受状态。我们将其修改为仅当以下条件成立时,DFA状态才为接受状态:
=1个 NFA状态是接受状态,且
该算法将为我们提供一个可以识别具有前瞻的正则表达式的DFA。因此,前瞻是正则的。请注意,后顾需要单独证明。
a
,因为在匹配 a
后,我们可以使用 NFA->DFA 算法转换到状态 4、3、1 和 5。但是,状态 5 是一个反接受状态,所以根据我写的底部规则,与状态 4、3、1 和 5 相对应的 DFA 状态 不是接受状态。 - Josh Habermanaa(v)
的定义不是依赖于字符串s
吗?也就是说,集合aa(v)
可以随着s
的变化而变化。同时,你说反接受状态起初是一个接受状态。如果机器最终进入该状态,那么任何匹配都将失败。如果我理解错了,对不起。 - Aryabhatta