Uploaded by Настя Макарова

Алгоритмы и анализ сложности

advertisement
Алгоритмы и анализ сложности
Часть 2
Поиск
Достоинства отсортированных массивов:
- быстрый поиск в ОП ( log n ),
- возможность поиска в диапазоне, на  или  , простота
получения порядковых статистик.
Недостатки отсортированных массивов:
- неприспособленность к динамическим изменениям,
- поиск, недостаточно быстрый для ВУ.
=>
Во многих случаях необходимы динамические структуры
данных для быстрого поиска информационных объектов по
ключам. Как правило, такие структуры хранят ключевые
значения и ссылки на соответствующие объекты (реже – сами
объекты).
Хеширование
Основная идея – получать адрес искомого объекта на основе
функционального преобразования его ключевого значения:
по ключу p вычисляется значение f ( p) , определяющее
номер записи в некоторой таблице объектов (ссылок).
Требования к функции f ( p) :
1. p1  p 2 => f ( p1 )  f ( p2 )
2. простота вычисления (быстрое вычисление адреса)
Для сохранения n объектов\ссылок необходимо выделить
таблицу с q  n записями. Если диапазон возможных значений
ключа pmin  p  pmax , то, как правило, pmax  pmin  q .
=>
в общем случае возможно образование конфликтов
(коллизий): p1  p 2 , но f ( p1 )  f ( p2 ) .
Общий подход:
- не исключать коллизии, а предусмотреть их обработку,
- выбирать f ( p) таким образом, чтобы она по возможности
равномерно распределяла pmax  pmin потенциальных
значений ключа по q адресам.
f ( p) - хеш-функция (hash=смесь).
Хеш-таблица – информационная структура – таблица записей,
номера\адреса которых вычисляются на основе f ( p) .
Пример хорошей ф-ции: f ( p)  ord ( p) mod q , где q - простое
(значения f ( p) достаточно равномерно распределяются на
[0...q  1], т.е. хеш-таблица используется эффективно).
Использование хеш-функции для быстрого сравнения
объектов и в криптографии
Обработка коллизий – метод цепочек
Идея: объединять эл-ты, образующие коллизии, в q списков.
Вариант 1: значения только добавляются, их макс.количество
n известно заранее. Хеш-таблица состоит из 2 частей – n элтов для массива списков и q начальных эл-тов списков.
Пусть p1  p2 , f ( p1 )  f ( p2 )  i , значение p1 размещается в
таблице на шаге j , p 2 – на шаге k  j :
0
0
-1
i
j
p1
k
p2
k
-1
n 1
j
q 1
Выбор q . Добавление и поиск эл-тов. Удаление элементов
Вариант 2: q  n , таблица длины q с динамической областью
переполнения (незанятые эл-ты содержат значение  ):
0

i
p1
s
q 1
p0
s
p2
t
p4
t
-1
-1
Добавление элементов. Успешный и безуспешный поиск.
Удаление элементов, «удаленное» значение p0 .
Равномерное размещение ключей по q  O(n) спискам:
трудоемкость построения таблицы  O(n) , поиска  O(1) .
Наихудший случай – O (1) списков (коллизий):
трудоемкость построения таблицы  O(n 2 ) , поиска  O(n) .
Обработка коллизий – метод открытой адресации
Идея:
- хеш-таблица длины q  n содержит только ключи,
- элементы, образующие коллизии, объединяются в списки,
- если некоторый список образуют эл-ты, для которых f ( p)  i ,
то они займут незанятые ячейки таблицы с номерами
(i  s j ) mod q , j  0 , где s0  0 , а остальные шаги s j не
хранятся, а вычисляются.
Пример: в таблицу последовательно включаются p x , p1 , p2 , p3
(все значения разные), f ( p1 )  f ( p2 )  f ( p3 )  i , f ( p x )  i
0
A
p3

s2
i
p1
q 1
px
p2
s1
s3
Поиск элемента со значением p :
k = i = ord(p) % q;
j = 0;
// j – номер шага
while (A[k] != p && A[k] !=  ) {
j++; s = текущий_шаг(j);
k = (i + s) % q;
}
if (A[k] == p) return k; // поиск успешный
else return -1;
// поиск безуспешный
Добавление и удаление элементов
Методы вычисления текущего шага s j должны исключать
зацикливание при поиске элементов.
Вычисление шага – линейные пробы:
- выбирается c  const , c  1, c и q взаимно простые,
- шаги s j  cj , j  0 , а соответствующие адреса ячеек таблицы
k  ( f ( p)  cj ) mod q .
Недостатки, приводящие к росту трудоемкости поиска:
- первичное скучивание – группировка элементов одного
списка возле начального эл-та (особенно при c  1),
- вторичное скучивание – группировка эл-тов разных списков.
Желательно, чтобы размещение элементов в таблице было по
возможности равномерным и выглядело, как случайное.
Вычисление шага – квадратичные пробы:
s j  j 2 , адреса k  ( f ( p)  j 2 ) mod q .
Поиск приведет к повторному выбору эл-та на шаге l  q , если
для l и некоторого предыдущего шага j  l выполнится
( f ( p)  l 2 ) mod q  ( f ( p)  j 2 ) mod q .
Тогда (l 2  j 2 ) mod q  (l  j )(l  j ) mod q  0 , т.е. l  j  mq .
Это может выполниться только при числе шагов (длине
списка) l  q / 2 .
Вычисление шага – двойное хеширование:
s j  h( f ( p))  j – различные шаги для разных списков.
В список входят эл-ты с разными ключами pi , но одинаковыми
f ( pi ) => можно взять h( f ( p))  f ( p) mod r  1, где r  q .
Пусть в таблице хранится n эл-в, и   n / q – заполненность
таблицы. Рассмотрим асимптотический случай q  , n   .
Будем полагать, что при поиске на каждом шаге события «эл-т
таблицы занят» и «эл-т таблицы свободен» независимы, и их
вероятности равны, соответственно,  и 1   .
Вероятности событий «получение свободного элемента за…»:
1 шаг – (1   ) ,
2 шага –  (1   ) ,
i  1 шаг –  i (1   ) .
В силу независимости данных событий:

  i (1   )  1 (условие нормировки).
i0
Средняя длина безуспешного поиска (или трудоемкость
размещения нового элемента) при заполненности  :

lunsucc    i (1   )(i  1)  (1   )(1  2   3 2  4  3  ...) 
i0


 (1   ) (1     2  ...)  (    2   3  ...)  ... 
1
1 

0.1
0.25
0.5
0.75
0.9
0.99
lunsucc
1.11
1.33
2
4
10
100
Пусть эл-ты таблицы не удаляются. Тогда при успешном
поиске эл-та проводятся те же самые действия, что и при
размещении данного эл-та в таблице.
Трудоемкость успешного поиска эл-та равна числу шагов при
размещении данного эл-та, а это число зависело от текущей
заполненности таблицы.
Пусть  - текущая заполненность таблицы. При размещении n
элементов  изменяется от 0 до n / q   .
Средняя длина успешного поиска:
l succ
1  d
1 1 dt 1
1
 


ln

 0 1    1  t  1  

lsucc
t  1   , dt   d
0.1
0.25
0.5
0.75
0.9
0.99
1.05
1.15
1.39
1.85
2.56
4.66
Вывод: задавать q 
4n
; если при добавлении новых эл-тов
3
(увеличении n ) достигается указанная граница, следует
выделить новую таблицу с q1  q записями и последовательно
разместить в ней все эл-ты из старой (рехеширование).
Применение хеширования для данных на ВУ
Для размещения N элементов (ключей) выделяется q блоков
по m записей т.о., что q  N  mq .
Блок k ( 0  k  q  1) содержит от 0 до m эл-тов pi , для к-рых
f ( pi )  k , и еще 2 значения – текущее число эл-тов и  или
номер блока в области переполнения, куда попадает m  1-й
эл-т с таким же значением хеш-функции
0
4
6
2
m
q-1
5
1
При увеличении m (и соответствующем уменьшении q )
происходит более равномерное распределение записей по
блокам.
Кнут: при m  50 и заполненности таблицы
N
 0.9 среднее
mq
число чтений блоков составляет 1.04.
Недостатки хеширования:
- недостаточная приспособленность к динамическим
изменениям (число эл-тов таблицы должно быть известно
заранее, удаление эл-тов повышает трудоемкость поиска),
- высокая трудоемкость в наихудшем,
- поиск только по совпадению ключей.
Информационные деревья
Деревья в теории графов и теории алгоритмов
Деревья как информационные структуры (списки):
-  некоторая начальная вершина – корень дерева,
- каждой вершине соот-ет, по кр.мере, одно значение ключа,
- каждая вершина имеет от 0 до m  2 ссылок на другие
вершины (макс. число ссылок m – степень дерева).
Случайное бинарное дерево ( m  2 , 1 значение  вершины)
Строится по заданному набору ключевых значений без их
предварительной обработки (случайно выбранные значения).
1-е значение помещается в корень, для каждого последующего
выделяется новая вершина. Поиск места для новой вершины
начинается от корня и проводится по определенному правилу.
Пример: дерево, построенное по значениям 7, 3, 1, 11, 6, 8, 4.
7
3
1
11
6
8
4
Корень, сыновья вершины, листья (вершины без потомков).
Левое и правое поддерево вершины.
Уровень вершины – длина пути от корня до вершины.
Глубина (высота) дерева – длина максимального пути.
Правило формирования бинарного дерева:
 вершины x все вершины его левого поддерева содержат
значения  x , все вершины правого –  x .
Добавление эл-тов и поиск в дереве начинаются с корня и
производятся в соответствии с правилом форм-ния дерева.
Добавляемая вершина всегда будет листом дерева.
7
7
7
7
3
3
3
1
1
7
3
1
7
11
6
11
7
3
1
11
6
8
3
1
11
6
4
8
Поиск вершины является составной частью всех операций над
деревом. Трудоемкость поиска вершины опр-ся длиной пути от
корня до данной вершины.
Наихудший вариант бинарного дерева с n вершинами –
вырождение в линейный список: длина пути составляет
lworst (n)  n и lmid (n)  n / 2 .
Наилучший вариант – идеальное (оптимальное, идеально
сбалансированное) дерево, для которого выполняется:
- число вершин в поддеревьях одной вершины отличаются  1,
или
- в дереве заполнены все уровни, кроме, м\б, последнего.
При заданном числе вершин n идеальное дерево имеет
минимальную высоту среди всех бинарных деревьев.
Наихудший по трудоемкости поиска случай для идеального
дерева: заполнены все уровни (т.е. построено максимальное
число вершин максимального уровня)
уровень
число вершин
1
2
3
20  1
21
22
k 1
k
2k 2
2 k 1
Общее число вершин n 
k 1
 2i  2 k  1.
i0
Максимальная длина пути в полном идеальном дереве:
lworst (n)  k  log(n  1) .
Средняя длина пути в полном идеальном дереве:
l mid (n) 



1
0
1
2
k 2
k 1
1

2

2

2

3

2

...

(
k

1
)
2

k

2

k
2 1

1
k 1
k 1
k 2
k 1
k 1
(
1

2

...

2
)

(
2

4

...

2
)

(
2

2
)

2

k
2 1
1
 k
(2 k  1)  (2 k  21 )  ...  (2 k  2 k  2 )  (2 k  2 k 1 ) 
2 1
k  2 k  2 k  1 (k  1)2 k  1


 k  1  log(n  1)  1  l worst (n)  1
k
k
2 1
2 1



В общем случае идеальное дерево с k уровнями содержит
2 k 1  n  2 k  1 вершин, и оценки длин путей сохраняются:
lworst (n)  log(n  1) , lmid (n)  lworst (n)  1.
Построение идеального дерева (сортировка набора ключей).
Недостатки идеального дерева
Средняя длина пути в случайном дереве
Предположим, что n ключевых значений были отсортированы,
но при построении дерева выбирались случайным образом.
Если корень содержит эл-т с номером i , то в его левое
поддерево попадают эл-ты 1...i  1, а в правое – i  1...n .
=>
Средняя длина пути в дереве с n вершинами и корнем i :
i 1
ni
li 1 
ln  i , где li 1 – это средняя длина пути
n
n
i 1
для левого поддерева, а
– вероятность попадания в левое
n
ni
поддерево (аналогично ln  i и
для правого поддерева).
i
ln(i )  1 
Усредняем по всем возможным значениям i  1...n и получаем
среднюю длину пути в случайном бинарном дереве:
1 n (i ) 1 n  i  1
ni

ln   ln   1 
li 1 
ln  i  
n i 1
n i 1
n
n

1 n
2 n 1
 1  2  (i  1)li 1  (n  i )l n  i   1  2  ili
n i 1
n i0
Очевидно, что l0  0 , l1  1 и l 2  1.5 .
Для n  3 на основе мат. индукции можно показать, что
ln  2 ln n  2 ln 2  log n  1.39 log n .
Средняя трудоемкость построения случайного дерева:
Tbuild (n)  nln  O(n log n) .
Удаление эл-тов из случайного бинарного дерева
Пусть требуется удалить вершину со значением d .
Если эта вершина – лист, то нужно просто удалить ссылку на
нее в родительской вершине p .
Если удаляется не лист, то в дереве необходимо найти
вершину-замену и перестроить дерево, не нарушая правила
его формирования.
Пусть оба поддерева вершины d не пустые, {L} – мн-во
значений вершин левого поддерева, l max – макс.эл-т левого
поддерева, {R} – мн-во значений правого поддерева и rmin –
мин.эл-т правого поддерева.
Тогда {L}  lmax  d  rmin  {R}, и заменой d могут быть l max
или rmin . Но если в дереве допустимы одинаковые значения (в
левом поддереве), то d должно заменяться на l max .
Варианты удаления вершины со значением d (штриховые
стрелки показывают правило замены значения d , красным
отмечены вершины, которые исключаются из дерева):
лист
1 поддерево
2 поддерева
p
p
p
p
d
d
d
d
l
l
f
Вариант 1
c
Вариант 2
Структура, представляющая вершину дерева, должна
содержать, по крайней мере, 3 поля:
- значение (val),
- 2 ссылки на сыновей (left, right).
Такую же структуру выгодно использовать вместо начального
указателя на корень дерева. Пусть root – имя
соответствующей переменной, тогда можно положить:
root.val = MAX_VAL; // макс.возможное значение ключа
root.right = NULL;
// не используется
root.left = адрес_корня; // NULL, пока дерево пустое
Ниже в алгоритмах используются переменные pp, pd, pa –
указатели на данную структуру.
Поиск вершины d (указатель pd) и ее родительской
вершины (указатель pp):
pp = &root; pd = pp->left;
while (pd != NULL && pd->val != d) {
pp = pd;
pd = (d > pd->val)? pd->right : pd->left;
}
if (pd != NULL) вершина_найдена; // pp - родитель
else вершина_не_найдена;
Удаление вершины d после ее успешного поиска
(получены значения указателей pd и pp):
if (pd->left==NULL && pd->right==NULL) {//лист
if (pd == pp->left) pp->left = NULL;
else pp->right = NULL;
}
else if (pd->left==NULL) {// только пр.поддерево
if (pd == pp->left) pp->left = pd->right;
else pp->right = pd->right;
}
else if (pd->right==NULL) {//только лев.поддерево
if (pd == pp->left) pp->left = pd->left;
else pp->right = pd->left;
}
else {
// 2 поддерева
pa = pd;
// запоминаем вершину d
pp = pd; pd = pd->left;
// поиск замены
while (pd->right != NULL) {
pp = pd; pd = pd->right;
}
if (pd == pp->left)
// вариант 1
pp->left = pd->left;
else
// вариант 2
pp->right = pd->left;
pa->val = pd->val;
// перенос значения
}
delete(pd);
Сбалансированные деревья (АВЛ-деревья)
Условие АВЛ-дерева: для любой вершины высоты ее левого и
правого поддерева отличаются не более, чем на 1.
Наихудший случай – деревья Фибоначчи (для всех вершин
высоты их поддеревьев отличаются на 1):
T0 – пусто, T1 – одна вершина (корень),
Tk – корневая вершина с поддеревьями Tk 1 и Tk  2 .
Деревья Фибоначчи имеют минимальное число вершин при
заданной высоте среди всех сбалансированных деревьев:
n0  0 , n1  1, nk  nk 1  nk  2  1 – числа Леонарда.
nk растут быстрее, чем числа Фибоначчи:
nk
f
 k   , но
nk 1 f k 1
при росте k этим различием можно пренебречь.
=>
При переходе к дереву Фибоначчи следующего уровня число
его вершин увеличивается не менее, чем в   1.62 раз.
Следовательно, любое сбалансированное дерево с n
вершинами имеет высоту (трудоемкость поиска в наихудшем)
 log n  log 2  log n  1.44 log n .
Доказано, что средняя трудоемкость поиска в АВЛ-дереве с n
вершинами  1.05 log n (немного хуже, чем в идеальных).
Важная особенность: для сбалансированных деревьев
существуют
эффективные
алгоритмы
восстановления
сбалансированности при добавлении и удалении элементов.
Добавление вершин к АВЛ-дереву
Состоит из 2 шагов:
- добавление вершины, как к случайному дереву,
- проверка и восстановление сбалансированности.
Добавление новой вершины всегда увеличивает высоту
какого-то поддерева (хотя бы у родительской вершины), но не
всегда нарушает сбалансированность.
Проверку сбалансированности нужно проводить только для
вершин, лежащих на пути от новой вершины до корня. Для ее
реализации
необходимо
добавить
в
структуру,
представляющую вершины дерева, еще 2 поля:
parent – ссылка на родительскую вершину,
balance – показатель сбалансированности (разность между
высотой правого и левого поддеревьев, в норме -1, 0 или 1,
при нарушениях -2 или 2).
Примеры изменения balance при добавлении вершин 10, 3, 5:
исходное
+ 10
8(-1)
8(0)
/
\
/
\
4(0) 9(0)
4(0) 9(1)
/
\
/
\
\
2(0) 6(0)
2(0) 6(0) 10(0)
+3
8(-2)
/
\
4(-1) 9(0)
/
\
2(1) 6(0)
\
3(0)
+5
8(-2)
/
\
4(1) 9(0)
/
\
2(0) 6(-1)
/
5(0)
Очевидно, что для новой вершины balance=0, а для
остальных вершин на пути к корню balance изменяется по
след.правилам:
- уменьшается на 1 при переходе из левого поддерева,
- увеличивается на 1 при переходе из правого поддерева.
Если для некоторой вершины V новое значение balance=0,
то это означает, что общая высота поддерева с корнем V не
изменилась.
=>
Для всех вершин, лежащих на пути от V к корню значение
balance не изменится, поэтому и проверять их не надо.
Ниже приведены алгоритмы добавления новой вершины
(листа) и последующей проверки сбалансированности. Для
доступа к вершинам дерева используются указатели.
Добавление новой вершины (листа) со значением a :
pp = &root;
// фиктивный отец корня
pa = pp->left;
// корень АВЛ-дерева
while (pa != NULL) {
pp = pa;
pa = (a > pa->val)? pa->right : pa->left;
}
pa = new(тип_для_вершины);
pa->val = a;
pa->left = pa->right = NULL;
pa->balance = 0;
pa->parent = pp;
// pp – отец новой вершины pa
if (a > pp->val) pp->right = pa;
else pp->left = pa;
Пересчет и проверка сбалансированности от a до корня
(на вершину a указывает pa):
pp = pa->parent;
// pp – родительская вершина
while (pp != &root) {
pp->balance += (pa == pp->left)? -1 : 1;
if (pp->balance == 0) break;
if (abs(pp->balance) >= 2) break;
pa = pp; pp = pp->parent;
}
if (pp == &root || pp->balance == 0)
сбалансированность_не_нарушена;
else
сбалансированность_нарушена;
//pp – вершина с нарушенным балансом
//pa – сын pp (корень поддерева,содержащего a )
Для восстановления сбалансированности выполняются
повороты АВЛ-дерева отн-но вершины, в которой баланс
нарушен.
Ниже показано, как выполняются повороты, если добавление
вершины x увеличивает высоту левого поддерева вершины D
и приводит к нарушению баланса в D ( LL - и LR -повороты).
Изображенные на рисунках поддеревья 1-6 имеют одинаковую
высоту k  0 . Для обрабатываемого поддерева вершины P
указана его высота до и после включения x , а также после
поворота. x может добавляться только в одно из поддеревьев
1-4, но на рисунках приводятся все возможные случаи.
Выделены вершины, в которых изменяются ссылки left,
right и\или parent. Очевидно, что для всех вершин от x до
P пересчитывается значение balance.
LL-поворот:
P
P
k  3 (исх.)
k  4 ( x)
k  3 (= исх.)
D
B
B
E
C
A
5
C
6
1
1
2
3
E
2
3
4
x
x
D
A
x
x
По условию формирования бинарного дерева:
{1}  A  {2}  B  {3}  C  {4}  D  {5}  E  {6}.
4
5
6
LR-поворот:
P
P
k  4 ( x)
k  3 (исх.)
k  3 (=исх.)
D
C
B
B
E
C
A
5
D
A
6
E
3
1
2
3
x
4
1
2
4
5
x
6
x
x
При LR -повороте у корневых вершин деревьев 3 и 4 также
изменяются ссылки на родительскую вершину (parent).
После перебалансировки (поворота) высота изменяемого
поддерева вершины P становится такой же, как до
добавления x . Поэтому значения balance для вершин от P
до корня дерева уже не изменятся, и дальнейших проверок не
требуется.
Для симметричных относительно D вариантов (правое
поддерево на 1 выше левого, новая вершина x попадает в
правое поддерево и увеличивает его высоту) используются
симметричные к указанным выше RR - и RL -повороты.
Добавление вершины x к сбалансированному дереву требует
в наихудшем O(logn) шагов на поиск положения x , O(logn)
на проверку сбалансированности и O (1) на поворот дерева.
Экспериментально установлено, что на 2 добавления вершин
приходится в среднем 1 поворот.
Удаление вершин из АВЛ-дерева
Состоит из 2 шагов:
- удаление вершины, как в случайном дереве,
- проверка и восстановление сбалансированности от низа
дерева (вершины-замены) до корня включительно.
Удаление вершины всегда уменьшает высоту какого-то
поддерева (хотя бы у родительской вершины), но не всегда
нарушает сбалансированность.
Для всех вершин на пути к корню значение balance:
- увеличивается на 1 при переходе из левого поддерева,
- уменьшается на 1 при переходе из правого поддерева.
Пусть для некоторой вершины V новое значение balance
равно 1 или -1. Это означает, что старым значением для V
было balance=0.
=>
Высоты поддеревьев V до удаления были равны, а после
удаления высота одного поддерева уменьшилась на 1, а
другого – не изменилась.
=>
Общая высота дерева с корнем V не изменилась.
=>
Для всех вершин, лежащих на пути от V к корню значение
balance не изменится, поэтому и проверять их не надо.
Перебалансировка производится с помощью LL -, LR -, RR - и
RL -поворотов (в среднем 1 раз на 5 удаленных вершин).
Пример удаления вершины E (для проверяемых вершин
указаны значения balance):
A
исходное дерево
удалена вершина E
E (1)
D(1)
C (1)
K (1)
B D(0)
I (1) M
G J
FH
L N
C (2)
K (1)
B(1)
A
I (1) M
G J
FH
L N
после LL -поворота в C
после RL -поворота в D
I (0)
D(2)
B(0)
D(0)
K (1)
A C ( 0)
I (1) M
G J
L N
B(0)
A C
G
FH
K (1)
J
M
L N
FH
Для всех вершин, в которых при поворотах изменяются
указатели left и\или right, должно пересчитываться
значение balance.
После RL -поворота в вершине I balance=0, поэтому
проверка продолжается от I до корня.
Красно-черные деревья (лучше сбалансированных в
перестроениях, немного похуже в поиске).
Прошитые деревья (2 типа дополнительных ссылок).
Преимущества бинарных деревьев перед упорядоченными
массивами и хеш-таблицами:
- динамические структуры,
- позволяют проводить поиск в диапазоне, выделять min и max,
- позволяют найти предыдущее\следующее значение ключа и
соответствующие объекты.
Недостатком бинарных деревьев явл-ся высокая трудоемкость
поиска вершин при хранении деревьев на ВУ: в среднем
O(log N ) чтений файла с произвольных позиций.
Разделение идеального дерева на блоки
Идеальное дерево с N  2  1 вершинами делится на блоки
m
(страницы), содержащие по n  2  1 вершин. Чтение из
файла производится целыми страницами – в наихудшем
нужная вершина находится за k / m обращений к файлу.
k
Непригодность данного метода для неидеальных деревьев:
- большие затраты памяти на пустые страницы,
- неприспособленность к динамическим изменениям.
B-деревья (сильно ветвящиеся деревья)
B-дерево порядка k  1 строится по следующим правилам:
- все значения в дереве различные,
- каждая вершина занимает 1 блок (страницу),
- каждая вершина может содержать от k до 2k упорядоченных
по возрастанию значений (от 1 до 2k значений для корня),
- каждая вершина с m значениями содержит m  1 ссылку на
вершины-потомки (у листьев эти ссылки пустые),
- все листья находятся на одном уровне и не имеют
потомков.
Структура вершины В-дерева (  i  1...m  1 : xi  xi 1)
x1
s1
 x1
x2
s2
 x1 &  x2
...
s3
 x2 &  x3
xm 1
xm
sm
 xm1 &  xm
sm1
 xm
Поиск вершины со значением p в В-дереве начинается с
корня В-дерева. Если текущая вершина (страница) не
содержит p , то следующая вершина выбирается по ссылке,
соответствующей диапазону значений, в к-рый попадает p :
p  x1 – переход по s1 ,
xi 1  p  xi , 1  i  m – переход по si ,
p  xm – переход по sm1 .
Добавление значения к В-дереву
Новое значение всегда должно включаться в соответствующий
лист.
Если данный лист содержит  2k значений, то новое значение
просто добавляется к листу с соблюдением условия
упорядоченности.
Если же лист уже содержит 2k значений, то производится его
сбалансированное расщепление:
- 2k  1 значений ( 2k старых + 1 новое) сортируются по
возрастанию,
- k начальных значений формируют один новый лист (левый),
k конечных – другой (правый), а медианное значение
выталкивается наверх, в родительскую вершину.
c
a
c
d
f
+ b => { a , b , c , d , f } =>
a
b
d
f
Если родительская вершина изменилась (в нее было
добавлено новое значение), то она должна обрабатываться
так же, как начальный лист. Процесс расщепления вершин
может продолжаться вплоть до корня В-дерева.
Если происходит расщепление корневой вершины, то
образуется новый корень (содержащий 1 значение), то есть
В-дерево растет корнем вверх.
Пример построения В-дерева порядка 2 (красным указаны
добавляемые значения, в фигурных скобках – множества
значений, используемых при расщеплении вершин):
+3
3
+ 1, 6, 12
1
3
6
12
6
+ 17 => { 1, 3, 6, 12, 17 } =>
1
3
12
17
6
+ 2, 5, 8,10
1
2
3
5
8
+ 15 => { 8, 10, 12, 15, 17 } =>
1
2
3
5
3
+ 4 => { 1, 2, 3, 4, 5 } =>
1
2
4
6
5
10
12
6
12
8
10
17
15
17
12
8
10
15
17
3
+ 7, 11, 14, 16
1
2
4
5
+ 9 => { 7, 8, 9, 10, 11 } =>
1
2
4
5
1
2
6
4
5
8
10
11
3
6
9
12
7
7
12
7
8
+ 13 => { 13, 14, 15, 16, 17 }
=> { 3, 6, 9, 12, 15 } =>
3
6
10
14
11
15
14
15
16
17
16
17
16
17
9
8
10
11
12
15
13
14
Удаление значения v из В-дерева
Если v находится в листе, то v удаляется непосредственно
оттуда.
Если v находится в вершине более высокого уровня W , то
нужно найти в дереве замену – максимальное значение,
меньшее v . Такое значение всегда располагается в некотором
листе L (пусть это значение t , как на рисунке). t должно
заменить v в W , а затем t нужно удалить из L .
…
Вершина W
f
g
h
f<g<h<v
k
l
лист L
v
…
m
h<k<l<m<v
r
s
t
m<r<s<t<v
При удаления значения из некоторого листа L возможны 2
случая:
1. L содержит  k значений. Необходимо просто удалить
нужное значение, сохраняя упорядоченность оставшихся.
2. L содержит ровно k значений. В этом случае нужно
объединить остающиеся значения из L со значениями правого
или левого соседа L (листа R ), и тем значением p из
родительской (для L и R ) вершины, для которого выполняется
условие {L}  p  {R} или {R}  p  {L} – в зависимости от
взаимного расположения L и R .
родитель
d
f
L
m
n
p
z
u
v
{ m, p, u, v, w, x }
w
x
R
Пусть множество {L, R, p} содержит j значений.
=>
1. Если j  2k , то медианный элемент перемещается в
родительскую вершину, а из значений меньших и больших
медианного формируются новые вершины L и R .
{ m, p, u, v, w, x }
родитель
d
f
L
m
p
u
z
v
w
x
R
2. Если j  2k , то из данных значений нужно сформировать
одну новую вершину L , удалить старую вершину R , и
провести с родительской вершиной такие же проверки, как с L
(родительская вершина изменилась, т.к. из нее было удалено
значение p ).
{ m, p, u, v }
родитель
…
…
d
f
z
L
m
p
u
v
Процесс удаления вершин из В-дерева демонстрируют
рисунки из пункта «Построение В-дерева» – в обратном
порядке.
Наихудшим для В-дерева порядка k с N значениями является
случай, когда все вершины содержат по k значений и k  1
ссылке (а также 1 значение и 2 ссылки в корне). При этом
память используется на 50%, а высота дерева (трудоемкость
поиска в наихудшем) равна logk 1 N   1.
Например, при k  100 (относительно небольшие страницы)
6
высота В-дерева, содержащего 10 значений, составляет
всего 4 в наихудшем.
При большом числе операций поиска выгодно хранить верх
В-дерева в оперативной памяти.
Простейший вариант – В-деревья порядка 1 или 2-3-деревья
– используются при работе с динамическими объектами в ОП
и являются альтернативой сбалансированным деревьям (2-3деревья немного лучше в перестроениях, но похуже в поиске).
Вершины 2-3-дерева можно хранить как вершины бинарного
дерева, в которых правая ссылка указывает либо на вершину
следующего уровня, либо на соседнюю вершину по
горизонтали (должен храниться и тип ссылки):
a
a
b
 a&  b
a
b
Другие типы деревьев:
- деревья оптимального поиска,
- декартовы, тетра- и R-деревья.
a
 a&  b
b
b
Абстрактные типы данных:
внутренняя структура и конкретная реализация скрыты от
пользователя, представление в виде интерфейсов.
Список (list): head, tail, next, previous.
Стек (stack): push, pop.
Очередь (queue): push, pop.
Ассоциативный массив (словарь, map) – хранит
[уникальный ключ, значение]: insert, find, remove, each.
пары
Очередь с приоритетом (priority queue) – хранит пары [ключ,
значение]: insert, min, extract_min, change_key.
Информационные таблицы
Записи, атрибуты, ключевые и неключевые поля.
Поиск – основа операций добавления, удаления и
модификации записей.
3 типа запросов на поиск:
- по конкретным значениям одного или нескольких атрибутов,
- по диапазонам значений,
- по наборам значений с использованием логических операций.
Варианты организации поиска по ключевому полю
Сортировка записей.
Хеш-таблица.
Разреженный индекс - <макс.значение в блоке, ссылка на
начало блока>, главный файл отсортирован. Покрытие ключа
и поиск – линейный, двоичный, вычислением адреса.
Плотный индекс - <значение ключа, ссылка на запись>.
Реализация на основе упорядоченного файла или В-дерева.
Поиск по неключевому полю (поиск по частичному
соответствию): записи специфицированы не полностью,
результатом всегда может быть множество записей.
Можно использовать аналогичные стр-ры данных, но их выбор
ограничен тем, как организован поиск по ключам.
Вторичный индекс - <значение атрибута, список ссылок на
записи\блоки\значения ключа> и формирование на его основе
инвертированного файла (значения атрибута упорядочены и
встречаются только 1 раз).
Многоаспектный поиск (поиск по совокупности ключей).
Варианты:
1. Индексы для каждого ключа; сокращение множества
выбранных записей последовательным выбором по ключам A,
B, C…
2. Индексы для каждого ключа; пересечение множеств
записей, полученных при поиске по отдельным ключам.
3. Построение индексных
ключей (плотный индекс).
файлов
по
совокупности
Если всего используется m ключей, из них в запросах ровно k ,
то нужно C mk индексов (например, для ключей A, B, C при
m  3 и k  2 нужны индексы по AB , AC и BC ).
Если есть m ключей, а запросы произвольны, то требуется
1
Cm
 Cm2  ...  Cmm  2 m  1 индексов (например, 7 индексов для
A, B, C – A, B, C , AB, AC , BC , ABC ).
Метод сокращения их числа: ABC включает также A и AB ,
BC включает B , CA  AC включает C , поэтому достаточно
построить индексы по совокупностям ключей ABC , BC , CA .
Сортировка по нескольким ключам аналогична сортировке
строк:
- производится последовательно по каждому отдельному
ключу, начиная от последнего (правого),
- продолжается для последующих ключей таким образом,
чтобы не нарушалась упорядоченность по предыдущим (т.е.
сортировка должна быть устойчивой).
Операции над таблицами
Варианты реализации проекции и естественного соединения:
- сортировка,
- хеш-таблицы,
- инвертированные файлы.
Реализация запроса на поиск с помощью естественного
соединения (каждой строке исходной таблицы соответствует
не более одной строки таблицы-запроса).
Информационно-поисковые системы (ИПС)
Имеется N поисковых объектов (статей, документов и т.д.),
каждый объект характеризуется набором из 5-10 ключевых
слов (полей для поиска). Общее число m ключевых слов в
ИПС может достигать 1000 и более.
Типичным для ИПС является включающий поиск (мн-во
ключевых слов запроса  мн-ва ключевых слов документа).
Фактически каждое ключевое слово – это отдельный атрибут,
который в любом документе равен 1 (присутствует) или 0
(отсутствует).
Если m не слишком велико, то можно каким-то образом задать
порядок для всех ключевых слов и для каждого документа
хранить битовую строку длины m , в к-рой 1 в определенной
позиции отмечает присутствие определенного ключевого
слова. Запрос на поиск также задается битовой строкой, а
нужные документы выбираются на основе логического
умножения строк запроса и документа.
Если m велико, то наилучшим выходом будет построение
инвертированного файла по всем значениям ключевых слов.
Поиск цепочек символов
Текстовые редакторы, трансляторы, антивирусные программы
Алфавит  – мн-во знаков (символов), из к-рых строятся
цепочки.  – мощность алфавита.
Цепочки (строки) образуются с помощью конкатенации:
a, b   => ab   2 , ab  2 – мн-во цепочек длины 2.
 n – мн-во цепочек  длины n :  n   n 1 ,    n ,   n .
 единственная пустая цепочка  ,   0 .
{}     2  3  ...  * – итерация алфавита.
Основные понятия и обозначения
Цепочка (строка, текст) длины n  0 :
T n  T1T2 ...Tn – конкатенация n символов алфавита (Ti   ,
T n  * ). Для простоты изложения будем индексировать
символы в строке от 1 (в том числе, и в программах).
Префикс (длины k ) строки T n :
R k [ T n , 0  k  n , R k  T1T2 ...Tk – k начальных символов T n
(можно обозначить просто T k ).
Суффикс (длины j ) строки T n :
S j ] T n , 0  j  n , S j  Tn  j 1Tn  j  2 ...Tn – j конечных
символов T n .
Задача поиска подстроки
Даны строка (текст) T n и подстрока (образец) P m , m  n ,
T n , P m  * .
Найти все случаи вхождения P m в T n , т.е. все значения s  0 ,
для которых выполняется:
Ts 1Ts  2 ...Ts  m  P1P2 ...Pm .
Величины s , определяющие, какие символы строки T n будут
сравниваться с символами подстроки P m , будем называть
сдвигами ( P m относит-но T n ). Очевидно, что 0  s  n  m .
Сдвиги, соответствующие вхождениям P m в T n , называются
допустимыми.
Простейший алгоритм:
последовательно проверить условие Ts 1Ts  2 ...Ts  m  P1P2 ...Pm
для всех сдвигов 0  s  n  m .
for (s = 0; s <= n-m; s++) {
for (i=s+1, j=1; j<=m && T[i]==P[j]; j++,i++);
if (j > m) найдено_вхождение;
}
Основной недостаток данного алгоритма – постоянный сдвиг
подстроки на 1, независимо от результатов сравнения
символов. Трудоемкость наиболее высока при малой
мощности алфавита (много совпадений символов P m и T n ) и в
наихудшем составляет Om(n  m) .
Алгоритм Бойера-Мура (вариант с таблицей сдвигов)
Основная идея: сравнивать пары символов T и P от конца
подстроки и по результатам сравнения исключать заведомо
недопустимые сдвиги.
Пример (красн. стрелками отмечены несовпадения символов)
T a
c
a
b
P a
b
a
c
a
c
a
b
a
c
a
b
a
d
a
a
a
c
a
b
a
c
a
b
a
c
a
b
a
c
c
Символы строки T и подстроки P сравниваются от конца
подстроки. Независимо от того, является ли текущий сдвиг s
допустимым (найдено вхождение) или нет, величину
следующего сдвига определяет символ Ts  m (который
сравнивался с последним символом подстроки Pm ).
Для подстроки P необходимо построить таблицу сдвигов
длины  , в которой каждому символу алфавита соответствует
величина приращения текущего сдвига (на сколько позиций от
текущего положения нужно сдвинуть P ). k -й элемент данной
таблицы соответствует символу char(k ) и равен:
m , если char(k ) не входит в P m 1 (первые m  1 символов
P m );
m  r , где r - позиция самого правого вхождения char(k ) в
P m 1, в противном случае.
Построение таблицы сдвигов D для подстроки P длины m и
стандартного набора из 256 символов:
for (k = 0; k < 256; k++) D[k] = m;
for (k = 1; k < m; k++) D[ord(P[k])] = m – k;
Поиск подстроки P в тексте T длины n :
for (s = 0; s <= n-m; ) {
for (i=s+m, j=m; j>0 && T[i]==P[j]; j--, i--);
if (j == 0) найдено_вхождение;
s += D[ord(T[s+m])];
}
Трудоемкость в наилучшем: O(   n / m) .
Трудоемкость в наихудшем: O   m(n  m) .
Алгоритм наиболее пригоден для большого входного
алфавита и нечастого появления подстроки в тексте.
Поиск подстроки с помощью конечного автомата
КА – это пятерка ( Q, q0 , A, ,  ), где
Q - конечное множество состояний,
q0  Q - начальное состояние,
A  Q - множество допускающих состояний,
 - входной алфавит,
 - функция переходов ( Q    Q ).
строки-образца P m определим суффикс-функцию
 : *  {0,1,2,..., m}: для произвольной строки    *
значение  ( ) равно длине максимального суффикса  ,
являющегося префиксом P , т.е.
Для
 ( )  max{k : P k ] }.
Примеры значений суффикс-функции для P  aabab:
 ( )  0 ,  (abcaaba)  4 ,  (abca)  1,  (aababb)  0 .
Определим мн-во состояний КА для распознавания P m :
Q  {0,1,2,..., m}, q0  0 , A  {m}.
КА просматривает символы входной строки T n слева направо.
Если после проверки символа Ti текущее состояние равно q ,
то это означает, что ровно q последних проверенных
символов T совпадают с начальными символами P :
P1P2 ...Pq  Ti  q 1Ti  q  2 ...Ti , т.е. P q ] T i .
По определению суффикс-функции из этого следует:
 (T i )   ( P q )  q .
КА находится в состоянии q , проверяет очередной символ Ti 1
и переходит в новое состояние q1   (T i 1 )   ( P qTi 1 ) .
Используем это условие для задания функции переходов КА:
 (q, x)   ( P q x) ,  q  {0,..., m}, x   .
Примеры переходов для P  aabab (текущее состояние q  4 ,
анализируемые символы выделены красным):
…
с
a
a
b
a
a
…
 (4, a)  2
…
с
a
a
b
a
b
…
 (4, b)  5
…
с
a
a
b
a
c
…
 (4, c)  0
Таблица переходов и КА для   {a, b, c} и
(переходы в состояние 0 не указаны):
a b c
a
a
0 1 0 0
1 2 0 0
0
a
1
a
2
b
3
2 2 3 0
3 4 0 0
4 2 5 0
5 1 0 0
P  aabab
a
a
4
b
5
Построение таблицы переходов D для произвольной
подстроки P m и алфавита  :
for (q = 0; q <= m; q++)
foreach a  {
for (k = min(m, q+1); k > 0; k--) {
q
// P k является суффиксом P a ?
if (P[k] != a) continue;
for (i = k-1, j = q; i > 0; j--, i--)
if (P[i] != P[j]) break;
if (i == 0) break;
} D[q][a] = k;
q
// P k - суффикс P a
}
Трудоемкость данного алгоритма составляет O(m 3  ) ,
размер таблицы – (m  1)  элементов.
Поиск всех вхождений подстроки P m в строку T n после
построения таблицы переходов D :
for (q = 0, i = 1; i <= n; i++) {
q = D[q][T[i]];
if (q == m) найдено_вхождение(i–m);
}
Трудоемкость поиска подстроки составляет O(n) .
Наиболее выгодно использовать КА для распознавания
подстрок, если входной алфавит содержит мало символов и
длина подстроки-образца m невелика.
Алгоритм Кнута-Морриса-Пратта
Алгоритм КМП похож на алг-м распознавания с помощью КА,
но позволяет избавиться от главного недостатка последнего –
высокой трудоемкости построения таблицы переходов и
затрат памяти на ее сохранение.
В алгоритме КМП строится не КА, а похожая на него машина
распознавания,
которая
может
делать
несколько
последовательных переходов из текущего состояния при
анализе очередного символа текста.
Рассмотрим пример распознавания подстроки P  aabaab
(выделены совпадающие символы текста и P , текущее
состояние q  5, анализируемый символ Ti 1 – красный):
6
Ti
…
с
Ti 1
a
a
b
a
a
a
a
a
b
a
a
b
b
…
Pq
P q ] T i , но Ti 1  Pq 1 , поэтому машина не может перейти в
q  1, а должна перейти в новое состояние q1  q (т.е. P
сдвинется вправо относительно T ). Используя уже
проверенную P q , перейдем в наибольшее возможное
q
состояние q1  q , для к-го P 1 ] T i , и сравним Ti 1 и Pq1 1 :
Ti
…
с
a
a
b
Ti 1
a
a
a
b
…
a
a
b
a
a
b
P q1
Ti 1  Pq1 1, повторяем попытку для макс-ного q2  q1 , P q2 ] T i :
Ti
…
с
a
a
b
Ti 1
a
a
a
b
…
a
a
b
a
a
b
P q2
Ti 1  Pq2 1: переходим в сост-е q 2  1, символ Ti 1 проверен.
Отметим, что текущий анализируемый символ Ti 1 никак не
влияет
на
выбор
следующего
состояния
машины
распознавания: в состоянии q (для к-рого P q ] T i ) выбирается
q
максимальное состояние q1  q , для к-рого P 1 ] T i .
q
При этом P 1 [ P q , т.е. P
q1
является максимальным по длине
префиксом P q , который совпадает с суффиксом P q . Отсюда
следует, что очередное состояние определяется только
подстрокой P и никак не зависит от проверяемого текста T .
Для поиска нужного состояния при распознавании подстрокиобразца P m заранее вычисляется целочисленная префиксфункция (функция отказов):
 (q)  max{k : 0  k  q & P k ] P q }, 1  q  m .
6
Таблица значений  (q ) для подстроки P  aabaab :
q
1
2
3
4
5
6
 (q )
0
1
0
1
2
3
6
Машина распознавания подстроки P  aabaab :
0
a
1
a
2
b
3
a
4
a
5
b
Последовательность состояний при анализе строк:
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…
6
При анализе очередного символа Ti 1 возможен возврат через
несколько состояний.
Если текущее состояние q  0 , то можно сделать
переходов в предыдущие состояния.
q
Но если машина распознавания находится в состоянии q ,
должно выполняться условие P1P2 ...Pq  Ti  q 1Ti  q  2 ...Ti .
=>
При анализе символов Ti  q 1 ,...,Ti было сделано ровно q
переходов в последующие состояния (по одному переходу на
каждый символ, без переходов в предыдущие).
=>
На каждый проверяемый символ текста приходится в среднем
O (1) переходов.
Вычисление префикс-функции F для подстроки P m :
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 можно сделать максимум k  1 шаг, но это
возможно, если до этого был выполнен k  1 шаг цикла for, на
которых цикл while вообще не выполнялся.
=>
Общая трудоемкость данного алгоритма составляет O(m) .
Поиск всех вхождений подстроки P m в строку T n после
вычисления префикс-функции F :
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];
}
}
Рассуждая по аналогии с алгоритмом вычисления F , можно
сделать вывод, что трудоемкость данного алгоритма  O(n) .
=>
Общая трудоемкость алгоритма КМП составляет O(m  n) и не
зависит от количества символов в алфавите.
Вариант алгоритма Бойера-Мура с вычислением функций
стоп-символа и безопасного суффикса (Кормен).
Алгоритм Рабина-Карпа
При поиске подстроки P m в строке T n необходимо найти все
допустимые сдвиги, т.е. такие значения s , 0  s  n  m , для
которых Ts 1Ts  2 ...Ts  m  P1P2 ...Pm .
Основная идея алгоритма:
- преобразовать подстроку P m и сравниваемую с ней часть
текста Ts 1...Ts  m в целые числа, заданные m цифрами в
системе счисления с основанием  (обозначим эти числа,
соответственно, p и t s );
- сравнивать не P m и Ts 1...Ts  m , а p и t s (одно сравнение
чисел вместо O(m) сравнений символов);
- обеспечить быстрое вычисление числа t s 1 , соответствущего
сдвигу s  1, преобразованием значения t s .
Пусть d   . Тогда:
p  P1d m1  P2 d m 2  P3 d m 3  ...  Pm1d  Pm
t s  Ts 1d m1  Ts  2 d m 2  Ts  3 d m 3  ...  Ts  m1d  Ts  m
Сдвиг s является допустимым, если p  t s .
Для вычисления p не требуется знание строки T . Используя
схему Горнера, можно получить p за время O(m) :
p  (...((P1d  P2 )d  P3 )d  ...  Pm1 )d  Pm
Также за O(m) можно вычислить t 0 :
t0  (...((T1d  T2 )d  T3 )d  ...  Tm1 )d  Tm
Значения t s 1 и t s связаны рекуррентным соотношением:
t s 1  (t s  Ts 1d m1 )d  Ts  m1
m 1
Если заранее получить константу d
(за время O(m) ), то t s 1
будет вычисляться по известному t s за O (1) ЭШ, а проверку
всего текста T n можно провести за O(n) .
К сожалению, числа p и t s могут быть очень большими. Если
d  256, то уже при m  5 мы выйдем за пределы
представления 4-байтовых целых чисел. Поэтому отдельные
арифметические операции, на основании которых получены
оценки трудоемкости, нельзя считать элементарными
шагами.
Для преодоления этой трудности нужно перейти к модульной
арифметике: вместо сверхдлинных целых чисел использовать
их остатки от деления по некоторому модулю q .
Если задано некоторое целое q  0 , то любое произвольное
целое x можно единственным образом представить в виде:
x  cq  r , где 0  r  q ( c - частное, r - остаток).
Для любых целых x, y, z выполняется равенство:
( xy  z ) mod q  ( x mod q  y mod q  z mod q) mod q ,
т.е. операцию деления по модулю можно вносить в скобки и
применять к отдельным значениям.
Чтобы исключить переполнение для целых в алгоритме
Рабина-Карпа необходимо выбрать такое q (простое), чтобы
значение dq помещалось в машинное слово, и при расчете
d m 1, p , t 0 и t s 1 постоянно вычислять остатки по mod q :
вместо d
использовать h  d m1 mod q ; при вычислении h
брать остаток после каждого умножения текущего числа на d ,
m 1
p  ((...(P1d  P2 ) mod q  d  ...  Pm1 ) mod q  d  Pm ) mod q ,
t0  ((...(T1d  T2 ) mod q  d  ...  Tm1 ) mod q  d  Tm ) mod q ,
t s 1  ((t s  Ts 1h) mod q  d  Ts  m1 ) mod q .
Приведенные выше оценки трудоемкостей сохранятся, но
теперь все вычисления проводятся с короткими числами, и
каждая арифметическая операция будет одним ЭШ.
Пример вычисления t s 1 для строки из десятичных цифр
( d  10 , q  13, Кормен и др.):
3
1
4
1
7
8
5
2
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;
Если t s mod q  p mod q , то и t s  p . Но из t s mod q  p mod q
еще не следует t s  p . Поэтому при совпадении остатков
необходимо проверить, найдено ли действительно вхождение
подстроки (т.е. выполняется ли Ts 1Ts  2 ...Ts  m  P1P2 ...Pm ) или
произошло холостое срабатывание.
В наихудшем случае (например, если T и P являются
последовательностями из одного символа алфавита) будет
постоянно выполняться t s  p и последующее посимвольное
сравнение. Поэтому трудоемкость в наихудшем составляет
O(m(n  m)) , как в простейшем алгоритме.
Но в большинстве практических приложений совпадений
остатков и холостых срабатываний должно быть немного, и
трудоемкость алгоритма Рабина-Карпа составит O(n  m) .
Поиск всех вхождений P m в T s ( d   ):
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);
}
Возможность использования приведенных алгоритмов для
распознавания набора подстрок.
Download