这个问题是在PCRE模式中使用前瞻,嵌套引用和条件来匹配所有回文的教育演示,包括那些无法通过递归模式匹配的回文。 PCRE手册页。
在PHP代码段中检查此PCRE模式:
$palindrome = '/(?x)
^
(?:
(.) (?=
.*
(
\1
(?(2) \2 | )
)
$
)
)*
.?
\2?
$
/';
这种模式似乎可以检测到回文,如本测试案例(see also on ideone.com)所示:
$tests = array(
# palindromes
'',
'a',
'aa',
'aaa',
'aba',
'aaaa',
'abba',
'aaaaa',
'abcba',
'ababa',
# non-palindromes
'aab',
'abab',
'xyz',
);
foreach ($tests as $test) {
echo sprintf("%s '%s'\n", preg_match($palindrome, $test), $test);
}
那么这种模式如何运作?
此模式使用nested reference,这是How does this Java regex detect palindromes?中使用的类似技术,但与Java模式不同,它没有后瞻(但确实使用conditional)。
另外,请注意,PCRE man page提供了一个递归模式来匹配一些回文:
# the recursive pattern to detect some palindromes from PCRE man page
^(?:((.)(?1)\2|)|((.)(?3)\4|.))$
该手册页警告此递归模式无法检测到所有回文(请参阅: Why will this recursive regex only match when a character repeats 2n - 1 times? 和also on ideone.com),但此处显示的是嵌套参考/正向前瞻模式问题可以。
答案 0 :(得分:25)
让我们尝试通过构造它来理解正则表达式。首先,回文必须在相反的方向上以相同的字符序列开始和结束:
^(.)(.)(.) ... \3\2\1$
我们想要重写这一点,...
之后只有有限长度的模式,因此我们可以将其转换为*
。这可以通过前瞻来实现:
^(.)(?=.*\1$)
(.)(?=.*\2\1$)
(.)(?=.*\3\2\1$) ...
但仍有不常见的部分。如果我们能够“记录”以前捕获的群组怎么办?如果有可能,我们可以将其重写为:
^(.)(?=.*(?<record>\1\k<record>)$) # \1 = \1 + (empty)
(.)(?=.*(?<record>\2\k<record>)$) # \2\1 = \2 + \1
(.)(?=.*(?<record>\3\k<record>)$) # \3\2\1 = \3 + \2\1
...
可以转换为
^(?:
(.)(?=.*(\1\2)$)
)*
几乎不错,除了\2
(记录的捕获)最初不为空。它将无法匹配任何东西。如果记录的捕获不存在,我们需要它匹配空。这就是条件表达式的用法。
(?(2)\2|) # matches \2 if it exist, empty otherwise.
所以我们的表达成为
^(?:
(.)(?=.*(\1(?(2)\2|))$)
)*
现在它匹配回文的上半部分。下半场怎么样?好吧,在上半场匹配后,记录的捕获\2
将包含下半场。所以,让我们把它放在最后。
^(?:
(.)(?=.*(\1(?(2)\2|))$)
)*\2$
我们也想照顾奇长的回文。在第一和第二半之间会有一个自由的角色。
^(?:
(.)(?=.*(\1(?(2)\2|))$)
)*.?\2$
在一种情况下,除了之外的效果很好 - 当只有一个字符时。这也是由于\2
没有匹配。所以
^(?:
(.)(?=.*(\1(?(2)\2|))$)
)*.?\2?$
# ^ since \2 must be at the end in the look-ahead anyway.