Закрепление материала: Поиск

Модуль "Поиск" • урок 6 из 7 • ~9 минут

Суть урока. В этом модуле мы изучили различные подходы к поиску данных: от базовых алгоритмов в массиве до продвинутых методов поиска подстроки в тексте. Мы узнали, как отсортированные данные позволяют ускорить поиск с O(N) до O(log N) с помощью дихотомии, и рассмотрели семейство алгоритмов поиска по образцу: от наивного метода с его проблемой возвратов до эффективных алгоритмов Кнута-Морриса-Пратта (КМП), Бойера-Мура и Рабина-Карпа.

Краткое резюме модуля

За прошедшие уроки мы разобрали следующие важные алгоритмы и концепции:

Сравнение алгоритмов поиска по образцу: наивный тратит 21 сравнение символов, КМП 15, Рабин-Карп 10, Бойер-Мур 7
Один и тот же поиск образца ABABAC в тексте ABABADABABAC: наивный алгоритм тратит 21 сравнение символов, КМП - 15, Рабин-Карп - 10 плюс семь проверок хеша, Бойер-Мур - всего 7. Ровно эти числа выдают счетчики тренажеров в уроках модуля.

Ключевые идеи

Чтобы уложить модуль в голове, по каждому алгоритму поиска по образцу достаточно помнить крючок-образ, плюсы и минусы:

1977
Кнута-Морриса-Пратта
"никогда не пятиться"
  • гарантированное O(N+M) на любых данных
  • указатель по тексту идет только вперед
  • нужна префикс-функция и память O(M)
  • на обычном тексте медленнее Бойера-Мура
1977
Бойера-Мура
"смотреть с конца и прыгать"
  • пропускает куски текста целиком, до O(N/M)
  • чем длиннее образец, тем быстрее поиск
  • нужна таблица сдвигов по алфавиту
  • на коротком алфавите прыжки мельчают
1987
Рабина-Карпа
"сравнивать числа, а не буквы"
  • ищет сразу много образцов за один проход
  • пересчет хеша окна стоит O(1)
  • при совпадении хешей все равно сверяет символы
  • при неудачном модуле вырождается в O(M(N-M+1))

Развитие методов поиска по образцу

Пять алгоритмов модуля - это не вся тема, а ее классическое ядро. Поиском по образцу занимались и до Бойера и Мура, и во все последующие десятилетия; полезно знать хотя бы направления, в которых развивалась тема.

Поиск нескольких образцов за один проход

Идею КМП можно растянуть с одного образца на целый список. Алгоритм Ахо-Корасик (1975) строит из всех образцов бор - дерево их общих начал - и добавляет в него те же ссылки отката, что и префикс-функция, только ведут они теперь не вдоль строки, а по дереву. Один проход по тексту находит вхождения всех образцов сразу. В модуле встречались еще два ответа на тот же вопрос: обобщение Калинина в уроке про КМП и поиск по хеш-таблице у Рабина-Карпа.

Предобработка текста вместо предобработки образца

Все пять алгоритмов устроены одинаково: предобрабатывается образец, а текст затем прочитывается целиком. Значит, каждый новый запрос стоит не меньше O(N), даже если текст не менялся. Если же текст постоянен, а искать в нем будут многократно, разумнее один раз построить индекс по самому тексту: суффиксное дерево (Укконен, 1995), суффиксный массив (Манбер и Майерс, 1990) или FM-индекс (Феррагина и Манци, 2000). Тогда ответ на запрос стоит порядка длины образца и от длины текста не зависит вовсе. Так устроены полнотекстовый поиск и программы выравнивания ДНК, где текст - геном, а образцов миллионы.

Какие алгоритмы применяются в стандартных библиотеках

Ни чистый КМП, ни чистый Бойер-Мур в стандартных библиотеках обычно не применяются. В glibc функции strstr и memmem используют алгоритм two-way (Крошмор и Перрен, 1991): он дает линейное время и при этом обходится постоянной дополнительной памятью, без таблицы на весь алфавит. В Python внутри in и .find() работает FastSearch, сочетание Бойера-Мура-Хорспула с алгоритмом воскресного дня, а с версии 3.10 на длинных образцах включается тот же two-way - как страховка от квадратичного худшего случая.

Смежные задачи

Рядом лежит вопрос не "есть ли вхождение", а "насколько похоже". Расстояние Левенштейна и его вариант Дамерау-Левенштейна считают, сколькими правками одна строка превращается в другую; алгоритмы Нидлмана-Вунша и Смита-Ватермана выравнивают последовательности в биоинформатике; алгоритм bitap (Баэса-Йейтс и Гоннет, 1992) ищет с заданным числом ошибок, сводя сравнение к битовым операциям. Это уже отдельная тема, но выросла она из той же задачи.

Не остановилась и теория. Для поиска подстроки известен квантовый алгоритм Рамеша и Виная: он справляется за время порядка корня из длины текста плюс корня из длины образца. Классическому алгоритму в худшем случае приходится прочитать текст целиком, квантовому хватает порядка корня от его длины.

Коротко: главное

  • Бинарный поиск невероятно быстр, но неприменим к классическим связным спискам из-за отсутствия прямого доступа.
  • Наивный поиск подстроки часто возвращается назад по тексту, теряя драгоценное время.
  • КМП двигается по тексту только вперед - это свойство называют однопроходностью - и гарантирует стабильное время работы O(N+M).
  • Бойер-Мур проверяет символы с конца и перепрыгивает целые слова, достигая сверхбыстрой скорости O(N/M).
  • Рабин-Карп сравнивает хеши за O(1), что делает его лучшим выбором для мульти-поиска.

Проверьте себя

Вопрос 1

Какой алгоритм поиска подстроки обладает парадоксальным свойством: чем длиннее искомый образец, тем быстрее в лучшем случае происходит поиск?

Вопрос 2

Зачем алгоритму Рабина-Карпа требуется посимвольная проверка строк, если их хеши совпали?

Вопрос 3

В чем главное преимущество алгоритма КМП перед наивным поиском подстроки?

Вопрос 4

Таблицу сдвигов d нумеруют позицией несовпадения, и в нулевой ячейке у нее стоит -1. Что это значение означает?

Как вам урок? Спасибо за отзыв!