%! TEX root = ../main.tex

\section{Wprowadzenie}
Tworząc oprogramowanie jednym z aspektów, na które powinniśmy zwrócić szczególną uwagę jest wydajność - zarówno czasowa, 
jak i pamięciowa. Wydajność czasową rozumiemy oczywiscie przez czas, w którym wykonuje się nasz program, a przez 
pamięciową ilość pamięci operacyjnej, którą potrzebuje program do działania.

Wydajność czasowa nie może być rozumiana jako ilość sekund potrzebna do wykonania danego programu. Absolutny wymiar czasu 
- podany w sekundach - jest zależny od wielu czynników, w tym szybkości komputera czy zajętości jego zasobów. Dlatego
w celu badania wydajnosci czasowej programu najczęściej rozpatrujemy liczbę operacji jaką musi wykonać program dla
pewnych danych. Dlatego w dalszej mówiąc o czasie będziemy się skupiać na liczbie takich kroków.

\subsection{Przedstawienie liczby operacji w formie sumy}
Załóżmy, że mamy pętlę:
\begin{algorithmic}[1]
    \For{$i = 1 \ldots n$}
        \State \Call{$f$}{$i$}
    \EndFor
\end{algorithmic}

Wiemy, że czas działania (rozumiany jako liczba operacji) funkcji $f$ możemy wyznaczyć za pomocą funkcji $T_f(n)$ (ta 
konwencja oznaczania będzie utrzymana przez dalsze rozdziały). Pętla wywołuję funkcję $f$ dla każdego $i$ od 1 do $n$, 
zatem czas działania funkcji możemy przedstawić następująco:

\begin{equation}
    T = T_f(1) + T_f(2) + T_f(3) + \cdots + T_f(n) = \sum_{i = 1}^{n} T_f(i) \label{eq:1-intro:loop-to-sum}
\end{equation}

Weźmy inny program:
\begin{algorithmic}[1]
    \For{$i = 1; i < n; i \gets 2i$}
        \State \Call{$f$}{$i$}
    \EndFor
\end{algorithmic}

W tym wypadku, kolejne wartości $i$ będą kolejnymi potęgami 2, zatem czas jaki przeznaczymy na wykonanie tej pętli
zapiszemy jako:
\begin{equation*}
    T = T_f(1) + T_f(2) + T_f(4) + T_f(8) + \cdots
\end{equation*}

Na pierwszy rzut oka ciężko zamienić ten program na matematyczną sumę. Jak jednak zostało zauważone, kolejne wartości
$i$ są kolejnymi potęgami 2. Załóżmy zatem, że $l$ jest licznikiem wykonań pętli, tj. reprezentuje ilość już wykonanych 
iteracji. W takim wypadku moglibyśmy powiedzieć, że:
\begin{equation*}
    i = 2^l
\end{equation*}

czyli warunek $i < n$ byłby spełniony wtedy i tylko wtedy, gdy:
\begin{equation*}
    2^l < n \iff l < \log_2 n
\end{equation*}

Ponieważ $l$ z założenia jest licznikiem pętli - to wiemy, że pętla wykona się nie więcej niż $\log_2 n$ razy. 
Łącząc te 2 informacje uzyskamy taki wzór na czas:
\begin{equation*}
    T = T_f(1) + T_f(2^1) + T_f(2^2) + T_f(2^3) + \cdots = \sum_{i=0}^{\log_2 n} T_f(2^i)
\end{equation*}

Oczywiście nic nie stoi na przeszkodzie, aby rozważać pętle w pętlach:
\begin{program}[H]
    \begin{algorithmic}[1]
        \For{$i = 1\ldots n$}
            \For{$j = 1\ldots n$}
                \State \Call{$f$}{$i, j$}
            \EndFor
        \EndFor
    \end{algorithmic}
\end{program}
Zgodnie z wcześniejszymi ustaleniami wewnętrzna pętla wykona sie przez czas $T_w = \sum_{j = 1}^n T_f(i, j)$. 
Zewnętrzną rozpatrzymy analogicznie, uzyskując:
\begin{equation}
    T = \sum_{i=1}^n T_w = \sum_{i=1}^n \sum_{j = 1}^n T_f(i, j)
\end{equation}

Załóżmy, że mamy program który sumuje nam wszystkie liczby od 1 do $n$, program prezentuje się następująco:
\begin{program}[H]
    \begin{algorithmic}[1]
        \Require
        \Statex $n$ - liczba naturalna \label{line:1-intro:n}
        \Statex

        \State $S \gets 0$ \Comment{$S \gets 0$ oznacza, że do zmiennej $S$ przypisujemy wartość $0$} \label{line:1-intro:S}
        \For{$i = 1 \ldots n$}
            \State $S \gets S + i$ \label{line:1-intro:loop}
        \EndFor
    \end{algorithmic}
\end{program}

Linia \ref{line:1-intro:S} to jedna operacja przypisania, wykonująca się w czasie $T_\gets$, a w pętli wykonujemy
operacje przpisania i dodania o łącznym czasie $T_\gets + T_+$. Ponieważ w komputerach operujemy zwykle na liczbach
o stałym rozmiarze to możemy założyć, że oba te czasy są niezależne od zmiennej $i$. Zgodnie z równaniem 
\ref{eq:1-intro:loop-to-sum} możemy zapisac, że łączny czas wykonania tego programu wynosi
\begin{equation*}
    T = T_\gets + \sum_{i = 1}^{n}(T_\gets + T_+)
\end{equation*}

Ponieważ $T_\gets + T_+$ jest stałe (tj. nie zależy od zmiennej $i$) to możemy wyciągnąć to wyrażenie przed znak sumy:
\begin{equation*}
    T = T_\gets + (T_\gets + T_+)\sum_{i = 1}^{n} 1
\end{equation*}

A ponieważ $\sum_{i = 1}^n 1 = n$ (patrz \ref{eqn:sum:1} z strony \pageref{appendix:sum-formulas}), to 
całe równanie możemy sprowadzić do:
\begin{equation}
    T = T_\gets + (T_\gets + T_+)n = (n+1)T_\gets + nT_+
\end{equation}

Program wykona zatem $n+1$ operacji przypisania i $n$ operacji dodawania.

\subsection{Asymptotyczne tempo wzrostu - notacja dużego $O$}
Jak widac po powyższych przykładach, nawet dla prostych programów ustalenie dokładnej liczby kroków może być uciążliwe
i czasochłonne. Biorąc pod uwagę, że i tak nie jesteśmy zainteresowani czasem w sekundach często bardziej interesuje
nas ogólny pogląd na zachowanie programu. Wiemy np, że każda (rosnąca) funkcja liniowa rośnie szybciej od funkcji
stałej - gdzie przez ,,rośnie szybciej'' rozumiemy, że od pewnego momentu wykres funkcji liniowej na pewno znajdzie 
się nad wykresem funkcji stałej (obie są prostymi, w dodatku nie równoległymi, więc na pewno się przetną) i tak już
pozostanie do końca. 

Udowodnijmy, że żadna funkcja liniowa postaci $a_lx + b_l$ nie będzie rosła szybciej niż jakakolwiek funkcja kwadratowa 
$a_kx^2 + b_kx + c_k$. Na nasze potrzeby potrzebujemy tylko rozważyć funkcje rosnące - ponieważ czas nie płynie do tyłu, 
zatem $a_l, a_k > 0$. Chcąc wyznaczyć, kiedy funkcja liniowa jest większa od kwadratowej musimy rozpatrzyć następującą 
nierówność:
\begin{equation}
    a_lx + b_l > a_kx^2 b_kx + c_k
\end{equation}

Przenieśmy wszystkie składniki na lewą stronę nierówności:
\begin{equation*}
    -a_kx^2 a_lx - b_kx + b_l - c_k > 0
\end{equation*}
\begin{equation*}
    -a_kx^2 + (a_l - b_k)x + (b_l - c_k) > 0
\end{equation*}

Ponieważ z założenia $a_k > 0$ to parabola ma ramiona skierowane w dół, czyli od pewnego momentu na pewno znajduje się
poniżej osi \texttt{x}. Zatem nawet przy skrajnie dużej wartości współczynnika $a_l$ (określającego szybkość wzrostu funkcji 
liniowej) oraz jednocześnie skrajnie małej wartości współczynnika $a_k$ funkcja liniowa w pewnym momencie znajdzie sie
pod funkcją kwadratową. Czyli nawet najszybsza funkcja liniowa rośnie wolniej od najwolniej rosnącej funkcji kwadratowej.
Jak widać po powyższym, dokładne wartości stałych nie mają w tym wypadku znaczenia. Analogiczny dowód możemy 
przeprowadzić dla dowolnej innej funkcji wielomianowej, również o wykładniku niebędącym liczbą całkowitą. 

Jeżeli między funkcjami $f$ i $g$ zachodzi taka zależność, to jest - bez względu na mnożnik, funkcja $f$ rośnie wolniej
od $g$ - to mówimy, że $f = o(g)$. Formalnie zależność te definiujemy następująco:
\begin{equation}
    \forall_{c > 0}\exists_{n_0}\forall_{n > n_0} f(n) < c \cdot g(n)
\end{equation}
