Почему это рекурсивное регулярное выражение будет соответствовать только тогда, когда символ повторяется 2 ^ n - 1 раз?

После чтения polygenelubricants серии статей по передовым методам регулярных выражений (особенно Как это регулярное выражение Java обнаруживает палиндромы?), я решил попытаться создать собственное регулярное выражение PCRE для анализа палиндрома, используя рекурсию (в PHP).

То, что я придумал, было:

^(([a-z])(?1)\2|[a-z]?)$

Мое понимание этого выражения состоит в том, что оно должно либо соответствовать нулю, либо одному символу (каждая строка из менее чем двух символов неявно является палиндром, а также для учета палиндромов с нечетной длиной в рекурсии) или два из один и тот же символ, разделенный рекурсией шаблона.

К сожалению, похоже, что это не так, как вы можете видеть на www.ideone.com/a9T3F. Вместо этого только строки 2 n - 1 (т.е. Пустая строка, a, aaa, aaaaaaa, a 15) повторяющиеся символы соответствуют регулярному выражение.

Как ни странно, если я изменяю свой шаблон так, чтобы рекурсия была необязательной (т.е. ^(([a-z])(?1)?\2|[a-z]?)$, см. www.ideone.com/D6lJR, это только соответствует строкам с повторением символа 2 n (т.е. пустая строка, a, aa, aaaa, aaaaaaaa, a 16).

Почему мое регулярное выражение не работает так, как я ожидаю?

Примечание для людей, которые испытывают зуд, предлагая не использовать регулярное выражение:
В этом вопросе нужно научиться правильно использовать рекурсивные регулярные выражения. Я знаю, что это не эффективный способ определить, является ли строка палиндром, и я бы не использовал рекурсивное регулярное выражение, если мне почему-то пришлось определять палиндромы в производственном коде; Мне просто интересно узнать больше о расширенных аспектах регулярного выражения.

Ответ 1

Явление, которое вы наблюдаете, связано с тем, что рекурсия подрешетки PCRE является атомарной, в отличие от Perl. справочная страница действительно подробно описывает эту проблему:

В PCRE (например, Python, но в отличие от Perl), рекурсивный вызов подшаблона всегда рассматривается как атомная группа. То есть, как только он сопоставит некоторые из строка темы, она никогда не вводится повторно, даже если она содержит незавершённые альтернативы и последующий сбой.

Это может быть проиллюстрировано следующим шаблоном, который подразумевает совпадение с палиндромной строкой, содержащей нечетное число символов (например, "a", "aba", "abcba", "abcdcba"):

    ^(.|(.)(?1)\2)$

Идея состоит в том, что она либо соответствует одному символу, либо двум идентичным персонажей, окружающих суб-палиндром. В Perl эта модель работает; в PCRE это не так, если шаблон длиннее трех символов.

Рассмотрим строку темы "abcba":

На верхнем уровне первый символ сопоставляется, но поскольку он не находится на конец строки, первая альтернатива терпит неудачу; вторая альтернатива берется и рекурсия начинается. Рекурсивный вызов подшаблона 1 успешно соответствует следующему символу ("b"). (Заметим, что начало и тесты конца строки не являются частью рекурсии).

На верхнем уровне следующий символ ("c") сравнивается с тем, что подшаблон 2, который был "a". Это не удается. Потому что рекурсия рассматривается как атомная группа, теперь нет точек возврата, и поэтому весь матч терпит неудачу. (Perl может, на данный момент, введите рекурсию и попробуйте вторую альтернативу.) Однако, если шаблон написан с альтернативами в другом порядке, все разные:

    ^((.)(?1)\2|.)$

На этот раз рекурсивная альтернатива сначала проверяется и продолжается recurse, пока не закончится символ, после чего рекурсия выходит из строя. Но на этот раз у нас есть еще одна альтернатива, чтобы попытаться высший уровень. Это большая разница: в предыдущем случае оставшаяся альтернатива находится на более глубоком уровне рекурсии, который PCRE не может использовать.

Чтобы изменить шаблон так, чтобы он соответствовал всем палиндромным строкам, а не только с нечетным количеством символов, возникает соблазн изменить шаблон к этому:

    ^((.)(?1)\2|.?)$

Опять же, это работает в Perl, но не в PCRE, и по той же причине. Когда более глубокая рекурсия соответствует одному символу, это не может быть введенный снова, чтобы соответствовать пустой строке. Решение заключается в разделить два случая и выписать нечетные и четные случаи в качестве альтернатив на более высоком уровне:

    ^(?:((.)(?1)\2|)|((.)(?3)\4|.))$

ВНИМАНИЕ!!!

Шаблоны соответствия палиндрома выше работают только в том случае, если строка темы не начинается с палиндрома, который короче, чем целая строка. Например, хотя "abcba" правильно сопоставляется, если объект "ababa", PCRE находит палиндром "aba" в начале, то не выполняется на верхнем уровне, потому что конец строки не следует. Опять же, он не может вернуться в рекурсию, чтобы попробовать другие альтернативы, поэтому весь матч терпит неудачу.

Дополнительные ссылки

  • regular-expressions.info/Атомная группировка
    • (?>…) в некотором вкусе есть синтаксис атомной группировки
    • Оценки (?=…), (?!…), (?<=…), (?<!…), все атомы
    • Possessive квантификатор (например, a*+) также является атомарным
    • Рекурсивные вызовы подпрограммы PCRE и подпрограммы также являются атомарными

Более пристальный взгляд на шаблон

Аргумент атомарности правильный, но, возможно, неясно, как он объясняет, почему поведение шаблона ведет себя как наблюдаемое. Давайте поближе рассмотрим, как все это подходит:

Мы будем использовать первый шаблон:

^(([a-z])(?1)\2|[a-z]?)$

Я буду использовать следующие обозначения для обозначения рекурсии:

  • 1 означает, что символ был захвачен в группу 2 в первом альтернативном
  • 2 означает, что символ был сопоставлен вторым заместителем
    • Или, если 2 не находится над символом, используется опция повторения нуля ?
  • \ означает, что символ соответствовал обратной ссылке на группу 2 в первом альтернативном
  • _ обозначает нижнюю часть рекурсивной ветки
    • Эта ветка НЕ ​​будет возвращена, даже если есть другие альтернативы!

Теперь рассмотрим "aaa" как ввод:

      _
1 1 1 2 
a a a   # This is the first bottom of the recursion,
        # now we go back to the third 1 and try to match \.
        # This fails, so the third 1 becomes 2.
    _
1 1 2
a a a   # Now we go back to the second 1 and try to match \.
        # This fails, so the second 1 becomes 2.
  _
1 2
a a a   # The second level matched! now we go back to the first level...

_____
1 2 \
a a a   # Now the first 1 can match \, and entire pattern matches!!

Теперь рассмотрим "aaaaa":

          _
1 1 1 1 1 2
a a a a a  # Fifth 1 can't match \, so it becomes 2. 
        _
1 1 1 1 2
a a a a a  # Fourth 1 can't match \, so it becomes 2.
    _____
1 1 1 2 /
a a a a a  # Here a crucial point. The third 1 successfully matched.
           # Now we're back to the second 1 and try to match \, but this fails.
           # However, since PCRE recursion is atomic, the third 1 will NOT be
           # reentered to try 2. Instead, we try 2 on the second 1.
_____
1 2 \
a a a a a  # Anchors don't match, so the first 1 becomes 2, and then also the
           # anchors don't match, so the pattern fails to match.

Обратите внимание, что как только уровень рекурсии соответствует первому альтернативу, вторая альтернатива не будет предпринята в будущем (даже если это может привести к тому, что совпадение может совпадать), поскольку рекурсия подпапки PCRE является атомарной.


Теперь рассмотрим "aa":

    _
1 1 2 
a a
  _
1 2
a a  # The second level matched by taking the one repetition option on ?.
     # We now go back to the first level, and we can't match \.
     # Since PCRE recursion is atomic, we can't go back to the second level
     # to try the zero repetition option on ?.
_    
2
a a  # Anchors don't match, trying zero option on ? also doesn't help,
     # so the pattern fails to match!

Обратите внимание, что как только уровень рекурсии совпадает с одним повторением ? во втором альтернативе, опция повторения нуля не будет предпринята в будущем (даже если это может привести к тому, что совпадение может совпадать), поскольку PCRE подшаблонная рекурсия является атомарной.


Теперь рассмотрим aaaaaaa

              _
1 1 1 1 1 1 1 2  
a a a a a a a 
            _
1 1 1 1 1 1 2  
a a a a a a a 
        _____
1 1 1 1 1 2 \  
a a a a a a a  # A crucial point: the fifth level matched and now the fourth
               # level can't match \, but it does NOT reenter the fifth level to
               # try 2. Instead, the fourth level tries 2.
    _____    
1 1 1 2 \  
a a a a a a a 
  _________    
1 1 1 2 \ \ 
a a a a a a a 
_____________    
1 1 1 2 \ \ \
a a a a a a a  # Entire pattern is a match! 

Обратите внимание, что даже несмотря на то, что рекурсия подпрограммы PCRE является атомарной, она все же может успешно соответствовать палиндрому, состоящему из символа, повторяющего 2 n -1 раз.


Теперь, просто для удовольствия, попробуйте "abcba":

          _
1 1 1 1 1 2
a b c b a
        _
1 1 1 1 2
a b c b a

1 1 1 2 
a b c b a   # Third level attempts \, but c does not match a!
            # So we go back to third 1 and try 2.
  _____
1 1 2 \ 
a b c b a 
_________
1 1 2 \ \
a b c b a   # Entire pattern is a match!

То есть шаблон не просто соответствует "только тогда, когда символ повторяет 2 n -1 раз". Он действительно может соответствовать "abcba" (как видно на ideone.com). Однако он НЕ может совпадать с "ababa", а также не может соответствовать "aaaaa" (см. ПРЕДУПРЕЖДЕНИЕ на странице руководства!), Потому что рекурсия подкласса в PCRE является атомарной.

Вы можете применить этот же процесс трассировки, чтобы объяснить поведение шаблона на любом входе.

Ответ 2

Если вы хотите, чтобы полностью функциональное выражение PCRE соответствовало палиндромам, вы можете использовать следующее:

/^.? (?.?.?() (= * (\ 1 ((2)\2)) $)) * +\2 $/