Наглядно ли это влияет на то, какие языки могут быть сопоставлены регулярными выражениями?
В современных механизмах регулярных выражений есть некоторые функции, которые позволяют вам сопоставлять языки, которые невозможно сопоставить без этой функции. Например, следующее регулярное выражение, использующее обратные ссылки, соответствует языку всех строк, которые состоят из слова, которое повторяется: (.+)\1
. Этот язык не является регулярным и не может соответствовать регулярному выражению, которое не использует обратные ссылки.
Может ли поиск ссылок повлиять на то, какие языки могут быть сопоставлены регулярным выражением? То есть есть ли какие-либо языки, которые могут быть сопоставлены с использованием lookaround, которые нельзя было бы сопоставить иначе? Если да, то это верно для всех вкусов поиска (отрицательный или положительный взгляд или lookbehind) или только для некоторых из них?
Ответы
Ответ 1
Как утверждают другие ответы, образы не добавляют дополнительной мощности регулярным выражениям.
Я думаю, мы можем показать это, используя следующее:
One Pebble 2-NFA (см. раздел "Введение" в документе, который ссылается на него).
1-pebble 2NFA не имеет дело с вложенными lookaheads, но мы можем использовать вариант multi-pebble 2NFA (см. раздел ниже).
Введение
A 2-NFA - это недетерминированный конечный автомат, который имеет возможность перемещать либо левое, либо правое на нем.
Одна камешковая машина - это то место, где машина может разместить галерею на входной ленте (т.е. отметить конкретный входной символ галькой) и, возможно, разные переходы на основе того, есть ли галька в текущей позиции ввода или нет.
Известно, что One Pebble 2-NFA имеет ту же мощность, что и обычный DFA.
Не вложенные Lookaheads
Основная идея заключается в следующем:
2NFA позволяет нам отступать (или "переднюю дорожку" ), перемещаясь вперед или назад во входной ленте. Таким образом, для просмотра мы можем выполнить соответствие регулярному выражению lookahead, а затем отменить то, что мы потребляем, в соответствии с выражением lookahead. Чтобы точно знать, когда остановить откат, мы используем гальку! Мы бросаем гальку, прежде чем мы войдем в dfa для просмотра, чтобы отметить место, где нужно остановить обратный путь.
Таким образом, в конце выполнения нашей строки через галерею 2NFA мы знаем, соответствовали ли мы выражению lookahead или нет, а входной слева (то есть то, что осталось использовать) именно то, что требуется для соответствия оставшимся.
Итак, для вида вида u (? = v) w
Имеем DFA для u, v и w.
Из принимающего состояния (да, мы можем предположить, что существует только один) DFA для u, мы делаем e-переход в начальное состояние v, отмечая вход с галькой.
Из принимающего состояния для v мы e-transtion в состояние, которое продолжает перемещать входной сигнал слева, пока не найдет камешек, а затем перейдет в состояние начала w.
Из отклоняющего состояния v мы е-переход в состояние, которое продолжает двигаться влево до тех пор, пока оно не найдет гальку, и переходы к принимающему состоянию u (то есть, где мы остановились).
Доказательство, используемое для регулярных NFA, чтобы показать r1 | r2, или r * и т.д., переносятся для этих гальки 2nfas. См. http://www.coli.uni-saarland.de/projects/milca/courses/coal/html/node41.html#regularlanguages.sec.regexptofsa для получения дополнительной информации о том, как компонованные машины объединяются, чтобы дать большую машину для выражения r * и т.д.
Причина, по которой вышеприведенные доказательства для работы r * и т.д. заключается в том, что обратное отслеживание гарантирует, что указатель ввода всегда находится в нужном месте, когда мы вводим компонент nfas для повторения. Кроме того, если галька используется, то она обрабатывается одним из компонентов компонентных машин. Так как нет переходов с машиной взгляда на смотровую машину без полного возврата и возврата назад, одна галька - это все, что нужно.
Например, рассмотрим ([^ a] | a (? =... b)) *
и строка abbb.
У нас есть abbb, который проходит через peb2nfa для a (? =... b), в конце которого мы находимся в состоянии: (bbb, согласовано) (т.е. на входе bbb остается, и он соответствует 'a', за которым следует '..b'). Теперь из-за * мы вернемся к началу (см. Конструкцию в ссылке выше) и введите dfa для [^ a]. Сопоставьте b, вернитесь к началу, дважды введите [^ a] два раза, а затем примите.
Работа с вложенными Lookaheads
Чтобы обрабатывать вложенные образы, мы можем использовать ограниченную версию k-pebble 2NFA, как определено здесь: Результаты сложности для двухсторонних и многопериодных автоматов и их Логика (см. Определение 4.1 и теорема 4.2).
В общем случае два галька-автомата могут принимать нерегулярные множества, но со следующими ограничениями можно показать, что автомашины k-pebble являются регулярными (теорема 4.2 в приведенной выше статье).
Если гальки P_1, P_2,..., P_K
-
P_ {i + 1} не может быть помещен, если P_i уже на ленте и P_ {i} не может быть поднят, если P_ {i + 1} не находится на ленте. В основном гальки должны использоваться в режиме LIFO.
-
Между временем размещения P_ {i + 1} и временем, когда либо P_ {i} выбрано, либо P_ {i + 2}, автомат может пересекать только подслово, расположенное между текущим местоположение P_ {i} и конец входного слова, лежащего в направлении P_ {i + 1}. Более того, в этом подслове автомат может действовать только как 1-галечный автомат с Pebble P_ {i + 1}. В частности, не разрешается поднимать, размещать или даже ощущать присутствие другого гальки.
Итак, если v является вложенным выражением глубины k, то (? = v) является вложенным выражением глубины k + 1. Когда мы входим в поисковую машину внутри, мы точно знаем, сколько гальки нужно было разместить до сих пор, и поэтому можно точно определить, какой камешек разместить, и когда мы выходим из этой машины, мы знаем, какой галька поднять. Все машины на глубине t вводятся путем размещения гальки t и выхода (то есть мы возвращаемся к обработке машины t-1 глубины) путем удаления гальки t. Любой запуск всей машины выглядит как рекурсивный вызов dfs дерева, и указанные выше ограничения на многоуровневую машину можно обслуживать.
Теперь, когда вы комбинируете выражения, для rr1, поскольку вы concat, числа гальки r1 должны быть увеличены на глубину r. Для r * и r | r1 нумерация гальки остается той же.
Таким образом, любое выражение с lookaheads может быть преобразовано в эквивалентную машину с несколькими шагами с указанными ограничениями в размещении гальки и поэтому является регулярным.
Заключение
Это в основном устраняет недостаток оригинального доказательства Фрэнсиса: возможность предотвратить выражение выражений в выражениях, которые нужны для будущих матчей.
Поскольку Lookbehinds - это просто конечная строка (не очень регулярные выражения), мы можем сначала разобраться с ними, а затем обработать взгляды.
Извините за неполную запись, но полное доказательство потребует рисования большого количества цифр.
Это выглядит правильно, но я буду рад узнать о любых ошибках (которые, как мне кажется, увлекаются: -)).
Ответ 2
Ответ на вопрос, который вы задаете, является ли более высокий класс языков, чем обычные языки, распознаваться с регулярными выражениями, дополненными lookaround, - нет.
Доказательство относительно просто, но алгоритм для перевода регулярного выражения, содержащего образы в один из них, является беспорядочным.
Во-первых: обратите внимание, что вы всегда можете отрицать регулярное выражение (над конечным алфавитом). Учитывая конечный автомат состояния, который распознает язык, сгенерированный выражением, вы можете просто обменивать все принимающие состояния для не принимающих состояний на получение FSA, который точно распознает отрицание этого языка, для которого существует семейство эквивалентных регулярных выражений,
Второе: поскольку регулярные языки (и, следовательно, регулярные выражения) закрыты при отрицании, они также замкнуты относительно пересечения, так как A пересекает B = neg (neg (A) union neg (B)) по законам Моргана. Другими словами, учитывая два регулярных выражения, вы можете найти другое регулярное выражение, которое соответствует обоим.
Это позволяет имитировать отображаемые выражения. Например, u (? = V) w соответствует только выражениям, которые будут соответствовать uv и uw.
Для отрицательного обзора вам нужно регулярное выражение, эквивалентное теореме множеств A\B, которая является просто пересечением A (neg B) или эквивалентно neg (neg (A) union B). Таким образом, для любых регулярных выражений r и s вы можете найти регулярное выражение r-s, которое соответствует тем выражениям, которые соответствуют r, которые не соответствуют s. В отрицательных выражениях: u (?! V) w соответствует только тем выражениям, которые соответствуют uw - uv.
Есть две причины, почему поиск полезен.
Во-первых, потому что отрицание регулярного выражения может привести к чему-то гораздо менее аккуратному. Например q(?!u)=q($|[^u])
.
Во-вторых, регулярные выражения выполняют больше, чем соответствуют выражениям, они также потребляют символы из строки - или, по крайней мере, то, как нам нравится думать о них. Например, в python я забочусь о .start() и .end(), поэтому, конечно:
>>> re.search('q($|[^u])', 'Iraq!').end()
5
>>> re.search('q(?!u)', 'Iraq!').end()
4
В-третьих, и я думаю, что это довольно важная причина, отрицание регулярных выражений отлично не подтягивается над конкатенацией. neg (a) neg (b) - это не то же самое, что neg (ab), а это означает, что вы не можете перевести поиск из контекста, в котором вы его нашли, - вам нужно обработать всю строку. Я полагаю, что людям неприятно работать и разрывает интуицию людей относительно регулярных выражений.
Надеюсь, я ответил на ваш теоретический вопрос (его поздно вечером, так что простите меня, если я не понял). Я согласен с комментатором, который сказал, что у этого есть практические приложения. Я встретил очень ту же проблему, пытаясь очистить некоторые очень сложные веб-страницы.
ИЗМЕНИТЬ
Мои извинения за неясность: я не верю, что вы можете дать доказательство регулярности регулярных выражений + образы структурной индукцией, мой u (?! v) w example должен был быть именно этим, примером и легкий в этом. Причина, по которой структурная индукция не будет работать, заключается в том, что образы ведут себя не-композиционным способом - точкой, которую я пытался сделать с отрицаниями выше. Я подозреваю, что у любого прямого формального доказательства будет много грязных деталей. Я попытался придумать простой способ показать это, но не могу придумать одну из них на моей голове.
Чтобы проиллюстрировать с использованием первого примера Josh из ^([^a]|(?=..b))*$
, это эквивалентно 7-му состоянию DFSA со всеми состояниями, принимающими:
A - (a) -> B - (a) -> C --- (a) --------> D
Λ | \ |
| (not a) \ (b)
| | \ |
| v \ v
(b) E - (a) -> F \-(not(a)--> G
| <- (b) - / |
| | |
| (not a) |
| | |
| v |
\--------- H <-------------------(b)-----/
Регулярное выражение для состояния A выглядит следующим образом:
^(a([^a](ab)*[^a]|a(ab|[^a])*b)b)*$
Другими словами, любое регулярное выражение, которое вы получите, устраняя образы, в общем случае будет намного длиннее и намного беспорядочно.
Чтобы ответить на комментарий Джоша - да, я думаю, что самый прямой способ доказать эквивалентность - через FSA. То, что делает этот беспорядок, заключается в том, что обычный способ построения FSA осуществляется через недетерминированную машину - гораздо проще выразить u | v как просто машину, построенную из машин для u и v с переходом epsilon на два из них. Конечно, это эквивалентно детерминированной машине, но с риском экспоненциального раздутия состояний. В то время как отрицание гораздо проще делать с помощью детерминированной машины.
Общее доказательство будет включать в себя декартовое произведение двух машин и выбор тех состояний, которые вы хотите сохранить в каждой точке, которую хотите вставить. Пример выше иллюстрирует, что я имею в виду в некоторой степени.
Приношу свои извинения за то, что вы не поставляете конструкцию.
ДАЛЬНЕЙШЕЕ ИЗДАНИЕ:
Я нашел сообщение в блоге, в котором описывается алгоритм генерации DFA из регулярного выражения, дополненного поисковыми окнами. Он прост, потому что автор расширяет идею NFA-e с "помеченными переходами эпсилона" очевидным образом, а затем объясняет, как преобразовать такой автомат в DFA.
Я думал, что это будет способ сделать это, но я рад, что кто-то написал это. Мне было трудно придумать что-то такое аккуратное.
Ответ 3
Я согласен с другими сообщениями, которые выглядят регулярно (это означает, что он не добавляет фундаментальных возможностей для регулярных выражений), но у меня есть аргумент, который является более простой ИМО, чем другие, которые я видел.
Я покажу, что обратная связь является регулярной, обеспечивая конструкцию DFA. Язык является регулярным тогда и только тогда, когда он имеет DFA, который его распознает. Обратите внимание, что Perl фактически не использует DFA внутри (подробнее см. Эту статью: http://swtch.com/~rsc/regexp/regexp1.html), но мы строим DFA для целей доказательство.
Традиционным способом построения DFA для регулярного выражения является создание NFA с использованием алгоритма Томпсона. Учитывая два фрагмента регулярных выражений r1
и r2
, Алгоритм Томсона предоставляет конструкции для конкатенации (r1r2
), чередования (r1|r2
) и повторения (r1*
) регулярных выражений. Это позволяет вам по-разному создавать NFA, которые распознают исходное регулярное выражение. Для получения более подробной информации см. Статью выше.
Чтобы показать, что положительный и отрицательный внешний вид являются регулярными, я создам конструкцию для конкатенации регулярного выражения u
с положительным или отрицательным видом: (?=v)
или (?!v)
. Только конкатенация требует специального лечения; обычные конструкции чередования и повторения работают нормально.
Конструкция для u (? = v) и u (?! v) равна:
Другими словами, подключите каждое конечное состояние существующего NFA для u
как к состоянию принятия, так и к NFA для v
, но измените следующим образом. Функция f(v)
определяется как:
- Пусть
aa(v)
- функция на NFA v
, которая изменяет каждое принимающее состояние в "состояние анти-принятия". Состояние анти-принятия определяется как состояние, которое вызывает совпадение, если какой-либо путь через NFA заканчивается в этом состоянии для данной строки s
, даже если другой путь через v
для s
заканчивается на состояние принятия.
- Пусть
loop(v)
- функция на NFA v
, которая добавляет самопереход в любом состоянии принятия. Другими словами, как только путь ведет к состоянию принятия, этот путь может оставаться в состоянии принятия навсегда независимо от того, какой ввод следует.
- Для отрицательного просмотра
f(v) = aa(loop(v))
.
- Для положительного просмотра
f(v) = aa(neg(v))
.
Чтобы обеспечить интуитивный пример того, почему это работает, я буду использовать регулярное выражение (b|a(?:.b))+
, которое представляет собой слегка упрощенную версию регулярного выражения, которое я предложил в комментариях доказательства Фрэнсиса. Если мы используем мою конструкцию вместе с традиционными конструкциями Томпсона, мы получим:
e
- это эпсилонные переходы (переходы, которые могут быть приняты без потребления какого-либо ввода), а состояния анти-принятия помечены знаком X
. В левой половине графика вы видите представление (a|b)+
: любой a
или b
помещает граф в состояние accept, но также позволяет перейти к состоянию начала, чтобы мы могли сделать это снова. Но обратите внимание, что каждый раз, когда мы сопоставляем a
, мы также вводим правую половину графика, где мы находимся в состояниях анти-принятия, пока не сопоставим "any", а затем b
.
Это не традиционный NFA, потому что традиционные NFA не имеют анти-принимающих состояний. Однако мы можем использовать традиционный алгоритм NFA- > DFA, чтобы преобразовать его в традиционный DFA. Алгоритм работает, как обычно, где мы моделируем несколько прогонов NFA, заставляя наши состояния DFA соответствовать подмножествам состояний NFA, в которых мы могли бы быть. Один поворот в том, что мы немного увеличиваем правило для принятия решения о том, является ли состояние DFA принять (окончательное) состояние или нет. В традиционном алгоритме состояние DFA является условием принятия, если какое-либо из состояний NFA является условием принятия. Мы модифицируем это, чтобы сказать, что состояние DFA является принимающим состоянием тогда и только тогда, когда:
-
= 1 состояния NFA - это состояние принятия, а
- 0 состояния NFA являются состояниями анти-принятия.
Этот алгоритм даст нам DFA, который распознает регулярное выражение с помощью lookahead. Эрго, взгляд регулярный. Обратите внимание, что lookbehind требует отдельного доказательства.
Ответ 4
У меня такое ощущение, что здесь задаются два разных вопроса:
- Являются ли двигатели Regex, которые поддерживают "lookaround" больше
мощные, чем двигатели Regex, которые этого не делают?
- Есть ли "lookaround"
разрешить движок Regex с возможностью анализа языков, которые
более сложные, чем те, которые генерируются из Chomsky Type 3 - Regular grammar?
Ответ на первый вопрос в практическом смысле - это да. Lookaround даст движок Regex, который
использует эту функцию существенно больше мощности, чем та, которая этого не делает. Это потому что
он обеспечивает более богатый набор "якорей" для процесса сопоставления.
Lookaround позволяет вам определить полное Regex как возможную опорную точку (утверждение о нулевой ширине). Ты можешь
получите довольно хороший обзор возможностей этой функции здесь.
Lookaround, хотя и мощный, не поднимает двигатель Regex за пределы теоретического
пределы, установленные на нем грамматикой типа 3. Например, вы никогда не сможете надежно
проанализировать язык, основанный на Контекст Free - Тип 2 Грамматика с использованием механизма Regex
оборудованный смотровой площадкой. Двигатели Regex ограничены мощностью Конечная автоматизация состояния
и это принципиально ограничивает выразительность любого языка, который они могут анализировать до уровня грамматики Типа 3. Не важно
сколько "трюков" добавлено в ваш механизм Regex, языки, созданные с помощью Контекстная свободная грамматика
всегда будет оставаться вне его возможностей. Разбор контекстного контекста - для грамматики типа 2 требуется автоматизация сжимания, чтобы "запомнить", где она находится
рекурсивная языковая конструкция. Все, что требует рекурсивной оценки правил грамматики, не может быть проанализировано с использованием
Regex.
Подводя итог: Lookaround предоставляет некоторые практические преимущества для двигателей Regex, но не "изменяет игру" на
теоретический уровень.
ИЗМЕНИТЬ
Есть ли какая-то грамматика со сложностью где-то между Type 3 (Regular) и Type 2 (Context Free)?
Я считаю, что ответ отрицательный. Причина в том, что теоретический предел отсутствует
размещенный на размер NFA/DFA, необходимый для описания обычного языка. Он может стать сколь угодно большим
и поэтому нецелесообразно использовать (или указывать). Здесь полезны уловки, такие как "lookaround" . Oни
предоставить короткий механизм для указания того, что в противном случае привело бы к очень большому/сложному NFA/DFA
технические характеристики. Они не повышают выразительность
Регулярные языки, они просто делают их более практичными. Как только вы получите этот момент, он становится
ясно, что существует много "функций", которые могут быть добавлены в двигатели Regex, чтобы сделать их более
полезны в практическом смысле - но ничто не сделает их способными выйти за рамки
пределы регулярного языка.
Основное различие между языком Regular и Context Free заключается в том, что обычный язык
не содержит рекурсивных элементов. Чтобы оценить рекурсивный язык, вам нужно
Автоматизация Push Down
"помнить", где вы находитесь в рекурсии. NFA/DFA не складывает информацию о состоянии, поэтому не может
обрабатывать рекурсию. Поэтому, учитывая нерекурсивное определение языка, будет существовать NFA/DFA (но
не обязательно практическое выражение в выражении), чтобы описать его.