ГЛАВА 3
Регулярные выражения и языки
В этой главе вводится система записи «регулярных выражений». Такие выражения представляют собой еще один способ определения языков, рассмотренный вкратце в разделе 1.1.2. Регулярные выражения можно рассматривать также как «язык программирования» для описания некоторых важных приложений, например, программ текстового поиска или компонентов компилятора. Регулярные выражения тесно связаны с НКА и могут служить удобной альтернативой для описания программных компонентов.
Определив регулярные выражения, покажем, что они могут задавать регулярные, и только регулярные, языки. Обсудим также, каким образом регулярные выражения используются в различных системах программного обеспечения. Затем исследуем алгебраические законы для регулярных выражений. Эти законы во многом подобны алгебраическим законам арифметики, однако между алгебрами регулярных и арифметических выражений есть и существенные различия.
3.1. Регулярные выражения
Перейдем от «машинного» задания языков с помощью ДКА и НКА к алгебраическому описанию языков с помощью регулярных выражений. Установим, что регулярные выражения определяют точно те же языки, что и различные типы автоматов, а именно, регулярные языки. В то же время, в отличие от автоматов, регулярные выражения позволяют определять допустимые цепочки декларативным способом. Поэтому регулярные выражения используются в качестве входного языка во многих системах, обрабатывающих цепочки. Рассмотрим два примера.
- Команды поиска, например, команда grep операционной системы UNIX или аналогичные команды для поиска цепочек, которые можно встретить в Web-броузерах или системах форматирования текста. В таких системах регулярные выражения используются для описания шаблонов, которые пользователь ищет в файле. Различные поисковые системы преобразуют регулярное выражение либо в ДКА, либо в НКА и применяют этот автомат к файлу, в котором производится поиск.
- Генераторы лексических анализаторов, такие как Lex или Flex. Напомним, что лексический анализатор — это компонент компилятора, разбивающий исходную программу на логические единицы {лексемы), которые состоят из одного или нескольких символов и имеют определенный смысл. Примерами лексем являются ключевые слова (например while), идентификаторы (любая буква, за которой следует нуль или несколько букв и/или цифр) и такие знаки, как + или < = . Генератор лексических анализаторов получает формальные описания лексем, являющиеся по существу регулярными выражениями, и создает ДКА, который распознает, какая из лексем появляется на его входе.
3.1.1. Операторы регулярных выражений
Регулярные выражения обозначают (задают, или представляют) языки. В качестве простого примера рассмотрим регулярное выражение 01*+10*. Оно определяет язык всех цепочек, состоящих либо из одного нуля, за которым следует любое количество единиц, либо из одной единицы, за которой следует произвольное количество нулей. В данный момент мы не рассчитываем, что читатель знает, как интерпретируются регулярные выражения, поэтому наше утверждение о языке, задаваемом этим выражением, пока должно быть принято на веру. Чтобы понять, почему наша интерпретация заданного регулярного выражения правильна, необходимо определить все использованные в этом выражении символы, поэтому вначале познакомимся со следующими тремя операциями над языками, соответствующими операторам регулярных выражений[1].
- Объединение двух языков L и М, обозначаемое L\J М, — это множество цепочек, которые содержатся либо в L, либо в М, либо в обоих языках. Например, если L = {001, 10, 111} и М= {£, 001}, то LUМ= {£, 10, 001, 111}.
- Конкатенация языков L и М— это множество цепочек, которые можно образовать путем дописывания к любой цепочке из L любой цепочки из М. Выше в разделе 1.5.2 было дано определение конкатенации двух цепочек: результатом ее является запись одной цепочки вслед за другой. Конкатенация языков обозначается либо точкой, либо вообще никак не обозначается, хотя оператор конкатенации часто называют «точкой». Например, если L= {001, 10, 111} и М- {£,001}, то L.M, или просто LM, — это {001, 10, 111, 001001, 10001, 111001}. Первые три цепочки в LM— это цепочки из L, соединенные с £. Поскольку £ является единицей (нейтральным элементом) для операции конкатенации, результирующие цепочки будут такими же, как и цепочки из L. Последние же три цепочки в LM образованы путем соединения каждой цепочки из L со второй цепочкой из М, т.е. с 001. Например, 10 из L, соединенная с 001 из М, дает 10001 для LM.
- Итерация («звездочка», или замыкание Клини[2]) языка I обозначается L* и представляет собой множество всех тех цепочек, которые можно образовать путем конкатенации любого количества цепочек из L. При этом допускаются повторения, т.е. одна и та же цепочка из L может быть выбрана для конкатенации более одного раза. Например, если L = {0, 1}, то L* — это все цепочки, состоящие из нулей и единиц. Если L = {0, 11}, то в L’ входят цепочки из нулей и единиц, содержащие четное количество единиц, например, цепочки 011, 11110 или £, и не входят цепочки 01011 или 101. Более формально язык L* можно представить как бесконечное объединение Ueo L\ где L0 = {£},/,’=/, и L1 для i > 1 равен LL…L (конкатенация i копий L).
Пример 3.1. Поскольку идея итерации языка может показаться довольно сложной, рассмотрим несколько примеров. Для начала возьмем L= {0, 11}. L° = {£} независимо от языка L\ нулевая степень означает выбор нулевого количества цепочек из языка L. Ll = L, что означает выбор одной цепочки из L. Таким образом, первые два члена в разложении L* дают {е, 0, 11}.
Далее рассмотрим /Л Выберем две цепочки из L и, поскольку их можно выбирать с повторениями, получим четыре варианта, которые дают L2 = {00, 011, 110, 1111}. Аналогично, Z,[3] представляет собой множество цепочек, образованных троекратным выбором из двух цепочек языка L. Следовательно, ll имеет вид
{000, 0011,0110, 1100,01111, 11011, 11110, 111111}
Для вычисления L необходимо вычислить L’ для каждого i и объединить все эти языки. Язык Z,1 содержит 21 элементов. Хотя каждое множество V конечно, объединение бесконечного числа множеств L1 образует, как правило, бесконечный язык, что справедливо, в частности, и для нашего примера.
Пусть теперь L — множество всех цепочек, состоящих из нулей. Заметим, что такой язык бесконечен, в отличие от предыдущего примера, где был рассмотрен конечный язык. Однако нетрудно увидеть, что представляет собой L*. Как всегда, L0 = {£}, l) = L. L2 — это множество цепочек, которые можно образовать, если взять одну цепочку из нулей и соединить ее с другой цепочкой из нулей. В результате получим цепочку, также состоящую из нулей. Фактически, любую цепочку из нулей можно записать как конкатенацию двух цепочек из нулей (не забывайте, что £ — тоже «цепочка из нулей»; она всегда может быть одной из двух соединяемых цепочек). Следовательно, L2 = L. Аналогично, I? = L и так далее. Таким образом, бесконечное объединение L* = Z,0 U Ll U L2 U •■■ совпадает с L в том особом случае, когда язык L является множеством всех нулевых цепочек.
В качестве последнего примера рассмотрим 0 = {е}. Заметим, что 0°= {£}, тогда как 01 для любого / > 1 будет пустым множеством, поскольку мы не можем выбрать ни одной цепочки из пустого множества. Фактически, 0 является одним из всего двух языков, итерация которых не является бесконечным множеством. □
3.1.2. Построение регулярных выражений
Все алгебры начинаются с некоторых элементарных выражений. Обычно это константы и/или переменные. Применяя определенный набор операторов к этим элементарным выражениям и уже построенным выражениям, можно конструировать более сложные выражения. Обычно необходимо также иметь некоторые методы группирования операторов и операндов, например, с помощью скобок. К примеру, обычная арифметическая алгебра начинается с констант (целые и действительные числа) и переменных и позволяет нам строить более сложные выражения с помощью таких арифметических операторов, как + или х.
Использование оператора «звездочка»
Впервые оператор «звездочка» появился в разделе 1.5.2, где применялся к алфавиту, например Z . С помощью этого оператора образуются все цепочки, символы которых принадлежат алфавиту Z. Оператор итерации в значительной мере похож на «звездочку», хотя существует несколько тонких отличий в типах.
Предположим, что L — это язык, содержащий цепочки длины 1, и для каждого символа а из Е существует цепочка а в [.. Тогда, хотя L и £ «выглядят» одинаково, они принадлежат к различным типам: L представляет собой множество цепочек, а 2 — множество символов. С другой стороны, L* обозначает тот же язык, что и £*.
Алгебра регулярных выражений строится по такой же схеме: используются константы и переменные для обозначения языков и операторы для обозначения трех операций из раздела 3.1.1 — объединение, точка и звездочка. Регулярные выражения можно определить рекурсивно. В этом определении не только характеризуются правильные регулярные выражения, но и для каждого регулярного выражения Е описывается представленный им язык, который обозначается через L(E).
Базис. Базис состоит из трех частей.
- Константы £ и 0 являются регулярными выражениями, определяющими языки {£} и 0, соответственно, т.е. L(e) = {£} и Ц0) = 0.
- Если а — произвольный символ, то а — регулярное выражение, определяющее язык {а}, т.е. 1(a) = {а}. Заметим, что для записи выражения, соответствующего символу, используется жирный шрифт. Это соответствие, т.е. что а относится к а, должно быть очевидным.
- Переменная, обозначенная прописной курсивной буквой, например, L, представляет произвольный язык.
Индукция. Индуктивный шаг состоит из четырех частей, по одной для трех операторов и для введения скобок.
- Если Е и F— регулярные выражения, то E+F— регулярное выражение, определяющее объединение языков L(E) и L(F), т.е. L(E + F) = L(E) U L(F)-
- Если E и F — регулярные выражения, то EF — регулярное выражение, определяющее конкатенацию языков L(E) и L(F). Таким образом, L(EF) = L(E)L(F). Заметим, что для обозначения оператора конкатенации— как операции над языками, так и оператора в регулярном выражении — можно использовать точку. Например, регулярное выражение 0.1 означает то же, что и 01, и представляет язык {01}. Однако мы избегаем использовать точку в качестве оператора конкатенации в регулярных выражениях[4].
- Если Е— регулярное выражение, то Е*— регулярное выражение, определяющее итерацию языка Ь(Е). Таким образом, ЦБ’) = (ЦБ))’.
- Если Е — регулярное выражение, то (Е) — регулярное выражение, определяющее тот же язык L(E), что и выражение Е. Формально, L{{E)) = L(E).
Выражения и соответствующие языки
Строго говоря, регулярное выражение Е— это просто выражение, а не язык. Мы используем L(E) для обозначения языка, который соответствует Е. Однако довольно часто говорят «£», на самом деле подразумевая «/,(£)». Это соглашение используется в случаях, когда ясно, что речь идет о языке, а не о регулярном выражении.
Пример 3.2. Напишем регулярное выражение для множества цепочек из чередующихся нулей и единиц. Сначала построим регулярное выражение для языка, состоящего из одной-единственной цепочки 01. Затем используем оператор «звездочка» для того, чтобы построить выражение для всех цепочек вида 0101…01.
Базисное правило для регулярных выражений говорит, что 0 и 1 — это выражения, обозначающие языки {0} и {1}, соответственно. Если соединить эти два выражения, то получится регулярное выражение 01 для языка {01}. Как правило, если мы хотим написать выражение для языка, состоящего из одной цепочки w, то используем саму w как регулярное выражение. Заметим, что в таком регулярном выражении символы цепочки w обычно выделяют жирным шрифтом, но изменение шрифта предназначено лишь для того, чтобы отличить выражение от цепочки, и не должно восприниматься как что-то существенное.
Далее, для получения всех цепочек, состоящих из нуля или нескольких вхождений 01, используем регулярное выражение (01) . Заметим, что выражение 01 заключается в скобки, чтобы не путать его с выражением 01*. Цепочки языка 01* начинаются с 0, за которым следует любое количество 1. Причина такой интерпретации объясняется в разделе 3.1.3 и состоит в том, что операция «звездочка» имеет высший приоритет по сравнению с операцией «точка», и поэтому аргумент оператора итерации выбирается до выполнения любых конкатенаций.
Однако Ц(01)*)— не совсем тот язык, который нам нужен. Он включает только те цепочки из чередующихся нулей и единиц, которые начинаются с 0 и заканчиваются 1. Мы должны также учесть возможность того, что вначале стоит 1 и/или в конце 0. Одним из решений является построение еще трех регулярных выражений, описывающих три другие возможности. Итак, (10)* представляет те чередующиеся цепочки, которые начинаются символом 1 и заканчиваются символом 0, 0(10) можно использовать для цепочек, которые начинаются и заканчиваются символом 0, а 1(01) — для цепочек, которые и начинаются, и заканчиваются символом 1. Полностью это регулярное выражение имеет следующий вид.
(01)*+ (10)*+ 0(10)* + 1(01)*
Заметим, что оператор + используется для объединения тех четырех языков, которые вместе дают все цепочки, состоящие из чередующихся символов 0 и 1.
Однако существует еще одно решение, приводящее к регулярному выражению, которое имеет значительно отличающийся и к тому же более краткий вид. Снова начнем с выражения (01) . Можем добавить необязательную единицу в начале, если слева к этому выражению допишем выражение £+1. Аналогично, добавим необязательный 0 в конце с помощью конкатенации с выражением £ + 0. Например, используя свойства оператора +, получим, что
L(e + 1) = Де) U 1(1) ={£}{/} = {£, 1}.
Если мы допишем к этому языку любой другой язык L, то выбор цепочки е даст нам все цепочки из L, а выбрав 1, получим \м> для каждой цепочки w из L. Таким образом, совокупность цепочек из чередующихся нулей и единиц может быть представлена следующим выражением.
(£+ 1)(01)*(£+0)
Обратите внимание на то, что суммируемые выражения необходимо заключать в скобки, чтобы обеспечить правильную группировку операторов. □
3.1.3. Приоритеты регулярных операторов
Как и в других алгебрах, операторы регулярных выражений имеют определенные «приоритеты», т.е. операторы связываются со своими операндами в определенном порядке. Мы знакомы с понятием приоритетности для обычных арифметических выражений. Например, мы знаем, что в выражении ху + z умножение ху выполняется перед сложением, так что это выражение эквивалентно выражению со скобками (ху) + z, а не х(у + z). Аналогично, в арифметике мы группируем одинаковые операторы слева направо, поэтому x—y — z эквивалентно выражению (x-y)-z, а не х- (у -z). Для операторов регулярных выражений определен следующий порядок приоритетов.
- Оператор «звездочка» имеет самый высокий приоритет, т.е. этот оператор применяется только к наименьшей последовательности символов, находящейся слева от него и являющейся правильно построенным регулярным выражением.
- Далее по порядку приоритетности следует оператор конкатенации, или «точка». Связав все «звездочки» с их операндами, связываем операторы конкатенации с соответствующими им операндами, т.е. все смежные (соседние, без промежуточных операторов) выражения группируются вместе. Поскольку оператор конкатенации является ассоциативным, то не имеет значения, в каком порядке мы группируем последовательные конкатенации. Если же необходимо сделать выбор, то следует группировать их, начиная слева. Например, 012 группируется как (01)2.
- В заключение, со своими операндами связываются операторы объединения (операторы +). Поскольку объединение тоже является ассоциативным оператором, то и здесь не имеет большого значения, в каком порядке сгруппированы последовательные объединения, однако мы будем придерживаться группировки, начиная с левого края выражения.
Конечно, иногда нежелательно, чтобы группирование в регулярном выражении определялось только приоритетом операторов. В таких случаях можно расставить скобки и сгруппировать операнды по своему усмотрению. Кроме того, не запрещается заключать в скобки операнды, которые вы хотите сгруппировать, даже если такое группирование подразумевается правилами приоритетности операторов.
Пример 3.3. Выражение 01* + 1 группируется как (0(1*))+ 1. Сначала выполняется оператор «звездочка». Поскольку символ 1, находящийся непосредственно слева от оператора, является допустимым регулярным выражением, то он один будет операндом «звездочки». Далее группируем конкатенацию 0 и (1)* и получаем выражение (0(1*)). Наконец, оператор объединения связывает последнее выражение с выражением, которое находится справа, т.е. с 1.
Заметим, что язык данного выражения, сгруппированного согласно правилам приоритетности, содержит цепочку 1 плюс все цепочки, начинающиеся с 0, за которым следует любое количество единиц (в том числе и ни одной). Если бы мы захотели сначала сгруппировать точку, а потом звездочку, то следовало бы использовать скобки: (01)* + 1. Язык этого выражения состоит из цепочки 1 и всех цепочек, в которых 01 повторяется нуль или несколько раз. Для того чтобы сначала выполнить объединение, его нужно заключить в скобки: 0(1*+ 1). Язык этого выражения состоит из цепочек, которые начинаются с 0 и продолжаются любым количеством единиц. □
3.1.4. Упражнения к разделу 3.1
- Напишите регулярные выражения для следующих языков:
а) (*) множество цепочек с алфавитом {а, Ь, с}, содержащих хотя бы один символ а и хотя бы один символ Ь\
б) множество цепочек из нулей и единиц, в которых десятый от правого края символ равен 1;
в) множество цепочек из нулей и единиц, содержащих не более одной пары последовательных единиц.
- (!) Напишите регулярные выражения для следующих языков:
а) (*) множество всех цепочек из нулей и единиц, в которых каждая пара смежных нулей находится перед парой смежных единиц;
б) множество цепочек, состоящих из нулей и единиц, в которых число нулей кратно пяти.
- (!!) Напишите регулярные выражения для следующих языков:
а) множество всех цепочек из нулей и единиц, в которых нет подцепочки 101;
б) множество всех цепочек, в которых поровну нулей и единиц и ни один их префикс не содержит нулей на два больше, чем единиц, или единиц на две больше, чем нулей;
в) множество всех цепочек из нулей и единиц, в которых число нулей делится на пять, а количество единиц четно.
- (!) Опишите обычными словами языки следующих регулярных выражений:
а) (*) (1 + e)(00*l)V;
б) (0Y)*000(0 + 1)*;
в) (0 + 10)*1*.
- (*!) В примере 3.1 отмечено, что 0 — это один из двух языков, итерация которых является конечным множеством. Укажите второй язык.
3.2. Конечные автоматы и регулярные выражения
Хотя описание языков с помощью регулярных выражений принципиально отличается от конечноавтоматного, оказывается, что обе эти нотации представляют одно и то же множество языков, называемых «регулярными». Выше мы показали, что детерминированные конечные автоматы, а также два вида недетерминированных конечных автоматов — с £-переходами и без £-переходов — допускают один и тот же класс языков. Для того чтобы показать, что регулярные выражения задают тот же класс языков, необходимо доказать следующее.
- J. Любой язык, задаваемый одним из этих автоматов, может быть также определен регулярным выражением. Для доказательства можно предположить, что язык допускается некоторым ДКА.
- Любой язык, определяемый регулярным выражением, может быть также задан с помощью одного из вышеуказанных автоматов. Для этой части доказательства проще всего показать, что существует НКА с £-переходами, допускающий тот же самый язык.
На рис. 3.1 показаны все эквивалентности, которые уже доказаны или будут доказаны. Дуга, ведущая от класса X к классу Y, означает, что каждый язык, определяемый классом X, определяется также классом Y. Поскольку данный граф является сильно связным (в нем можно перейти от каждой из четырех вершин к любой другой вершине), понятно, что все четыре класса на самом деле эквивалентны.
Рис. 3.1. План доказательства эквивалентности четырех различных нотаций для регулярных языков |
3.2.1. От ДКА к регулярным выражениям
Построение регулярного выражения для языка, допускаемого некоторым ДКА, оказывается на удивление сложным. Приблизительно это выглядит так: мы строим выражения, описывающие множества цепочек, которыми помечены определенные пути на диаграмме ДКА. Однако эти пути могут проходить только через ограниченное подмножество состояний. При индуктивном определении таких выражений мы начинаем с самых простых выражений, описывающих пути, которые не проходят ни через одно состояние (т.е. являются отдельными вершинами или дугами). Затем индуктивно строим выражения, которые позволяют этим путям проходить через постепенно расширяющиеся множества состояний. В конце этой процедуры получим пути, которые могут проходить через любые состояния, т.е. сгенерируем выражения, представляющие все возможные пути. Эти идеи используются в доказательстве следующей теоремы.
Теорема 3.4. Если L = L(A) для некоторого ДКА А, то существует регулярное выражение R, причем L = L(R).
Доказательство. Предположим, что {1,2, …, п) — множество состояний автомата А для некоторого натурального п. Независимо от того, какими эти состояния являются на самом деле, их конечное число п, поэтому их можно переименовать, используя первые п положительных целых чисел. Нашей первой и наиболее сложной задачей является построение набора регулярных выражений, которые описывают постепенно расширяющиеся множества путей в диаграмме переходов автомата Л.
Обозначим через R.p регулярное выражение, язык которого состоит из множества меток w путей, ведущих от состояния i к состоянию j автомата А и не имеющих промежуточных состояний с номерами больше к. Заметим, что начальная и конечная точки пути не являются «промежуточными», поэтому мы не требуем, чтобы i и/или j были меньше или равны к.
Условия, налагаемые на пути выражениями , представлены на рис. 3.2. Здесь на вертикальной оси расположены состояния, начиная с 1 внизу до п вверху, а горизонтальная ось представляет движение вдоль пути. Заметим, что на этой диаграмме показан случай, когда i и j больше, чем к, но любое из этих чисел, или оба, могут быть меньше или равны к. Также обратите внимание на то, что путь дважды проходит через вершину к, но только в крайних точках поднимается выше, чем к.
Рис. 3.2. Путь, отметка которого принадлежит языку регулярного выражения |
Для построения выражения R-р используют следующее индуктивное определение, которое начинается с к = 0 и достигает к = п. Заметим, что при к = п пути ничем не ограничиваются, поскольку нет состояний с номерами, которые больше, чем п.
Базис. В качестве базиса примем к = 0. Поскольку все состояния пронумерованы от 1 и далее, то это условие означает, что у пути вообще нет промежуточных состояний. Существует только два вида путей, удовлетворяющих такому условию.
- Дуга, ведущая от вершины (состояния) i к вершине j.
- Путь длины 0, состоящий лишь из некоторой вершины /.
Если i Ф j, то возможен только первый случай. Необходимо проанализировать данный ДКА А и найти такие входные символы а, по которым есть переход из состояния i в состояние j:
а) если таких символов нет, то Л^0‘ =0;
б) если существует только один такой символ а, то = а;
в) если существуют такие символы аь а2, …, ак, которыми помечены дуги из состояния i в состояние j, то = а, + а2 + … + ак.
В то же время, если i =j, то допустимыми путями являются путь длины 0 и все петли, которые начинаются и заканчиваются в состоянии Л Путь длины 0 может быть представлен регулярным выражением £, потому что вдоль этого пути нет символов. Следовательно, добавляем е к выражениям, полученным выше в пунктах (а)—(в). Таким образом, в случае (а) [нет ни одного символа a] получим выражение £, в случае (б) [один символ а] выражение примет вид е + а, и в случае (в) [несколько символов] получим выражение £+ + а2 + … + ак.
Индукция. Предположим, что существует путь из состояния i в состояние j, не проходящий через состояния с номерами, которые больше, чем к. Необходимо рассмотреть две ситуации.
- Путь вообще не проходит через состояние к. В этом случае метка пути принадлежит языку Rf~X).
- Путь проходит через состояние к по меньшей мере один раз. Тогда мы можем разделить путь на несколько частей, как показано на рис. 3.3. Первая часть ведет от состояния / к состоянию к, но не проходит через к, последняя ведет из А: в j, также не проходя через к, а все части, расположенные внутри пути, ведут из к в к, не проходя через к. Заметим, что если путь проходит через состояние к только один раз, то «внутренних» частей нет, а есть только путь из / в к и путь из А: в j. Множество меток для всех путей такого вида может быть представлено регулярным выражением К-л Ч) (^li» )* • Таким образом, первое выражение представляет часть пути, ведущую в состояние к в первый раз, второе — часть, ведущую т кв к нуль или несколько раз, и третье выражение— часть пути, которая выходит из состояния к в последний раз и ведет в состояние j.
Нуль или несколько цепочек в
Рис. 3.3. Путь из i в j может быть разбит на несколько сегментов в каждой точке, в которой он проходит через состояние к
После объединения выражений для путей двух рассмотренных выше типов получим следующее выражение для меток всех путей, ведущих из состояния / в состояние j, которые не проходят через состояния с номерами, которые больше, чем к.
/#> = + дГЧдГ’ГдГ»
Поскольку данные выражения строятся в порядке возрастания верхнего индекса, можно построить любое выражение R(,p, так как оно зависит только от выражений с меньшими значениями верхнего индекса.
В итоге получим R\»] для всех i и j. Можно предположить, что состояние 1 является начальным, а множество допускающих (заключительных) состояний может быть любым. Тогда регулярным выражением для языка, допускаемого данным автоматом, будет сумма (объединение) всех тех выражений R\»?, в которых состояние j является допускающим. □
Пример 3.5. Преобразуем ДКА, представленный на рис. 3.4, в соответствующее регулярное выражение. Этот ДКА допускает все цепочки, содержащие хотя бы один 0. Чтобы понять, почему это так, заметим, что автомат переходит из начального состояния 1 в заключительное состояние 2, как только на входе появляется 0. Далее автомат остается в заключительном состоянии 2 при любой входной последовательности.
1
Начало 0
Рис. 3.4. ДКА, допускающий все цепочки, которые содержат хотя бы один О Ниже приведены базисные выражения для построений согласно теореме 3.4.
|
Например, в выражении присутствует член е, потому что и начальным, и конечным является состояние 1. Это выражение включает также 1, поскольку существует путь из состояния 1 в состояние 1 по входу 1. Выражение R^ равно 0, потому что есть дуга с меткой 0, ведущая из состояния 1 в состояние 2. Здесь нет члена £, поскольку начальное и конечное состояния различаются. И, наконец, R^ = 0, так как нет путей, ведущих из состояния 2 в состояние 1.
Теперь применим индукцию для построения более сложных выражений. Вначале они соответствуют путям, проходящим через состояние 1, а затем путям, которые могут проходить через состояния 1 и 2, т.е. любым путям. Правило для вычисления выражения р^Р представляет собой пример общего правила из индуктивной части теоремы 3.4.
4° = + ^(/ОЧ? (ЗЛ)
В таблице на рис. 3.5 сначала представлены выражения, полученные с помощью прямой подстановки в приведенную выше формулу, а затем упрощенные выражения, которые определяют те же языки, что и более сложные выражения.
Прямая подстановка | Упрощенное выражение | |
D( О П11 | £+ 1 + (£+ 1)(£+ 1)*(£+ 1) | 1* |
Rd) 12 | 0 + (£+ 1)(£+ 1)*0 | 1*0 |
D(l) 21 | 0 + 0(£+1)*(£+1) | 0 |
D( 1) 22 | £+0 + 1 +0(£+ 1)*0 | £+0+1 |
Рис. 3.5. Регулярные выражения для путей, которые могут проходить только через состояние 1 |
Например, рассмотрим выражение Rff. Подставив /= 1 и у = 2 в (3.1), получим
Общим принципом упрощения является следующий: если R— произвольное регулярное выражение, то (е + R)* = R*. Он основан на том, что обе части этого равенства описывают язык, образованный конкатенациями нуля или нескольких цепочек из L(R). В нашем случае (£ + 1)* = 1*; отметим, что оба выражения описывают цепочки, состоящие из любого количества единиц. Далее, (£ + 1)1* = 1*. Опять-таки, легко заметить, что оба выражения означают «любое количество единиц». Следовательно, исходное выражение rV> эквивалентно выражению 0 + 1*0. Последнее описывает язык, содержащий цепочку 0 и все цепочки, заканчивающиеся символом 0, перед которым стоит произвольное количество единиц. Такой язык также можно определить более простым выражением 1 *0.
Выражение Я® упрощается аналогично рассмотренному выше выражению R[2 ■ Упрощение /г’,1» и Я® зависит от двух следующих правил, описывающих операции с 0 и выполнимых для любого регулярного выражения R.
- 0R = R0 = 0, т.е. 0 является нулем (аннулятором) для конкатенации. В результате конкатенации 0, слева или справа, с любым другим выражением получается 0. Это правило очевидно, поскольку для того, чтобы в результате конкатенации получить некоторую цепочку, мы должны взять цепочки из обоих аргументов конкатенации. Если один из аргументов равен 0, выбор цепочки из него становится невозможным.
- 0+R-R+0 = R, т.е. 0 является единицей для операции объединения. В результате объединения любого выражения с 0 получим то же самое выражение.
Итак, выражение 0(£ + 1)*(£ + 1) можно заменить 0. После сказанного должны быть понятны и два последних упрощения.
Теперь вычислим выражения R. Индуктивное правило для к = 2 имеет следующий вид.
«^«f + j^^j’i» (3.2)
Если подставим упрощенные выражения из таблицы на рис. 3.5 в уравнение (3.2), то получим выражения, представленные на рис. 3.6. На этом рисунке также приведены упрощенные выражения, полученные согласно правилам, описанным для рис. 3.5.
Прямая подстановка | Упрощенное выражение | |
R{2) | 1*+ 1*0(£+0 + 1)*0 | 1* |
г>(2) 12 | 1*0+ 1*0(£+0+ 1)*(£+0+ 1) | 1*0(0 + 1)* |
Ы2) 21 | 0 + (£+0+ 1)(£+0 + 1)*0 | 0 |
р( 2) 22 | £ + 0 + 1 +(£+0 + 1)(£ + 0 + 1)*(£ +0+1) | (0+1)* |
Рис. 3.6. Регулярные выражения для путей, которые могут проходить через любое состояние |
Окончательное регулярное выражение, эквивалентное автомату, представленному на рис. 3.4, строится путем объединения всех тех выражений, для которых первое состояние является начальным, а второе — заключительным. В нашем примере 1 -— начальное состояние, а 2 — заключительное, поэтому нам нужно лишь выражение R^), равное 1*0(0 + 1)*. Оно очень просто интерпретируется. Его язык состоит из всех цепочек, начинающихся с нулевого или некоторого количества единиц, за которыми следует 0, а за ним — любая цепочка из нулей и единиц. Иначе говоря, это все цепочки из 0 и 1, содержащие хотя бы один 0. □
3.2.2. Преобразование ДКА в регулярное выражение методом исключения состояний
Метод преобразования ДКА в регулярное выражение, представленный в разделе 3.2.1, работает всегда. Как вы, возможно, заметили, он на самом деле не зависит от того, детерминирован ли этот автомат, и точно так же применим и к НКА, и даже к е- НКА. Однако такой метод построения регулярного выражения очень трудоемок. Не только потому, что для автомата с п состояниями необходимо построить порядка п выражений, но и потому, что с каждым из п шагов индукции длина выражения может возрастать в среднем в четыре раза, если эти выражения не упрощать. Таким образом, размеры результирующих выражений могут достигать порядка 4″ символов.
Существует аналогичный метод, избавляющий от некоторых повторных действий. Например, во всех выражениях с верхним индексом (к) в доказательстве теоремы 3.4 используется одно и то же подвыражение (R.fk~l) )*, которое приходится выписывать в общей сложности п2 раз.
Метод построения регулярных выражений, который мы изучим в этом разделе, предполагает исключение состояний. Если исключить некоторое состояние s, то все пути автомата, проходящие через это состояние, исчезают. Для того чтобы язык автомата при этом не изменился, необходимо написать на дуге, ведущей непосредственно из некоторого состояния q в состояние р, метки всех тех путей, которые вели из состояния q в состояние р, проходя через состояние s. Поскольку теперь метка такой дуги будет содержать цепочки, а не отдельные символы, и таких цепочек может быть даже бесконечно много, то мы не можем просто записать список этих цепочек в качестве метки. К счастью, существует простой и конечный способ для представления всех подобных цепочек, а именно, использование регулярных выражений.
Таким образом, мы пришли к рассмотрению автоматов, у которых метками являются регулярные выражения. Язык такого автомата представляет собой объединение по всем путям, ведущим от начального к заключительному состоянию, языков, образуемых с помощью конкатенации языков регулярных выражений, расположенных вдоль этих путей. Обратите внимание на то, что это правило согласуется с определением языка для любого рассмотренного выше типа автоматов. Каждый символ а или £, если он разрешен, можно рассматривать как регулярное выражение, языком которого является единственная цепочка, т.е. {а} или {£}. Можно принять это замечание за основу описываемой ниже процедуры исключения состояний.
На рис. 3.7 показано состояние s, которое мы собираемся исключить. Предположим, что автомат, содержащий j, содержит состояния qh q2, …, qu, предшествующие j, и Ри Pi, …,Рпи следующие за s. Возможно, некоторые из состояний q совпадают с состояниями р, но мы предполагаем, что среди q и р нет s, даже если существует петля, которая начинается и заканчивается в s, как показано на рис. 3.7. Над каждой дугой, ведущей к состоянию s, указано регулярное выражение; дуга, ведущая из состояния qt, помечена выражением Qi. Аналогично, регулярным выражением Р{ помечена дуга, ведущая из s в состояние р\, для каждого г. Петля на s имеет метку S. Наконец, регулярным выражением помечена каждая дуга, ведущая из qx в pt для всех i и j. Заметим, что некоторых из этих дуг в автомате может не быть. В этом случае в качестве выражения над дугой используется символ 0.
На рис. 3.8 показано, что получится, если исключить состояние s. Все дуги, проходящие через s, удалены. Чтобы это компенсировать, для каждого состояния <у„ предшествующего s, и для каждого рр следующего за s, вводится регулярное выражение, представляющее все пути, которые начинаются в q-„ идут к s, возможно, проходят петлю на s нуль или несколько раз, и наконец ведут в ру Выражение для таких путей равно Q\S*Py Это выражение добавляется (с помощью оператора объединения) к выражению над дугой из q\ в ру Если дуга <7; —> pi отсутствует, то вначале ей соответствует регулярное выражение 0.
Рис. 3.7. Состояние s, подлежащее исключению |
Рис. 3.8. Результат исключения состояния s из автомата, изображенного на рис. 3.7 |
Стратегия построения регулярного выражения по конечному автомату такова.
- Для каждого допускающего состояния q применить описанный выше процесс сокращения, чтобы построить эквивалентный автомат с дугами, помеченными регулярными выражениями. Исключить все состояния, кроме q и начального состояния q0.
- Если q Ф q0, то должен остаться автомат с двумя состояниями, подобный автомату на рис. 3.9. Регулярное выражение для допустимых цепочек может быть записано по- разному. Один из видов — (R + SU*T)*SU*. Поясним его. Можно переходить из начального состояния в него же любое количество раз, проходя путями, метки которых принадлежат либо L(R), либо L(SU*T). Выражение SU*T представляет пути, которые ведут в допускающее состояние по пути с меткой из языка L(S), затем, возможно, несколько раз проходят через допускающее состояние, используя пути с метками из L(U), и наконец возвращаются в начальное состояние, следуя по пути с меткой из 1(7). Отсюда нужно перейти в допускающее состояние, уже никогда не возвращаясь в начальное, вдоль пути с меткой из L(S). Находясь в допускающем состоянии, можно сколько угодно раз вернуться в него по пути с меткой из L(U).
Рис. 3.9. Обобщенный автомат с двумя состояниями |
- Если же начальное состояние является допускающим, то необходимо точно так же исключить состояния исходного автомата, удалив все состояния, кроме начального. В результате получим автомат с одним состоянием, подобный представленному на рис. 3.10. Регулярное выражение R’ задает цепочки, допускаемые этим автоматом.
R
- Искомое выражение представляет собой сумму (объединение) всех выражений, полученных с помощью сокращенного автомата для каждого допускающего состояния согласно правилам 2 и 3.
Пример 3.6. Рассмотрим НКА, представленный на рис. 3.11, допускающий цепочки из нулей и единиц, у которых либо на второй, либо на третьей позиции с конца стоит 1. Вначале преобразуем этот автомат в автомат с регулярными выражениями в качестве меток.
Пока исключение состояний не производилось, то все, что нам нужно сделать, это заменить метки «О, 1» эквивалентным регулярным выражением 0+1. Результат показан на рис. 3.12.
о, 1
Начало |
0,1 |
0, 1 |
0 1 /»»Л
0 + 1
Рис. 3.12. Автомат, изображенный нарис. 3.11, с регулярными выражениями в качестве меток |
Исключим сначала состояние В. Поскольку это состояние не является ни начальным, ни допускающим, то его не будет ни в одном из сокращенных автоматов. Мы избавимся от лишней работы, если исключим это состояние до того, как начнем строить два сокращенных автомата, соответствующих двум его допускающим состояниям.
Существует одно состояние А, предшествующее В, и одно последующее состояние С. Используя обозначения регулярных выражений диаграммы, представленной на рис. 3.7, получим: Qi = 1, Р\ = 0 + 1, Rn = 0 (потому что из А в С дуги нет) и 5=0 (поскольку нет петли в состоянии В). В результате, выражение над новой дугой из А в С равно 0 + 10*(О + 1).
Для сокращения полученного выражения сначала исключаем первый элемент 0, который можно игнорировать при объединении. Выражение приобретает вид 10*(О + 1). Напомним, что регулярное выражение 0* эквивалентно регулярному выражению £, поскольку
Рис. 3.11. НКА, допускающий цепочки, у которых 1 находится либо на второй, либо на третьей позиции с конца цепочки |
1(0*) = {£} и Ц0) и Ц0)Ц0) и…
Все члены этого объединения, кроме первого, пусты, поэтому 1(0*) = {£}, что совпадает с L(e). Следовательно, 10*(О+1) эквивалентно выражению 1(0+1), которое использовано для дуги А —> С на рис. 3.13.
0 + 1
Рис. 3.13. Исключение состояния В |
Далее нужно по отдельности исключить состояния С и D. Процедура исключения состояния С аналогична описанному выше исключению состояния В, в результате чего получится автомат, представленный на рис. 3.14.
0 + 1
Рис. 3.14. Автомат с двумя состояниями А и D |
В обозначениях обобщенного автомата с двумя состояниями, изображенного на рис. 3.9, соответствующие регулярные выражения для рис. 3.14 равны: R= 0+1, S- 1(0 + 1)(0 + 1), Т= 0 и {/=0. Выражение U* можно заменить на £, т.е. исключить его из конкатенации, поскольку, как показано выше, 0* = е. Кроме того, выражение SU*T эквивалентно 0, потому что Т— один из операндов конкатенации— равен 0. Таким образом, общее выражение (R + SUT) SU в данном случае упрощается до R S, или (0 + 1)1(0 + 1)(0 + 1). Говоря неформально, язык этого выражения состоит из цепочек, заканчивающихся единицей с двумя последующими символами, каждый из которых может быть либо нулем, либо единицей. Этот язык представляет одну часть цепочек, которые допускаются автоматом, изображенным на рис. 3.11: у них на третьей позиции с конца стоит 1.
Теперь снова нужно вернуться к рис. 3.13 и исключить состояние D. Поскольку в этом автомате нет состояний, следующих за D, то согласно рис. 3.7 необходимо лишь удалить дугу, ведущую из С в D, вместе с состоянием D. В результате получится автомат, показанный на рис. 3.15.
0 + 1
Начало 1(0 +1) S/^S —— Aj ►ПСП
Рис. 3.15. Автомат с двумя состояниями, полученный в результате исключения состояния D
Порядок исключения состояний
Как было отмечено в примере 3.6, если состояние не является ни начальным, ни допускающим, то оно исключается во всех сокращенных автоматах. Таким образом, одно из преимуществ процесса исключения состояний по сравнению с механической генерацией регулярных выражений, описанной в разделе 3.2.1, состоит в том, что сначала мы раз и навсегда исключаем все состояния, которые не являются ни начальными, ни допускающими. Мы вынуждены повторять процедуру сокращения, только когда необходимо исключить несколько допускающих состояний. Но даже и в этом случае можно совместить некоторые действия процедуры сокращения. Например, если автомат содержит три допускающих состояния р, q кг, то можно вначале исключить р, а затем отдельно исключить либо q, либо г, получив автоматы для допускающих состояний г и q, соответственно. Затем можно снова начать с трех допускающих состояний и, исключив состояния q и г, получить автомат для р.
Этот автомат очень похож на автомат, изображенный на рис. 3.14; различаются только метки над дугами, ведущими из начального состояния в допускающее. Следовательно, можно применить правило для автомата с двумя состояниями и упростить данное выражение, получив в результате (0 + 1)*1(0 + 1). Это выражение представляет другой тип цепочек, допустимых этим автоматом, — цепочки, у которых 1 стоит на второй позиции с конца.
Осталось объединить оба построенные выражения, чтобы получить следующее выражение для всего автомата (см. рис. 3.11).
(О + 1)*1(0 + 1) + (0 + 1)*1(0 + 1)(0 + 1)
□
3.2.3. Преобразование регулярного выражения в автомат
Теперь завершим план, представленный на рис. 3.1, показав, что любой язык L, являющийся языком L(R) для некоторого регулярного выражения R, будет также языком ЦЕ) для некоторого £-НКА Е. Это доказательство проведем методом структурной индукции по выражению R. Сначала покажем, как строить автоматы для базовых выражений: отдельных символов, £ и 0. Затем опишем, каким образом объединять эти автоматы в большие автоматы, которые допускают объединение, конкатенацию или итерацию языков, допускаемых меньшими автоматами.
Все конструируемые автоматы представляют собой £-НКА с одним допускающим состоянием.
Теорема 3.7. Любой язык, определяемый регулярным выражением, можно задать некоторым конечным автоматом.
Доказательство. Предположим, что L = L(R) для регулярного выражения R. Покажем, что L = L(E) для некоторого £-НКА Е, обладающего следующими свойствами.
- Он имеет ровно одно допускающее состояние.
- У него нет дуг, ведущих в начальное состояние.
- У него нет дуг, выходящих из допускающего состояния.
Доказательство проводится структурной индукцией по выражению R, следуя рекурсивному определению регулярных выражений из раздела 3.1.2.
Базис. Базис состоит из трех частей, представленных на рис. 3.16. В части (а) рассматривается выражение е. Языком такого автомата является {е}, поскольку единственный путь из начального в допускающее состояние помечен выражением £. В части (б) показана конструкция для 0. Понятно, что, поскольку отсутствуют пути из начального состояния в допускающее, языком этого автомата будет 0. Наконец, в части (в) представлен автомат для регулярного выражения а. Очевидно, что язык этого автомата состоит из одной цепочки а и равен L(а). Кроме того, все эти автоматы удовлетворяют условиям (1), (2) и (3) индуктивной гипотезы.
___________________ /
©
V_________________ /
б)
V___________ У
В)
Рис. 3.16. Базис построения автомата по регулярному выражению
Индукция. Три части индукции представлены на рис. 3.17. Предположим, что утверждение теоремы истинно для непосредственных подвыражений данного регулярного выражения, т.е. языки этих подвыражений являются также языками £-НКА с единственным допускающим состоянием. Возможны четыре случая.
- Данное выражение имеет вид R + S для некоторых подвыражений R и S. Тогда ему соответствует автомат, представленный на рис. 3.17, а. В этом автомате из нового начального состояния можно перейти в начальное состояние автомата для выражения либо R, либо S. Затем мы попадаем в допускающее состояние одного из этих автоматов, следуя по пути, помеченному некоторой цепочкой из языка L(R) или L(S), соответственно. Попав в допускающее состояние автомата для R или S, можно по одному из £-путей перейти в допускающее состояние нового автомата. Следовательно, язык автомата, представленного на рис. 3.17, а, равен L(R) (J L(S).
- Выражение имеет вид RS для некоторых подвыражений R и S. Автомат для этой конкатенации представлен на рис. 3.17, б. Отметим, что начальное состояние первого автомата становится начальным состоянием для всего автомата, представляющего конкатенацию, а допускающим для него будет допускающее состояние второго автомата. Идея состоит в том, что путь, ведущий из начального в допускающее состояние, сначала проходит через автомат для R по некоторому пути, помеченному цепочкой из L(R), а потом — через автомат для S по пути, помеченному цепочкой из L(S). Следовательно, путями автомата, представленного на рис. 3.17, б, будут те и только те пути, которые помечены цепочками из языка L(R)L(S>).
- Выражение имеет вид R* для некоторого подвыражения R. Используем автомат, представленный на рис. 3.17, е. Этот автомат позволяет пройти по следующим путям:
а) из начального состояния непосредственно в допускающее по пути, помеченному е. Этот путь позволяет допустить цепочку е, которая принадлежит L(R*) независимо от выражения R;
б) перейти в начальное состояние автомата для R, пройти через этот автомат один или несколько раз, и затем попасть в допускающее состояние. Это множество путей дает возможность допускать цепочки, которые принадлежат языкам L(R), L(R)L(R), L(R)L(R)L( R) и так далее, порождая таким образом все цепочки из £(/?*), за исключением, возможно, цепочки е. Но она получена в п. 3, а как отметка дуги непосредственно из начального в допускающее состояние.
- Выражение имеет вид (R) для некоторого подвыражения R. Автомат для R может быть автоматом и для (R), поскольку скобки не влияют на язык, задаваемый выражением.
б) |
Рис. 3.17. Индуктивный шаг преобразования регулярного выражения в е-НКА |
Легко заметить, что построенные автоматы удовлетворяют всем трем условиям индуктивной гипотезы: одно допускающее состояние, отсутствие дуг, ведущих в начальное состояние, и дуг, выходящих из допускающего состояния. □
Пример 3.8. Преобразуем регулярное выражение (0 + 1)*1(0 + 1) в £-НКА. Построим сначала автомат для 0 + 1. Для этого используем два автомата, построенные согласно рис. 3.16, в: один с меткой 0 на дуге, другой— с меткой 1. Эти автоматы соединены с помощью конструкции объединения (см. рис. 3.17, а). Результат изображен на рис. 3.18, а.
Рис. 3.18. Автомат, построенный для примера 3.8 |
Далее, применим к автомату (см. рис. 3.18, а) конструкцию итерации (см. рис. 3.17, в). Полученный автомат изображен на рис. 3.18, б. На последних двух шагах применяется конструкция конкатенации (см. рис. 3.17, б). Сначала автомат, представленный на рис. 3.18, б, соединяется с автоматом, допускающим только цепочку 1. Последний получается путем еще одного применения базисной конструкции (см. рис. 3.16, в) с меткой 1 на дуге. Отметим, что для распознавания цепочки 1 необходимо создать новый автомат; здесь нельзя использовать автомат для 1, являющийся частью автомата, изображенного на рис. 3.18, а. Третьим автоматом в конкатенации будет еще один автомат для выражения 0+1. Опять-таки, необходимо создать копию автомата (см. рис. 3.18, а), поскольку нельзя использовать автомат для 0+1, представляющий собой часть автомата (см. рис. 3.18, б).
Полный автомат показан на рис. 3.18, е. Заметим, что если удалить е-переходы, то этот е-НКА будет весьма похож на более простой автомат (см. рис. 3.15), также допускающий цепочки с 1 на предпоследней позиции. □
3.2.4. Упражнения к разделу 3.2
3.2.1. ДКА представлен следующей таблицей переходов:
0 | 1 | |
~><7i | Яг | <7i |
42 | <7з | <7i |
*<7з | <7з | 42 |
а) (*) выпишите все регулярные выражения . Замечание. Состояние с/, можно рассматривать как состояние с целым номером /;
б) (*) выпишите все регулярные выражения . Постарайтесь максимально упростить эти выражения;
в) выпишите все регулярные выражения R(2). Постарайтесь максимально упростить эти выражения;
г) напишите регулярное выражение для языка заданного автомата;
д) (*) постройте диаграмму переходов для этого ДКА и напишите регулярное выражение для его языка, исключив состояние q2.
3.2.2. Повторите упражнение 3.2.1 для следующего ДКА.
0 | 1 | |
~><7i | Яг | <7з |
42 | <7i | <7з |
*<7з | Яг | Я\ |
Отметим, что решения для пунктов а, 6 и д непригодны в данном упражнении. 3.2.3. Преобразуйте следующий ДКА в регулярное выражение, используя технику исключения состояний из раздела 3.2.2.
0 | 1 | |
—> *р | s | Р |
ч | р | s |
г | г | Ч |
s | Я | г |
- Преобразуйте следующие регулярные выражения в НКА с £-переходами;
а) (*) 01*;
б) (0+ 1)01; в) 00(0+1)*.
- Исключите e-переходы из НКА, полученных вами в упражнении 3.2.4. Решение для пункта а можно найти на Web-страницах этой книги.
- (!) Пусть А = (Q, £, <5, q0, {<7f}) — это такой £-НКА, в котором нет переходов в состояние q0 и из состояния с/{. Опишите язык, допускаемый каждой из следующих модификаций автомата А (в терминах языка L = L(A))\
а) (*) автомат, образованный по А путем добавления ^-перехода из q{ в д0;
б) (*) автомат, построенный по А с помощью добавления е-перехЬда из состояния q0 в каждое состояние, достижимое из д0 (по путям, метки которых могут содержать как символы из £, так и е);
в) автомат, полученный по А посредством добавления ^-перехода в qf из каждого состояния, из которого по какому-либо пути достижимо с/г;
г) автомат, построенный по А путем одновременного выполнения пунктов бив.
- (!!) Существует несколько упрощений конструкции теоремы 3.7, в которой регулярное выражение преобразовывалось в g-НКА. Вот три из них.
- Для оператора объединения вместо создания новых начального и допускающего состояний можно сгруппировать оба начальных состояния в одно, сохранив все их переходы. Аналогично, можно сгруппировать оба допускающих состояния в одно; к нему ведут все переходы, которые вели к каждому из исходных состояний.
- Для оператора конкатенации можно объединять допускающее состояние первого автомата с начальным состоянием второго.
- Для оператора итерации можно просто добавить ^-переходы из допускающего состояния в начальное, и наоборот.
В результате каждого из этих упрощений мы по-прежнему получаем правильную конструкцию, т.е. искомый е-НКА, который для любого регулярного выражения допускает язык этого выражения. Сочетание каких усовершенствований (1, 2 или 3) можно применить к этой конструкции, чтобы в результате получался правильный автомат для любого регулярного выражения?
3.2.8. (*!!) Постройте алгоритм, который по данному ДКА А вычисляет количество цепочек длины п, допускаемых ДКА А (п не связано с количеством состояний автомата А). Ваш алгоритм должен быть полиномиальным как относительно п, так и относительно количества состояний А. Указание. Используйте технику, предложенную в доказательстве теоремы 3.4.
3.3. Применение регулярных выражений
Основным средством приложений для поиска образцов (образов, шаблонов) в тексте являются регулярные выражения, задающие «схему» образца, который нужно распознать. Регулярные выражения компилируются в детерминированные или недетерминированные автоматы, которые затем моделируются для получения программы распознавания образов в тексте. В этом разделе мы рассмотрим два важных класса приложений, основанных на регулярных выражениях: лексические анализаторы и поиск в тексте.
3.3.1. Регулярные выражения в UNIX
Прежде чем рассмотреть данные приложения, ознакомимся с системой обозначений, используемой в UNIX для расширенных регулярных выражений. Эти обозначения предоставляют много дополнительных возможностей. На самом деле, расширения UNIX включают некоторые особенности, в частности, возможность именовать и ссылаться на предыдущие цепочки, соответствующие шаблону, что, фактически, позволяет распознавать нерегулярные языки. Здесь эти особенности не рассматриваются, но вводятся сокращения, позволяющие записывать сложные регулярные выражения в сжатом виде.
Первое усовершенствование в системе обозначений регулярных выражений связано с тем, что большинство приложений работает с символами в коде ASCII. В наших примерах обычно использовался алфавит {0, 1). Наличие только двух символов позволяет использовать сокращенные выражения вроде 0 + 1 для обозначения «любого символа». Однако если алфавит состоит, скажем, из 128 символов, то аналогичное выражение включало бы список всех этих символов и стало бы весьма неудобным для написания. Таким образом, регулярные выражения в UNIX позволяют задавать классы символов для представления множеств символов в наиболее кратком виде. Существуют следующие правила для классов символов.
- Символ . (точка) обозначает «любой символ».
- Последовательность [а^-.-а^] обозначает регулярное выражение а{ + а2 + … + ак
Такое обозначение позволяет записывать примерно вдвое меньше символов, поскольку нет необходимости писать знак «+». Например, четыре символа, используемые в операторах сравнения языка С, можно выразить в виде [<>= ! ].
- В квадратных скобках записывается диапазон вида х—у для обозначения всех символов от х до у из последовательности символов в коде ASCII. Поскольку коды цифр, а также символов верхнего и нижнего регистров упорядочены, то многие важные классы символов можно записывать с помощью нескольких ключевых цепочек. Например, все цифры могут быть представлены в виде [0-9], символы верхнего регистра могут быть выражены как [A-Z], а множество всех букв и цифр можно записать как [A-Za-z0-9]. Если необходимо включить в такой список символов знак минуса, то его помещают в самом начале или в самом конце списка, чтобы не было путаницы с использованием его для обозначения диапазона символов. Например, набор цифр вместе с точкой и знаками плюс и минус, используемый для образования десятичных чисел со знаком, можно записать в виде выражения [-+.0-9]. Квадратные скобки и другие символы, имеющие специальные значения в регулярных выражениях UNIX, задаются в качестве обычных символов с помощью обратной косой черты (\)перед ними.
- Для некоторых наиболее часто используемых классов символов введены специальные обозначения. Рассмотрим несколько примеров:
а) [: digit: ] обозначает множество из десяти цифр, как и [ 0-9][5];
б) [: alpha: ] обозначает любой символ (латинского) алфавита, как и [ A-Za-z ];
в) [ :alnum: ] обозначает все цифры и буквы (буквенные и цифровые символы), как и [A-Za-z0-9].
Кроме того, в регулярных выражениях UNIX используется несколько операторов, с которыми мы раньше не сталкивались. Ни один из этих операторов не расширяет множество выражаемых языков, но иногда облегчает выражение того, что нам нужно.
- Оператор | используется вместо + для обозначения объединения.
- Оператор ? значит «ни одного или один из». Таким образом, R? в UNIX означает то же, что и £ + R в системе записи регулярных выражений, принятой в этой книге.
- Оператор + значит «один или несколько из». Следовательно, R+ в UNIX является сокращением для RR* в наших обозначениях.
- Оператор {п} обозначает «и копий». Таким образом, R{5} в UNIX является сокращенной записью для RRRRR в наших обозначениях.
Отметим, что в регулярных выражениях UNIX для группирования подвыражений используются скобки, как и в регулярных выражениях из раздела 3.1.2, и тот же порядок приоритетов операторов (в этом смысле ?, + и {п} трактуются как *). Оператор * используется в UNIX (конечно же, не как верхний индекс) в рассмотренном выше значении.
3.3.2. Лексический анализ
Одним из наиболее ранних применений регулярных выражений было использование их для спецификации компонента компилятора, называемого «лексическим анализатором». Этот компонент сканирует исходную программу и распознает все лексемы, т.е. подцепочки последовательных символов, логически составляющие единое целое. Типичными примерами лексем являются ключевые слова и идентификаторы, но существует и множество других примеров.
Полное описание регулярных выражений в UNIX
Читатель, желающий ознакомиться с полным списком операторов и сокращений, используемых в системе записи регулярных выражений в UNIX, может найти их на учебных страницах для различных команд. Существуют некоторые различия между версиями UNIX, но команда типа man grep выдаст вам обозначения, используемые для команды grep, которая является основной. Кстати, «grep» означает «Global (search for) Regular Expression and Print» (Глобальный поиск регулярного выражения и печать).
UNIX-команда lex и ее GNU-версия flex получают на вход список регулярных выражений в стиле UNIX, за каждым из которых в фигурных скобках следует код, указывающий, что должен делать лексический анализатор, если найдет экземпляр этой лексемы. Такая система называется генератором лексического анализатора, поскольку на ее вход поступает высокоуровневое описание лексического анализатора и по этому описанию она создает функцию, которая представляет собой работающий лексический анализатор.
Такие команды, как lex и flex, оказались очень удобными, поскольку мощность нотации регулярных выражений необходима и достаточна для описания лексем. Эти команды способны использовать процедуру преобразования регулярного выражения в ДКА для того, чтобы генерировать эффективную функцию, разбивающую исходную программу на лексемы. Они превращают задачу построения лексического анализатора в «послеобеденную работу», тогда как до создания этих основанных на регулярных выражениях средств построение лексического анализатора вручную могло занимать несколько месяцев. Кроме того, если по какой-либо причине возникает необходимость модифицировать лексический анализатор, то намного проще изменить одно или два регулярных выражения, чем забираться внутрь загадочного кода, чтобы исправить дефект.
Пример 3.9. На рис. 3.19 приведен пример фрагмента входных данных команды lex, описывающих некоторые лексемы языка С. В первой строке обрабатывается ключевое слово else, и ее действие заключается в возвращении символьной константы (в данном примере это ELSE) в программу синтаксического анализа для дальнейшей обработки. Вторая строка содержит регулярное выражение, описывающее идентификаторы: буква, за которой следует нуль или несколько букв и/или цифр. Ее действие заключается в следующем. Сначала идентификатор заносится в таблицу символов (если его там еще нет). Затем lex выделяет найденную лексему в буфере, так что в этой части кода известно, какой именно идентификатор был обнаружен. Наконец, лексический анализатор возвращает символьную константу ID, с помощью которой в этом примере обозначаются идентификаторы.
Третий вход на рис. 3.19 предназначен для знака >=, который является двухсимволь- ным оператором. В последнем показанном примере обрабатывается односимвольный оператор =. На практике используют выражения, описывающие ключевые слова, знаки и такие символы пунктуации, как запятые или скобки, а также семейства констант, например, числа или цепочки. Многие из этих выражений чрезвычайно просты — это последовательности, состоящие из одного или нескольких определенных символов. Однако для некоторых выражений требуется вся мощь нотации регулярных выражений. Другими примерами успешного применения возможностей регулярных выражений в командах типа lex служат целые числа, числа с плавающей точкой, символьные цепочки и комментарии. □
else {возвращает (ELSE);}
[A-Za-z][A-Za-zO-9]* {код для ввода найденного идентификатора
в таблицу символов;
возвращает (ID); }
>= {возвращает (GE);}
= {возвращает (EQ);}
Рис. 3.19. Пример входных данных команды lex
Преобразование набора выражений, подобных представленным на рис. 3.19, в автомат происходит почти так же, как это было формально описано в предыдущих разделах. Сначала строится автомат для объединения всех этих выражений. Вообще говоря, этот автомат свидетельствует лишь о том, что распознана какая-то лексема. Однако если учесть конструкцию теоремы 3.7 для объединения выражений, состояние е-НКА точно указывает, к какому типу принадлежит распознанная лексема.
Единственная проблема заключается в том, что лексема может одновременно иметь сразу несколько типов; например, цепочка else соответствует не только регулярному выражению else, но и выражению для идентификаторов. В генераторе лексического анализатора применяется следующее стандартное решение: приоритет отдается выражению, находящемуся в списке первым. Таким образом, если необходимо, чтобы ключевые слова типа else были зарезервированными (т.е. не использовались в качестве идентификаторов), то они просто перечисляются в списке перед выражением для идентификаторов.
3.3.3. Поиск образцов в тексте
В разделе 2.4.1 мы отметили, что автоматы могут применяться для эффективного поиска наборов определенных слов в таких больших хранилищах данных, как Web. Хотя инструментальные средства и технология для этого пока не настолько хорошо развиты, как для лексических анализаторов, регулярные выражения все же очень полезны для описания процедур поиска желаемых образцов. Как и для лексических анализаторов, возможность перехода от естественного, описательного представления в виде регулярных выражений к эффективной (основанной на автоматах) реализации является важным интеллектуальным средством решения поставленной задачи.
Общая проблема, для решения которой технология регулярных выражений оказалась весьма полезной, заключается в описании нечетко определенного класса образцов в тексте. Нечеткость описания, в сущности, является гарантией того, что с самого начала нет нужды корректно и полно описывать образцы. Не исключено, что мы никогда не сможем получить точное и полное описание. С помощью регулярных выражений можно, не прилагая больших усилий, описывать такие образцы и быстро изменять эти описания, если результат нас не устраивает. Кроме того, «компилятор» для регулярных выражений пригоден и для преобразования записываемых выражений в выполнимый код.
В качестве примера обсудим одну типичную проблему, возникающую во многих Web- приложениях. Предположим, необходимо просмотреть огромное количество Web-страниц и отметить адреса. Возможно, мы хотим составить список электронных адресов. Или пытаемся классифицировать фирмы по их месторасположению, чтобы отвечать на запросы типа «найдите мне ресторан в пределах 10-ти минут езды от того места, где я сейчас нахожусь».
В частности, мы сосредоточим внимание на распознавании названий улиц. Что такое название улицы? Необходимо это выяснить, и, если во время тестирования программы будет установлено, что пропущены какие-то варианты, то нужно будет изменять выражения таким образом, чтобы включить все, что не было учтено. Начнем с того, что название улицы может заканчиваться словом «Street» (улица) или его сокращением «St.» Однако некоторые люди живут на «Avenues» (проспектах) или «Roads» (шоссе), что тоже может быть записано в сокращенном виде. Таким образом, в качестве окончания нашего выражения можно использовать следующие варианты. Street | St\. I Avenue | Ave\. |Road | Rd\.
В этом выражении использованы обозначения в стиле UNIX с вертикальной чертой вместо + для оператора объединения. Обратите также внимание, что перед точками стоит обратная косая черта, поскольку в выражениях UNIX точка имеет специальное значение— «любой символ», а в этом случае нам необходимо, чтобы в конце сокращений стоял символ «точка».
Перед таким обозначением, как Street, должно стоять название улицы. Обычно оно состоит из прописной буквы, за которой следует несколько строчных букв. В UNIX этот образец можно описать с помощью выражения [A-Z] [a-z] *. Однако названия некоторых улиц состоят из нескольких слов, например, «Rhode Island Avenue in Washington DC». Поэтому, обнаружив, что названия такого вида пропущены, необходимо исправить наше описание таким образом, чтобы получилось следующее выражение. ‘ [A-Z] [a-z]* ( [A-Z] [a-z]*)*’
Это выражение начинается с группы, состоящей из прописной буквы и, возможно, нескольких строчных букв. Далее может следовать несколько групп, состоящих из пробела, еще одной прописной буквы и, возможно, нескольких строчных. В выражениях UNIX пробел является обычным символом, и, чтобы представленное выше выражение не выглядело в командной строке UNIX как два выражения, разделенных пробелом, нужно все выражение заключить в апострофы. Сами апострофы не являются частью выражения.
Теперь в адрес нужно включить номер дома. Большинство номеров домов представляют собой цепочки из цифр. Однако в некоторых номерах после цифр стоит буква, например, «123А Main St.» Поэтому выражение для номеров должно включать необязательную прописную букву после цифр: [ 0-9] + [A-Z] ?. Заметьте, что мы здесь использовали UNIX-оператор + для «одной или нескольких» цифр и оператор ? для «ни одной или одной» прописной буквы. Полное выражение для адресов улиц будет иметь следующий вид.
‘ [0-9] + [A-Z]? [A-Z] [a-z] *( [A-Z] [a-z]*)* (Street ISt\. |Avenue IAve\. |Road|Rd\.) ‘
Используя это выражение, получим вполне приемлемый результат. Однако в какой-то момент мы обнаружим, что пропустили следующие случаи.
- Улицы, которые называются иначе, чем «street», «avenue» или «road». Например, мы упустили «Boulevard» (бульвар), «Place» (площадь), «Way» (дорога) и их сокращения.
- Названия улиц, которые являются числами или частично содержат числа, например, «42nd Street» (42-я улица).
- Почтовые ящики и маршруты сельской доставки.
- Названия улиц, которые не оканчиваются словом типа «Street». Например, «Е1 Camino Real in Silicon Valley». С испанского это переводится как «Королевское шоссе в Силиконовой Долине», но если сказать «El Camino Real Road» («Королевское
шоссе шоссе»), то это будет повторением. Так что приходится иметь дело с адресами типа «2000 El Camino Real». 5. Все другие странные ситуации, которые мы даже не можем вообразить. А вы можете?
Таким образом, с помощью компилятора регулярных выражений процесс постепенного приближения к полному распознавателю адресов существенно упрощается по сравнению с использованием традиционного языка программирования.
3.3.4. Упражнения к разделу 3.3
- (!) Напишите регулярное выражение для описания телефонных номеров всех видов, которые только можно себе представить. Учтите международные номера, а также тот факт, что в разных странах используется разное количество цифр в кодах областей и в локальных номерах телефонов.
- (!!) Напишите регулярное выражение для представления заработной платы в том виде, в котором она указывается в объявлениях о работе. Учтите, что может быть указан размер зарплаты в час, в неделю, месяц или год. Она может включать или не включать знак доллара или какой-либо другой единицы, например «К». Рядом может находиться слово или слова, обозначающие, что речь идет о зарплате. Предложение: просмотрите рекламные объявления в газетах или списки вакансий в режиме on-line, чтобы получить представление о том, какие образцы могут вам пригодиться.
- (!) В конце раздела 3.3.3 мы привели несколько примеров изменений, которые могут быть внесены в регулярное выражение, описывающее адреса. Модифицируйте построенное там выражение таким образом, чтобы включить все упомянутые варианты.
3.4. Алгебраические законы для регулярных выражений
В примере 3.5 мы столкнулись с необходимостью упрощения регулярных выражений для того, чтобы их размер не превышал разумные пределы. Мы объяснили, почему одно выражение можно было заменить другим. Во всех рассмотренных ситуациях основной вывод заключался в том, что эти выражения оказывались эквивалентными, т.е. задавали один и тот же язык. В этом разделе будет предложен ряд алгебраических законов, позволяющих рассматривать вопрос эквивалентности двух регулярных выражений на более высоком уровне. Вместо исследования определенных регулярных выражений, мы рассмотрим пары регулярных выражений с переменными в качестве аргументов. Два выражения с переменными являются эквивалентными, если при подстановке любых языков вместо переменных оба выражения представляют один и тот же язык.
Рассмотрим подобный пример в алгебре обычной арифметики. Одно дело сказать, что 1+2 = 2+1. Данный частный случай закона коммутативности операции сложения легко проверить: выполнив операцию сложения в обеих частях этого равенства, получим 3 = 3. Однако коммутативный закон сложения утверждает большее, а именно, что х+у = у + х, где х и у — переменные, которые можно заменять любыми двумя числами. Следовательно, он гласит, что, какие бы числа мы ни складывали, получим один и тот же результат независимо от порядка суммирования.
Регулярные выражения, подобно обычным арифметическим, подчиняются ряду законов. Многие из них подобны законам арифметики, если рассматривать объединение как сложение, а конкатенацию — как умножение. Однако в некоторых случаях эта аналогия нарушается. Кроме того, существует ряд законов для регулярных выражений, которым в арифметике аналогов нет, особенно если речь идет об операторе итерации. Следующие разделы содержат перечень главных законов для регулярных выражений. В завершение обсуждается вопрос, как вообще можно проверить, является ли некоторый формулируемый для регулярных выражений закон на самом деле законом, т.е. выполняется ли он для любых языков, подставляемых вместо его переменных.
3.4.1. Ассоциативность и коммутативность
Коммутативность— это свойство операции, заключающееся в том, что при перестановке ее операндов результат не меняется. Арифметический пример мы уже приводили выше: х + у = у + х. Ассоциативность— это свойство операции, позволяющее перегруппировывать операнды, если оператор применяется дважды. Например, ассоциативный закон умножения имеет вид: (xxy)xz = xx(yxz). Для регулярных выражений выполняются три закона такого типа.
- L + М= M+L, т.е. коммутативный закон для объединения утверждает, что два языка можно объединять в любом порядке.
- (L + М) + N = L + (М + N). Этот закон— ассоциативный закон объединения — говорит, что для объединения трех языков можно сначала объединить как два первых, так и два последних из них. Обратите внимание на то, что вместе с коммутативным законом объединения этот закон позволяет объединять любое количество языков в произвольном порядке, разбивая их на любые группы, и результат будет одним и тем же. Очевидно, что некоторая цепочка принадлежит объединению L\ U L2 U … U Lk тогда и только тогда, когда она принадлежит одному или нескольким языкам Lv
- (LM)N — L(MN). Этот ассоциативный закон конкатенации гласит, что для конкатенации трех языков можно сначала соединить как два первых, так и два последних из них.
В предложенном списке нет «закона» LM=ML, гласящего, что операция конкатенации является коммутативной, поскольку это утверждение неверно.
Пример 3.10. Рассмотрим регулярные выражения 01 и 10. Эти выражения задают языки {01} и {10}, соответственно. Поскольку эти языки различаются, то общий закон LM=ML для них не выполняется. Если бы он выполнялся, то можно было бы подставить регулярное выражение 0 вместо L и 1 вместо Ми получить неверное заключение 01 = 10. □
- Единичные и нулевые элементы
Единичным (нейтральным) элементом (единицей) операции называется элемент, для которого верно следующее утверждение: если данная операция применяется к единичному элементу и некоторому другому элементу, то результат равен другому элементу. Например, 0 является единичным элементом операции сложения, поскольку 0 + х = л: + 0 = х, а 1 — единичным элементом для умножения, потому что 1 х х = х х 1 = х. Нулевым элементом (нулем, аннулятором) операции называется элемент, для которого истинно следующее: результатом применения данной операции к нулевому и любому другому элементу является нулевой элемент. Например, 0 является нулевым элементом умножения, поскольку 0хх = хх0 = 0. Операция сложения нулевого элемента не имеет.
Для регулярных выражений существует три закона, связанных с этими понятиями.
- 0 + L = L + 0 = L. Этот закон утверждает, что 0 является единицей объединения.
- eL = Le = L. Этот закон гласит, что е является единицей конкатенации.
- 0L = L0 = 0. Этот закон утверждает, что 0 является нулевым элементом конкатенации.
Эти законы являются мощным средством упрощения выражений. Например, если необходимо объединить несколько выражений, часть которых упрощена до 0, то 0 можно исключить из объединения. Аналогично, если при конкатенации нескольких выражений некоторые из них можно упростить до е, то е можно исключить из конкатенации. Наконец, если при конкатенации любого количества выражений хотя бы одно из них равно 0, то результат такой конкатенации — 0.
- Дистрибутивные законы
Дистрибутивный закон связывает две операции и утверждает, что одну из операций можно применить отдельно к каждому аргументу другой операции. Арифметическим примером этого закона является дистрибутивный закон умножения относительно сложения, т.е. xx(y + z) = xxy+x~xz. Поскольку умножение коммутативно, то не имеет значения, с какой стороны, слева или справа от суммы, применяется умножение. Для регулярных выражений существует аналогичный закон, но, поскольку операция конкатенации некоммутативна, то мы сформулируем его в виде следующих двух законов.
- L(M + N) = LM + LN.Этот закон называется левосторонним дистрибутивным законом конкатенации относительно объединения.
- (М + N)L= ML+ NL.Этот закон называется правосторонним дистрибутивным законом конкатенации относительно объединения.
Докажем левосторонний дистрибутивный закон. Правосторонний доказывается аналогично. В доказательстве используются только языки, и оно не зависит от того, существуют ли для этих языков регулярные выражения.
Теорема 3.11. Если L, MnN— произвольные языки, то
L(M(J N) = LM\J LN
Доказательство. Это доказательство аналогично доказательству дистрибутивного закона, представленного в теореме 1.10. Требуется показать, что цепочка w принадлежит ДА/U N) тогда и только тогда, когда она принадлежит LM U LN.
(Необходимость) Если w принадлежит L(M U N), то w = xy, где х принадлежит L, а у принадлежит либо М, либо N. Если у принадлежит М, то ху принадлежит LM и, следовательно, принадлежит LM\JLN. Аналогично, если у принадлежит N, то ху принадлежит LN и, следовательно, принадлежит LM U LN.
(Достаточность) Предположим, что w принадлежит LM (J LN. Тогда w принадлежит либо LM, либо LN. Пусть и> принадлежит LM. Тогда и> = ху, где х принадлежит L, ay принадлежит М. Если у принадлежит М, то у также содержится в A/U N. Следовательно, ху принадлежит L(M\JN). Если w не принадлежит LM, то она точно содержится в LN, и аналогичные рассуждения показывают, что w принадлежит L(M[j N). □
Пример 3.12. Рассмотрим регулярное выражение 0 + 01 . В объединении можно «вынести за скобки 0», но сначала необходимо представить выражение 0 в виде конкатенации 0 с чем-то еще, а именно с е. Используем свойства единичного элемента для конкатенации, меняя 0 на Ое, в результате чего получим выражение 0е + 01*. Теперь можно применить левосторонний дистрибутивный закон, чтобы заменить это выражение на 0(е+ 1*). Далее, учитывая, что е принадлежит L(l’), заметим, что е+ 1* = 1*, и окончательно упростим выражение до 01*. □
3.4.4. Закон идемпотентности
Операция называется идемпотентной, если результат применения этой операции к двум одинаковым значениям как операндам равен этому значению. Обычные арифметические операции не являются идемпотентными. В общем случаех+х*хяхх.х*х (хотя существуют некоторые значения х, для которых это равенство выполняется, например 0 + 0 = 0). Однако объединение и пересечение являются идемпотентными операциями, поэтому для регулярных выражений справедлив следующий закон.
- L+ L = L. Этот закон — закон идемпотентности операции объединения — утверждает, что объединение двух одинаковых выражений можно заменить одним таким выражением.
- Законы, связанные с оператором итерации
Существует ряд законов, связанных с операцией итерации и ее разновидностями + и ? в стиле UNIX. Перечислим эти законы и вкратце поясним, почему они справедливы.
- (L*)* = L*. Этот закон утверждает, что при повторной итерации язык уже итерированного выражения не меняется. Язык выражения (Z,*)* содержит все цепочки, образованные конкатенацией цепочек языка L*. Последние же цепочки построены из цепочек языка L. Таким образом, цепочки языка (X*)* также являются кон- катенациями цепочек из L и, следовательно, принадлежат языку L*.
- 0* = е. Итерация языка 0 состоит из одной-единственной цепочки е, что уже обсуждалось в примере 3.6.
- е* = е. Легко проверить, что единственной цепочкой, которую можно образовать конкатенацией любого количества пустых цепочек, будет все та же пустая цепочка.
- L+ = LL* = L*L. Напомним, что V по определению равно L+ LL + LLL + …. Поскольку L* = е + L + LL + LLL + …, то
LL* = Le + LL + LLL + LLLL + …
Если учесть, что Le = L, то очевидно, что бесконечные разложения для LL* и для V совпадают. Это доказывает, что V = LL’. Доказательство того, что L* = L*L, аналогично[6].
- L* = L+ + е. Это легко доказать, поскольку в разложении L+ присутствуют те же члены, что и в разложении Z,*, за исключением цепочки е. Заметим, что если язык L содержит цепочку е, то слагаемое «+е» лишнее, т.е. в этом случае V = V.
- Ll = е + L. В действительности это правило является определением оператора «?».
- Установление законов для регулярных выражений
Каждый из вышеперечисленных законов был доказан, формально или неформально. Однако есть еще бесконечно много законов для регулярных выражений, которые можно было бы сформулировать. Существует ли некая общая методика, с помощью которой можно было бы легко доказывать истинность таких законов? Оказывается, что вопрос о справедливости некоторого закона сводится к вопросу о равенстве двух определенных языков. Интересно, что эта методика тесно связана с регулярными операциями и не может быть распространена на выражения, использующие некоторые другие операции, например пересечение.
Чтобы проиллюстрировать суть этой методики, рассмотрим следующий закон.
(L + М) = (См*)»
Этот закон утверждает, что в результате итерации объединения двух произвольных языков получим тот же язык, что и в результате итерации языка L*Af’, состоящего из всех цепочек, образованных из нуля или нескольких цепочек из L, за которыми следует нуль или несколько цепочек из М.
Чтобы доказать этот закон, сначала предположим, что цепочка w принадлежит языку выражения (L + M)*[7]. Тогда vv = vv1vv2…vvk для некоторого к, где каждая цепочка vv, принадлежит либо L, либо М. Из этого следует, что каждая такая цепочка vv; принадлежит языку L*M*. Действительно, если цепочка vv, принадлежит языку L, то она также принадлежит языку L . Тогда из М не берем ни одной цепочки, т.е. из М выбираем е. Если w* принадлежит м, доказательство аналогично. Если каждая >v, принадлежит L’a/, то w принадлежит итерации этого языка.
Чтобы завершить доказательство, необходимо доказать обратное утверждение, т.е. что все цепочки из (/,*Л/)* принадлежат также (L + М) . Опустим эту часть доказательства, поскольку нашей целью является не доказательство данного закона, а установление следующего важнейшего свойства регулярных выражений.
Любое регулярное выражение с переменными можно рассматривать как конкретное регулярное выражение, не содержащее переменных, если считать, что каждая переменная — это просто отдельный символ. Например, в выражении (L + М) переменные L и М можно заменить символами а и Ь, соответственно, и получить регулярное выражение (а + Ь)*.
Язык конкретного выражения дает представление о виде цепочек любого языка, образованного из исходного выражения в результате подстановки произвольных языков вместо переменных. Например, при анализе выражения (L + М) мы отметили, что любая цепочка w, составленная из последовательности цепочек, выбираемых либо из L, либо из М, принадлежит языку (L + М) . Можно прийти к тому же заключению, рассмотрев язык конкретного выражения L((а + Ь)*), который, очевидно, представляет собой множество всех цепочек, состоящих из символов а и Ь. В одну из цепочек этого множества можно подставить любую цепочку из L вместо символа а и любую цепочку из М вместо символа Ь. При этом различные вхождения символов а и b можно заменять различными цепочками. Если такую подстановку осуществить для всех цепочек из (а + Ь)*, то в результате получим множество всех цепочек, образованных конкатенацией цепочек из L и/или М в любом порядке.
Сформулированное выше утверждение может показаться очевидным, но, как указано во врезке «Расширение данной проверки за пределы регулярных выражений может оказаться ошибочным», оно будет неверным, если к трем операциям регулярных выражений добавить некоторые другие операции. В следующей теореме доказывается общий закон для регулярных выражений.
Теорема 3.13. Пусть Е— регулярное выражение с переменными Lx, Ь2, …, Lm. Построим конкретное регулярное выражение С, заменив каждое вхождение L, символом а„ i= 1, 2, …, т. Тогда для произвольных языков Lu L2, …, Lm любую цепочку w из L(E) можно представить в виде w = wxw2…wк, где каждая принадлежит одному из этих языков, например а цепочка a|laJ2…a|k принадлежит языку ЦС). Говоря менее формально, мы можем построить ЦЕ), исходя из каждой цепочки языка ЦС), скажем, ajXasl…a^, и заменяя в ней каждый из символов asi любой цепочкой из соответствующего языка Ц.
Доказательство. Доказательство проведем структурной индукцией по выражению Е.
Базис. Базисными являются случаи, когда Е представляет собой е, 0 или переменную L. В первых двух случаях нечего доказывать, потому что конкретное выражение С совпадает с Е. Если же Е есть переменная L, то ЦЕ) = L. Конкретное выражение С равно просто а, где а— символ, соответствующий переменной L. Следовательно, ЦС) = {а}. Если в эту единственную цепочку вместо символа а подставить любую цепочку из L, то получим язык L, который есть также ЦЕ).
Индукция. Рассмотрим три случая в зависимости от заключительной операции выражения Е. Сначала предположим, что Е =F + G, т.е. заключительной является операция объединения. Пусть С и D — конкретные выражения, построенные соответственно по F и G с помощью подстановки в эти выражения определенных символов вместо языковых переменных. Заметим, что в оба выражения F и G вместо всех одинаковых переменных должны быть подставлены одинаковые символы. Тогда конкретное выражение, полученное из выражения Е, равно С + D, и ЦС + D) = L(C) + L(D).
Предположим, что w — цепочка из языка ЦЕ), полученная в результате замены языковых переменных выражения Е некоторыми определенными языками. Тогда w принадлежит либо L(F), либо L(G). Согласно индуктивной гипотезе цепочка w получена, исходя из некоторой конкретной цепочки wx, принадлежащей ЦС) или L(D), соответственно, с помощью подстановки цепочек из соответствующих языков вместо символов цепочки W], Таким образом, в обоих случаях цепочка w может быть построена, начиная с некоторой конкретной цепочки Wi из L(C + D), путем одних и тех же подстановок цепочек вместо символов.
Необходимо также рассмотреть случаи, когда Е представляет собой FG или F’. Однако доказательства для конкатенации и итерации аналогичны приведенному выше доказательству для объединения, поэтому оставляем их читателю. □
3.4.7. Проверка истинности алгебраических законов для регулярных выражений
Теперь можно сформулировать и обосновать проверку истинности законов для регулярных выражений. Проверка истинности закона Е = F, где Е и F— два регулярных выражения с одним и тем же набором переменных, состоит в следующем.
- Преобразуем Е и F в конкретные регулярные выражения С и D соответственно, заменяя каждую переменную конкретным символом.
- Проверим равенство L(C) = L(D). Если оно выполняется, то закон Е = F истинен, а если нет — ложен. Заметим, что проверять, определяют ли два регулярных выражения один и тот же язык, мы научимся в разделе 4.4. Однако можно использовать некоторые специальные (ad-hoc) средства для проверки равенства пар интересующих нас языков. Напомним, что если языки не совпадают, то достаточно построить контрпример, т.е. найти хотя бы одну цепочку, принадлежащую только одному из них.
Теорема 3.14. Предложенная выше проверка правильно определяет истинность законов для регулярных выражений.
Доказательство. Докажем, что L(E) = L(F) для любых языков, подставленных вместо переменных Е иЕ, тогда и только тогда, когда L(C) = L(D).
(Необходимость) Предположим, что L(E) = L(F) для любых языков, подставляемых вместо переменных. В частности, выберем для каждой переменной L конкретный символ а, заменяющий L в выражениях С и D. Тогда L(C) = L(E) и L(D) = L(F). Поскольку мы предположили, что L(E) = L(F), то L(C) = L(D).
(Достаточность) Теперь предположим, что L(C) = L(D). Согласно теореме 3.13 Ь(Е) и L(F) построены с помощью замены конкретных символов в цепочках из L(C) и L(D) цепочками из языков, соответствующих этим символам. Если L(C) и L(D) состоят из одних и тех же цепочек, то оба языка, построенные таким способом, тоже будут совпадать; т.е. L(E) = L(F). □
Пример 3.15. Проанализируем предполагаемый закон (L + М) = (Z*M*)*. Если заменить переменные L и М, соответственно, конкретными символами а и Ь, получим регулярные выражения (а + Ь)* и (а*Ь*)\ Легко убедиться в том, что оба эти выражения задают язык всех возможных цепочек, составленных из а и Ь. Следовательно, оба конкретных выражения представляют один и тот же язык, и данный закон выполняется.
В качестве еще одного примера рассмотрим закон L* = L*J*. Конкретными языками будут а* и а*а*, соответственно, и каждый из них представляет собой множество всех цепочек, состоящих из а. Снова видим, что данный закон выполняется, т.е. конкатенация итераций одного и того же языка дает ту же самую итерацию.
Наконец, рассмотрим предполагаемый закон L + ML = (L + M)L. Если заменить символами а и Ь переменные L и М, соответственно, то получим два конкретных выражения а + Ьа и (а + Ь)а. Однако языки этих выражений не совпадают. Например, цепочка аа
принадлежит второму языку, но не принадлежит первому. Следовательно, этот предполагаемый закон ложен. □
Расширение данной проверки за пределы регулярных выражений может оказаться ошибочным
Рассмотрим расширенную алгебру регулярных выражений, включающую операцию пересечения. Интересно, что добавление операции П к трем представленным ранее операциям регулярных выражений не увеличивает множество задаваемых языков, что будет доказано ниже в теореме 4.8. В то же время сформулированная выше проверка алгебраических законов перестает работать.
Рассмотрим «закон» L П А/Г)N = Lf]M, утверждающий, что пересечение некоторых трех языков равно пересечению только двух первых из них. Очевидно, что этот закон ложен. Например, если L = М= {а}, а N = 0. Но проверка, основанная на конкретизации переменных, может не определить ложность этого закона. Если мы заменим L, М и N символами a, b и с, соответственно, то должны будем проверить равенство {а} П {6} П {с} = {а} П {Ь}- Поскольку обе части этого соотношения являются пустым множеством, равенство языков выполняется, и согласно нашей проверке этот «закон» будет истинным, хотя в действительности это не так.
3.4.8. Упражнения к разделу 3.4
- Проверьте следующие тождества для регулярных выражений:
а) (*)R + S = S+R;
б) (R + S) + Т= R + (S + Т);
в) (RS)T= R(ST)~,
г) R(S+ T) = RS + RT;
д) (R + S)T= RT+ST-,
е) (*)(Л*)* = Л*;
ж) (е + R)* = R*;
з) (R’sy = (R + S)\
- (!) Докажите или опровергните каждое из следующих утверждений для регулярных выражений:
а) (*) (R + S)* = R* + Sf;
б) (RS + R)’R = R(SR + /?)*;
в) (*)(RS +R)*RS =(/Уг,5)*;
г) (R+S)*S=(R’S)*;
д) S(RS + S)’R = RR’S(RR’S)’.
- В примере 3.6 было построено регулярное выражение (О + 1)*1(0 + 1) + (0 + 1)*1(0 + 1)(0 + 1).
С помощью дистрибутивных законов преобразуйте его в два различных, более простых, эквивалентных выражения.
- В начале раздела 3.4.6 приведена часть доказательства того, что (L*м’)’ = (L + М) . Завершите это доказательство, показав, что все цепочки из (£*Л/)* принадлежат также (L + М) .
- (!) Завершите доказательство теоремы 3.13 для случаев, когда регулярное выражение Е представляет собой FG или F*.
Резюме
- Регулярные выраженш. Этот алгебраический способ описания задает те же языки, что и конечные автоматы, а именно, регулярные языки. Регулярными операторами являются объединение, конкатенация («точка») и итерация («звездочка»).
- Регулярные выраженш на практике. Системы, подобные UNIX, и различные их команды используют язык расширенных регулярных выражений, существенно упрощающий записи многих обычных выражений. Классы символов позволяют легко записывать выражения для наборов символов, а такие операторы, как «один или несколько из» и «не более, чем один из», расширяют круг обычных регулярных операторов.
- Эквивалентность регулярных выражений и конечных автоматов. Произвольный ДКА можно преобразовать в регулярное выражение с помощью индуктивной процедуры, в которой последовательно строятся выражения для меток путей, проходящих через постепенно увеличивающиеся множества состояний. В качестве альтернативы преобразованию ДКА в регулярное выражение можно также использовать метод исключения состояний. С другой стороны, мы можем рекурсивно построить е-НКА по регулярному выражению, а потом в случае необходимости преобразовать полученный е-НКА в ДКА.
- Алгебра регулярных выражений. Регулярные выражения подчиняются многим алгебраическим законам арифметики, хотя есть и различия. Объединение и конкатенация ассоциативны, но только объединение коммутативно. Конкатенация дистрибутивна относительно объединения. Объединение идемпотентно.
- Проверка истинности алгебраических тождеств. Чтобы проверить эквивалентность регулярных выражений с переменными в качестве аргументов, необходимо подставить вместо этих переменных различные константы и проверить, будут ли совпадать языки, полученные в результате.
Литература
Идея регулярных выражений и доказательство их эквивалентности конечным автоматам представлены в работе Клини [3]. Преобразование регулярного выражения в £-НКА в том виде, как оно выглядит в этой книге, известно как «Метод Мак-Нотона-Ямады» из работы [4]. Проверку тождеств регулярных выражений, в которой переменные рассматриваются как константы, предложил Дж. Гишер [2]. В его отчете, который считается устным, продемонстрировано, как добавление нескольких других операций, например, пересечения или перемешивания (см. упражнение 7.3.4), приводит к ошибочности данной проверки, хотя класс задаваемых языком при этом не изменяется.
Еще до появления системы UNIX К. Томпсон исследовал возможность применения регулярных выражений в таких командах, как grep, и придуманный им алгоритм выполнения таких команд можно найти в [5]. На ранней стадии развития UNIX появились другие команды, в которых активно использовались расширенные регулярные выражения, например, команда lex, предложенная М. Леском. Описание этой команды и других технологий, связанных с регулярными выражениями, можно найти в [1].
- А. V. Aho, R. Sethi, and J. D. Ullman, Compilers: Principles, Techniques, and Tools, Addison-Wesley, Reading MA, (Ахо А. В., Сети P., Ульман Дж. Компиляторы: принципы, технологии и инструменты. — М.: Издательский дом «Вильяме», 2001.)
- L. Gisher, STAN-CS-TR-84-1033 (1984).
- С. Kleene, «Representation of events in nerve nets and finite automata», In С. E. Shannon and J. McCarthy, Automata Studies, Princeton Univ. Press, 1956, pp. 3-42. (Клини С.К. Представление событий в нервных сетях. / сб. «Автоматы». — М.: ИЛ, 1956, — С. 15-67.)
- McNaughton and Н. Yamada, «Regular expressions and state graphs for automata», IEEE Trans. Electronic Computers 9:1 (Jan., 1960), pp. 39-47.
- Thompson, «Regular expression search algorithm», Comm. ACM 11:6 (June, 1968), pp. 419-422.
[1] Будем употреблять также термины «регулярные операции» и «регулярные операторы», принятые в русскоязычной литературе. — Прим. ред.
[2] Термин «замыкание Клини» связан с именем С. К. Клини, который ввел понятие регулярного
выражения и, в частности, эту операцию.
[4] В UNIX точка в регулярных выражениях используется для совершенно другой цели — представления любого знака кода ASCII.
[5] Преимущество обозначения [: digit: ] состоит в том, что если вместо кода ASCII используется другой, в котором коды цифр расположены не по порядку, то [: digi t: ] все так же будет обозначать [0123456789], тогда как [0-9] будет представлять все символы, коды которых находятся в промежутке между кодами для 0 и для 9 включительно.
[6] Как следствие, отметим, что любой язык коммутирует со своей итерацией: LZ,* = Ь’Ь. Это свойство не противоречит тому, что в общем случае конкатенация не является коммутативной.
[7] Для простоты отождествим регулярные выражения с их языками, чтобы не повторять фразу «язык выражения» перед каждым регулярным выражением.