Ответ 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 $