这种PCRE模式如何检测回文?

时间:2010-09-19 16:27:24

标签: php regex pcre palindrome nested-reference

  

这个问题是在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),但此处显示的是嵌套参考/正向前瞻模式问题可以。

1 个答案:

答案 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.