Как этот PCRE паттерн обнаруживает палиндромы?
Этот вопрос является образовательной демонстрацией использования прогнозных данных, вложенных ссылок и условных выражений в шаблоне PCRE для сопоставления со всеми палиндромами, включая те, которые не могут быть сопоставлены с рекурсивным шаблоном, приведенным на справочной странице PCRE.
Изучите этот шаблон PCRE в фрагменте PHP:
$palindrome = '/(?x)
^
(?:
(.) (?=
.*
(
\1
(?(2) \2 | )
)
$
)
)*
.?
\2?
$
/';
Похоже, что этот паттерн обнаруживает палиндромы, как видно из этого теста ( см. Также на 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);
}
Так как же работает этот шаблон?
Заметки
В этом паттерне используется вложенная ссылка, аналогичная технике, используемой в разделе Как это регулярное выражение Java обнаруживает палиндромы?, но в отличие от этого шаблона Java, нет никакого взгляда назад (но он использует условное выражение).
Также обратите внимание, что справочная страница PCRE представляет рекурсивный шаблон для соответствия некоторым палиндромам:
# the recursive pattern to detect some palindromes from PCRE man page
^(?:((.)(?1)\2|)|((.)(?3)\4|.))$
Страница man предупреждает, что этот рекурсивный шаблон НЕ МОЖЕТ обнаружить все палиндромы (см.: Почему это рекурсивное регулярное выражение совпадает только тогда, когда символ повторяется 2n - 1 раз?, А также на ideone.com), но представлен вложенный шаблон ссылки / позитивного просмотра. в этом вопросе можно.
2 ответа
Давайте попробуем понять регулярное выражение, построив его. Во-первых, палиндром должен начинаться и заканчиваться одинаковой последовательностью символов в противоположном направлении:
^(.)(.)(.) ... \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|))$)
)*
Теперь это соответствует первой половине палиндрома. Как насчет второй половины? Ну, после того, как 1-я половина соответствует, записанный захват \2
будет содержать вторую половину. Итак, давайте просто положим это в конце.
^(?:
(.)(?=.*(\1(?(2)\2|))$)
)*\2$
Мы хотим позаботиться и о палиндроме нечетной длины. Между 1-й и 2-й половиной будет свободный персонаж.
^(?:
(.)(?=.*(\1(?(2)\2|))$)
)*.?\2$
Это работает хорошо, за исключением одного случая, когда есть только 1 символ. Это опять из-за \2
ничего не соответствует. Так
^(?:
(.)(?=.*(\1(?(2)\2|))$)
)*.?\2?$
# ^ since \2 must be at the end in the look-ahead anyway.
Я хочу предложить свое собственное решение. Это регулярное выражение, которое я написал некоторое время назад для решения сопоставления палиндромов с помощью PCRE / PCRE2.
^((\w)(((\w)(?5)\5?)*|(?1)|\w?)\2)$