Алгоритм Кнута-Морриса-Пратта




 

Алгоритм КМП похож на алг-м распознавания с помощью КА, но позволяет избавиться от главного недостатка последнего – высокой трудоемкости построения таблицы переходов и затрат памяти на ее сохранение.

 

В алгоритме КМП строится не КА, а похожая на него машина распознавания, которая может делать несколько последовательных переходов из текущего состояния при анализе очередного символа текста.


Рассмотрим пример распознавания подстроки (выделены совпадающие символы текста и , текущее состояние , анализируемый символ – красный):

 

 


, но , поэтому машина не может перейти в , а должна перейти в новое состояние (т.е. сдвинется вправо относительно ). Используя уже проверенную , перейдем в наибольшее возможное состояние , для к-го , и сравним и :


 

 

 


, повторяем попытку для макс-ного , :

 

: переходим в сост-е , символ проверен.


Отметим, что текущий анализируемый символ никак не влияет на выбор следующего состояния машины распознавания: в состоянии (для к-рого ) выбирается максимальное состояние , для к-рого .

 

При этом , т.е. является максимальным по длине префиксом , который совпадает с суффиксом . Отсюда следует, что очередное состояние определяется только подстрокой и никак не зависит от проверяемого текста .

 

Для поиска нужного состояния при распознавании подстроки-образца заранее вычисляется целочисленная префикс-функция (функция отказов):

, .


Таблица значений для подстроки :

 

           
           

 

Машина распознавания подстроки :

 
 

 

 


Последовательность состояний при анализе строк:

aabaaa … 1-2-3-4-5-2-1-2...

aabaab … 1-2-3-4-5-6…

aabaac … 1-2-3-4-5-2-1-0…


При анализе очередного символа возможен возврат через несколько состояний.

 

Если текущее состояние , то можно сделать переходов в предыдущие состояния.

 

Но если машина распознавания находится в состоянии , должно выполняться условие .

=>

При анализе символов было сделано ровно переходов в последующие состояния (по одному переходу на каждый символ, без переходов в предыдущие).

=>

На каждый проверяемый символ текста приходится в среднем переходов.

 


Вычисление префикс-функции для подстроки :

 

F[1] = 0;

for (q = 2; q <= m; q++) {

k = F[q–1];

while (k > 0 && P[q]!= P[k+1]) k = F[k];

F[q] = (P[q] == P[k+1])? k+1: 0;

}

 

В цикле while можно сделать максимум шаг, но это возможно, если до этого был выполнен шаг цикла for, на которых цикл while вообще не выполнялся.

=>

Общая трудоемкость данного алгоритма составляет .


Поиск всех вхождений подстроки в строку после вычисления префикс-функции :

 

for (q = 0, i = 1; i <= n; i++) {

while (q > 0 && P[q+1]!= T[i]) q = F[q];

if (P[q+1] == T[i]) q++;

if (q == m) {

найдено_вхождение(i–m); q = F[q];

}

}

 

Рассуждая по аналогии с алгоритмом вычисления , можно сделать вывод, что трудоемкость данного алгоритма .

=>

Общая трудоемкость алгоритма КМП составляет и не зависит от количества символов в алфавите.


Вариант алгоритма Бойера-Мура с вычислением функций стоп-символа и безопасного суффикса (Кормен).

 


Алгоритм Рабина-Карпа

При поиске подстроки в строке необходимо найти все допустимые сдвиги, т.е. такие значения , , для которых .

 

Основная идея алгоритма:

- преобразовать подстроку и сравниваемую с ней часть текста в целые числа, заданные цифрами в системе счисления с основанием (обозначим эти числа, соответственно, и );

- сравнивать не и , а и (одно сравнение чисел вместо сравнений символов);

- обеспечить быстрое вычисление числа , соответствущего сдвигу , преобразованием значения .


Пусть . Тогда:

 

Сдвиг является допустимым, если .

 

Для вычисления не требуется знание строки . Используя схему Горнера, можно получить за время :

 

Также за можно вычислить :

 


Значения и связаны рекуррентным соотношением:

 

Если заранее получить константу (за время ), то будет вычисляться по известному за ЭШ, а проверку всего текста можно провести за .

 

К сожалению, числа и могут быть очень большими. Если , то уже при мы выйдем за пределы представления 4-байтовых целых чисел. Поэтому отдельные арифметические операции, на основании которых получены оценки трудоемкости, нельзя считать элементарными шагами.

 


Для преодоления этой трудности нужно перейти к модульной арифметике: вместо сверхдлинных целых чисел использовать их остатки от деления по некоторому модулю .

 

Если задано некоторое целое , то любое произвольное целое можно единственным образом представить в виде:

, где ( - частное, - остаток).

 

Для любых целых выполняется равенство:

,

т.е. операцию деления по модулю можно вносить в скобки и применять к отдельным значениям.


Чтобы исключить переполнение для целых в алгоритме Рабина-Карпа необходимо выбрать такое (простое), чтобы значение помещалось в машинное слово, и при расчете , , и постоянно вычислять остатки по :

 

вместо использовать ; при вычислении брать остаток после каждого умножения текущего числа на ,

 

,

,

.

 

Приведенные выше оценки трудоемкостей сохранятся, но теперь все вычисления проводятся с короткими числами, и каждая арифметическая операция будет одним ЭШ.


Пример вычисления для строки из десятичных цифр (, , Кормен и др.):

 
 

 

 


14152 = (31415 - 3∙10000) ∙10 + 2 – для длинных целых.

14152 mod 13 =

= ((31415 mod 13 – (3∙10000) mod 13) ∙10 + 2) mod 13 =

= ((7 – 9) ∙10 + 2) mod 13 = 8 – для остатков.

 

Правильное вычисление r = a mod q (при q > 0):

r = a % q;

if (r < 0) r += q;


Если , то и . Но из еще не следует . Поэтому при совпадении остатков необходимо проверить, найдено ли действительно вхождение подстроки (т.е. выполняется ли ) или произошло холостое срабатывание.

 

В наихудшем случае (например, если и являются последовательностями из одного символа алфавита) будет постоянно выполняться и последующее посимвольное сравнение. Поэтому трудоемкость в наихудшем составляет , как в простейшем алгоритме.

 

Но в большинстве практических приложений совпадений остатков и холостых срабатываний должно быть немного, и трудоемкость алгоритма Рабина-Карпа составит .


Поиск всех вхождений в ():

p = P[1]; t = T[1]; h = 1;

for (i = 2; i <= m; i++) {

h = mod(h*d, q);

p = mod(p*d + P[i], q);

t = mod(t*d + T[i], q);

}

for (s = 0; s <= n-m; s++) {

if (p == t) {

for (i = 1; i <= m && P[i] == T[s+i]; i++);

if (i > m) найдено_вхождение(s);

}

if (s < n-m)

t = mod(mod(t–T[s+1]*h, q)*d+T[s+m+1], q);

}


Возможность использования приведенных алгоритмов для распознавания набора подстрок.

 



Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2019-04-28 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: