- •Алгоритмы и алгоритмические языки
- •Лекция 1
- •Представление чисел в эвм
- •Вещественные
- •Ошибки вычислений
- •Лекция 2
- •Алгоритмы. Сведение алгоритмов.
- •Нижние и верхние оценки.
- •Сортировки
- •Постановка задачи
- •Сортировка пузырьком.
- •Сортировка слиянием с рекурсией.
- •Сортировка слиянием без рекурсии.
- •Лекция 3
- •Алгоритмы. Сведение алгоритмов.
- •Сортировки и связанные с ними задачи.
- •Доказательство корректности работы алгоритма.
- •Оценки времени работы алгоритма.
- •Некоторые задачи, сводящиеся к сортировке.
- •Лекция 4
- •Алгоритмы. Сведение алгоритмов.
- •Сортировки и связанные с ними задачи.
- •HeapSort или сортировка с помощью пирамиды.
- •Алгоритмы сортировки за время o(n)
- •Сортировка подсчетом
- •Цифровая сортировка
- •Сортировка вычерпыванием
- •Лекция 5
- •Алгоритмы. Сведение алгоритмов.
- •Порядковые статистики.
- •Поиск порядковой статистики за время (n) в среднем
- •Поиск порядковой статистики за время (n) в худшем случае
- •Язык программирования c.
- •Переменные
- •Структуры данных.
- •Вектор.
- •Лекция 6
- •Стек. Реализация 1 (на основе массива).
- •Стек. Реализация 2 (на основе массива с использованием общей структуры).
- •Стек. Реализация 3 (на основе указателей).
- •Стек. Реализация 4 (на основе массива из двух указателей).
- •Стек. Реализация 5 (на основе указателя на указатель).
- •Очередь.
- •Стандартная ссылочная реализация списков
- •Ссылочная реализация списков с фиктивным элементом
- •Реализация l2-списка на основе двух стеков
- •Реализация l2-списка с обеспечением выделения/освобождения памяти
- •Лекция 7
- •Структуры данных. Графы.
- •Поиск пути в графе с наименьшим количеством промежуточных вершин
- •Представление графа в памяти эвм
- •Массив ребер
- •Матрица смежности
- •Матрица инцидентности
- •Списки смежных вершин
- •Реберный список с двойными связями (рсдс) (для плоской укладки планарных графов)
- •Лекция 8
- •Структуры данных. Графы.
- •Поиск кратчайшего пути в графе
- •Алгоритм Дейкстры
- •Конец вечного цикла
- •Алгоритм Дейкстры модифицированный
- •Конец вечного цикла
- •Лекция 9
- •Бинарные деревья поиска
- •Поиск элемента в дереве
- •Добавление элемента в дерево
- •Поиск минимального и максимального элемента в дереве
- •Удаление элемента из дерева
- •Поиск следующего/предыдущего элемента в дереве
- •Слияние двух деревьев
- •Разбиение дерева по разбивающему элементу
- •Сбалансированные и идеально сбалансированные бинарные деревья поиска
- •Операции с идеально сбалансированным деревом
- •Операции со сбалансированным деревом
- •Поиск элемента в дереве
- •Добавление элемента в дерево
- •Удаление элемента из дерева
- •Поиск минимального и максимального элемента в дереве
- •Поиск следующего/предыдущего элемента в дереве
- •Слияние двух деревьев
- •Разбиение дерева по разбивающему элементу
- •Лекция 10
- •Красно-черные деревья
- •Отступление на тему языка с. Поля структур.
- •Отступление на тему языка с. Бинарные операции.
- •Высота красно-черного дерева
- •Добавление элемента в красно-черное дерево
- •Однопроходное добавление элемента в красно-черное дерево
- •Удаление элемента из красно-черного дерева
- •Лекция 11
- •Высота b-дерева
- •Поиск вершины в b-дереве
- •Отступление на тему языка с. Быстрый поиск и сортировка в языке с
- •Добавление вершины в b-дерево
- •Удаление вершины из b-дерева
- •Лекция 12
- •Хеширование
- •Метод многих списков
- •Метод линейных проб
- •Метод цепочек
- •Лекция 14
- •Поиск строк
- •Отступление на тему языка с. Ввод-вывод строк из файла
- •Алгоритм поиска подстроки с использованием хеш-функции (Алгоритм Рабина-Карпа)
- •Конечные автоматы
- •Отступление на тему языка с. Работа со строками
- •Алгоритм поиска подстроки, основанный на конечных автоматах
- •Лекция 15
- •Алгоритм поиска подстроки Кнута-Морриса-Пратта (на основе префикс-функции)
- •Алгоритм поиска подстроки Бойера-Мура (на основе стоп-символов/безопасных суффиксов)
- •Эвристика стоп-символа
- •Эвристика безопасного суффикса
- •Форматы bmp и rle
- •Bmp без сжатия.
-
Лекция 15
-
Алгоритм поиска подстроки Кнута-Морриса-Пратта (на основе префикс-функции)
Основная проблема алгоритма поиска подстроки, основанного на конечных автоматах – необходимость вычисления функции перехода. Алгоритм Кнута-Морриса-Пратта обходит эту проблему за счет некоторого удорожания, собственно, процесса поиска и существенного сокращения предварительных вычислений.
Основная идея алгоритма следующая. Пусть Sk – подстрока строки S длины k. Пусть нам известно значение функции перехода h(Sk,W) (см. предыдущий параграф). Требуется вычислить значение функции h(Sk+1,W), т.е. найти максимальный префикс W, являющийся суффиксом Sk+1.
Если S[k]==W[h(Sk,W)], то h(Sk+1,W)= h(Sk,W)+1 (как уже отмечалось ранее – больше быть не может, а то, что в этой ситуации h(Sk+1,W) h(Sk,W)+1 – получается по определению). Пример:
char S[]=”ababab”,W[]=”abaa”; int k=4;
h(S,4,W)==2
S : abab ab
W : __ab
h(S,5,W)==3
S : ababa b
W : __aba
Пусть S[k]!=W[h(Sk,W)], то h(Sk+1,W)< h(Sk,W)+1. В приведенном примере:
char S[]=”ababab”,W[]=”abaa”; int k=5;
h(S,5,W)==3
S : ababab
W : __aba
h(S,6,W)==2
S : ababab
W : ____ab
Для вычисления h(Sk+1,W) при отсутствии функции перехода можно не перебирать все префиксы W. Действительно, h(Sk+1,W) == длине l максимального префикса W, для которого S[k]==W[l], плюс 1. Тогда, для вычисления h(Sk+1,W) следует перебрать все префиксы W, являющиеся суффиксами Sk, в порядке убывания их длины и найти первый из них, для которого S[k]==W[l], где l – длина префикса. Тогда h(Sk+1,W) ==l+1.
Итак, если бы мы могли быстро вычислять длины всех префиксов W, являющиеся суффиксами Sk, в порядке их убывания, то задача поиска подстроки выполнялась бы за время T1=(N). Действительно, исходя из рассуждений, приведенных в предыдущих абзацах, T1 пропорционально количеству изменений переменной l в процессе работы алгоритма. Но переменная l может увеличиваться на 1 не более N раз, поэтому и уменьшаться она может не более N раз. Что и требовалось доказать.
Осталось понять, как вычислять длины префиксов W, являющихся суффиксами Sk.
Легко заметить, что если мы знаем, что имеется префикс W, являющийся суффиксом Sk, длины l, то для вычисления максимального префикса W меньшей длины, являющегося суффиксом Sk, не надо ничего знать о S. Достаточно информации только о строке W. Действительно, т.к. Wl - суффикс Sk, то следует найти максимальный префикс W, длины меньше l, являющийся суффиксом Wl.
Введем функцию p: {1,…,N}{1,…,N-1}, такую что p(l)=длина максимального префикса Wl , являющегося суффиксом Wl , длиной меньше l.
Теперь заметим, что Wp(l) является, одновременно, суффиксом Wl , поэтому следующий по длине (в порядке убывания) суффикс Wl , являющийся префиксом Wl , является суффиксом Wp(l). Осталось найти длину максимального суффикса Wp(l) , с длиной меньше p(l), являющегося префиксом Wp(l). Данная величина, по определению, равна p(p(l))=по определению=p2(l).
Т.о., по индукции, получаем, что последовательность длин суффиксов Wl , совпадающих с префиксами Wl и расположенных по убыванию длин, совпадает с последовательностью {l,p(l),p(p(l))…}={ p0(l), p1(l), p2(l), …}. Т.о., если бы мы имели таблицу значений функции p(*), то задача вычисления длин префиксов W, являющихся суффиксами Sk, оказалась бы решенной, что, в свою очередь, решило бы задачу поиска подстроки в строке.
Займемся вычислением табличной функции p(*).
Префикс-функция p(*) вычисляется в точности по уже приведенному алгоритму.
Пусть требуется вычислить p[k+1], если p[i] для ik уже известны.
Если W [k]==W[p[k]], то p[k+1]= p[k]+1 .
Если W [k]!=W[p[k]], то, как и ранее, перебираем в порядке уменьшения длин l все префиксы W , совпадающие с суффиксами Wk , пока не выполнится
W[k]==W[l]
Каждое последующее l получается из предыдущего по формуле
l=p(l);
Положим в начале цикла l= p[k], то случай W [k]==W[p[k]] подпадет под вычисления внутри последнего цикла и его отдельное рассмотрение будет излишним.
Внутренний цикл следует продолжать пока k0. Если окажется, что k<0, то p[k+1]=0. Иначе, в конце внутреннего цикла имеем: p[k+1]= l+1 .
Отметим, что мы можем положить
p[0]=-1;
после чего случай k<0 перестанет быть выделенным (в этом случае l=-1;p[k+1]=l+1; из чего сразу получаем p[k+1]=0).
Итак, на языке С подготовка функции (массива) p может выглядеть следующим образом
void MakeP(int *p, char *W, int M)
{int k,l; p[0]=-1; p[1]=0; l=0;
for(k=1;k<M;k++)
{
l=p[k];
while(l>=0 && W[k]!=W[l])l=p[l];
p[k+1]=l+1;
}
}
Основная функция, ищущая первое вхождение строки W в строку S, может выглядеть следующим образом
char *Search(char *S,int N, char *W, int M, int *p)
{int l=0,k;
for(k=0;k<N;k++)
{
while(l>=0 && S[k]!=W[l])l=p[l];
l++;
if(l==M)return S+k-l+1;
}
return NULL;
}
Пример программы на языке С, использующей данные функции прилагается. Программа написана по аналогии с функцией grep, которые мы обсудили в начале лекции.