cin >> n;
sum = 0;
for (i=0; i<n; i++)
for (j=0; j<n; j++) sum++;
* Колко бързо ще
работи горната програма, т.е. какви са критериите по които
се определя бързината й? * Eкспериментално да проверим за колко време ще се изпълни програмата. * За да изследваме по-общо нейното поведение е ще я изпълним с различни стойности на n. * Резултатите са обобщени в следната таблица (могат да бъдат такива): |
|
* Сравняване на две
функции: g1(n)= 2n2 и g2(n)= 200n, които показват времето за изпълнение на два дадени алгоритъма А1 и A2, в зависимост от n. * Асимптотично алгоритъмът A2 е по-бърз и неговата сложност е линейна, докато тази на A1 е квадратична. |
|
* Нека е дадена задача, в която размерът на
входните данни е определен от дадено цяло число n.
* Почти всички задачи, които ще разглеждаме, притежават това
свойство.
* Ще поясним последното като разгледаме няколко примера:
Пример 1.
Да се сортира масив с n елемента.
Размерът на входните данни се определя от
броя n на елементите на масива .
Пример 2.
Да се намери най-големият общ делител на a
и b.
В този пример размерът на входните данни се
определя от броя на двоичните цифри (битовете) на по-голямото от
числата a и b.
Пример 3.
Да се намери покриващо дърво на граф.
В този случай характеризираме размера на
входа с две числа: брой на върховете и брой на ребрата.
* Когато се интересуваме от сложността на
алгоритъм най-често се интересуваме как ще работи при достатъчно
голям размер n на
входните данни.
* При формалното оценяване на сложността на алгоритмите
изследваме поведението им при "достатъчно голямо" n
(клонящо към безкрайност).
1. O(f) определя множеството от всички функции g, които нарастват не по-бързо от f, т.е. съществува константа c > 0 такава, че g (n) <= cf(n), за всички достатъчно големи стойности на n.
2. Theta (f) определя множеството от всички функции g, които нарастват толкова бързо, колкото и f (с точност до константен множител), т.е. съществуват константи c1 > 0 и c2 > 0 такава, че c1f(n) <= g (n) <= c2f(n), за всички достатъчно големи стойности на n.
3. Omega (f) определя множеството от всички функции g, които нарастват не по-бавно от f, т.е. съществува константа c > 0 такава, че g(n) >= cf(n), за всички достатъчно големи стойности на n.
O(f): Свойства и примери
* Нотацията О(f) е най-често
използваната при оценка на сложност на алгоритми и програми.
* По-важни свойства на О(f)
(с ~ означаваме принадлежност):
Нарастване на най-често използваните функции:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
** Определяне на сложност на алгоритъм.
* Намиране на функцията, определяща зависимостта между
големината на входните данни и времето за изпълнение.
* Винаги разглеждаме най-лошия случай - при най-неблагоприянти
входни данни.
- елементарна операция - не зависи от
размера на обработваните данни - O(1) ;
- последователност от оператори - определя
се от асимтотично най-бавния - f + g ~ max(O( f ), O(g));
- композиция на оператори - произведение от
сложностите - f (g) ~ O( f*g);
- условни оператори - определя се от
асимтотично най-бавния между условието и различните случаи;
- цикли, два вложени цикъла, p
вложени цикли - O(n), O(n2), O(np) .
// 1
for (i = 0; i < n; i++)
for (j = 0; j < n; j++,
sum++);
// 2
for (i = 0; i < n; i++)
for (j = 0; j < n; j++) if
(a[i] == b[j]) return;
// 3
for (i = 0; i < n; i++)
for (j = 0; j < n; j++) if
(a[i] != b[j]) return;
// 4
for (i = 0; i < n; i++)
for (j = 0; j < n; j++) if
(a[i] == a[j]) return;
// 5
for (i = 0; i < n; i++)
for (j = 0; j < i; j++)
sum++;
// 6
for (i = 0; i < n; i++)
for (j = 0; j < n*n; j++)
sum++;
// 7
for (i = 0; i < n; i++)
for (j = 0; j < i*i; j++)
sum++;
// 8
for (i = 0; i < n; i++)
for (j = 0; j < i*i; j++)
for (k = 0; k < j*j;
k++) sum++;
Да разгледаме цикъла:
for (sum = 0, i = 0; i < n; i *=
2) sum++;
Променливата i приема стойности 1, 2, 4, ..., 2k,
... докато надмине n. Цикълът се изпълнява
[log n] пъти. Сложността е O(log n).
Изчисляване на сложност при
рекурсия.
* Двоично търсене в сортиран масив - рекурсивен алгоритъм.
int binary_search(vector<int> v, int from, int to, int a) { if (from > to) return -1; int mid = (from + to) / 2; if (v[mid] == a) return mid; else if (v[mid] < a) return binary_search(v, mid + 1, to, a); else return binary_search(v, from, mid - 1, a); }
* Броим обръщенията към елементите на масива.
* В рекурсивната функция се разглежда средния елемент и се прави
едно рекурсивно извикване с два пъти по-малък масив.
* Следователно, ако T(n) е функцията, която задава
броя на обръщенията, то T(n) = T(n/2)
+ 1.
* От равенствата
T(n) = T(n/2) + 1 = T(n/4)
+ 2 = T(n/8) + 3 = ... = T(n/2k)
+ k
получаваме за n = 2k, че T(n)
= T(1) + log n, т.е. сложността на алгоритъма е O(log
n).