W ostatnim poście napisałem na końcu, że mój algorytm jest O(n^2). Co to właściwie oznacza?
Problem złożoności obliczeniowej to pytanie “Jak długo mój algorytm będzie działał?”
Najprostsza odpowiedź jest “To zależy”. Ale od czego?
Algorytm działający w czasie stałym
Algorytm wykonywany jest w czasie stałym, czyli niezależnie od jego parametrów, będzie zawsze wykonywał co najwyżej P kroków. P jest dla danego algorytmu jakąś stałą. Powiemy o takim algorytmie, że jest O(1).
Przykład:
let suma a b = a + b
let head list =
match list with
| [] -> None
| h::_ -> Some h
Algorytm działający w czasie liniowym
Algorytm działa w czasie liniowym względem jakiegoś jego parametru. Np. algorytm przetwarzający listę wykona tyle kroków ile jest elementów w tej liście. Będzie liniowo zależeć od długości tej listy. Innym przykładem jest algorytm liczący n-tą liczbę Fibonacciego. Im większe n tym dłużej będzie liczył, proporcjonalnie. 2 razy większe n, 2 razy dłużej liczy. Taki algorytm jest O(n).
let suma lista =
match lista with
| [] -> 0
| h::t -> h + (suma t)
let fib n =
if n < 2 then 1
else
let rec fibpom prev1 prev2 i =
if i = 0 then prev1
else fibpom (prev1 + prev2) prev1 (i-1)
fibpom 2 1 (n-2)
Algorytm działający w czasie logarytmicznym
Mając do czynienia ze strukturą słownikową, np. drzewem BST, jesteśmy w stanie wyszukać element szybciej niż w czasie liniowym, t.j. przeglądając wszystkie elementy po kolei. W BST mamy posortowane elementy i patrzymy czy to czego szukamy jest większe czy mniejsze od elementu po środku (chyba, że nim jest). I następnie powtarzamy ten algorytm dla połowy elementów. Dzieląc na pół kilkukrotnie dojdziemy do szukanego elementu (lub nie jeśli go nie ma). Okazuje się, że maksymalnie możemy wykonać to dzielenie na pół “logarytm przy podstawie 2 z n” razy. Więc ten algorytm jest O(log n).
let find x (array :int array) =
let rec findpom start stop =
if stop < start then false
else
let middle = (start + stop) / 2
if array.[middle] = x then true
else if x < array.[middle] then findpom start (middle - 1)
else findpom (middle + 1) stop
findpom 0 (array.Length - 1)
Bardziej złożone algorytmy
Większość algorytmów o złożoności wielomianowej to złożenia algorytmów liniowych i logarytmicznych. Np. algorytm O(n^2) to algorytm gdzie dla każdego elementu listy przeglądamy całą listę.
Są też algorytmy wykładnicze O(c^n). Te zajmują bardzo dużo czasu. Przykładem takiego algorytmu jest backtracking.
Big O
To duże O, które widzimy przy określaniu złożoności obliczeniowej to skrót zapisu
algorytm(n) ≤ g(n)∙c ⟷ algorytm(n) = O(g(n))
Więcej o tym znajdziecie na Wikipedii.