Найдите Подстроку. Алгоритм Кнута – Морриса – Пратта

В задачах поиска информации одной из важнейших задач является поиск точно заданной подстроки в строке.

Примитивный алгоритм поиска подстроки в строке основан на переборе всех подстрок, длина которых равна длине шаблона поиска, и посимвольном сравнении таких подстрок с шаблоном поиска.

По традиции шаблон поиска или образец обычно обозначают иглой (англ.

«игла»), а линию, в которой ведется поиск, называют стогом сена (англ.

«haystack»).

В Python примитивный алгоритм выглядит так:

  
  
   

index = -1 for i in xrange(len(haystack)-len(needle)+1): success = True for j in xrange(len(needle)): if needle[j]<>haystack[i+j]: success = False break if success: index = i break print index

Обозначим n=|стог сена|, m=|игола|.

Самый простой алгоритм поиска, даже в лучшем случае, выполняет n–m+1 сравнений; если имеется много частичных совпадений, скорость снижается до O(n*m).

Хотя рассматриваемый ниже алгоритм имеет низкую скорость на «хороших» данных, это компенсируется отсутствием регрессии на «плохих» данных.

Алгоритм Кнута-Морриса-Пратта — один из первых алгоритмов с линейной оценкой наихудшего случая.

Прежде чем перейти к описанию алгоритма, необходимо рассмотреть понятие префиксной функции.

Строковая префиксная функция π(S,i) — это длина наибольшего префикса строки S[1.i], который не совпадает с этой строкой и одновременно является ее суффиксом.

Проще говоря, это длина самого длинного начала строки, которое одновременно является ее концом.

Для строки S удобно представить префиксную функцию в виде вектора длины |S|-1. Мы можем рассмотреть префиксную функцию длины |S| установив π(S,1)=0. Пример префиксной функции для строки «abcdabcabcdabcdab»:

С [я] а б с д а б с а б с д а б с д а б
π(S,i) 0 0 0 0 1 2 3 1 2 3 4 5 6 7 4 5 6
Предположим, что π(S,i)=k. Отметим следующие свойства префиксной функции.

  1. Если S[i+1]=S[k+1], то π(S,i+1)=k+1.
  2. S[1.π(S,k)] — суффикс строки S[1.i].

    Действительно, если строка S[1.i] заканчивается строкой S[1. π(S,i)]=S[1.k], а строка S[1.k] заканчивается строкой строку S[1.π (S,k)], то строка S[1.i] заканчивается строкой S[1.π(S,k)].

  3. ∀ jε(k,i), S[1.j] не является суффиксом строки S[1.i].

    В противном случае предположение π(S,i)=k было бы неверным, поскольку j> k.

Рассмотренные свойства позволяют получить алгоритм вычисления префиксной функции.

Пусть π(S,i)=k. Необходимо вычислить π(S,i+1).

  1. Если S[i+1]=S[k+1], то π(S,i+1)=k+1.
  2. В противном случае, если k=0, то π(S,i+1)=0.
  3. В противном случае установите k:=π(S,k) и перейдите к шагу 1.
Ключевым моментом для понимания сути алгоритма является тот факт, что если суффикс, найденный на предыдущем шаге, невозможно расширить до следующей позиции, то мы стараемся как можно дольше рассматривать меньшие суффиксы.

Алгоритм вычисления префиксной функции в Python:

def prefix(s): v = [0]*len(s) for i in xrange(1,len(s)): k = v[i-1] while k > 0 and s[k] <> s[i]: k = v[k-1] if s[k] == s[i]: k = k + 1 v[i] = k return v

Покажем, что время работы алгоритма равно O(n), где n=|S|.

Обратите внимание, что асимптотическое поведение алгоритма определяется общим количеством итераций цикла while. Это верно, поскольку без цикла while каждая итерация цикла for выполняется менее чем за постоянное время.

На каждой итерации цикла for k увеличивается не более чем на единицу, что означает максимально возможное значение k=n–1. Поскольку значение k уменьшается только внутри цикла while, то получается, что суммарно k нельзя уменьшить более чем в n–1 раз.

Это означает, что цикл while в конечном итоге будет выполнен не более n раз, что дает окончательную оценку времени работы алгоритма O(n).

Рассмотрим алгоритм Кнута-Морриса-Пратта, основанный на использовании префиксной функции.

Как и в алгоритме примитивной подстроки, шаблон «перемещается» по строке слева направо, чтобы найти совпадение.

Однако ключевое отличие состоит в том, что с помощью префиксной функции мы можем избежать заведомо бесполезных сдвигов.

Пусть S[0.m–1] – выборка, T[0.n–1] – строка, в которой осуществляется поиск.

Рассмотрим сравнение строк в позиции i, то есть образец S[0.m–1] сравнивается с частью строки T[i.i+m–1].

Предположим, что первое несовпадение произошло между символами S[j] и T[i+j], где i < j < m. Let us denote P = S[0.j–1] = T[i.i+j–1].

When shifting, we can expect that the prefix S will converge with some suffix of the string P. Since the length of the longest prefix, which is also a suffix, is the prefix function of the string S for index j, we arrive at the following algorithm:

  1. Построим префиксную функцию выборки S, обозначим ее F.
  2. Установите k = 0, i = 0.
  3. Сравните символы S[k] и T[i].

    Если символы равны, увеличьте k на 1. Если при этом k станет равным длине шаблона, то вхождение шаблона S в строку T найдено, индекс вхождения равен i – |S | + 1. Алгоритм завершается.

    Если символы не равны, мы используем префиксную функцию для оптимизации сдвигов.

    Пока k > 0, положим k = F[k–1] и перейдем к началу шага 3.

  4. В то время как я < |T|, increase i by 1 and go to step 3.
Возможная реализация алгоритма Кнута-Морриса-Пратта на Python выглядит следующим образом:

def kmp(s,t): index = -1 f = prefix(s) k = 0 for i in xrange(len(t)): while k > 0 and s[k] <> t[i]: k = f[k-1] if s[k] == t[i]: k = k + 1 if k == len(s): index = i - len(s) + 1 break return index

Теги: #Алгоритмы #поиск подстроки в строке #Алгоритмы
Вместе с данным постом часто просматривают: