- •Основные алгоритмы
- •1.1. Линейный поиск
- •Весь массив просмотрен и совпадения не обнаружено.
- •1.2. Поиск делением пополам (двоичный поиск)
- •If found then writeln( ‘искомый элемент ‘,a [ m ] );
- •1.3. Поиск в таблице
- •X: String;
- •1.4. Прямой поиск строки
- •1.5. Поиск в строке. Алгоритм Кнута, Мориса и Пратта
- •I, j, k, k0, m, n: integer;
- •1.6. Поиск в строке. Алгоритм Боуера и Мура
1.5. Поиск в строке. Алгоритм Кнута, Мориса и Пратта
Приблизительно в 1970 г. Д. Кнут, Д. Морис и В. Пратт изобрели алгоритм, фактически требующий только N сравнений даже в самом плохом случае. Новый алгоритм основывается на том соображении, что, начиная каждый раз сравнение образа с самого начала, мы можем уничтожать ценную информацию. После частичного совпадения начальной части образа с соответствующими символами строки мы фактически знаем пройденную часть строки и можем «вычислить» некоторые сведения (на основе самого образа), с помощью которых потом быстро продвинемся по тексту. Приведенный пример поиска слова «иваны» показывает принцип работы такого алгоритма. Символы, подвергшиеся сравнению, здесь подчеркнуты. Обратите внимание: при первом несовпадении двух символов образ сдвигается на все пройденное расстояние, поскольку меньшие сдвиги не могут привести к полному совпадению
КМП-алгоритм записывается так (опять используется предикат Р (1.47) и Q (1.48))
i:=1; j:=1; (1.50)
WHILE ( j <= M ) AND ( i <= N) DO BEGIN
(* Q( i-j ) & P( i-j, j ) *)
WHILE ( j >= 1 ) AND ( s[ i ] <> p[ j ] ) DO j:=D;
i:=i+1; j:=j+1;
END;
Однако такая запись умышленно не совсем точная, поскольку в ней есть сдвиг на неопределенную величину D. Вскоре мы к ней вернемся, а теперь сначала отметим, что условия Q( i-j ) и P( i-j, j ) сохраняются как глобальные инварианты и к ним можно добавить отношения 1 ≤ i ≤ N и 1 ≤ j ≤ M. Это предполагает, что мы должны отказаться от соглашения, что i всегда отмечает в тексте текущее положение первого символа образа. Точнее, выравненное положение образа теперь i - j.
Если алгоритм заканчивает работу по причине j=М+1, то из составляющей P( i-j, j ) следует справедливость P( i-М, М ), т. е., согласно (1.47), совпадение начинается с позиции i - М. Если же работа окончена из-за i = N+1, то поскольку j < М+1, из инварианта Q( i ) следует, что совпадения вообще нет.
Теперь мы должны показать, что алгоритм никогда не делает инвариант ложным. Легко видеть, что в начале i=j=1, и он истинен. Сначала исследуем эффект от двух операторов, увеличивающих i и j на единицу. Они, очевидно, не сдвигают образ вправо и не делают ложным Q( i – j ), поскольку разность остается неизменной. Но, может быть, они делают ложным P( i—j, j ) - вторую составляющую инварианта? Обращаем внимание, что в этой точке истинно отрицание выражения в заголовке внутреннего цикла, т. е. либо j < 1, либо si=pj. Последнее увеличивает частичное совпадение и устанавливает P( i-j, j+1 ), а первое так же постулирует истинность P( i-j, j+1 ). Следовательно, увеличение на единицу i и j не может также сделать ложным тот или другой инвариант. Но в алгоритме остается только еще присваивание j:=D. Мы просто постулируем, что значение D всегда таково, что замена j на D оставляет инвариант справедливым.
Для того чтобы найти соответствующее выражение для D, мы должны вначале понять смысл этого присваивания. При условии что D < j, присваивание соответствует сдвигу образа вправо на j - D позиций. Естественно, мы хотим, чтобы сдвиг был настолько больше, насколько это возможно, т. е. D должно быть как можно меньше. Этот процесс показан на рис. 1.1.
i
|
|
a |
b |
c |
d |
|
|
||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
a |
b |
c |
e |
|
|
||||||||||||||||
|
|
|
|
|
|
|
|
|
|
||||||||||||||
|
|
|
|
|
|
|
|
|
a |
b |
c |
d |
|
|
|
|
|
|
|
|
a |
b |
c |
e |
|
j=4 D=1 j=1
Рис. 1.1. Присваивание j:=D сдвигает образ на j—D позиции вправо (в данном случае D=1, до присваивания j=4).
Если инвариант P( i-j, j ) & Q( i-j ) после присваивания j значения D истинен, то перед ним должно быть истинно P( i-D, D ) & Q( i-D ). Это предусловие и будет поэтому нашим ориентиром при поиске подходящего выражения для D. Основное соображение:
благодаря P( i—j, j ) мы знаем, что
si- j. … si-1 = p1 … pj-1
(мы только что просмотрели первые j символов образа и убедились в их совпадении с текстом). Поэтому условие P( i-D, D ) с D ≤ j, т. е.
si-D. … si-1 = p1 … pD-1
превращается в
pj-D. … pj-1 = p1 … pD-1 (1.51)
и предикат ~P(i—k, М) для k=1...j-D (чтобы убедиться в инвариантности Q(i-D)) превращается в
pj-k. … pj-1 ≠ p1 … pk-1 (1.52)
Важный вывод: очевидно, что значение D определяется одним лишь образом и не зависит от строки текста. Условия (1.51) и (1.52) говорят, что для определения D мы должны для каждого j найти наименьшее D, т. е. самую длинную последовательность символов образа, непосредственно предшествующих позиции j, которая совпадает полностью с началом образа. Для каждого j такое D мы будем обозначать через dj. Так как эти значения зависят только от образа, то перед началом фактического поиска можно вычислить вспомогательную таблицу d; эти вычисления сводятся к некоторой предтрансляции образа. Соответствующие усилия будут оправданными, если размер текста значительно превышает размер образа (М<<N). Если нужно искать многие вхождения одного и того же образа, то можно пользоваться одними и теми же D. Приведенные примеры объясняют функцию D.
Примеры:
-
a
a
a
a
a
a
j=6; d[6]=4+1; (макс.сдвиг=j-d[j]=1)
p[1]…p[4]=p[2]…p[5]
a
a
a
a
a
b
a
a
a
a
a
b
a
b
c
a
b
c
j=6; d[6]=2+1; (макс.сдвиг=j-d[j]=3)
p[1]…p[2]=p[4]…p[5]
p[1]…p[3]≠p[3]…p[5]
p[1]…p[4]≠p[2]…p[5]
a
b
c
a
b
d
a
b
c
a
b
c
-
a
b
c
d
e
a
j=6; d[6]=0+1; (макс.сдвиг=j-d[j]=5)
p[1]…p[1]≠p[5]…p[5]
p[1]…p[2]≠p[4]…p[5]
p[1]…p[3]≠p[3]…p[5]
p[1]…p[4]≠p[2]…p[5]
a
b
c
d
e
f
a
b
c
d
e
f
Рис.1.2. Вычисление dj
Последний пример на рис.1.2. и примеры на рис.1.3. (два его частных случая) наводят на мысль, что мы можем поступать еще лучше: так как рj равно А вместо F (рис.1.3.), то соответствующий символ строки не может быть символом А из-за того, что условие si≠рj заканчивает цикл. Следовательно, сдвиг на 5 не приведет к последующему совпадению, и поэтому мы можем увеличить размер сдвига до шести (см. рис. 1.3., верхний пример). Учитывая это, мы переопределим вычисление dj как поиск самой длинной совпадающей последовательности
p1. … pdj-1 = pj-dj … pj-1
с дополнительным ограничением pdj ≠ pj. Если никаких совпадений нет, то мы считаем dj=0, что указывает на сдвиг «на целый» образ относительно его текущей позиции (см. рис. 1.3., нижняя часть).
-
a
b
c
d
e
f
a
b
c
d
e
a
a
b
c
d
e
j=6, d[6]=0; сдвиг=j-d[j]=6
-
a
b
c
d
e
f
a
b
c
d
e
g
a
b
c
d
e
f
j=6, d[6]=0; сдвиг=j-d[j]=6
Рис. 1.3. Сдвиг образа за последний символ
Ясно, что вычисление dj само представляет собой первое приложение поиска в строке, и мы для этого можем использовать быструю версию КМП-алгоритма. Это и демонстрирует программа 1.2, состоящая из двух частей. В первой читается строка s, а затем идут итерации, начинающиеся с чтения образа, затем следуют его предтрансляция и, наконец, сам поиск.
CONST Mmax = 100; Nmax = 1000; ESC = ЗЗС;
VAR