Toggle menu
Toggle preferences menu
Toggle personal menu
Neprihlásený/á
Your IP address will be publicly visible if you make any edits.
Bez shrnutí editace
Bez shrnutí editace
 
(32 medziľahlých úprav od rovnakého používateľa nie je zobrazených.)
Riadok 1: Riadok 1:
Čo je asymptotická notácia, všeobecný postup pre výpočet ''Big-O'' notácie, príklady výpočtov
Čo je asymptotická (''Big-O'') notácia, príklady a interpretácia najčastejších výsledkov.


{{Pojmová mapa}}
{{Pojmová mapa}}


== Asymptotická notácia? ==
== Typy asymptotických notácií ==
Asymptotická notácia <math>O(f(n))</math> predstavuje, koľko operácií vykoná určitý kód pre <math>n</math> prvkov na vstupe. Inými slovami, majme napríklad tento kód pre zistenie názvu dňa podľa jeho poradia v týždni (1 až 7):
''Asymptotická notácia'' sa v informatike používa pre vyjadrenie '''asymptotického správania algoritmu''', teda ako sa jeho výkon mení pri veľkých hodnotách <math>n</math>.


<syntaxhighlight lang="python3">DNI = ["Pondelok", "Utorok", "Streda", "Štvrtok", "Piatok", "Sobota", "Nedeľa"]
Poznáme tri hlavné typy:
#          0        1          2        3        4        5          6
 
poradie_v_tyzdni = 3          # tretí deň v týždni
den = DNI[poradie_v_tyzdni-1] # -1, pretože indexujeme od 0


print(den)
* ''Big-O'': <math>O(n)</math> – horná hranica, opisuje zložitosť algoritmu v '''najhoršom prípade''';
# "Streda"
* ''Big-Omega'': <math>\Omega(n)</math> – dolná hranica, opisuje zložitosť algoritmu v '''najlepšom prípade''';
</syntaxhighlight>
* ''Theta'': <math>\Theta(n)</math> – priemerný prípad;


Operácie, ktoré kód vyššie vykonáva, môžeme rozpísať a analyzovať. Interpretácia mojej analýzy je zobrazená nižšie:
V praxi sa snažíme, aby sa horná aj spodná hranica algoritmu čo najviac rovnala (snažíme sa o priemerný prípad). Ak sa rovnajú, znamená to že takáto výpočtová zložitosť algoritmu je ideálna.


<syntaxhighlight lang="python3">
== Príklady pre určovanie asymptotických hraníc ==
DNI = ["Pondelok", "Utorok", "Streda", "Štvrtok", "Piatok", "Sobota", "Nedeľa"]
Matematicky sa '''horná asymptotická hranica''' definuje ako:
# alokuje sa premenná "DNI" a priradí sa jej hodnota
# z hľadiska časovej náročnosti sú konštanty O(1)
# (pretože prebehne práve 1 operácia, teda vytvorenie premennej)


poradie_v_tyzdni = 3
<math>\begin{align}
# toto je stále konštanta, takže O(1)
f(n) &= O\bigl(g(n)\bigr) \\
\exists c, m &> O \forall n \geq m \\
f(n) &\leq c \times g(n)
\end{align}</math>


den = DNI[poradie_v_tyzdni-1]
V preklade: ''O(n) je definovaná ako funkcia, pre ktorú existujú konštanty'' <math>c</math> ''a <math>m</math> ktoré sú väčšie ako O pre všetky'' <math>n \geq m</math>''. Platí, že všetky hodnoty tejto funkcie sú menšie alebo rovné ako hodnoty pôvodnej funkcie'' <math>g(n)</math> ''prenásobené konštantou'' <math>c</math>.
# na prvý pohľad sa môže zdať, že v tomto riadku je opäť
# iba jedna operácia - vytvorenie premennej
# no netreba však zabudnúť, že kód by sa mohol rozdeliť aj takto:
#  poradie_minus_1 = poradie_v_tyzdni - 1 # 1. operácia - výpočet správneho indexu, O(1)
#  prvok_v_poli = DNI[poradie_minus_1]    # 2. operácia - výber prvku z poľa, O(1)
#  den = prvok_v_poli                    # 3. operácia - priradenie vybraného prvku do premennej, O(1)
# ...a toto sú až 3 operácie, z toho každá má náročnosť O(1)


print(den)
'''Dolná asymptotická hranica''' sa definuje podobne, iba hľadáme všetky hodnoty väčšie alebo rovné ako <math>c \times g(n)</math>:
# ak by sme zobrazili zdrojový kód funkcie "print",
# zistili by sme že tiež vykonáva nejaké operácie
# a každá z nich môže mať inú časovú komplexitu
# ("print" musí predsa obsahovať ďalšie funkcie,
# ktoré sa starajú o výpis jednotlivých znakov do terminálu)
#
# ...avšak, ak by sme mali analyzovať aj tieto vstavané funkcie, boli by
# sme tu do rána - a vôbec, ak analyzujeme efektivitu kódu, pravdepodobne nás bude
# zaujímať náš vlastný kód, nie efektivita funkcie "print"
# zjednodušene povedané, budeme predstierať že aj toto je O(1)
</syntaxhighlight>
 
Tuto sa ešte nenachádza žiadna premenná <math>n</math>, keďže vstup sa pre kód nikdy nemení – vždy máme pevne danú množinu v ktorej je 7 prvkov (dní) a vyberáme práve jeden, nie sú tam žiadne cykly ani iné vetvenie).
 
Nezáleží na tom, koľkokrát kód vyššie spustíme, môžeme tvrdiť že vždy sa vykoná práve 6 operácií:
 
# vytvorenie premennej <code>DNI</code>;
# vytvorenie premennej <code>poradie_v_tyzdni</code>;
# výpočet správneho indexu;
# zobratie prvku z poľa;
# vloženie hodnoty do premennej <code>den</code>;
#výpis tejto premennej na terminál;
 
Teda, výsledok by teoreticky mohol byť <math>O(6)</math>. Číslo v zátvorke by predstavovalo počet operácií ktoré kód vykoná - avšak, v praxi to funguje trochu inak. Nemá zmysel analyzovať konštantný vstup, pretože chceme vidieť ako sa výkon algoritmu mení s rôznym množstvom prvkov (údajov) na vstupe, a to zabezpečíme iba pomocou premennej <math>n</math>.
 
Poďme si to teda vysvetliť tak, že si to skomplikujeme a pridáme do rovnice premennú <math>n</math>:
 
<syntaxhighlight lang="python3">
# premenná "n" môže byť akékoľvek prirodzené číslo (väčšie ako alebo rovné nule)
# deklarácia by mohla vyzerať napríklad takto:
n = int(input())


def scitaj_prvych(n):
<math>\begin{align}
  """
f(n) &= \Omega\bigl(g(n)\bigr) \\
  Funkcia, ktorá sčíta prvých `n` čísel a vráti výsledok sčítania.
\exists c, m &> \Omega \forall n \geq m \\
  """
f(n) &\geq c \times g(n)
  sucet = 0 # konštantné priradenie, O(1)
\end{align}</math>


  # toto si vysvetlíme nižšie
V podstate pri týchto príkladoch iba hľadáme konštanty <math>c</math> a <math>m</math> pre daný typ hranice.
  # (spoiler: bude nás zaujímať iba toto :))
  for cislo in range(n):
    sucet += cislo


  return sucet # aj operácia vrátenia ("return") sa pokladá za náročnosť O(1)
Majme napríklad:
</syntaxhighlight>


V prvom rade vidíme problém: nemôžme pre všetky prípady jednoznačne určiť konštantný počet operácií, pretože veľkosť poľa (respektíve, počet čísiel ktoré sa sčítajú) sa mení v závislosti od vstupu, teda premennej <math>n</math>
<math>{\color{orange}\frac{n + 1}{2}} = O({\color{green}n})</math>


Z toho vyplýva, že musíme odvodiť všeobecnú rovnicu ktorá bude hovoriť, ako sa časová náročnosť kódu bude meniť v závislosti od počtu prvkov na vstupe – aspoň približne. Mohlo by to vyzerať nejako takto:
Máme zistiť, či výraz naľavo patrí asymptotickej hornej hranici. To znamená, že:


<syntaxhighlight lang="python3">
<math>{\color{orange}\frac{n + 1}{2}} \leq c \times {\color{green}n}</math>
n = int(input()) # tvárime sa, že O(1), ale tento riadok nebudeme brať do úvahy, neskôr zistíme prečo


# teoreticky aj vytvorenie a priradenie funkcie do pamäte je operácia,
Za konštantu <math>c</math> si môžme zvoliť akékoľvek kladné číslo. Predvolene je <math>c = 1</math>:
# ale neberieme ju do úvahy
def scitaj_prvych(n):
  sucet = 0 # O(1), ale neberieme do úvahy


  # zaujíma nás iba táto časť
<math>\begin{align}
  for cislo in range(n): # n-krát
\frac{n + 1}{2} &\leq 1 \times n \\
    sucet += cislo # O(1)
\frac{n + 1}{2} &\leq n \\
\end{align}</math>


  return sucet # O(1), ale toto tiež neberieme do úvahy
Dostali sme rovnicu ktorú iba vypočítame. To, čo patrí výslednému <math>n</math> bude naša konštanta <math>m</math>:
 
</syntaxhighlight>


Teda, ak berieme do úvahy iba cyklus, tak časová náročnosť je: <math>O(1 \times n) = O(n)</math>.
<math>\begin{align}
\frac{n + 1}{2} &\leq n \\
n + 1 &\leq 2n \\
{\color{hotpink}1} &\leq n \\
\\
& m = {\color{hotpink}1};\ c = 1
\end{align}</math>


{{Úvaha|otazka=Ale prečo sa vlastne tvárime, že polovica kódu neexistuje a neberieme ho do úvahy? Prečo nás zaujíma iba cyklus?}}
Skúsme to pre:


V predošlom príklade sme dospeli k záveru, že kód vykonal presne 6 operácií, a teda by sme to mohli teoreticky zapísať ako <math>O(6)</math>, pretože sme zistili že číslo v zátvorke predstavuje počet operácií ktoré kód vykoná.
<math>{\color{orange}\frac{n + 1}{2}} = \Omega({\color{green}n})</math>
[[Súbor:Comparison computational complexity.svg|alt=Porovnanie výpočtovej zložitosti|žiadny|náhľad|287x287bod|Porovnanie výpočtovej zložitosti pre rôzne asymptotické správania funkcií - os Y (<math>N</math>) predstavuje (približný) počet operácií, os X (<math>n</math>) predstavuje počet prvkov na vstupe.]]
To ale nie je celkom pravda. Pravdou je, že pri ''Big-O'' (asymptotickej notácií) nás zaujíma iba '''hodnota ktorá má tendenciu rásť čo najrýchlejšie s meniacim sa vstupom'''. Inými slovami, ''notácia Big-O'' je len '''teoretická horná hranica''' – nehovorí o presnom počte operácií, ale '''o <u>trende</u>'''.


V poslednom príklade by sme pri aplikovaní predošlého postupu (teda, ak by sme brali do úvahy všetky alokácie konštánt a ostatné operácie) dostali ako výsledok niečo ako <math>O(1 + \ldots + n + \ldots)</math>. No napokon sme nebrali súčty jednotiek do úvahy, a ako výsledok nám zostalo iba <math>O(n)</math>. Je to preto, že <math>n</math> sa zo všetkých hodnôt mení najrýchlejšie v závislosti od vstupu. Dáva to zmysel, pretože konštantné hodnoty, teda všetky čísla 1, sa nikdy nemenia. To čo sa mení najviac je samotné <math>n</math>.
Postup zostáva rovnaký, iba použijeme znamienko <math>\geq</math> a konštantu <math>c = \frac{1}{2}</math>:


''Big-O'' sa používa na vyjadrenie '''asymptotického správania algoritmu''', teda ako sa jeho výkon mení pri '''<u>veľkých</u>''' hodnotách <math>n</math>. Áno, mohli by sme brať do úvahy aj konštantné faktory <math>O(1)</math>, ale nemá to zmysel pretože pri veľkom množstve vstupných hodnôt sú alokácie pár (globálnych) premenných pred hlavnou slučkou programu (to jest, v našom prípade pred cyklom <code>for</code>) zanedbateľné. Napokon, skúmame iba '''trend''', to znamená že ak máme dva výsledky: <math>O(1 + n)</math>  a <math>O(1 000 000 + n)</math>, oba naznačujú že trend je '''lineárny''' – konkrétny presný počet operácií nás teda nemusí zaujímať.
<math>\begin{align}
\frac{n + 1}{2} &\geq c \times n \\
\frac{n + 1}{2} &\geq \frac{1}{2}n \\
n + 1 &\geq n \\
{\color{hotpink}1} &\geq 0 \rightarrow \text{platí pre všetky}\ n \\
& m = {\color{hotpink}1};\ c = \frac{1}{2} \\
\end{align}</math>


Ak by sme mali ako výsledok napríklad <math>O(n^2 + n + 1)</math>, zredukovali by sme ho iba na <math>O(n^2)</math>, pretože táto hodnota má tendenciu rásť najrýchlejšie podľa toho ako by sa menilo <math>n</math> ('''rastie kvadraticky''', a to má prednosť pred lineárnym alebo konštantným správaním). To, že to rastie kvadraticky nám stačí a neberieme do úvahy že to ešte rastie aj lineárne a že je tam aj konštantné správanie.
== Interpretácia výsledkov asymptotickej časovej zložitosti ==
[[Súbor:Comparison computational complexity.svg|alt=Porovnanie výpočtovej zložitosti|žiadny|náhľad|345x345pixelů|Porovnanie časovej zložitosti pre rôzne asymptotické správania funkcií. '''Os Y''' (<math>N</math>) predstavuje počet operácií, '''os X''' (<math>n</math>) predstavuje počet prvkov na vstupe


No, možno by bolo dobré urobiť si prehľad najčastejších výsledkov (pozri graf vyššie) a vysvetliť si ich:
]]V tabuľke možno vidieť porovnanie najčastejších výsledkov asymptotickej notácie (pre '''časovú zložitosť'''):
{| class="wikitable"
{| class="wikitable"
!''Big-O'' notácia
!''Big-O'' notácia
Riadok 123: Riadok 83:
!Príklad algoritmu
!Príklad algoritmu
|-
|-
|[[Asymptotická notácia#O(1)|<math>O(1)</math>]]
|[[Asymptotická notácia#Konštantná časová zložitosť – O(1)|<math>O(1)</math>]]
|Konštantná
|Konštantná
|Prístup k prvku v množine pomocou indexu alebo k hodnote v slovníku pomocou kľúča.
|Prístup k prvku v množine pomocou indexu alebo k hodnote v slovníku pomocou kľúča.
|-
|-
|[[Asymptotická notácia#O(log⁡n)|<math>O(\log{n})</math>]]
|[[Asymptotická notácia#Logaritmická časová zložitosť – O(log2n)|<math>O(\log_2{n})</math>]]
|Logaritmická
|Logaritmická
|Binárne vyhľadávanie (ak sú všetky prvky vopred správne zoradené)
|Binárne vyhľadávanie (ak sú všetky prvky vopred správne zoradené)
|-
|-
|[[Asymptotická notácia#O(n)|<math>O(n)</math>]]
|[[Asymptotická notácia#Lineárna časová zložitosť – O(n)|<math>O(n)</math>]]
|Lineárna
|Lineárna
|Prechod cez všetky prvky v množine
|Prechod cez všetky prvky v množine
|-
|-
|<math>O(n \log{n})</math>
|[[Asymptotická notácia#Kvázilineárna časová zložitosť – O(n⋅log2n)|<math>O(n \cdot \log_2{n})</math>]]
|Quasilineárna
|Kvázilineárna
|Efektívne triedenie (''merge-sort'', ''quick-sort'')
|Efektívne triedenie (''merge-sort'', ''quick-sort'')
|-
|-
|<math>O(n^2)</math>
|[[Asymptotická notácia#Kvadratická časová zložitosť – O(n2)|<math>O(n^2)</math>]]
|Kvadratická
|Kvadratická
|Vnorené cykly (''bubble-sort'')
|Vnorené cykly (''bubble-sort'')
|-
|-
|<math>O(2^n)</math>
|[[Asymptotická notácia#Exponenciálna časová zložitosť – O(2n)|<math>O(2^n)</math>]]
|Exponenciálna
|Exponenciálna
|Rekurzívny výpočet Fibonacciho postupnosti
|Rekurzívny výpočet Fibonacciho postupnosti
|-
|-
|[[Asymptotická notácia#O(n!)|<math>O(n!)</math>]]
|[[Asymptotická notácia#Faktoriálová časová zložitosť – O(n!)|<math>O(n!)</math>]]
|Faktoriálová
|Faktoriálová
|''Brute-force'' permutácie
|''Brute-force'' permutácie
|}
|}
Pri Big-O Skúmame iba '''trend''', to znamená že ak máme napríklad dva výsledky: <math>O(1 + n)</math>  a <math>O(1 000 000 + n)</math>, oba naznačujú že trend je '''lineárny''' – presný počet operácií nás nemusí zaujímať. Ak by sme mali ako výsledok napríklad <math>O(n^2 + n + 1)</math>, zredukovali by sme ho iba na <math>O(n^2)</math>, pretože táto hodnota má tendenciu rásť najrýchlejšie podľa toho ako by sa menilo <math>n</math> ('''rastie kvadraticky''', a to má prednosť pred lineárnym alebo konštantným správaním pretože rastie najrýchlejšie v závislosti od <math>n</math>).


=== <math>O(1)</math> ===
Ďalej, ak je za sebou viacero operácií s konštantným časom, neovplyvní to graf časovej zložitosti a tieto operácie môžu byť spolu chápané ako len jedna operácia s časom <math>O(1)</math><ref>"Time Complexity Theory" (W3 Schools; upravené, odporúčané) – https://www.w3schools.com/dsa/dsa_timecomplexity_theory.php</ref> (<math>O(1 + 5 + n + 1) = O(n)</math> – oba grafy sú lineárne).
 
=== Konštantná časová zložitosť – <math>O(1)</math> ===
Najlepší prípad je konštantný faktor, teda <math>O(1)</math>. V analógií z reálneho života by to napríklad znamenalo, že máme balíček kariet a chceme vybrať prvú kartu – a to urobíme okamžite, bez ohľadu na to koľko kariet je v balíčku. Môže ich tam byť 32, ale mohlo by ich byť aj 1 000, no prvú kartu vyberieme vo všetkých prípadoch vždy prakticky okamžite, teda je to iba akoby sme vykonali len jednu operáciu.
Najlepší prípad je konštantný faktor, teda <math>O(1)</math>. V analógií z reálneho života by to napríklad znamenalo, že máme balíček kariet a chceme vybrať prvú kartu – a to urobíme okamžite, bez ohľadu na to koľko kariet je v balíčku. Môže ich tam byť 32, ale mohlo by ich byť aj 1 000, no prvú kartu vyberieme vo všetkých prípadoch vždy prakticky okamžite, teda je to iba akoby sme vykonali len jednu operáciu.


Konkrétne príklady z programovania sú napríklad jednoduché aritmetické operácie (sčítanie, odčítanie, násobenie, delenie...), výber konkrétneho prvku na n-tej pozícií v indexovanom poli, vrátenie hodnoty a podobne...
Konkrétne príklady z programovania sú napríklad jednoduché aritmetické operácie (sčítanie, odčítanie, násobenie, delenie...), výber konkrétneho prvku na n-tej pozícií v indexovanom poli, vrátenie hodnoty a podobne...


=== <math>O(\log{n})</math> ===
=== Logaritmická časová zložitosť – <math>O(\log_2{n})</math> ===
Aha, máme tu aj obľúbené logaritmy! Samozrejme je za tým matematika ktorá funguje, ale pre uľahčenie života si stačí iba zapamätať:<blockquote>'''''Ak sa veľkosť spracovávaného vstupu''' (prvkov) '''skráti o polovicu, tak tento algoritmus bude mať logaritmickú rýchlosť.'''''</blockquote>Obľúbeným príkladom z praxe je napríklad binárne vyhľadávanie. Ak nevieš ako fungujú binárne stromy, tu je jeden príklad. Vľavo sú prvky ktoré sú menšie ako aktuálny rodič a vpravo sú prvky väčšie ako aktuálny rodič:


<div style="display: flex; justify-content: center;">[[Súbor:Jednoduchý binárny strom.png|alt=Jednoduchý binárny strom|644x644bod]]</div>


Vždy je garantované, že (pri správnom aplikovaní algoritmu) vyhľadávanie nejakého prvku zaberie o polovicu menej krokov ako keby sme prehľadávali celú množinu, prvok po prvku.
Aha, máme tu aj obľúbené logaritmy! Samozrejme je za tým matematika ktorá funguje, ale pre uľahčenie života si stačí iba zapamätať:<blockquote>'''Ak sa veľkosť spracovávaného vstupu''' (prvkov) '''<u>postupne skracuje</u> (neskráti sa naraz!) o polovicu, tak tento algoritmus bude mať logaritmickú rýchlosť.'''</blockquote>Obľúbeným príkladom z praxe je napríklad binárne vyhľadávanie. Binárne vyhľadávanie hľadá prvok vo '''vopred''' '''zoradenom poli''' tak, že:


Napríklad, povedzme že chceme nájsť číslo 13. Začíname od čísla 4 úplne hore. A zakaždým sa pýtame: je hľadané číslo (''13'') menšie alebo väčšie ako aktuálne číslo (''4'')? Ak je '''väčšie = doprava''', ak je '''menšie = doľava'''. Teda, 13 je väčšie, ideme doprava na číslo 5. 13 je stále väčšie ako 5, ideme doprava na 18. 13 je menšie ako 18, ideme doľava. A práve tam končíme, pretože sme našli prvok 13 ktorí sme hľadali.
# Skontroluje '''stredný prvok''';
# Ak je hľadaný prvok '''menší''', pokračuje v ľavej polovici; ak je '''väčší''', pokračuje v pravej polovici;
# Opakuje sa to, kým nenájdeme prvok ktorý hľadáme alebo kým už nezostane žiadna podmnožina na hľadanie (v tom prípade sa tam prvok nenachádza);


Dáva to zmysel - nech je množina akákoľvek dlhá s akýmikoľvek číslami, pri vyhľadávaní budeme mať vždy iba 2 možnosti, pričom vždy môžme ísť iba po jednej vetve. Teda, '''počet vstupu ktorý musíme spracovať sa skrátil o polovicu''' - o polovicu sa skráti aj množstvo krokov v algoritme ktoré musíme aplikovať, a teda sa matematicky musí skrátiť aj celkový čas hľadania (o polovicu), čo je vyjadrené ako <math>O(\log{n})</math>.
Každým krokom '''zmenšíme veľkosť vyhľadávanej časti na polovicu'''.
{{Úvaha|otazka=Prečo je tam logaritmus a nie napríklad <math>O\Bigl( \frac{n}{2} \Bigr)</math>, ak sa vstup skráti o poloviu?}}
Pretože vstup sa skracuje '''postupne''', neskráti sa celý naraz o polovicu. Ak začneme s poľom veľkosti <math>n</math>, postupne sa veľkosť vyhľadávanej časti zmenšuje:


=== <math>O(n)</math> ===
{| class="wikitable"
!Iterácia (<math>k</math>)
!Počet zostávajúcich prvkov (<math>n</math>)
!Poznámka
|-
|1. iterácia
|<math>n</math>
|Začiatočný počet všetkých prvkov (<math>n</math>).
|-
|2. iterácia
| <math>\frac{n}{2}</math>
|Prvý krát rozdelíme množinu na polovicu.
|-
|3. iterácia
|<math>\frac{n}{2} \div 2 = \frac{n}{4}</math>
|To, čo sme rozdelili ešte rozdelíme na polovicu.
|-
|4. iterácia
|<math>\frac{n}{4} \div 2 = \frac{n}{8}</math>
|Ešte delíme ďalej...
|-
|...
|...
|...až pokým nezostane nič, čo by sa dalo deliť
(matematicky to avšak pokračuje donekonečna).
|}
 
Vidíme, že číslo ktoré je v zlomku v menovateli (dole) je vždy výsledok <math>2^k</math>. Všeobecný vzorec pre výpočet zostávajúceho počtu prvkov v iterácií <math>k</math> je teda: <math>\frac{n}{2^k}</math>.
 
Zastavíme sa keď zostane iba 1 prvok, teda keď <math>\frac{n}{2^k} = 1</math>. Ak chceme určiť <math>k</math>, tak najprv vyjadríme <math>n</math> ako <math>n = 2^k</math> a použijeme logaritmus aby sme dostali: <math>k = \log_2 n</math>. Časová zložitosť je teda vyjadrená ako: <math>O(\log_2{n})</math>.
 
{{Box
| text = <p><b>Poznámka:</b> <math>O(\log_2{n})</math> je to isté ako <math>O(\log{n})</math>.</p>
| emoji = ℹ️
}}
 
=== Lineárna časová zložitosť – <math>O(n)</math> ===
Pravdepodobne asi najčastejší prípad v praxi. Predstavuje '''lineárnu rýchlosť''' algoritmu - inými slovami, '''o koľko viac prvkov pridáme do vstupu, o toľko sa spomalí algoritmus'''. Ak by sme mali balík kariet, znamenalo by to napríklad že chceme zrátať koľko tam tých kariet je. No a aby sme to spravili, musíme prejsť každou kartou v balíku. Ak máme 10 kariet, zrátame ich relatívne rýchlo, no pri milióne by sme už mali problém.
Pravdepodobne asi najčastejší prípad v praxi. Predstavuje '''lineárnu rýchlosť''' algoritmu - inými slovami, '''o koľko viac prvkov pridáme do vstupu, o toľko sa spomalí algoritmus'''. Ak by sme mali balík kariet, znamenalo by to napríklad že chceme zrátať koľko tam tých kariet je. No a aby sme to spravili, musíme prejsť každou kartou v balíku. Ak máme 10 kariet, zrátame ich relatívne rýchlo, no pri milióne by sme už mali problém.
{{Box
{{Box
| text = <p><i><b>Poznámka:</b></i> Pri výpočtovej zložitosti vždy predpokladáme že každá operácia - v našom prípade prirátanie každej karty do celkového počtu - nám zaberie rovnaký čas, napríklad pol sekundy. To znamená, že 10 kariet by sme rátali 5 sekúnd, milión kariet by sme v tom prípade rátali približne 140 hodín).<br/>Ale ako sme hovorili predtým, zaujíma nás iba trend - pretože aj tak nedokážeme rozpočítať koľko trvajú jednotlivé procesy v počítači (mení sa to v závislosti od výkonu hardvéru, aktuálneho zaťaženia počítača a tak ďalej...).</p>
| text = <p><i><b>Poznámka:</b></i> Pri výpočtovej zložitosti vždy predpokladáme že každá operácia - v našom prípade prirátanie každej karty do celkového počtu - nám zaberie rovnaký čas, napríklad pol sekundy. To znamená, že 10 kariet by sme rátali 5 sekúnd, milión kariet by sme v tom prípade rátali približne 140 hodín.<br/>Ale ako sme hovorili predtým, zaujíma nás iba trend - pretože aj tak nedokážeme rozpočítať koľko trvajú jednotlivé procesy v počítači, napríklad koľko trvá prístup k prvku v poli (mení sa to v závislosti od výkonu hardvéru, aktuálneho zaťaženia počítača a tak ďalej...). Zaujíma nás všeobecný trend toho, ako sa výkon mení a to nám stačí pre jednoznačné určenie lepšieho algoritmu.</p>
| emoji = ℹ️
| emoji = ℹ️
}}
}}


=== <math>O(n!)</math> ===
=== Kvázilineárna časová zložitosť – <math>O(n \cdot \log_2{n})</math> ===
Kým v logaritmickej zložitosti (<math>O(\log_2{n})</math>) sa rozdeľuje v každom kroku celkový počet prvkov na polovicu, kvázilineárna zložitosť hovorí že '''každý prvok na vstupe je spracovávaný logaritmicky''' (teda, logaritmus <math>n</math> sa násobí <math>n</math>-krát).
 
Táto zložitosť sa objavuje '''hlavne v efektívnych triediacich algoritmoch''' a v algoritmoch, ktoré '''kombinujú lineárnu a logaritmickú prácu''' (preto sa nazýva "kvázilineárna" – nie je úplne lineárna, ale ani úplne logaritmická). Medzi príklady patria: [[Triediace algoritmy a ich výpočtová zložitosť#Merge-sort|Merge sort]], [[Triediace algoritmy a ich výpočtová zložitosť#Quick-sort|Quick sort]], [[Triediace algoritmy a ich výpočtová zložitosť#Heap-sort|Heap sort]].
 
=== Kvadratická časová zložitosť – <math>O(n^2)</math> ===
V praxi predstavuje kvadratická zložitosť najčastejšie algoritmy, ktoré využívajú pre prácu vnorené cykly (napríklad, triediaci algoritmus [[Triediace algoritmy a ich výpočtová zložitosť#Bubble-sort|Bubble sort]]).
 
=== Exponenciálna časová zložitosť – <math>O(2^n)</math> ===
Horšia ako kvadratická zložitosť, exponenciálna zložitosť má ako exponent číslo <math>n</math>. Napríklad, povedzme že máme nájsť všetky kombinácie pre binárne číslo dĺžky <math>n</math>. Máme k dispozícií dva znaky (0 a 1), teda časová zložitosť pre algoritmus ktorý takéto kombinácie hľadá bude <math>O(2^n)</math>.
 
Ďalší prípad je, ak by sme mali napríklad nájsť a vyskúšať všetky kombinácie určitého hesla dĺžky <math>n</math> (ak predpokladáme, že funkcia ktorá heslo vyskúša má konštantnú časovú zložitosť <math>O(1)</math> a heslo sa skladá iba z čísiel od 0 po 9). Časová zložitosť by bola <math>O(10^n)</math>, teda vieme zistiť presný počet operácií pre dĺžku hesla <math>n</math>. Avšak, pre stanovenie typu časovej zložitosti sa ako základ väčšinou konvenčne uvádza iba číslo 2 (teda, <math>O(2^n)</math>), pretože nám to stačí pre vyjadrenie že čas rastie exponenciálne.
 
=== Faktoriálová časová zložitosť – <math>O(n!)</math> ===
Najhorší prípad je <math>O(n!)</math>. Ak si zabudol, ten "výkričník" v matematike sa nazýva ''faktoriál'' a je to súčin všetkých prvkov od <math>n</math> po 1 – napríklad, <math>5!</math> znamená: <math>5 \times 4 \times 3 \times 2 = 120</math>.
Najhorší prípad je <math>O(n!)</math>. Ak si zabudol, ten "výkričník" v matematike sa nazýva ''faktoriál'' a je to súčin všetkých prvkov od <math>n</math> po 1 – napríklad, <math>5!</math> znamená: <math>5 \times 4 \times 3 \times 2 = 120</math>.


<math>O(n!)</math>  znamená že máme napríklad balík kariet na ktorých sú čísla. Tieto karty chceme zoradiť od najväčšieho čísla po najmenšie. Ale robíme to takým spôsobom, že karty vyhodíme do vzduchu a dúfame že padnú na zem správne zoradené. Predpokladáme teda, že musíme vykonať všetky možné kombinácie operácií pre to, aby sme ich niekedy vytriedili správne (preto je tam ten faktoriál, kombinatorika funguje...). To môže pri malom množstve kariet trvať niekoľko minút, no pri väčších číslach sa môžeme trápiť bezvýsledne aj niekoľko rokov...
<math>O(n!)</math>  znamená že máme napríklad balík kariet na ktorých sú čísla. Tieto karty chceme zoradiť od najväčšieho čísla po najmenšie. Ale robíme to takým spôsobom, že karty vyhodíme do vzduchu a dúfame že padnú na zem správne zoradené. Predpokladáme teda, že musíme vykonať všetky možné kombinácie operácií pre to, aby sme ich niekedy vytriedili správne (preto je tam ten faktoriál, kombinatorika funguje...). To môže pri malom množstve kariet trvať niekoľko minút, no pri väčších číslach sa môžeme trápiť bezvýsledne aj niekoľko rokov. Takže, vo všeobecnosti sa vždy snažíme zložitosti <math>O(n!)</math> vyhnúť.
 
Čo je aj dôvod, prečo vlastne všetko toto počítame a analyzujeme. Je rozdiel, ak nejaký algoritmus trvá pár sekúnd versus pár rokov, že? Naším cieľom je samozrejme tvoriť algoritmy ktoré sú efektívne a prehľadné. A Big-O notácia nám umožňuje porovnávať výkon dvoch rozličných algoritmov matematicky pre veľké vstupy. A keďže sme v dobe kedy bežne pracujeme s veľkým objemom dát na dennej báze, je veľmi dôležité aby bolo všetko čo najrýchlejšie.
 
== Pamäťová zložitosť algoritmu ==
Podobne ako pri časovej, tak aj pri pamäťovej zložitosti môžeme využiť asymptotickú notáciu (''Big-O''). Má formu <math>O(g(n))</math>. Predstavuje, '''koľko dodatočnej pamäte spotrebuje algoritmus počas behu''' (dodatočnej preto, lebo neberieme ohľad na pamäť do ktorej sa uloží vstup alebo inštrukcie pre program). Pri pamäťovej zložitosti '''analyzujeme premenné, polia, pomocné štruktúry''' a '''rekurzívne zásobníky'''.
 
Niektoré algoritmy šetria pamäť, ale sú pomalšie. Naopak, iné sú rýchlejšie ale zaberajú viacej pamäte. Preto neexistuje jeden najlepší algoritmus, a treba si vybrať (aj po zvážení časovej zložitosti) aký algoritmus použijeme. Ak spracovávame malé množstvo dát, pravdepodobne budeme priorizovať lepšiu časovú zložitosť. Pri veľkom množstve dát budeme brať ohľad aj na zväčšujúcu sa pamäťovú náročnosť.
 
Tu je náš predošlý kód:
 
<syntaxhighlight lang="python3">
DNI = ["Pondelok", "Utorok", "Streda", "Štvrtok", "Piatok", "Sobota", "Nedeľa"]
#          0        1          2        3        4        5          6
 
poradie_v_tyzdni = 3          # tretí deň v týždni
den = DNI[poradie_v_tyzdni-1] # -1, pretože indexujeme od 0
 
print(den)
# "Streda"
</syntaxhighlight>
 
Je možné vypočítať, koľko zaberie celý takýto program miesta v pamäti, presne na bajty. Avšak, podobne ako pri časovej zložitosti kde neriešime presný počet operácií, aj pri pamäťovej zložitosti nás bude zaujímať iba '''trend''' (graf toho, ako náročnosť klesá alebo sa zvyšuje v závislosti od objemu dát na vstupe, teda <math>n</math>).
 
Kód vyššie by sa dal analyzovať takto:
 
<syntaxhighlight lang="python3">DNI = ["Pondelok", "Utorok", "Streda", "Štvrtok", "Piatok", "Sobota", "Nedeľa"]
# toto predstavuje akoby premennú "n"
# môžme ju ignorovať, pretože pamäťová zložitosť vstupných dát nás nezaujíma,
# zaujíma nás iba dodatočná pamäť ktorú program potrebuje počas behu
 
poradie_v_tyzdni = 3          # je to jednoduché priradenie konštanty...
den = DNI[poradie_v_tyzdni-1] # ...rovnako aj tu
# preto je zložitosť pre obidva riadky vyššie = O(1)
 
print(den) # jednoduchý print, nikde nič neukladáme
</syntaxhighlight>
 
Ak to všetko dáme dokopy, získame <math>O(1 + 1)</math>. Po zanedbaní nižších rádov má kód vyššie pamäťovú zložitosť <math>O(1)</math>.
 
Čo ak by sme mali iný kód. Napríklad, chceme všetky reťazce na vstupe transformovať na reťazce, kde budú všetky písmená veľké. Tu je jedno z riešení:
 
<syntaxhighlight lang="python3">
def na_velke(n):
  vsetky_velke = []
 
  for retazec in n:
    novy = retazec.upper()    # konverzia na reťazec kde sú všetky písmená veľké
    vsetky_velke.append(novy) # pridáme nový reťazec do výslednej množiny
 
  return vsetky_velke
 
</syntaxhighlight>
 
Časová zložitosť je samozrejme <math>O(n)</math> –  čím väčší máme zoznam na vstupe, tým viac operácií (iterácií) bude kód vykonávať, teda časová zložitosť rastie lineárne. Rovnako je to aj s pamäťovou zložitosťou, ktorá je tiež <math>O(n)</math> – dodatočná pamäť, ktorú program spotrebuje '''rastie lineárne''' s pamäťou kde sú uložené vstupné prvky. Inými slovami, ak máme na začiatku 10 prvkov, tak naspäť aj 10 prvkov dostaneme (s tým, že v pamäti bude spolu 20 prvkov, 10 na vstupe a 10 kde sme premenili všetky písmená na veľké - ale ako sme hovorili, zaujíma nás dodatočná pamäť a nie pamäť na vstupe, teda iba 10 prvkov s veľkými písmenami).
 
Detaily o výpočtovej zložitosti jednotlivých algoritmov sa budú nachádzať na ich stránkach ďalej v tejto téme.
 
== Referencie ==
<references />
 
{{Téma|Oblast=Kategória:Algoritmy a výpočtová zložitosť|Poradie=30}}


Čo je aj dôvod, prečo vlastne všetko toto počítame a analyzujeme. Je rozdiel, ak nejaký algoritmus trvá pár sekúnd versus pár rokov, že? Naším cieľom je samozrejme tvoriť algoritmy ktoré sú efektívne a prehľadné. A Big-O notácia nám umožňuje porovnávať výkon dvoch rozličných algoritmov matematicky pre veľké vstupy. A keďže sme v dobe kedy bežne pracujeme s veľkým objemom dát na dennej báze, je veľmi dôležité aby bolo všetko čo najrýchlejšie – pretože nikto nemá čas a každý sa vždy všade iba ponáhľa...
[[Kategória:Algoritmy a výpočtová zložitosť]]
{{Téma|Oblast=Kategória:Výpočtová zložitosť algoritmov|Poradie=20}}
[[Kategória:Výpočtová zložitosť algoritmov]]

Aktuálna revízia z 09:45, 3. máj 2025

Čo je asymptotická (Big-O) notácia, príklady a interpretácia najčastejších výsledkov.


Typy asymptotických notácií

Asymptotická notácia sa v informatike používa pre vyjadrenie asymptotického správania algoritmu, teda ako sa jeho výkon mení pri veľkých hodnotách n.

Poznáme tri hlavné typy:

  • Big-O: O(n) – horná hranica, opisuje zložitosť algoritmu v najhoršom prípade;
  • Big-Omega: Ω(n) – dolná hranica, opisuje zložitosť algoritmu v najlepšom prípade;
  • Theta: Θ(n) – priemerný prípad;

V praxi sa snažíme, aby sa horná aj spodná hranica algoritmu čo najviac rovnala (snažíme sa o priemerný prípad). Ak sa rovnajú, znamená to že takáto výpočtová zložitosť algoritmu je ideálna.

Príklady pre určovanie asymptotických hraníc

Matematicky sa horná asymptotická hranica definuje ako:

f(n)=O(g(n))c,m>Onmf(n)c×g(n)

V preklade: O(n) je definovaná ako funkcia, pre ktorú existujú konštanty c a m ktoré sú väčšie ako O pre všetky nm. Platí, že všetky hodnoty tejto funkcie sú menšie alebo rovné ako hodnoty pôvodnej funkcie g(n) prenásobené konštantou c.

Dolná asymptotická hranica sa definuje podobne, iba hľadáme všetky hodnoty väčšie alebo rovné ako c×g(n):

f(n)=Ω(g(n))c,m>Ωnmf(n)c×g(n)

V podstate pri týchto príkladoch iba hľadáme konštanty c a m pre daný typ hranice.

Majme napríklad:

n+12=O(n)

Máme zistiť, či výraz naľavo patrí asymptotickej hornej hranici. To znamená, že:

n+12c×n

Za konštantu c si môžme zvoliť akékoľvek kladné číslo. Predvolene je c=1:

n+121×nn+12n

Dostali sme rovnicu ktorú iba vypočítame. To, čo patrí výslednému n bude naša konštanta m:

n+12nn+12n1nm=1; c=1

Skúsme to pre:

n+12=Ω(n)

Postup zostáva rovnaký, iba použijeme znamienko a konštantu c=12:

n+12c×nn+1212nn+1n10platí pre všetky nm=1; c=12

Interpretácia výsledkov asymptotickej časovej zložitosti

Porovnanie výpočtovej zložitosti
Porovnanie časovej zložitosti pre rôzne asymptotické správania funkcií. Os Y (N) predstavuje počet operácií, os X (n) predstavuje počet prvkov na vstupe

V tabuľke možno vidieť porovnanie najčastejších výsledkov asymptotickej notácie (pre časovú zložitosť):

Big-O notácia Typická rýchlosť Príklad algoritmu
O(1) Konštantná Prístup k prvku v množine pomocou indexu alebo k hodnote v slovníku pomocou kľúča.
O(log2n) Logaritmická Binárne vyhľadávanie (ak sú všetky prvky vopred správne zoradené)
O(n) Lineárna Prechod cez všetky prvky v množine
O(nlog2n) Kvázilineárna Efektívne triedenie (merge-sort, quick-sort)
O(n2) Kvadratická Vnorené cykly (bubble-sort)
O(2n) Exponenciálna Rekurzívny výpočet Fibonacciho postupnosti
O(n!) Faktoriálová Brute-force permutácie

Pri Big-O Skúmame iba trend, to znamená že ak máme napríklad dva výsledky: O(1+n) a O(1000000+n), oba naznačujú že trend je lineárny – presný počet operácií nás nemusí zaujímať. Ak by sme mali ako výsledok napríklad O(n2+n+1), zredukovali by sme ho iba na O(n2), pretože táto hodnota má tendenciu rásť najrýchlejšie podľa toho ako by sa menilo n (rastie kvadraticky, a to má prednosť pred lineárnym alebo konštantným správaním pretože rastie najrýchlejšie v závislosti od n).

Ďalej, ak je za sebou viacero operácií s konštantným časom, neovplyvní to graf časovej zložitosti a tieto operácie môžu byť spolu chápané ako len jedna operácia s časom O(1)[1] (O(1+5+n+1)=O(n) – oba grafy sú lineárne).

Konštantná časová zložitosť – O(1)

Najlepší prípad je konštantný faktor, teda O(1). V analógií z reálneho života by to napríklad znamenalo, že máme balíček kariet a chceme vybrať prvú kartu – a to urobíme okamžite, bez ohľadu na to koľko kariet je v balíčku. Môže ich tam byť 32, ale mohlo by ich byť aj 1 000, no prvú kartu vyberieme vo všetkých prípadoch vždy prakticky okamžite, teda je to iba akoby sme vykonali len jednu operáciu.

Konkrétne príklady z programovania sú napríklad jednoduché aritmetické operácie (sčítanie, odčítanie, násobenie, delenie...), výber konkrétneho prvku na n-tej pozícií v indexovanom poli, vrátenie hodnoty a podobne...

Logaritmická časová zložitosť – O(log2n)

Aha, máme tu aj obľúbené logaritmy! Samozrejme je za tým matematika ktorá funguje, ale pre uľahčenie života si stačí iba zapamätať:

Ak sa veľkosť spracovávaného vstupu (prvkov) postupne skracuje (neskráti sa naraz!) o polovicu, tak tento algoritmus bude mať logaritmickú rýchlosť.

Obľúbeným príkladom z praxe je napríklad binárne vyhľadávanie. Binárne vyhľadávanie hľadá prvok vo vopred zoradenom poli tak, že:

  1. Skontroluje stredný prvok;
  2. Ak je hľadaný prvok menší, pokračuje v ľavej polovici; ak je väčší, pokračuje v pravej polovici;
  3. Opakuje sa to, kým nenájdeme prvok ktorý hľadáme alebo kým už nezostane žiadna podmnožina na hľadanie (v tom prípade sa tam prvok nenachádza);

Každým krokom zmenšíme veľkosť vyhľadávanej časti na polovicu.

🤔

Prečo je tam logaritmus a nie napríklad O(n2), ak sa vstup skráti o poloviu?

Pretože vstup sa skracuje postupne, neskráti sa celý naraz o polovicu. Ak začneme s poľom veľkosti n, postupne sa veľkosť vyhľadávanej časti zmenšuje:

Iterácia (k) Počet zostávajúcich prvkov (n) Poznámka
1. iterácia n Začiatočný počet všetkých prvkov (n).
2. iterácia n2 Prvý krát rozdelíme množinu na polovicu.
3. iterácia n2÷2=n4 To, čo sme rozdelili ešte rozdelíme na polovicu.
4. iterácia n4÷2=n8 Ešte delíme ďalej...
... ... ...až pokým nezostane nič, čo by sa dalo deliť

(matematicky to avšak pokračuje donekonečna).

Vidíme, že číslo ktoré je v zlomku v menovateli (dole) je vždy výsledok 2k. Všeobecný vzorec pre výpočet zostávajúceho počtu prvkov v iterácií k je teda: n2k.

Zastavíme sa keď zostane iba 1 prvok, teda keď n2k=1. Ak chceme určiť k, tak najprv vyjadríme n ako n=2k a použijeme logaritmus aby sme dostali: k=log2n. Časová zložitosť je teda vyjadrená ako: O(log2n).

ℹ️

Poznámka: O(log2n) je to isté ako O(logn).

Lineárna časová zložitosť – O(n)

Pravdepodobne asi najčastejší prípad v praxi. Predstavuje lineárnu rýchlosť algoritmu - inými slovami, o koľko viac prvkov pridáme do vstupu, o toľko sa spomalí algoritmus. Ak by sme mali balík kariet, znamenalo by to napríklad že chceme zrátať koľko tam tých kariet je. No a aby sme to spravili, musíme prejsť každou kartou v balíku. Ak máme 10 kariet, zrátame ich relatívne rýchlo, no pri milióne by sme už mali problém.

ℹ️

Poznámka: Pri výpočtovej zložitosti vždy predpokladáme že každá operácia - v našom prípade prirátanie každej karty do celkového počtu - nám zaberie rovnaký čas, napríklad pol sekundy. To znamená, že 10 kariet by sme rátali 5 sekúnd, milión kariet by sme v tom prípade rátali približne 140 hodín.
Ale ako sme hovorili predtým, zaujíma nás iba trend - pretože aj tak nedokážeme rozpočítať koľko trvajú jednotlivé procesy v počítači, napríklad koľko trvá prístup k prvku v poli (mení sa to v závislosti od výkonu hardvéru, aktuálneho zaťaženia počítača a tak ďalej...). Zaujíma nás všeobecný trend toho, ako sa výkon mení a to nám stačí pre jednoznačné určenie lepšieho algoritmu.

Kvázilineárna časová zložitosť – O(nlog2n)

Kým v logaritmickej zložitosti (O(log2n)) sa rozdeľuje v každom kroku celkový počet prvkov na polovicu, kvázilineárna zložitosť hovorí že každý prvok na vstupe je spracovávaný logaritmicky (teda, logaritmus n sa násobí n-krát).

Táto zložitosť sa objavuje hlavne v efektívnych triediacich algoritmoch a v algoritmoch, ktoré kombinujú lineárnu a logaritmickú prácu (preto sa nazýva "kvázilineárna" – nie je úplne lineárna, ale ani úplne logaritmická). Medzi príklady patria: Merge sort, Quick sort, Heap sort.

Kvadratická časová zložitosť – O(n2)

V praxi predstavuje kvadratická zložitosť najčastejšie algoritmy, ktoré využívajú pre prácu vnorené cykly (napríklad, triediaci algoritmus Bubble sort).

Exponenciálna časová zložitosť – O(2n)

Horšia ako kvadratická zložitosť, exponenciálna zložitosť má ako exponent číslo n. Napríklad, povedzme že máme nájsť všetky kombinácie pre binárne číslo dĺžky n. Máme k dispozícií dva znaky (0 a 1), teda časová zložitosť pre algoritmus ktorý takéto kombinácie hľadá bude O(2n).

Ďalší prípad je, ak by sme mali napríklad nájsť a vyskúšať všetky kombinácie určitého hesla dĺžky n (ak predpokladáme, že funkcia ktorá heslo vyskúša má konštantnú časovú zložitosť O(1) a heslo sa skladá iba z čísiel od 0 po 9). Časová zložitosť by bola O(10n), teda vieme zistiť presný počet operácií pre dĺžku hesla n. Avšak, pre stanovenie typu časovej zložitosti sa ako základ väčšinou konvenčne uvádza iba číslo 2 (teda, O(2n)), pretože nám to stačí pre vyjadrenie že čas rastie exponenciálne.

Faktoriálová časová zložitosť – O(n!)

Najhorší prípad je O(n!). Ak si zabudol, ten "výkričník" v matematike sa nazýva faktoriál a je to súčin všetkých prvkov od n po 1 – napríklad, 5! znamená: 5×4×3×2=120.

O(n!) znamená že máme napríklad balík kariet na ktorých sú čísla. Tieto karty chceme zoradiť od najväčšieho čísla po najmenšie. Ale robíme to takým spôsobom, že karty vyhodíme do vzduchu a dúfame že padnú na zem správne zoradené. Predpokladáme teda, že musíme vykonať všetky možné kombinácie operácií pre to, aby sme ich niekedy vytriedili správne (preto je tam ten faktoriál, kombinatorika funguje...). To môže pri malom množstve kariet trvať niekoľko minút, no pri väčších číslach sa môžeme trápiť bezvýsledne aj niekoľko rokov. Takže, vo všeobecnosti sa vždy snažíme zložitosti O(n!) vyhnúť.

Čo je aj dôvod, prečo vlastne všetko toto počítame a analyzujeme. Je rozdiel, ak nejaký algoritmus trvá pár sekúnd versus pár rokov, že? Naším cieľom je samozrejme tvoriť algoritmy ktoré sú efektívne a prehľadné. A Big-O notácia nám umožňuje porovnávať výkon dvoch rozličných algoritmov matematicky pre veľké vstupy. A keďže sme v dobe kedy bežne pracujeme s veľkým objemom dát na dennej báze, je veľmi dôležité aby bolo všetko čo najrýchlejšie.

Pamäťová zložitosť algoritmu

Podobne ako pri časovej, tak aj pri pamäťovej zložitosti môžeme využiť asymptotickú notáciu (Big-O). Má formu O(g(n)). Predstavuje, koľko dodatočnej pamäte spotrebuje algoritmus počas behu (dodatočnej preto, lebo neberieme ohľad na pamäť do ktorej sa uloží vstup alebo inštrukcie pre program). Pri pamäťovej zložitosti analyzujeme premenné, polia, pomocné štruktúry a rekurzívne zásobníky.

Niektoré algoritmy šetria pamäť, ale sú pomalšie. Naopak, iné sú rýchlejšie ale zaberajú viacej pamäte. Preto neexistuje jeden najlepší algoritmus, a treba si vybrať (aj po zvážení časovej zložitosti) aký algoritmus použijeme. Ak spracovávame malé množstvo dát, pravdepodobne budeme priorizovať lepšiu časovú zložitosť. Pri veľkom množstve dát budeme brať ohľad aj na zväčšujúcu sa pamäťovú náročnosť.

Tu je náš predošlý kód:

DNI = ["Pondelok", "Utorok", "Streda", "Štvrtok", "Piatok", "Sobota", "Nedeľa"]
#           0         1          2         3         4         5          6

poradie_v_tyzdni = 3          # tretí deň v týždni
den = DNI[poradie_v_tyzdni-1] # -1, pretože indexujeme od 0

print(den)
# "Streda"

Je možné vypočítať, koľko zaberie celý takýto program miesta v pamäti, presne na bajty. Avšak, podobne ako pri časovej zložitosti kde neriešime presný počet operácií, aj pri pamäťovej zložitosti nás bude zaujímať iba trend (graf toho, ako náročnosť klesá alebo sa zvyšuje v závislosti od objemu dát na vstupe, teda n).

Kód vyššie by sa dal analyzovať takto:

DNI = ["Pondelok", "Utorok", "Streda", "Štvrtok", "Piatok", "Sobota", "Nedeľa"]
# toto predstavuje akoby premennú "n"
# môžme ju ignorovať, pretože pamäťová zložitosť vstupných dát nás nezaujíma,
# zaujíma nás iba dodatočná pamäť ktorú program potrebuje počas behu

poradie_v_tyzdni = 3          # je to jednoduché priradenie konštanty...
den = DNI[poradie_v_tyzdni-1] # ...rovnako aj tu
# preto je zložitosť pre obidva riadky vyššie = O(1)

print(den) # jednoduchý print, nikde nič neukladáme

Ak to všetko dáme dokopy, získame O(1+1). Po zanedbaní nižších rádov má kód vyššie pamäťovú zložitosť O(1).

Čo ak by sme mali iný kód. Napríklad, chceme všetky reťazce na vstupe transformovať na reťazce, kde budú všetky písmená veľké. Tu je jedno z riešení:

def na_velke(n):
  vsetky_velke = []

  for retazec in n:
    novy = retazec.upper()    # konverzia na reťazec kde sú všetky písmená veľké
    vsetky_velke.append(novy) # pridáme nový reťazec do výslednej množiny

  return vsetky_velke

Časová zložitosť je samozrejme O(n) – čím väčší máme zoznam na vstupe, tým viac operácií (iterácií) bude kód vykonávať, teda časová zložitosť rastie lineárne. Rovnako je to aj s pamäťovou zložitosťou, ktorá je tiež O(n) – dodatočná pamäť, ktorú program spotrebuje rastie lineárne s pamäťou kde sú uložené vstupné prvky. Inými slovami, ak máme na začiatku 10 prvkov, tak naspäť aj 10 prvkov dostaneme (s tým, že v pamäti bude spolu 20 prvkov, 10 na vstupe a 10 kde sme premenili všetky písmená na veľké - ale ako sme hovorili, zaujíma nás dodatočná pamäť a nie pamäť na vstupe, teda iba 10 prvkov s veľkými písmenami).

Detaily o výpočtovej zložitosti jednotlivých algoritmov sa budú nachádzať na ich stránkach ďalej v tejto téme.

Referencie

  1. "Time Complexity Theory" (W3 Schools; upravené, odporúčané) – https://www.w3schools.com/dsa/dsa_timecomplexity_theory.php