
- •1 Лінійні динамічні структури даних. Списки.
- •2 Нелінійні структури даних
- •3 Алгоритми сортування послідовностей
- •Квадратичні алгоритми
- •Логарифмічні алгоритми
- •Лінійні алгоритми
- •4 Алгоритми пошуку
- •4.2.1 Пошук елементу в послідовності
- •4.2.2 Алгоритми пошуку послідовностей елементів
- •5 Меоди розробки алгоритмів
- •61166 Харкiв, просп. Ленiна, 14.
4.2.2 Алгоритми пошуку послідовностей елементів
Становлять інтерес також задачі пошуку підпослідовності в послідовності (підрядка в рядку). Мається на увазі пошук індексу елементу послідовності довжини n, з якого починається коротша послідовність довжини m, m < n.
Найпростіший алгоритм – це алгоритм грубої сили. Він базується на перевірці всіх позицій тексту з 0 по n - m на співпадання з початком зразка. Якщо співпадають - дивимося далі й т. ін. Алгоритм грубої сили не потребує попередньої обробки та простору для зберігання її результатів.
Proc RudeSearch(A,B,n,m,index_of_B) Index_of_B = -1 for i=1 To n-m+1 do Index_of_B = i for j=1 To m do if A[j+i-1] <> B[j] then Index_of_B = -1 break endif endfor if Index_of_B <> - 1 then break
endif endfor End RudeSearch
У гіршому випадку алгоритм грубої сили робить M*N кроків.
Алгоритм грубої сили нагадує прийом з віконцем довжини m, яке рухається по масиву довжини n. Нас цікавить, чи не співпадає слово у віконці зі зразком. Спроби прискорити цей алгоритм полягають в 1) прискоренні алгоритму порівняння віконця із підланцюжком та 2) зміщенні віконця більше ніж на 1 елемент у разі неспівпадання.
Алгоритм Карпа-Рабіна. Порівнювати по буквах віконце й підланцюжок довго. Замість цього оберемо деяку функцію на словах довжини m. Тільки якщо значення цієї функції на зразку та віконці довжини m співпадають, треба перевіряти поелементно. Виграш стає помітним, якщо значення функції не треба буде обчислювати заново для зміщеного віконця, а лише змінювати від попереднього кроку на деяку величину. Наприклад, якщо за функцію обрати суму кодів букв слова у віконці, то легко побачити, що під час зсуву слово не міняється повністю, лишень одна буква вилучається, а інша додається. Тож, зміщуючись на одну букву праворуч можна код найлівішої віднімати, а код найправішої додавати.
Як правило за таку hash -функцію обирається
hash( w[1, m ] ) = ( w[1] * 2m-1 + w[2] * 2 m-2 + ... + w[m] ) mod q
де q - велике число
Тоді при зміщені на один крок функція змінюється таким чином:
rehash(a, b, h ) = (( h - a * 2 m-1 ) * 2 + b) mod q,
де a – початковий символ слова до зсуву, b – новий останній символ слова після зсуву, h – значення hash-функції для слова за крок до зсуву віконця. Найгірший випадок трапляється під час пошуку ланцюжка am в an.
Наведемо метод Карпа-Рабіна у вигляді процедури:
Proc KR(Y, X, n, m, index_of_Y) //Шукаємо Y всередині X d = 1 << (m-1) hy = hx = 0 index_of_Y = -1 for i = 1 To m do hx = ( ( hx << 1) + X[i] ) hy = ( ( hy << 1) + Y[i] ) endfor for i=m+1 To n+1 do if hy = hx then index_of_Y = i-m for j = 1 to m do if X[i+j-m-1] <> Y[j] then Index_of_Y = -1 break endif endfor if index_of_Y <> -1OR i = n+1 then break endif hx = ((( hx - X[i-m] * d ) << 1) + X[i] )
endif endfor End KR
Алгоритм Рабіна-Карпа - це найкращий алгоритм для одночасного пошуку множини зразків.
Якщо треба знайти будь-який з великого набору зразок , скажімо, довжини k, у тексті, можна модифікувати простий варіант алгоритму Рабіна-Карпа, який використовує хеш-таблицю або іншу структуру даних «множина» (set data structure) для перевірки того, коли хеш віконця належатиме набору хеш зразків, що розшукуються.
Алгоритм Бойера-Мура. Він вважається найбільш швидким серед алгоритмів загального призначення, що використовуються для пошуку зразку в ланцюжку елементів.
Цей алгоритм робить те, що на перший погляд здається неможливим. У типовій ситуації він читає лише невелику кількість усіх букв слова, у якому шукаємо заданий зразок. Як таке може бути? Ідея проста. Наприклад, ми шукаємо зразок „abcd”. Подивимося на четверту букву слова: якщо, наприклад, це буква „е”, то немає ніякої необхідності читати перші три букви. І справді, у зразку букви „е” нема, тому він може початися не раніше п'ятої букви.
Простіший варіант алгоритму Бойера-Мура спирається на наступні принципи. Ми сполучаємо початок ланцюжка та зразка й починаємо порівняння з останнього елемента зразка. Якщо останній символ зразка й відповідний йому символ ланцюжка не співпадають, то зразок зміщується відповідно ланцюжка на величину, отриману зі спеціальної таблиці зміщень (про неї буде сказано нижче). Якщо символи співпадають, переходимо до порівняння лівішого елемента й так до початку зразка. Якщо всі елементи співпали, входження знайдено. Якщо якийсь, не останній символ зразка не співпав, то зразок зсувають відповідно ланцюжка на один елемент праворуч і знову починаємо перевірку з кінця зразка.
Величина зсуву у випадку неспівпадання останнього елементу обчислюється, виходячи з наступних зауважень: зсув зразка повинен бути мінімальним, таким, щоб не пропустити входження зразка в ланцюжок. Якщо поточний символ ланцюжка зустрічається в зразку, то ми зсуваємо зразок таким чином, щоб зіставити символ ланцюжка та найправіше входження цього символу в зразок. Якщо зразок взагалі не містить такого символу, то зсуваємо зразок на величину, що дорівнює його довжині, так що перший символ зразка накладається на наступний за вже перевіреними символами ланцюжка. Величина зсуву при неспівпаданні для кожного символу алфавіта залежить лише від його положення у зразку (відповідно до останнього символа зразка).
Наприклад, якщо в нас є зразок “abbad” та алфавіт {a,b,c,d,e}, то таблиця зсувів буде наступна:
-
символ
a
b
c
d
e
зсув
1
2
5
0
5
Зсув найлегше обчислюється таким прийомом:
m = Length(z) for i = 1 to Length(BM) do BM[i] = m endfor for i =1 to m do BM[z[i]] = m-i endfor
Нижче наведено ілюстрацію кроків пошуку зразка “abbad” у ланцюжку “abeccacbadbabbad”
-
abeccacbadbabbad
abbad
Початок пошуку. Останній символ зразка не співпадає з накладеним символом ланцюжка, зрушуємо зразок на 5 позицій праворуч
-
abecc
acbadbabbad
abbad
Три символи зразка й ланцюжка співпали, а четвертий - ні - зрушуємо зразок на одну позицію праворуч
-
abecca
cbadbabbad
abbad
Останній символ знову не співпадає, відповідно таблиці зсуву зміщуємо зразок на 2 позиції
-
abeccacb
adbabbad
abbad
Ще раз зрушуємо зразок на 2 позиції
-
abeccacbad
babbad
abbad
А тепер - на одну.
-
abeccacbadb
abbad
abbad
Входження знайдено на 12 позиції.
Такий алгоритм найефективніше використовувати, коли розмір ланцюжка, де проводиться пошук, порівняльно з довжиною зразка та алфавіту значно більший.
Алгоритм Кнута-Морріса-Пратта. Цей алгоритм з'явився в результаті копіткого аналізу алгоритму грубої сили. Дослідники хотіли знайти засоби більш повно використовувати інформацію, отриману під час сканування рядка (алгоритм грубої сили її просто викидає).
Розмір зсуву зразка можна збільшити (більше одного символу), тим самим, різко прискоривши пошук.
Метод аналогічно методу Бойера-Мура використовує попередню обробку шуканого ланцюжка, а саме: на його основі складається так звана префікс-функція. Суть цієї функції в знаходженні для кожного підрядка S[1..i] рядка S найбільшого підрядка S[1..j] (j<i), що міститься одночасно і з початку й у кінці підрядка, що вже співпав (як префікс і як суфікс). Наприклад, для підрядка abcHelloabc таким рядком є abc (одночасно і префіксом, і суфіксом). Сенс префікс-функції в тому, що ми можемо відкинути завідомо невірні варіанти, тобто, якщо під час пошуку зразка abcHelloabcq співпав підрядок abcHelloabc (наступний символ вже не співпав), то є сенс продовжити пошук з наступного символу рядка та вже з четвертого символу зразка (перші три і так співпадуть). Ось так можна обчислити цю функцію для всіх значень параметра від 1 до m шуканого зразка:
S - шуканий зразок
P - масив, у якому зберігається префікс-функція
Proc Prefix {процедура, що обчислює префікс-функцію}
begin
P[1]:=0 {префікс рядка з одного символу має нульову довжину}
k:=0
m:= Length(S)
for i:=2 to m do {обчислюється для префіксів рядка довжиною від 2 до m символів}
begin
while (k>0) and (S[k+1]<>S[i]) do k:=P[k] {значення функції може бути
отримане з раніше обчислених }
endwhile
if S[k+1]=S[i] then k:=k+1 endif
P[i]:=k {присвоєння префікс-функції}
endfor
end Prefix
Proс KnutMorrisPrattSearch
T - рядок, у якому проводимо пошук
begin
{Введення тексту та зразка}
…
Prefix {Обчислення префікс-функції}
k:=0 {кількість символів, що співпали на даний час }
n:= Length(T)
for i:=1 to n do
begin
while (k>0) and (S[k+1]<>T[i]) do k:=P[k] endwhile
if S[k+1]=T[i] then
k:=k+1
endif
if k=m then {якщо співпали не всі символи }
begin
writeln('Зразок входить в текст та починається з ',i-m+1,'-ого символу')
k:=P[k]
endif
endfor
end KnutMorrisPrattSearch
Зразок
a |
b |
c |
H |
e |
l |
l |
o |
a |
b |
c |
q |
|
|
|
|
|
|
|
a |
b |
c |
H |
e |
l |
l |
o |
a |
b |
c |
H |
e |
l |
l |
a |
b |
c |
q |
Таблиця префіксів
a |
b |
c |
H |
e |
l |
l |
o |
a |
b |
c |
q |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
0 |
1 |
2 |
3 |
0 |
4.3 Варіанти індивідуальних завдань
Реалізувати алгоритми пошуку відповідно варіанту та оцінити їх ефективність та найкращі умови використання.
пошук елементу – бінарний пошук; пошук підланцюжка – алгоритм Карпа-Рабіна.
пошук елементу – інтерполяційний пошук; пошук підланцюжка – алгоритм Бойера-Мура.
пошук елементу – пошук у хеш-таблиці з відкритим хешуванням; пошук підланцюжка – алгоритм Кнута-Морріса-Пратта.
пошук елементу – пошук у хеш-таблиці з закритим хешуванням; пошук підланцюжка – алгоритм Карпа-Рабіна з одночасним пошуком декількох зразків однакової довжини.
4.4 Контрольні запитання та завдання
1. За якими параметрами можливо оцінити алгоритм?
2. Як обчислюється часова складність алгоритму?
3. Назвіть основні прийоми, що використовуються під час пошуку елемента в послідовності.
4. Назвіть основні прийоми, що використовуються під час пошуку підпослідовності.