.Net中替代递归模式的方法是使用栈。挑战在于我们需要用栈来表达语法。
以下是一种实现方式:
语法的稍微不同表示法
- 首先,我们需要语法规则(例如问题中的
A
和Q
)。
- 我们有一个栈。该栈只能包含规则。
- 在每个步骤中,我们从栈中弹出当前状态,匹配所需的内容,并将其他规则推入栈中。当我们完成一个状态时,我们不会推送任何东西,并回到以前的状态。
这种表示法介于上下文无关文法和下推自动机之间,其中我们将规则推送到栈中。
示例:
让我们从一个简单的例子开始:anbn。这种语言通常的语法如下:
S -> aSb | ε
我们可以重新用符号表示:
# Start with <push S>
<pop S> -> "a" <push B> <push S> | ε
<pop B> -> "b"
解释:
- 我们从栈中开始使用
S
。
- 当我们从栈中弹出
S
时,我们可以选择:
- 匹配空,或...
- 匹配 "a",但是接下来我们必须将状态
B
推到栈中。这是我们会匹配 "b" 的承诺。然后我们还推入 S
,这样如果需要,我们就可以继续匹配 "a"。
- 当我们匹配足够多的 "a" 后,我们开始从栈中弹出
B
并为每个弹出的 B
匹配一个 "b"。
- 完成此操作后,我们已经匹配了偶数个 "a" 和 "b"。
或者,更松散地说:
当我们处于 S 状态时,匹配 "a" 并推入 B 和 S,或者什么也不匹配。
当我们处于 B 状态时,匹配 "b"。
在所有情况下,我们都从栈中弹出当前状态。
在 .Net 正则表达式中编写模式
我们需要以某种方式表示不同的状态。我们不能选择 '1' '2' '3' 或 'a' 'b' 'c',因为这些可能不在我们的输入字符串中 - 我们只能匹配存在于字符串中的内容。一种选择是给我们的状态编号(在上面的示例中,S
是状态号 0,而 B
是状态号 1)。对于状态 S,我们可以将字符推入堆栈。为方便起见,我们将从字符串开头推送前几个字符。再次强调,我们并不关心这些字符是什么,只关心它们的数量。
推送状态
在 .Net 中,如果我们想将字符串的前 5 个字符推到堆栈中,我们可以编写:
(?<=(?=(?<StateId>.{5}))\A.*)
这有点复杂:
(?<=…\A.*)
是一个回顾,一直到字符串的开头。- 当我们到达开头时,有一个向前查找:
(?=…)
。这样做是为了匹配当前位置之后的内容——如果我们在位置2,我们就没有5个字符在前面。所以我们既要向前,也要向后看。 (?<StateId>.{5})
将5个字符推入堆栈,表示在某个时刻我们需要匹配状态5。
弹出状态
按照我们的记号,我们总是从堆栈中取出顶部状态。这很容易:(?<-StateId>)
。
但在那之前,我们想知道那是什么状态——或者它捕获了多少个字符。更具体地说,我们需要显式地检查每个状态,就像switch
/case
块一样。因此,要检查堆栈当前是否持有状态5:
(?<=(?=.{5}(?<=\A\k<StateId>))\A.*)
- 再次强调,
(?<=…\A.*)
匹配一直到字符串的起始位置。
- 现在我们将
(?=.{5}…)
往前移动五个字符...
- 然后使用另一个回顾断言
(?<=\A\k<StateId>)
来检查堆栈是否确实有 5 个字符。
这种方法显然有一个缺点 - 当字符串太短时,无法表示大状态的数量。 解决这个问题的方法有:
- 语言中短单词的数量是有限的,因此我们可以手动添加它们。
- 使用比单个堆栈更复杂的东西 - 我们可以使用几个堆栈,每个堆栈都有零个或一个字符,有效地成为我们状态的位(末尾有一个示例)。
结果
我们匹配 anbn 的模式如下:
\A
(?<StateId>)
(?:
(?:
(?:
(?<=(?=.{0}(?<=\A\k<StateId>))\A.*)
(?<-StateId>)
(?:
(?<=(?=(?<StateId>.{1}))\A.*)
a
(?<StateId>)
|
)
)
|
(?:
(?<=(?=.{1}(?<=\A\k<StateId>))\A.*)
(?<-StateId>)
b
)
|\Z
){2}
)+
\Z
(?(StateId)(?!))
Regex Storm上的工作示例
注意事项:
- 请注意状态周围的量词为
(?:(?:…){2})+
- 也就是说,(状态数量)×(输入长度)。我还添加了一个 \Z
的替代方案。我们不需要深入讨论,但它是 .Net 引擎中的烦人优化的解决方法。
- 同样可以写成
(?<A>a)+(?<-A>b)+(?(A)(?!))
- 这只是一个练习。
回答问题
问题中的语法可以重写为:
<pop A> -> <push A> ( @"," | <push Q> ) | ε
<pop Q> -> \w
| "<" <push Q2Close> <push A>
| "[" <push Q1Close> <push QStar> <push Q1Comma> <push QStar> <push Q1Semicolon> <push A>
<pop Q2Close> -> ">"
<pop QStar> -> <push QStar> <push Q> | ε
<pop Q1Comma> -> ","?
<pop Q1Semicolon> -> ";"
<pop Q1Close> -> "]"
该模式:
\A
(?<StateId>)
(?:
(?:
(?:
(?<=(?=.{0}(?<=\A\k<StateId>))\A.*)
(?<-StateId>)
(?:
(?<StateId>)
(?:
,
|
(?<=(?=(?<StateId>.{1}))\A.*)
)
|
)
)
|
(?:
(?<=(?=.{1}(?<=\A\k<StateId>))\A.*)
(?<-StateId>)
(?:
\w
|
<
(?<=(?=(?<StateId>.{2}))\A.*)
(?<StateId>)
|
\[
(?<=(?=(?<StateId>.{6}))\A.*)
(?<=(?=(?<StateId>.{3}))\A.*)
(?<=(?=(?<StateId>.{4}))\A.*)
(?<=(?=(?<StateId>.{3}))\A.*)
(?<=(?=(?<StateId>.{5}))\A.*)
(?<StateId>)
)
)
|
(?:
(?<=(?=.{2}(?<=\A\k<StateId>))\A.*)
(?<-StateId>)
>
)
|
(?:
(?<=(?=.{3}(?<=\A\k<StateId>))\A.*)
(?<-StateId>)
(?:
(?<=(?=(?<StateId>.{3}))\A.*)
(?<=(?=(?<StateId>.{1}))\A.*)
|
)
)
|
(?:
(?<=(?=.{4}(?<=\A\k<StateId>))\A.*)
(?<-StateId>)
,?
)
|
(?:
(?<=(?=.{5}(?<=\A\k<StateId>))\A.*)
(?<-StateId>)
;
)
|
(?:
(?<=(?=.{6}(?<=\A\k<StateId>))\A.*)
(?<-StateId>)
\]
)
|\Z
){7}
)+
\Z
(?(StateId)(?!))
不幸的是,它太长了,无法放入URL中,因此没有在线示例。
如果我们使用只包含一个或零个字符的“二进制”栈,它应该是这样的:https://gist.github.com/kobi/8012361
以下是模式通过所有测试的屏幕截图:http://i.stack.imgur.com/IW2xr.png
额外奖励
.Net引擎不仅可以平衡 - 还可以捕获每个A
或Q
的实例。这需要对模式进行轻微修改:https://gist.github.com/kobi/8156968。
请注意,我们已向模式添加了组Start
、A
和Q
,但它们对流程没有影响,仅用于捕获。
结果:例如,对于字符串"[<a>b;<c,d>,<e,f>]"
,我们可以获得这些Capture
:
Group A
(0-17) [<a>b;<c,d>,<e,f>]
(1-4) <a>b
(2-2) a
(7-9) c,d
(13-15) e,f
Group Q
(0-17) [<a>b;<c,d>,<e,f>]
(1-3) <a>
(2-2) a
(4-4) b
(6-10) <c,d>
(7-7) c
(9-9) d
(12-16) <e,f>
(13-13) e
(15-15) f
开放性问题
- 每个语法都能转换为状态堆栈符号表示法吗?
- (状态数)×(输入长度)足以匹配所有单词吗?还有哪些公式可以使用?
源代码
用于生成这些模式和所有测试用例的代码可以在https://github.com/kobi/RecreationalRegex上找到。