- •1. Вычислительные модели. Показатели эффективности алгоритмов, задача анализа алгоритмов. Время выполнения алгоритма для худшего и среднего случая. Вычислительные модели
- •Задача анализа алгоритмов
- •Время работы алгоритма
- •Время выполнения в худшем и среднем случае
- •5. Анализ рекурсивных алгоритмов. Анализ вставки элемента в бинарное дерево поиска.
- •5.Анализ сложности рекурсивных алгоритмов
- •6. Связные списки: однонаправленные, двунаправленные, кольцевые. Примеры реализации.
- •Однонаправленные (односвязные) списки
- •Двунаправленные (двусвязные) списки
- •7. Структура данных "стек". Реализация с помощью массива и связного списка, смешанная реализация. Пример использования (кроме задачи о скобках)
- •8.Структура данных "очередь". Реализация с помощью массива, циклического массива, односвязного списка.
- •Очереди
- •Бинарные деревья
- •11. Обходы бинарных деревьев (клп - "Корень-Левое-Правое" ,лкп,лпк) с примерами программной реализации.
- •12. Удаление элемента из бинарного дерева поиска.
- •13.Рандомизированные деревья. Вставка элемента в рандомизированное бинарное дерево поиска.
- •Включение элемента в рандомизированное дерево
- •Рекурсивный алгоритм вставки в рандомизированное дерево:
- •14.Быстрая сортировка Хоара (QuickSort)
- •Краткое описание алгоритма
- •Алгоритм
- •Оценка эффективности
- •15. Сортировка слиянием (MergeSort)
- •16.Полный перебор подмножеств
- •17. Полный перебор перестановок
- •18. Перебор с возвратом и отсечением вариантов
- •3.1 Использование рекурсии для записи алгоритма
- •3.2 Примеры решения задач при помощи перебора с возвратом
- •.1 Отсечение лишних вариантов
- •19. Динамическое программирование. Разбиение задачи на подзадачи, принцип оптимальности. Задача о маршруте из верхнего угла таблицы в нижний
- •20. Пример использования динамического программирования для подсчета количеств комбинаторных объектов - найти количество разбиений числа на слагаемые
- •21. Жадные алгоритмы. Принцип жадного выбора, свойство оптимальности для подзадач, схема доказательства корректности жадного алгоритма. Задача о заявках.
- •Принцип жадного выбора
- •Оптимальность для подзадач
- •Примеры Размен монет
- •Выбор заявок
- •22. Хеширование с цепочками. Эффективность хеширования с цепочками. Хеш-функции, качество хеш-функций, подходы к построению хеш-функций.
- •23. Хеширование с открытой адресацией. Способы разрешения коллизий. Анализ эффективности.
Задача анализа алгоритмов
Основной задачей анализа алгоритма является получение зависимости того или иного показателя эффективности от размера входных данных (размера входа, размера задачи). Сразу возникает вопрос – как измерять размер входа? Это зависит от конкретной задачи. В одних случаях размером разумно считать число элементов на входе (например, поиск элемента в массиве или его сортировка). Иногда размер входа измеряется не одним числом, а несколькими (например, число вершин и число рёбер в графе). В некоторых случаях более естественно считать размером входа общее число бит, необходимое для представления всех входных данных. Последний способ рассматривается как основной в теории вычислений.
Зависимость времени выполнения алгоритма от размера задачи называется временнóй сложностью алгоритма, а зависимость необходимого размера памяти от размера задачи — пространственной (емкостной) сложностью алгоритма.
Время работы алгоритма
Пусть n – размер входа (для задач, где размер входа задаётся несколькими числами, рассуждения будут аналогичны). Определим более точно, что понимать под временем работы алгоритма. Поскольку алгоритм – это ещё не программа для конкретной вычислительной машины, время его работы нельзя измерять, например, в секундах. Обычно под временем работы алгоритма понимают число элементарных операций, которые он выполняет. При этом, если алгоритм записан на каком-то псевдокоде или языке высокого уровня, предполагается, что выполнение одной строки требует не более чем фиксированного числа операций (если, конечно, это не словесное описание каких-то сложных действий).
Примечание. Действуя более формально, мы могли бы записать алгоритм с помощью инструкций предполагаемой вычислительной модели, назначить каждой из них некую стоимость и вывести выражение для стоимости алгоритма. Однако, это достаточно трудоёмко и в большинстве случаев не требуется.
Поскольку почти любой алгоритм содержит ветвления, время его выполнения зависит не только от количества входных данных, но и от самих значений этих данных. В связи с этим записать аналитическую зависимость T(n) невозможно. Для сравнения различных алгоритмов обычно определяют время их выполнения для наихудшего или для среднего случая. Рассмотрим это подробнее.
Время выполнения в худшем и среднем случае
Существуют алгоритмы, время работы которых зависит только от размера входных данных, но не зависит от самих данных (например, поиск суммы элементов заданного массива). Однако чаще всего для разных входных данных одного и того же размера алгоритм работает разное время. Поэтому говорят о времени выполнения алгоритма в наихудшем случае (т.е. максимальное время выполнения по возможным входным данным) и о времени выполнения в среднем. Время выполнения в среднем можно определить по-разному:
среднее время работы алгоритма по всем возможных вариантам входных данных;
ожидаемое время его работы по всем возможным вариантам входных данных с учетом вероятности их появления
Недостатки есть и у того, и у другого способов:
Первый способ не учитывает, что в реальных задачах данные часто распределены неравномерно.
При втором способе получается, что мы анализируем алгоритм не в общем виде, а применительно к некой предполагаемой области.
Чаще всего при анализе времени работы алгоритма ограничиваются наихудшим случаем. Причины этого в следующем:
Время выполнения в наихудшем случае обычно найти гораздо проще, чем в среднем.
Зная верхнюю границу, мы можем быть уверены, что алгоритм не будет работать дольше ни на каких входных данных.
Для многих алгоритмов плохие случаи (или близкие к ним) могут происходить очень часто
Зачастую «средний случай» почти так же плох, как и наихудший. Например, сортировка вставками или любой другой квадратичный алгоритм сортировки.
Тем не менее, время выполнения в среднем также иногда анализируют – например, для тех алгоритмов, где оно существенно отличается от наиухудшего, и при этом вероятность появления «плохих» входных данных достаточно мала (алгоритм быстрой сортировки Хоара и др.)
Для примера найдём время выполнения алгоритма сортировки массива методом пузырька для худшего случая. Сортировка методом пузырька выполняется следующим образом. Двигаясь от конца массива к началу, мы на каждом шаге сравниваем очередные два соседних элемента. Если первый элемент больше второго, то меняем их местами. Таким образом, после первого прохода по массиву самый маленький элемент поднимется на самый верх массива и займёт нулевую позицию. Второй цикл сортировки выполняется для оставшейся части массива (без первого элемента), в результате следующий по величине элемент окажется в первой позиции массива, и т.д.
2. Асимптотические оценки сложности алгоритмов. Понятие точной, верхней и нижней оценки (для функций вообще и применительно к анализу алгоритмов в частности). Пример анализа для следующей задачи. Для каждого натурального числа от 1 до N найти количество его делителей. Оцените, до каких величин N ваша программа будет работать не более нескольких секунд.
Асимптотическая сложность
Несмотря на то, что функция временной сложности алгоритма в некоторых случаях может быть определена точно, в большинстве случаев искать точное её значение бессмысленно. Дело в том, что, во-первых, точное значение временной сложности зависит от определения элементарных операций (например, сложность можно измерять в количестве арифметических операций или операций на машине Тьюринга), а во-вторых, при увеличении размера входных данных вклад постоянных множителей и слагаемых низших порядков, фигурирующие в выражении для точного времени работы, становится крайне незначительным.
Асимптотическая сложность - рассмотрение входных данных большого размера и оценка порядка роста времени работы алгоритма. Обычно алгоритм с меньшей асимптотической сложностью является более эффективным для всех входных данных, за исключением лишь, возможно, данных малого размера.
Асимптотическая оценка сложности обозначается греческой буквой Θ (тета). f(n) = Θ(g(n)), если существуют c1, c2>0 и n0 такие, что c1*g(n)<=f(n)<=c2*g(n) , при n>n0. Функция g(n) является асимптотически точной оценкой сложности алгоритма - функции f(n), приведенное неравенство называется асимптотическим равенством, а само обозначение Θ символизирует множество функций, которые растут “так же быстро”, как и функция g(n) – т.е. с точностью до умножения на константу. Как следует из приведенного неравенства, оценка Θ являет собой одновременно и верхнюю и нижнюю оценки сложности. Не всегда есть возможность получить оценку в таком виде, поэтому верхнюю и нижнюю оценки иногда определяют отдельно. Верхняя оценка сложности обозначается греческой буквой Ο (омикрон), и является множеством функций, которые растут не быстрее, чем g(n). f(n)= Ο(g(n)), если существует c>0 и n0 такие, что 0<=f(n)<=cg(n), при n>n0. Нижняя оценка сложности обозначается греческой буквой Ω (омега), и является множеством функций, которые растут не медленнее, чем g(n). f(n)= Ω(g(n)), если существует c>0 и n0 такие, что 0<=cg(n)<=f(n), при n>n0. Как следствие: асимптотическая оценка существует только в том случае, если совпадают нижняя и верхняя оценки сложности алгоритма. В практике анализа алгоритмов чаще всего под оценкой сложности понимают верхнюю оценку сложности. Это вполне логично, поскольку наиболее важна оценка времени, за которое алгоритм гарантировано закончит работу, а не время, в пределах которого он точно не завершится.
{$APPTYPE CONSOLE} uses SysUtils; var n:Integer; function result(n:integer):Integer; //функция подсчета количества делителей var i:Integer; begin result:=0; for i := 2 to n div 2 do if n mod i =0 then result:=result+1; end; begin read(n); // ввод числа write(result(n)); readln; readln; end. end.
4. Рекурсия с запоминанием (прозрачный вариант динамического программирования). Пример быстрого вычисления биномиальных коэффициентов по формуле C(n,m)=C(n-1,m)+C(n-1,m-1)
Есть способ решить проблему повторных вычислений. Он очевиден — нужно запоминать найденные значения, чтобы не вычислять их каждый раз заново. Конечно, для этого придётся активно использовать память. Например, рекурсивный алгоритм вычисления чисел Фибоначчи легко дополнить тремя «строчками»:
создать глобальный массив FD, состоящий из нулей;
после вычисления числа F(n) поместить его значение в FD[n];
в начале рекурсивной процедуры сделать проверку на то, что FD[n] = 0 и, если , то вернуть FD[n] в качестве результата, а иначе приступить к рекурсивному вычислению F(n).
{Функция на Pascal}
function C(m, n :Byte):Longint;
Begin
If (m=0) or (m=n)
Then C:=1
Else C:=C(m, n-1)+C(m-1, n-1)
End;
{Процедура на Pascal}
Procedure C(m, n: Byte; Var R: Longint);
Var R1, R2 : Longint;
Begin
If (m=0) or (m=n)
Then R:=1
Else Begin
C(m, n-1, R1);
C(m-1, n-1, R2);
R:=R1+R2
End;