
Алгоритм Бойера — Мура
Данный алгоритм был разработан двумя учеными — Робертом Бойером (англ. Robert Stephen Boyer) и Джеем Муром (англ. J Strother Moore) в 1977 году. Он считается наиболее быстрым среди алгоритмов общего назначения, предназначенных для поиска подстроки в строке.
Примечание:
Вопрос: что значит алгоритмы «общего назначения»?
Ответ: это значит, что эти алгоритмы универсальны, т.е. предназначены для решения широкого класса задач.
Ключевые понятия:
Алфавит — конечное множество символов.
Подстрока — это последовательность подряд идущих символов в строке.
Строка — последовательность символов текста.
Суффикс — это подстрока, заканчивающаяся на последний символ строки.
Примечание:
В определении строки речь не обязательно должна идти именно о тексте.
В общем случае строка — это любая последовательность байтов.
Поиск подстроки в строке осуществляется по заданному образцу, т. е. некоторой последовательности байтов, длина которой не превышает длину строки.
Наша задача заключается в том, чтобы определить, содержит ли строка заданный образец.
Описание:
Алгоритма Бойера — Мура можно представить в виде двух простых шагов.
1 шаг
Для искомого образца строим две таблицы — таблицу стоп-символов и таблицу суффиксов.
Примечание:
Процесс построения таблиц будет описан ниже.
2 шаг
Совмещаем начало строки и образца и начинаем проверку с последнего символа образца.
Если символы совпадают, производится сравнение предпоследнего символа образца и т. д. Если все символы образца совпали с наложенными символами строки, значит, подстрока найдена и поиск окончен.
Если же какой-то символ образца не совпадает с соответствующим символом строки, образец сдвигается на несколько символов вправо, и проверка снова начинается с последнего символа.
Назовем эти «несколько символов», упомянутые в предыдущем абзаце, величиной сдвига.
В качестве величины сдвига берется большее из двух значений:
1) Значение, полученное с помощью таблицы стоп-символов по простому правилу:
Если
несовпадение произошло на позиции
,
а стоп-символ «
»,
то значение величины сдвига будет
равно
.
Примечание:
- позиция символа в образце (нумерация с 1);
— значение, записанное в таблице
стоп-символов, для символа «c».
2) Значение, полученное из таблицы суффиксов.
Подробное описание работы алгоритма
Псевдокод: алгоритм Бойера — Мура
Boyer-Moore-Matcher(
)
m
Compute-Last-Occurrence-Function(
)
Compute-Good-Suffix-Function(
)
while
do
while
and
do
if
then print «Образец входит со сдвигом» s
else
Алгоритм основан на трёх идеях.
1. Сканирование слева направо, сравнение справа налево.
Совмещается начало текста (строки) и шаблона, проверка начинается с последнего символа шаблона.
Если символы совпадают, производится сравнение предпоследнего символа шаблона и т. д.
Если все символы шаблона совпали с наложенными символами строки, значит, подстрока найдена, и поиск окончен.
Если же какой-то символ шаблона не совпадает с соответствующим символом строки, шаблон сдвигается на несколько символов вправо, и проверка снова начинается с последнего символа.
Примечание:
Эти «несколько», упомянутые в предыдущем абзаце, вычисляются по двум эвристикам.
Вопрос: что такое эвристика?
Ответ: эвристика — это не полностью математически обоснованный (или даже «не совсем корректный»), но при этом практически полезный алгоритм.
2. Эвристика стоп-символа (англ. bad-character heuristic).
Стоп-символ — это первый справа символ в строке, отличный от соответствующего символа в образце.
Эвристика стоп-символа предлагает попробовать новое значение сдвига, исходя из того, где в образце встречается стоп-символ (если вообще встречается).
В наиболее удачном случае стоп-символ выявляется при первом же сравнении и не встречается нигде в образце.
В
этом случае сдвиг можно сразу увеличить
на
:
любой меньший сдвиг заведомо не подойдет,
так как стоп- символ в тексте окажется
напротив какого-то символа из образца.
Если
этот наиболее удачный случай повторяется
постоянно, то при поиске подстроки мы
просмотрим всего лишь
часть текста (вот как полезно сравнивать
справа налево!).
В
общем случае эвристика стоп-символа
работает так. Предположим, что при
сравнении права налево мы наткнулись
на первое несовпадение:
,
где
.
Пусть
— номер самого правого вхождения
символа
в образец
(если этот символ вообще не появляется
в образце, считаем
равным
).
Мы утверждаем, что можно увеличить
на
,
не упустив ни одного допустимого сдвига.
В
самом деле, если
,
то стоп-символ
вообще не встречается в образце
,
так что можно сразу сдвинуть образец
на
позиций вправо.
Если
,
то образец можно сдвинуть на
позиций вправо, т.к. при меньших сдвигах
стоп-символ в тексте не совпадет с
соответствующим символом образца.
Наконец,
если
,
то эвристика предлагает сдвигать образец
не вправо, а влево; алгоритм Бойера —
Мура эту рекомендацию игнорирует,
поскольку эвристика безопасного суффикса
всегда предлагает ненулевой сдвиг
вправо.
Чтобы
применять эвристику стоп-символа полезно
для каждого возможного стоп-символа
вычислить значение
.
Это делается простой процедурой
Compute-Last-Occurrence-Function
(«найти последнее вхождение»), которая
для каждого
вычисляет
— номер крайнего правого вхождения
в
,
или нуль, если
в
не входит. В этих обозначениях приращение
сдвига, диктуемое эвристикой стоп-символа,
есть
,
как и написано в строке 13 алгоритма
Boyer-Moore-Matcher.
Псевдокод: найти последнее вхождение стоп-символа в подстроку
Compute-Last-Occurrence-Function( )
for each character
do
for
to
do
return
Примечание:
Время
работы
процедуры
Compute-Last-Occurrence-Function есть
Пример использования эвристики стоп-символа:
Предположим, что мы производим поиск слова «колокол».
Первая же буква не совпала — «к» (назовём эту букву стоп-символом).
Тогда можно сдвинуть шаблон вправо до последней буквы «к».
!
Строка: * * * * * * к * * * * * *
Шаблон: к о л о к о л
Следующий шаг: к о л о к о л
Если стоп-символа в шаблоне вообще нет, шаблон смещается за этот стоп-символ.
!
Строка: * * * * * а л * * * * * * * *
Шаблон: к о л о к о л
Следующий шаг: к о л о к о л
В данном случае стоп-символ — «а», и шаблон сдвигается так, чтобы он оказался прямо за этой буквой. В алгоритме Бойера — Мура эвристика стоп-символа вообще не смотрит на совпавший суффикс (см. ниже), так что первая буква шаблона («к») окажется под «л», и будет проведена одна заведомо холостая проверка.
Если стоп-символ «к» оказался за другой буквой «к», эвристика стоп-символа не работает.
!
Строка: * * * * к к о л * * * * *
Шаблон: к о л о к о л
Следующий шаг: к о л о к о л ?????
В таких ситуациях выручает третья идея алгоритма — эвристика совпавшего суффикса.
3. Эвристика безопасного (совпавшего) суффикса (англ. good-suffix heuristic).
Если
и
— строки, будем говорить, что они сравнимы
(обозначение:
),
если одна из них является суффиксом
другой. Если выровнять две сравнимые
строки по правому краю, то символы,
расположенные один под другим, будут
совпадать. Отношение
симметрично: если
,
то и
.
Эвристика
безопасного суффикса состоит в следующем:
если
,
где
(и число
— наибольшее с таким свойством), то мы
можем безбоязненно увеличить сдвиг на
Иными
словами,
— наименьшее расстояние, на которое мы
можем сдвинуть образец без того, чтобы
какой- то из символов, входящих в
«безопасный суффикс»
оказался напротив несовпадающего с
ним символа из образца. Поскольку строка
заведомо сравнима с пустой строкой
,
число
корректно определено для всех
.
Стоит также заметить, что
для всех
,
так что на каждом шаге алгоритма Бойера
— Мура образец будет сдвигаться вправо
хотя бы на одну позицию. Мы будем называть
функцией
безопасного суффикса
(англ. good-suffix
function),
ассоциированной со строкой
.
Псевдокод: вычисление функции безопасных суффиксов
Compute-Good-Suffix-Function( )
m = length(P)
pi[] = префикс-функция(P)
pi1[] = префикс-функция(обращение(P))
for j=0..m
suffshift[j] = m - pi[m]
for i=1..m
j = m - pi1[i]
suffshift[j] = min(suffshift[j], i - pi1[i])
Примечание:
Время
работы процедуры Compute-Good-Suffix-Function
есть
.
Пример использования эвристики безопасного (совпавшего) суффикса:
Если при сравнении строки и шаблона совпало один или больше символов, шаблон сдвигается в зависимости от того, какой суффикс совпал.
Строка: * * т о к о л * * * * *
Шаблон: к о л о к о л
Следующий шаг: к о л о к о л
В данном случае совпал суффикс «окол», и шаблон сдвигается вправо до ближайшего «окол». Если подстроки «окол» в шаблоне больше нет, но он начинается на «кол», сдвигается до «кол», и т. д.
Обе эвристики требуют предварительных вычислений — в зависимости от шаблона поиска заполняются две таблицы. Таблица стоп-символов по размеру соответствует алфавиту (например, если алфавит состоит из 256 символов, то её длина 256); таблица суффиксов — искомому шаблону. Именно из-за этого алгоритм Бойера — Мура не учитывает совпавший суффикс и несовпавший символ одновременно — это потребовало бы слишком много предварительных вычислений.
Опишем подробнее обе таблицы.
Таблица стоп-символов
Считается, что символы строк нумеруются с 1 (как в Паскале).
В таблице стоп-символов указывается последняя позиция в образце (исключая последнюю букву) каждого из символов алфавита. Для всех символов, не вошедших в образец , пишем 0 (для нумерации с 0 — соответственно, −1).
Например,
если
,
таблица стоп-символов будет выглядеть
так.
Символ a b c d [все остальные]
Последняя позиция 5 2 7 6 0
Обратите внимание, для стоп-символа «d» последняя позиция будет 6, а не 8 — последняя буква не учитывается. Это известная ошибка, приводящая к неоптимальности. Для АБМ она не фатальна («вытягивает» эвристика суффикса), но фатальна для упрощённой версии АБМ — алгоритма Хорспула.
Если несовпадение произошло на позиции , а стоп-символ , то сдвиг будет .
Таблица суффиксов
Для
каждого возможного суффикса
шаблона
указываем наименьшую величину, на
которую нужно сдвинуть вправо шаблон,
чтобы он снова совпал с
.
Если такой сдвиг невозможен, ставится
(в обеих системах нумерации). Например,
для того же
будет:
Суффикс [пустой] d cd dcd ... abcdadcd
Сдвиг 1 2 4 8 ... 8
Иллюстрация
было ? ?d ?cd ?dcd ... abcdadcd
стало abcdadcd abcdadcd abcdadcd abcdadcd ... abcdadcd
Если шаблон начинается и заканчивается одной и той же комбинацией букв, вообще не появится в таблице.
Например,
для
для всех суффиксов (кроме, естественно,
пустого) сдвиг будет равен 4.
Суффикс [пустой] л ол ... олокол колокол
Сдвиг 1 4 4 ... 4 4
Иллюстрация
было ? ?л ?ол ... ?олокол колокол
стало колокол колокол колокол ... колокол колокол
Пример:
Пусть у нас
есть набор символов (алфавит) из
пяти символов:
и мы хотим найти вхождение образца “
”
в строке “
”.
Следующие схемы иллюстрируют все этапы
выполнения алгоритма:
1 шаг
Таблица стоп-символов
-
a
b
c
d
e
4
3
0
0
0
Если несовпадение произошло на позиции , а стоп-символ , то сдвиг будет .
Таблица суффиксов для образца “ ”.
Суффикс [пустой] d ad bad bbad abbad
Сдвиг 1 5 5 5 5 5
Иллюстрация
было ? ?d ?ad ?bad ?bbad abbad
стало abbad abbad abbad abbad abbad abbad
2 шаг
a b e c c a a b a d b a b b a d
a b b a d
Накладываем образец на строку. Совпадения суффикса нет — таблица суффиксов даёт сдвиг на одну позицию. Для несовпавшего символа исходной строки «с» (5-я позиция) в таблице стоп-символов записан 0. Сдвигаем образец вправо на 5-0=5 позиций:
a b e c c a a b a d b a b b a d
a b b a d
Символы 3—5 совпали, а второй — нет. Эвристика стоп-символа для «а» не работает (2-4=-2). Но поскольку часть символов совпала, в дело включается эвристика совпавшего суффикса, сдвигающая образец сразу на пять позиций!
a b e c c a a b a d b a b b a d
a b b a d
И снова совпадения суффикса нет. В соответствии с таблицей стоп-символов сдвигаем образец на 1 позицию и получаем искомое вхождение образца:
a b e c c a a b a d b a b b a d
a b b a d