Как мы можем сопоставить a ^ n b ^ n с Java regex?

Это вторая часть серии учебных статей регулярных выражений.Он показывает, как lookaheads и вложенные ссылки могут использоваться для соответствия non- регулярному языку a n b n.Вложенные ссылки сначала вводятся в: Как это регулярное выражение находит треугольные числа?

Одним из архетипических non- обычных языков является:

L = { a nb n: n > 0 }

Это язык всех пустых строк non-, состоящих из некоторого числа a за которым следует равное количество b. Примерами строк на этом языке являются ab, aabb, aaabbb.

Этот язык может показаться non- регулярным по лемме о перекачке. Это фактически архетипический контекстно-свободный язык, который может быть сгенерирован контекстно-свободной грамматикой S → aSb | ab S → aSb | ab.

Тем не менее, современные реализаторы регулярных выражений четко распознают больше, чем просто обычные языки. То есть, они не являются "регулярными" по определению формальной теории языка. PCRE и Perl поддерживают рекурсивное регулярное выражение, а.NET поддерживает определение балансирующих групп. Еще менее "причудливые" функции, например, обратное сопоставление, означает, что регулярное выражение не является регулярным.

Но насколько сильны эти "основные" функции? Можем ли мы распознать L с помощью Java regex, например? Можем ли мы объединить образы и вложенные ссылки и иметь шаблон, который работает, например, с String.matches для соответствия строкам типа ab, aabb, aaabbb и т.д.?

Рекомендации

Связанные вопросы

Ответы

Ответ 1

Ответ, само собой разумеется, ДА! Вы, безусловно, можете написать шаблон регулярного выражения Java, соответствующий n b n. Он использует положительный прогноз для утверждения и одну вложенную ссылку для "подсчета".

Вместо того, чтобы сразу выдавать шаблон, этот ответ будет направлять читателей через процесс его получения. При построении решения создаются различные подсказки. В этом аспекте, надеюсь, этот ответ будет содержать гораздо больше, чем просто еще один аккуратный шаблон регулярного выражения. Надеюсь, читатели также узнают, как "думать в регулярном выражении", и как гармонично сочетать различные конструкции, чтобы они могли в будущем создавать больше шаблонов.

Язык, используемый для разработки решения, будет PHP для его краткости. Окончательный тест после завершения шаблона будет выполнен на Java.


Шаг 1: Lookahead для утверждения

Начнем с более простой проблемы: мы хотим сопоставить a+ в начале строки, но только если она сразу же следует за b+. Мы можем использовать ^ to anchor наш матч, и поскольку мы хотим только соответствовать a+ без b+, мы может использовать lookahead утверждение (?=…).

Вот наш шаблон с простой тестовой жгутом:

function testAll($r, $tests) {
   foreach ($tests as $test) {
      $isMatch = preg_match($r, $test, $groups);
      $groupsJoined = join('|', $groups);
      print("$test $isMatch $groupsJoined\n");
   }
}

$tests = array('aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb');

$r1 = '/^a+(?=b+)/';
#          └────┘
#         lookahead

testAll($r1, $tests);

Вывод: (как видно на ideone.com):

aaa 0
aaab 1 aaa
aaaxb 0
xaaab 0
b 0
abbb 1 a

Это именно тот результат, который мы хотим: мы сопоставляем a+, только если он находится в начале строки и только если он сразу же следует за b+.

Урок. Вы можете использовать шаблоны в поиске, чтобы делать утверждения.


Шаг 2: Захват в режиме просмотра (и f r e e - s p a c я n g mode)

Теперь скажем, что даже если мы не хотим, чтобы b+ был частью матча, мы хотим capture it в любом случае в группу 1. Кроме того, поскольку мы ожидаем иметь более сложный шаблон, позвольте использовать модификатор x для свободного интервала, чтобы мы могли сделать regex более читабельна.

Основываясь на нашем предыдущем фрагменте PHP, теперь у нас есть следующий шаблон:

$r2 = '/ ^ a+ (?= (b+) ) /x';
#             │   └──┘ │
#             │     1  │
#             └────────┘
#              lookahead

testAll($r2, $tests);

Вывод теперь (как видно на ideone.com):

aaa 0
aaab 1 aaa|b
aaaxb 0
xaaab 0
b 0
abbb 1 a|bbb

Обратите внимание, что, например, aaa|b является результатом join - для каждой группы, захваченной с помощью '|'. В этом случае группа 0 (то есть совпадение с образцом) захватила aaa, а группа 1 захватила b.

Урок. Вы можете захватить внутри поискового запроса. Вы можете использовать свободное пространство для повышения удобочитаемости.


Шаг 3: Рефакторинг lookahead в "loop"

Прежде чем мы сможем представить наш механизм подсчета, нам нужно сделать одну модификацию нашего шаблона. В настоящее время внешний вид находится за пределами цикла + повторения. Это пока прекрасно, потому что мы просто хотели утверждать, что существует b+ после нашего a+, но мы действительно хотим в конечном итоге утверждать, что для каждого a, который мы сопоставляем внутри цикла, есть соответствующий b, чтобы пойти с ним.

Давайте не будем беспокоиться о механизме подсчета на данный момент и просто выполните рефакторинг следующим образом:

  • Первый рефакторинг a+ - (?: a )+ (обратите внимание, что (?:…) - не захватывающая группа)
  • Затем переместите указатель мыши в эту группу, не связанную с захватом
    • Обратите внимание, что мы должны теперь "пропустить" a*, прежде чем мы сможем "увидеть" b+, поэтому соответствующим образом изменим шаблон

Итак, теперь мы имеем следующее:

$r3 = '/ ^ (?: a (?= a* (b+) ) )+ /x';
#          │     │      └──┘ │ │
#          │     │        1  │ │
#          │     └───────────┘ │
#          │       lookahead   │
#          └───────────────────┘
#           non-capturing group

Выход такой же, как и раньше (как видно на ideone.com), поэтому никаких изменений в этом отношении нет. Важно то, что теперь мы делаем утверждение на каждой итерации цикла +. С нашей нынешней моделью это не обязательно, но в следующий раз мы сделаем "подсчет" группы 1 для нас, используя саморекламу.

Урок. Вы можете выполнять захват внутри группы, не захваченной. Обзоры можно повторить.


Шаг 4: Это шаг, на котором мы начинаем считать

Вот что мы собираемся сделать: мы перепишем группу 1 таким образом, чтобы:

  • В конце первой итерации +, когда первый a соответствует, он должен зафиксировать b
  • В конце второй итерации, когда другой a сопоставляется, он должен захватить bb
  • В конце третьей итерации он должен зафиксировать bbb
  • ...
  • В конце n-й итерации группа 1 должна захватить b n
  • Если для записи в группу 1 недостаточно b, то утверждение просто терпит неудачу

Итак, группа 1, которая теперь (b+), должна быть переписана на что-то вроде (\1 b). То есть мы пытаемся "добавить" b к той группе, которая была записана в предыдущей итерации.

Здесь есть небольшая проблема в том, что в этом шаблоне отсутствует "базовый регистр", то есть случай, когда он может совпадать без самооценки. Требуется базовый регистр, потому что группа 1 запускает "неинициализированный"; он еще ничего не зафиксировал (даже пустую строку), поэтому попытка самосогласования всегда терпит неудачу.

Существует много способов обойти это, но теперь давайте просто сделаем совпадение optional, т.е. \1?. Это может или не может работать отлично, но пусть просто посмотрит, что это делает, и если возникнет какая-то проблема, мы перейдем через этот мост, когда мы придем к нему. Кроме того, мы добавим еще несколько тестовых примеров, пока мы на нем.

$tests = array(
  'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb'
);

$r4 = '/ ^ (?: a (?= a* (\1? b) ) )+ /x';
#          │     │      └─────┘ | │
#          │     │         1    | │
#          │     └──────────────┘ │
#          │         lookahead    │
#          └──────────────────────┘
#             non-capturing group

Вывод теперь (как видно на ideone.com):

aaa 0
aaab 1 aaa|b        # (*gasp!*)
aaaxb 0
xaaab 0
b 0
abbb 1 a|b          # yes!
aabb 1 aa|bb        # YES!!
aaabbbbb 1 aaa|bbb  # YESS!!!
aaaaabbb 1 aaaaa|bb # NOOOOOoooooo....

А-ха! Похоже, мы действительно близки к решению! Нам удалось получить группу 1 для "подсчета" с использованием саморекламы! Но подождите... что-то не так со вторым и последним тестовым случаем!! Недостаточно b s, и почему-то это считается неправильным! Мы рассмотрим, почему это произошло на следующем шаге.

Урок. Один из способов "инициализации" группы саморегуляции состоит в том, чтобы сделать привязку самооценки опциональной.


Шаг 4½: Понимание того, что пошло не так

Проблема заключается в том, что, поскольку мы сделали опцию самореференции опциональной, "счетчик" может "reset" вернуться к 0, когда недостаточно b. Давайте внимательно рассмотрим, что происходит на каждой итерации нашего шаблона с aaaaabbb в качестве входных данных.

 a a a a a b b b
↑
# Initial state: Group 1 is "uninitialized".
           _
 a a a a a b b b
  ↑
  # 1st iteration: Group 1 couldn't match \1 since it was "uninitialized",
  #                  so it matched and captured just b
           ___
 a a a a a b b b
    ↑
    # 2nd iteration: Group 1 matched \1b and captured bb
           _____
 a a a a a b b b
      ↑
      # 3rd iteration: Group 1 matched \1b and captured bbb
           _
 a a a a a b b b
        ↑
        # 4th iteration: Group 1 could still match \1, but not \1b,
        #  (!!!)           so it matched and captured just b
           ___
 a a a a a b b b
          ↑
          # 5th iteration: Group 1 matched \1b and captured bb
          #
          # No more a, + "loop" terminates

А-ха! На нашей 4-й итерации мы все равно могли бы соответствовать \1, но мы не смогли бы сопоставить \1b! Поскольку мы разрешаем совпадение самооценки быть необязательным с \1?, двигатель отступает и принимает параметр "нет благодарности", который затем позволяет нам сопоставлять и захватывать только b!

Однако обратите внимание, что, кроме самой первой итерации, вы всегда можете совместить только саморегуляцию \1. Это очевидно, конечно, так как это то, что мы только что зафиксировали на нашей предыдущей итерации, и в нашей настройке мы всегда можем повторить его снова (например, если мы захватили bbb в прошлый раз, мы гарантируем, что все еще будет bbb, но на этот раз может быть или не быть bbbb.

Урок: Остерегайтесь возврата. Механизм регулярных выражений будет делать столько же отрезков, сколько разрешено до тех пор, пока данный шаблон не совпадёт. Это может повлиять на производительность (т.е. катастрофическое обратное отслеживание) и/или правильность.


Шаг 5: Самообладание на помощь!

"Исправить" теперь должно быть очевидно: объединить необязательное повторение с possessive quantifier. То есть вместо простого ? вместо этого используйте ?+ (помните, что повторение, которое определяется количественно как притяжательное, не отступает, даже если такое "сотрудничество" может привести к совпадению общего шаблона).

В очень неофициальных терминах это то, что ?+, ? и ?? говорит:

?+

  • (необязательно) "Это не обязательно должно быть" ,
    • (притяжательный) ", но если он есть, вы должны принять его и не отпустить!"

?

  • (необязательно) "Это не обязательно должно быть" ,
    • (жадный) ", но если это вы можете взять сейчас,"
      • (backtracking) ", но вы можете попросить его отпустить позже!"

??

  • (необязательно) "Это не обязательно должно быть" ,
    • (неохотно) ", и даже если вам не нужно это делать,
      • (backtracking)", но вас могут попросить взять его позже!"

В нашей настройке \1 не будет там в первый раз, но он всегда будет там в любое время после этого, и мы всегда хотим его сопоставить. Таким образом, \1?+ выполнит именно то, что мы хотим.

$r5 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ /x';
#          │     │      └──────┘ │ │
#          │     │          1    │ │
#          │     └───────────────┘ │
#          │         lookahead     │
#          └───────────────────────┘
#             non-capturing group

Теперь вывод (как видно на ideone.com):

aaa 0
aaab 1 a|b          # Yay! Fixed!
aaaxb 0
xaaab 0
b 0
abbb 1 a|b
aabb 1 aa|bb
aaabbbbb 1 aaa|bbb
aaaaabbb 1 aaa|bbb  # Hurrahh!!!

Вуаля!!! Задача решена!!! Теперь мы рассчитываем правильно, точно так, как мы хотим!

Урок. Узнайте разницу между жадным, неохотным и притяжательным повторением. Необязательно-притяжательная может быть мощной комбинацией.


Шаг 6: Финишные касания

Итак, теперь у нас есть шаблон, который соответствует a несколько раз, и для каждого a, который был сопоставлен, есть соответствующий b, захваченный в группе 1. + завершается, когда нет more a, или если утверждение не выполнено, потому что для a не существует соответствующего b.

Чтобы закончить работу, нам просто нужно добавить к нашему шаблону \1 $. Это теперь обратная ссылка на то, что соответствует группе 1, а затем конец привязки линии. Якорь гарантирует, что в строке нет никаких дополнительных b; другими словами, на самом деле мы имеем a n b n.

Здесь завершен шаблон с дополнительными тестовыми примерами, в том числе длиной 10 000 символов:

$tests = array(
  'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb',
  '', 'ab', 'abb', 'aab', 'aaaabb', 'aaabbb', 'bbbaaa', 'ababab', 'abc',
  str_repeat('a', 5000).str_repeat('b', 5000)
);

$r6 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ \1 $ /x';
#          │     │      └──────┘ │ │
#          │     │          1    │ │
#          │     └───────────────┘ │
#          │         lookahead     │
#          └───────────────────────┘
#             non-capturing group

Он находит 4 совпадения: ab, aabb, aaabbb и a 5000 b 5000. Требуется всего 0.06s для запуска на ideone.com.


Шаг 7: Тест Java

Итак, шаблон работает в PHP, но конечной целью является создание шаблона, который работает на Java.

public static void main(String[] args) {

        String aNbN = "(?x) (?:  a  (?= a* (\\1?+ b))  )+ \\1";
        String[] tests = {
                "",      // false
                "ab",    // true
                "abb",   // false
                "aab",   // false
                "aabb",  // true
                "abab",  // false
                "abc",   // false
                repeat('a', 5000) + repeat('b', 4999), // false
                repeat('a', 5000) + repeat('b', 5000), // true
                repeat('a', 5000) + repeat('b', 5001), // false
        };
        for (String test : tests) {
                System.out.printf("[%s]%n  %s%n%n", test, test.matches(aNbN));
        }

}

static String repeat(char ch, int n) {
        return new String(new char[n]).replace('\0', ch);
}

Шаблон работает как ожидалось (как видно на ideone.com).


И теперь мы приходим к выводу...

Нужно сказать, что a* в представлении, а в самом деле и в "главном цикле +", оба допускают обратное отслеживание. Читателям рекомендуется подтвердить, почему это не проблема с точки зрения правильности и почему в то же время создание обоих притяжательных также будет работать (хотя, возможно, смешение обязательного и необязательного обладателя квантификатора в одной и той же схеме может привести к неправильным представлениям).

Следует также сказать, что, хотя он опрятен тем, что существует шаблон регулярного выражения, который будет соответствовать n b n это не всегда является "лучшим" решением в практика. Гораздо лучшее решение - просто сопоставить ^(a+)(b+)$, а затем сравнить длину строк, захваченных группами 1 и 2 на языке программирования хостинга.

В PHP это может выглядеть примерно так (как видно на ideone.com):

function is_anbn($s) {
   return (preg_match('/^(a+)(b+)$/', $s, $groups)) &&
      (strlen($groups[1]) == strlen($groups[2]));
}

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

Как упоминалось выше, хотя эта статья обязательно помечена [regex] для stackoverflow, это, возможно, более того. Несмотря на то, что здесь важно знать об утверждениях, вложенных ссылках, притяжательном квантере и т.д., Возможно, более важным уроком здесь является творческий процесс, с помощью которого можно попытаться решить проблемы, решительность и напряженную работу, которые она часто требует, когда вы подвергаетесь различные ограничения, систематический состав из различных частей для создания рабочего решения и т.д.


Бонусный материал! Рекурсивный шаблон PCRE!

Поскольку мы открыли PHP, нужно сказать, что PCRE поддерживает рекурсивный шаблон и подпрограммы. Таким образом, следующий шаблон работает для preg_match (как видно на ideone.com):

$rRecursive = '/ ^ (a (?1)? b) $ /x';

В настоящее время Java regex не поддерживает рекурсивный шаблон.


Еще больше бонусных материалов! Соответствие a n b n c n!!

Итак, мы видели, как сопоставить n b n который является нерегулярным, но все же не содержит контекста, но можем ли мы также сопоставить n b n c n что даже не является контекстно-свободным?

Ответ, конечно, ДА! Читателям предлагается попытаться решить это самостоятельно, но решение предоставляется ниже (с реализация в Java на ideone.com).

^ (?: a (?= a* (\1?+ b) b* (\2?+ c) ) )+ \1 \2 $

Ответ 2

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

/^(a(?1)?b)$/

Ответ 3

Как упоминалось в вопросе - с балансирующей группой .NET, шаблоны типа a n b n c n d n & hellip; z n легко сопоставляется как

^
  (?<A>a)+
  (?<B-A>b)+  (?(A)(?!))
  (?<C-B>c)+  (?(B)(?!))
  ...
  (?<Z-Y>z)+  (?(Y)(?!))
$

Например: http://www.ideone.com/usuOE


Edit:

Существует также шаблон PCRE для обобщенного языка с рекурсивным шаблоном, но необходим внешний вид. Я не думаю, что это прямой перевод выше.

^
  (?=(a(?-1)?b))  a+
  (?=(b(?-1)?c))  b+
  ...
  (?=(x(?-1)?y))  x+
     (y(?-1)?z)
$

Например: http://www.ideone.com/9gUwF