2. Анализ на алгоритми
- Алгоритъм е постъпкова процедура за решаване
на задача за крайно време.
- Повечето алгоритми трансформират входните
обекти (данни) в изходните обекти (данни).
Експериментални изследвания
- Напишете програма за изпълнение на алгоритъма.
- Изпълнете програмата с входове с различна
големина и от различен вид.
- Използвайте функция (като вградената clock()), за да
измерите действителното (точното) време за работа на
програмата.
- Начертайте графика с резултатите.
Ограничения на експериментите
- Необходимо е реализирането на алгоритъма,
което може да е трудно.
- Резултатите не могат да бъдат показателни за
времето на работа за други входове (входни данни), които не са
включени в експеримента.
- За да се сравнят два алгоритъма, трябва да се
използва един и същи хардуер и софтуер.
Време за изпълнение
- Времето за работа на алгоритъма обикновено
расте заедно с входния размер.
- Различни входни данни с една и съща големина
често дават различни времена.
- Средното време за работа (с различни входни
данни) за даден размер обикновено се определя трудно.
- Ще се фокусираме върху времето за работа в
най-лошия случай (входни данни, с които алгоритъмът работи
най-бавно):
- лесно за анализиране;
- от решаващо значение за приложения като
игри, финанси, роботика, ...
Теоретичен анализ
- Използва описание на алгоритъма на високо ниво, а не
изпълнението на реализиран в програма алгоритъм.
- Характеризира времето за работа като функция
T(n) на входния размер n.
- Взема под внимание всички възможни входове
(оценка отгоре с най-лошия вход).
- Позволява да се оцени скоростта на алгоритъма
независимо от хардуера и програмната среда.
- Дава възможност за теоретично сравнение на два
алгоритъма за решаване на една и съща задача.
Псевдокод
Псевдокод е програмен код, несвързан с хардуера на
даден компютър и изискващ пренаписване на кода за компютър, преди
програмата да може да се използва.
Няма строги правила за синтаксиса, предназначен е за хората, не за
компютрите.
- Описание на алгоритъма на високо ниво.
- По-структуриран от човешки език.
- С по-малко детайли от програма.
- Предпочитан нотация за описване алгоритми.
- Скрива някои проблеми (датайли), които
възникват при писане на програма.
Пример:
Намиране на най-голям елемент на масив.
Algorithm arrayMax(A, n)
Input: array A of n integers
Output: maximum element of A
currentMax <- A[0]
for i <- 1 to n−1 do
if A[i] >
currentMax then currentMax <- A[i]
return currentMax
Детайли на псевдокода
if…then…[else…]
while…do…
repeat…until…
for…do…
Отместването замества скобите.
Algorithm method(arg[,
arg…])
Input…
Output…
- Извикване на функция/метод
var.method (arg[, arg…])
return
expression
<-
Assignment (like = in C++)
= Equality
testing (like == in C++)
n2
- степени и др. математически означения са позволени.
Машина с пряк достъп - (RAM модел)
За изпълнение на псевдокода се дефинира теоретичен компютър.
- CPU.
- Потенциално неограничена памет на клетки, всяка от които
може да побере произволна цифра или символ.
- Клетките са номерирани и достъп до всяка клетка от
паметта се осъществява за единица време.
Елементарни операции (примитиви)
- Основни изчисления, извършвани от алгоритъма,
записвани (разпознавани) в псевдокода.
- В голяма степен независими от езика за
програмиране.
- Точна дефиниция не е важна (ще видим защо
по-късно).
- Приемаме, че се изпълняват за константно
време в RAM модела.
Примери:
- Пресмятане на израз.
- Присвояване на стойност на променлива.
- Индексиране в масив.
- Извикване на функция.
- Връщане на стойност от функция.
Броене на елементарните операции
- Чрез проверка на псевдокода можем да
определим максималния брой примитиви, в алгоритъма като
функция на входния размер.
Пример:
Algorithm arrayMax(A, n)
|
Брой на
операциите
|
currentMax <- A[0] |
2 |
for i <- 1 to n−1 do |
2 + n |
if A[i] > currentMax then |
2(n −
1) |
currentMax <- A[i] |
2(n −
1) |
{increment counter i} |
2(n −1) |
return currentMax
|
1
|
|
Общо 7n
− 1 |
Оценка на времето за изпълнение
- Алгоритъмът arrayMax изпълнява 7n−1
елементарни
операции в най-лошия случай. Дефинираме:
a = времето за
изпълнение на най-бързата елементарна операция.
b = времето за изпълнение на най-бавната елементарна
операция.
- НекаT(n)
е функцията, задаваща времето в най-лошия случай за arrayMax.
- Тогава
a (7n − 1) ≤ T(n) ≤ b(7n − 1)
- Следователно времето за работа T(n) е ограничено от две
линейни функции.
Темп на растеж (Growth Rate) на времето за
изпълнение (Running Time)
Промяната на хардуера и софтуера
- се отразява на T(n)
като умножение с константа, но
- не променя темпа на растеж (growth rate) на T(n).
Линейната скорост на растеж на времето за работа на
T(n) е присъщо (вътрешно)
свойство на алгоритъма аrrayMax.
Темпове на растеж (скорост на
нарастване)
Функция / n
|
1
|
2
|
10
|
100
|
1000
|
1
|
1
|
1
|
1
|
1
|
1
|
log n
|
0
|
1
|
3.32
|
6.64
|
9.96
|
n
|
1
|
2
|
10
|
100
|
1000
|
n log n
|
0
|
2
|
33.2
|
664
|
9996
|
n2
|
1
|
4
|
100
|
104
|
106
|
n3
|
1
|
8
|
1000
|
106
|
109
|
nk
|
1
|
2k
|
10k |
100k |
1000k |
2n
|
2
|
4
|
1024
|
1030
|
10300
|
n!
|
1
|
2
|
3628800
|
10157
|
102567
|
nn
|
1
|
4
|
1010
|
10200
|
103000
|
Константни фактори
Темпът на растеж не се влияе от
- константни множители и
- членове от по-нисък ред.
Примери:
- 102n+105 е
линейна функция (n)
- 105n2+108n
е квадратна функция (n2)
Асимптотична нотация
О-голямо (Big-Oh)
Дадени са функциите f(n) и
g(n). Казваме, че f(n) е O(g(n)), ако съществува положителна константа c и N > 0 такива, че f(n)
< c g(n) за всяко n
> N.
Пример 1: 2n +10 е O(n).
2n +10 ≤ cn; (c − 2) n ≥10; n ≥ 10/(c − 2).
Избираме c = 3 и N
= 10.
Пример 2: Функцията n2 не е O(n).
n2 ≤ c n; n ≤
c.
Горното неравенство не може да бъде изпълнено, тъй като c трябва да бъде константа.
Пример 3: 7n - 2 е O(n).
Необходимо е c > 0 и N ≥ 1 да са такива, че 7n - 2 ≤ c n за n
≥ N.
Това е вярно за c = 7 и
N = 1 (не са
единствени!).
Пример 4: 3n3+ 20n2+ 5 is O(n3).
Необходимо е c > 0 и N ≥ 1 да са такива, че 3n3+ 20n2+ 5 ≤ cn3 за n ≥ N.
Това е вярно за c = 4 и N = 21.
Пример 5: 3 log n + log log n е O(log n).
Необходимо е
c > 0 и N ≥ 1 да са такива, че 3 log n + log log n ≤ c log n
за n ≥ N.
Това е вярно
за c =
4 и N = 2.
Правила за О-голямо
- Ако f(n) е полином от степен k,
то f(n) е O(nk), т.е.
при асимптотична нотация
1. премахваме членовете от по-нисък ред и
2.
премахваме константите (константните множители).
- Използваме най-малкия възможен клас от функции
(препоръка и практика!).
“2n е O(n)” вместо
“2n е O(n2)”.
- Използваме най-простия израз на класа от
функции (функциите от таблицата по-горе)
“3n + 5 е O(n)” вместо “3n + 5 е O(3n)”.
Асимптотичен анализ на алгоритми
- Асимптотичен анализ на алгоритъм - определя
времето за работа в О-голямо нотация.
- За извършване на асимптотичния анализ:
1. Намираме в най-лошия случай (най-лоши данни)
броя на елементарните операции като функция на входния размер.
2.
Изразяваме тази функция с О-голямо нотация.
Пример:
Определихме, че алгоритъмът arrayMax изпълнява
най-много 7n−1 елементарни операции.
Казваме, че алгоритъмът arrayMax “работи за време O(n)”.
- Тъй като константите и членовете от по-нисък
ред отпадат, можем да ги пренебрегнем, когато преброяваме елементарните операции.
Пример: Изчисляване на префиксни средни стойности (квадратичен
алгоритъм)
- Ще илюстрираме асимптотичния анализ с два
алгоритъма за префиксни средни.
- i-тото префиксно средно на масива X е
средното на първите (i+1) елемента на X е:
A[i]=
(X[0] +X[1] +… +X[i])/(i+1)
- Изчисляване на масива A от префиксно средни
стойности на друг масив X има приложения във финансовия
анализ.
Префиксни средни (квадратичен алгоритъм)
Следният алгоритъм изчислява префиксни средни за квадратично
време, като прилага определението.
Algorithm
prefixAverages1(X, n)
Input array X of n integers
Output array A of prefix averages of X
A <- new array of n intege еrs
for i <- 0 to n−1 do
s <- X[0]
for j <- 1 to
i do
s
<- s+X[j]
A[i] <- s/(i+1)
return A
|
#operations
n
n
n
1 + 2 + …+(n−1)
1 + 2 + …+(n−1)
n
1
|
- Времето за изпълнение на prefixAverages1 е: O(1 + 2 + …+ n).
- Сумата на първите n цели числа е n(n
+ 1) / 2.
- Така алгоритъмът prefixAverages1 се изпълнява за време O(n2).
Пример: Префиксни средни (линеен алгоритъм)
Следният алгоритъм изчислява префиксни средни за
линейно време, като поддържа текущите суми.
Algorithm
prefixAverages2(X, n)
Input array X of n integers
Output array A of prefix averages of X
A <- new array of n integers
s <- 0
for i <- 0 to n−1 do
s <- s + X[i]
A[i] <- s/(i + 1)
return A
|
#operations
n
1
n
n
n
1
|
Така алгоритъмът prefixAverages2 се изпълнява за време O(n).
Роднини на О-голямо
Омега-голямо (Big-Omega)
f(n) е Ω(g(n)), ако съществува константа
c > 0 и цяла константа
N > 0 такива, че f(n) > c
g(n) за всяко n > N.
Тита-голямо (Big-Theta)
f(n) е Θ(g(n)), ако съществуват
константи c' > 0 и c" > 0 и цяла константа N > 0 такива, че c'g(n)
< f(n) < c"g(n) за
всяко n > N.
о-малко (little-oh)
f(n) е o(g(n)),
ако за всяка константа c
> 0, съществува цяла константа N > 0 такава, че f(n)
< c g(n) за всяко n
> N.
омега-малко (little-omega)
f(n) е ω(g(n)), ако за всяка константа c > 0, съществува цяла константа N > 0 такава, че
f(n) > c g(n) за
всяко n > N.
Интуиция за асимптотичната нотация
Big-Oh
f(n) е O(g(n)), ако f(n)
е асимпотично по-малка или равна на g(n).
big-Omega
f(n) е
Ω(g(n)),
ако f(n) е асимпотично по-голяма или равна на g(n).
big-Theta
f(n) е
Θ(g(n)), ако f(n) е асимпотично равна на
g(n).
little-oh
f(n) е
o(g(n)), ако f(n) е асимпотично
строго по-малка от g(n).
little-omega
f(n)) е
ω(g(n)), ако f(n) е асимпотично
строго по-голяма от g(n).
Примери за използване на роднините на О-голямо
Пример 1: 5n2 е
Ω(n2).
f(n) е Ω(g(n)), ако съществува
константа c > 0 и цяла константа N ≥ 1 такива,
че f(n) ≥ c g(n) за
всяко n ≥ N.
Нека c = 5 и N = 1.
Пример 2:
5n2 е
Ω(n).
f(n) е
Ω(g(n)), ако съществува константа c >
0 и цяла константа
N ≥ 1 такива, че
f(n) ≥c g(n) за всяко n ≥ N.
Нека c = 1 и N = 1, 5n2 ≥ n.
Пример
3: 5n2
е ω(n).
f(n)
е ω(g(n)), ако за всяка константа c > 0
съществува цяла константа N ≥ 0 такава, че f(n)
≥c g(n) за всяко n ≥ N.
Необходимо е 5N2 ≥ cN -
вземаме c, след това избираме N такова, че да
изпълнява неравенството N ≥ c/5 ≥ 0.