Алгоритм Кнута-Морриса-Пратта: поиск подстроки за O(n+m)

Алгоритм Кнута-Морриса-Пратта (KMP) - классический способ искать подстроку длины в тексте длины за гарантированное без единого возврата по тексту. Опубликован в 1977 году Дональдом Кнутом, Джеймсом Моррисом и Воганом Праттом, но открыт двумя независимыми путями раньше: Моррис придумал основную идею в 1970-м, работая над текстовым редактором, а Кнут и Пратт получили тот же алгоритм в 1973-м, отталкиваясь от теории конечных автоматов. Объединённая статья 1977 года и закрепила тройное имя.
Идея - префикс-функция
Наивный поиск при несовпадении возвращает позицию в тексте назад и начинает сравнивать паттерн с следующей позиции с нуля. Это и есть источник в худшем случае: на каждом сдвиге заново пересматривается уже прочитанный кусок текста. KMP избавляется от возвратов, заранее зная о паттерне всё, что нужно: для каждого префикса паттерна вычисляется длина наибольшего собственного префикса, который одновременно является суффиксом. Эта величина и называется префикс-функцией (failure function).
Зачем она нужна. Допустим, мы сопоставили первые символов паттерна с текстом, а -й уже не совпал. Если внутри уже сопоставленного префикса есть собственный суффикс, совпадающий с префиксом паттерна длины - мы «сдвигаем» паттерн так, чтобы этот префикс встал на место суффикса, и продолжаем сравнение со следующего символа в тексте. Позиция в тексте при этом не уменьшается.
Префикс-функция (failure function)
Формально префикс-функция паттерна длины - это массив длины , где
То есть - длина наибольшего собственного префикса подстроки , который совпадает с её суффиксом. По соглашению .
Алгоритм вычисления - амортизированно :
pi[0] = 0
k = 0
for i in 1..m-1:
while k > 0 and P[k] != P[i]:
k = pi[k - 1]
if P[k] == P[i]:
k += 1
pi[i] = k
Почему это . Переменная растёт максимум на 1 за итерацию (всего раз) и никогда не падает ниже нуля; во внутреннем while она строго уменьшается. Значит, суммарное число шагов while за весь прогон не превосходит числа инкрементов - то есть . Итого операций, как ни считай.
Пример вычисления для паттерна :
| 0 | 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|---|
| A | B | A | B | A | C | |
| 0 | 0 | 1 | 2 | 3 | 0 |
, потому что имеет собственный префикс , совпадающий с суффиксом . , потому что заканчивается на , а паттерн начинается с - ни один непустой собственный префикс с суффиксом не совпадает.
Сам поиск
Имея , основной цикл двигается по тексту только вперёд. Заводим указатели (позиция в тексте) и (длина уже сопоставленного префикса паттерна), оба стартуют с нуля:
j = 0
for i in 0..n-1:
while j > 0 and P[j] != T[i]:
j = pi[j - 1]
if P[j] == T[i]:
j += 1
if j == m:
report match at i - m + 1
j = pi[j - 1]
При несовпадении мы не возвращаем , а только уменьшаем согласно префикс-функции - то есть «выбираем» из уже сопоставленного префикса максимальный безопасный сдвиг. Тот же аргумент, что и для построения , даёт суммарное операций основного цикла. Вместе с построением - .
Подробный пример
Поищем паттерн () в тексте (). Префикс-функция - вычислена выше.
Шаг . Совпали → .
Шаг . , - совпало, .
Шаг . , - не совпало. Берём . Сравниваем с - совпало, .
Шаг . , - не совпало. , ≠ . , ≠ . остался 0, идём дальше.
Шаги . Совпали → .
Шаг . , - совпало, . Совпадение на позиции . Откатываемся .
Итого одно вхождение, указатель ни разу не возвращался назад - это и есть ключевое свойство KMP. Наивный алгоритм на той же задаче сделал бы около сравнений, KMP справился примерно за .
Сложность и сравнение с Бойером-Муром
KMP - детерминированно в любом случае, без зависимости от данных. Предобработка , поиск , дополнительная память под массив . Никаких таблиц размера алфавита.
Бойер-Мур (1977, та же эпоха) сравнивает паттерн справа налево и при несовпадении сдвигает на большие шаги по двум эвристикам - плохого символа и хорошего суффикса. В лучшем случае Бойер-Мур работает за - сублинейно, не читая большинство символов текста. На английском тексте при паттерне длины 10-20 это типичное поведение, поэтому grep и быстрый поиск в редакторах построены именно на нём.
Когда что выбирать:
- Большой алфавит, длинный паттерн, естественный текст - Бойер-Мур быстрее в среднем за счёт сублинейности.
- Малый алфавит (ДНК, бинарные данные) или важна гарантия в худшем случае - KMP. У него нет патологических входов; Бойер-Мур без модификации Galil деградирует до на конструкциях вроде паттерна в тексте из одних .
Применения
- Текстовые редакторы и
strstr(). РеализацияString.indexOfв OpenJDK для длинных паттернов использует KMP;memmemв некоторых libc-вариантах построена на нём же. Vim для/patternвыбирает между KMP и Бойером-Муром по длине паттерна. - Поиск в DNA-последовательностях. Алфавит из 4 символов - Бойер-Мур теряет преимущество, KMP с гарантированным линейным временем удобнее. В индустриальных пайплайнах чаще берут индексные структуры (суффиксные массивы, FM-индекс), но KMP остаётся опорным алгоритмом в обучении и в быстрых однократных проходах.
- Поиск patterns в логах. Когда текст течёт потоком и не помещается в индекс, KMP даёт стабильную пропускную способность вне зависимости от содержимого.
- Парсинг. Любой синтаксический разбор, где нужно найти конец конструкции - закрывающий тег HTML, разделитель multipart-формы, маркер границы протокола - может опираться на KMP. Префикс-функция строится один раз при инициализации парсера.
- Анализ периодичности. напрямую даёт длину наименьшего периода паттерна: , если это число делит . Побочный результат построения, используется в задачах сжатия.
Расширения
Алгоритм Ахо-Корасик (1975) обобщает KMP на множество паттернов сразу. Все паттерны укладываются в префиксное дерево (trie); поверх строится аналог префикс-функции - suffix link, указывающий на узел самого длинного собственного суффикса, который сам является префиксом какого-то паттерна. Поиск всех вхождений суммарной длины в тексте длины занимает , где - число совпадений. Так работает fgrep со множеством шаблонов и многие антивирусные сканеры сигнатур.
Z-функция - родственная конструкция: - длина наибольшей подстроки с позиции , совпадающей с префиксом всей строки. Строится тоже за , двойственна префикс-функции и часто проще в реализации. Вхождения паттерна ищутся через конкатенацию P\Tiz[i] = mT$.
Частые ошибки
- Возвращают назад при несовпадении. Тогда теряется главное свойство KMP - линейность; получается замаскированный наивный алгоритм.
- Откатывают к нулю вместо . То же самое: сдвиг становится единичным, и предобработка теряет смысл.
- Считают как «длину суффикса, который является префиксом» без слова "собственного". В этом случае всегда - алгоритм зацикливается.
- Путают с failure function в смысле конечных автоматов. В литературе встречаются обе формы - в варианте «автомат» хранится таблица переходов размера; для KMP это лишнее, достаточно одного массива длины .
- Применяют KMP там, где Бойер-Мур или Two-Way быстрее. Для одноразового поиска короткого паттерна в большом тексте на естественном языке Бойер-Мур заметно быстрее в среднем; для повторяющегося поиска одного паттерна - оба хороши, и выбор сводится к деталям реализации.
FAQ
Чем алгоритм Кнута-Морриса-Пратта отличается от наивного? Наивный при несовпадении возвращает позицию в тексте назад и начинает сравнивать паттерн заново - отсюда . KMP заранее строит префикс-функцию паттерна и при несовпадении сдвигает только указатель в паттерне, не трогая указатель в тексте. Каждый символ текста читается максимум константное число раз - суммарно .
Что такое префикс-функция простыми словами? Для каждого префикса паттерна - длина наибольшего собственного префикса, который одновременно является суффиксом. Например, для это 3 (префикс и суффикс ), для - 0 (общих префиксов-суффиксов нет). Она показывает, на сколько символов «откатить» сравнение, если паттерн начал совпадать, а потом не сошёлся.
Когда брать KMP, а когда Бойер-Мур? KMP - когда нужна гарантия в худшем случае, алфавит маленький (ДНК, биты), паттерн короткий, или важна простота кода. Бойер-Мур - когда алфавит большой (естественный язык), паттерн длинный, и средняя скорость важнее гарантий: он даёт сублинейное за счёт пропуска символов.
Коротко
Алгоритм Кнута-Морриса-Пратта ищет подстроку длины в тексте длины за гарантированное . Ключевая структура - префикс-функция паттерна: для каждого префикса хранит длину наибольшего собственного префикса, совпадающего с суффиксом. Строится за амортизированно, основной поиск ни разу не возвращает указатель в тексте назад. KMP проигрывает Бойеру-Муру в среднем на естественных текстах (тот сублинеен), но выигрывает на коротких алфавитах и гарантирует поведение в худшем случае. Обобщается на множество паттернов алгоритмом Ахо-Корасик и тесно связан с Z-функцией.
Читайте также

Алгоритм Рабина-Карпа: поиск подстроки за O(n+m)
Разбираем алгоритм Рабина-Карпа: как полиномиальный хеш и скользящее окно ускоряют поиск подстроки до O(n+m) в среднем, почему бывают ложные совпадения и при чём тут плагиат.

Алгоритм Бойера-Мура: почему он ищет подстроку так быстро
Разбираем алгоритм Бойера-Мура: зачем сравнивать паттерн справа налево, как работают эвристики плохого символа и хорошего суффикса и почему поиск выходит сублинейным.

Алгоритм Ахо-Корасик: поиск множества образцов в тексте
Разбираем алгоритм Ахо-Корасик: как из бора паттернов и суффиксных ссылок собрать автомат и найти все вхождения множества образцов в тексте за один линейный проход.