Při programování je důležité určit míru efektivity algoritmu a případné kroky pro jeho optimalizaci. Mohlo by se zdát, že nejlepším řešením, jak zjistit, jak je program rychlý je změřit čas spuštění a čas ukončení programu a ty od sebe odečíst. Tím, ale je možné zjistit, jen jak se program chová pro jeden konkrétní vstup na jednom konkrétním zařízení. Poměrně běžně se pak stává, že lokálně na počítači vývojáře se zdá řešení efektivní, nicméně po nasazení do produkčního prostředí dochází ke zjištění, že je výpočet pomalý.

Ukázka

Následující program provede součet celých čísel { 1, 2, … 10 }.

int result = 0;
int min = 1;
int max = 10;
 
for (int i = min; i <= max; i++)
{
    result += i;
}

Otázka: Kolikrát musí proběhnout cyklus? Čísla se sčítají postupně a celkem čísel je 10, takže desetkrát. U této úlohy je však otázkou, jestli na tento problém je vhodné psát program, protože výsledek bude vždy stejný.

Co když ale horní mez nebude nastavena staticky, ale může ji zvolit uživatel programu. Kolikrát pak program proběhne? No tolikrát, jak je veliký vstup.

int result = 0;
int min = 1;
 
if(!int.TryParse(Console.ReadLine(), out int max))
{
    Console.WriteLine("Nesprávný vstup");
    return;
}
 
for (int i = min; i <= max; i++)
{
    result += i;
}

Kdy efektivitu řešit?

V případě, že vstup programu je malého rozsahu, tak neoptimální a optimální řešení bude dosahovat velmi podobných výsledků. Pokud se ale začne zvětšovat vstup, tak optimální řešení může být rychlejší o stovky i tisíce procent.

Asymptotická časová složitost

V praxi se často stává, že je nutné optimalizovat nějaký kód a tím vznikají dvě verze algoritmu, které řeší ten stejný problém. Tyto verze je následně potřebné porovnat a určit, která je rychlejší. Co ale znamená, že je rychlejší? To je nutné formalizovat.

Typy složitosti

  • Časová složitost – doba výpočtu, která je potřebná pro vyřešení daného problému
  • Paměťová složitost – velikost paměti, která je potřebná pro provedení výpočtu

Asymptotická složitost jako matematická funkce

Každému algoritmu lze jednoznačně přiřadit neklesající funkci, která charakterizuje počet jeho operací v závislosti na rostoucí velikosti vstupních dat.

U této funkce je klíčové, že je možné ignorovat konstanty a také, že je vždy nejdůležitější člen s nejvyšším řádem. Tedy pokud má algoritmus časovou složitost , tak to lze zjednodušit na . To znamená tedy, že asymptotická mez je přibližná mez.

Dále platí i to, že každý problém lze sestavit ve větší časové složitosti (hůře to jde udělat vždy), ale naopak každý problém má minimální časovou a paměťovou složitost, pod kterou jej nelze vyřešit. Tuto mez lze pomocí matematických metod i dokázat.

Notace velké

Určuje horní mez složitosti algoritmu.

ZápisNázevPříklad algoritmu
konstantníPřístup k prvku v poli
logaritmickáBinární vyhledávání
lineárníProcházení seznamu
lineárně-logaritmickáEfektivní řadící algoritmy (merge sort, quick sort průměrně)
kvadratickáVnořené smyčky (například bubble sort)
exponenciálníRekurzivní řešení problému obchodního cestujícího
faktoriálníHrubou silou generování permutací

Asymptotické notace

ZnačkaNázevPopisSlovně
Horní mez (Upper Bound)Algoritmus nebude horší než pro dostatečně velké „nejhůře roste jako“
Horní mez nevčetněAlgoritmus roste pomaleji než „roste pomaleji než“
Dolní mez (Lower Bound)Algoritmus bude nejméně tak rychlý jako „nejméně roste jako“
Dolní mez nevčetněAlgoritmus roste rychleji než „roste rychleji než“
Střední mez (Theta)Algoritmus roste přesně jako – horní i dolní mez shodné„roste přibližně jako“

Ukázka asymptotické časové a prostorové složitosti algoritmu

V rámci příkladu je vytvořena kolekce DynamicArray, která reprezentuje dynamické pole, které mění svou velikost dle počtu prvků. Na kolekci jsou definovány operace:

  • Add - přidání prvku na první volné místo v kolekci
  • Remove - odebrání prvku z indexu a zarovnání kolekce doleva tak, aby prvky tvořily souvislý blok.
  • Get - získání prvku na indexu

Ukázka

using System;
 
public class DynamicArrayArray
{
    private int[] data;
    private int count;
 
    public DynamicArrayArray()
    {
        data = new int[4]; // počáteční kapacita
        count = 0;
    }
 
    public void Add(int value)
    {
        if (count == data.Length)
        {
            // zvětšit pole (například 2x větší)
            int newCapacity = data.Length * 2;
            int[] newData = new int[newCapacity];
            Array.Copy(data, newData, data.Length);
            data = newData;
        }
 
        data[count] = value;
        count++;
    }
 
    public int Get(int index)
    {
        if (index < 0 || index >= count)
            throw new IndexOutOfRangeException();
 
        return data[index];
    }
 
    public void Remove(int index)
    {
        if (index < 0 || index >= count)
            throw new IndexOutOfRangeException();
 
        // posun zbylých prvků vlevo
        for (int i = index; i < count - 1; i++)
        {
            data[i] = data[i + 1];
        }
        count--;
    }
 
    public int Count => count;
}
 

Prostorová složitost

Je nutné uložit prvků (resp. při zvětšování pole je potřebná velikost , ale konstanta se zanedbává). Není potřeba žádná paměť navíc, takže je asymptotická paměťová složitost .

Časová složitost

1. Get(index)

int value = array.Get(i);
  • je to přístup do pole
  • známe index prvku, tedy jen získáme data na indexu. To lze udělat v konstantním čase, tj.

→ žádné prohledávání, žádné cykly

2. Remove(index)

array.Remove(i);
  • prvky na indexech větší než i se musí všechny posunout o jedno doleva
    • Takovýchto prvků může být maximálnět tj. v nejhorším případě

Příklad:

0123
10203040
  • odeber index 1 (20)
  • posune se 30 na místo 20, 40 na místo 30

Tedy:

  • Remove na začátku
  • Remove na konci

průměrně

3. Add(value)

if (count == data.Length)
{
    int newCapacity = data.Length * 2;
    int[] newData = new int[newCapacity];
    Array.Copy(data, newData, data.Length);
    data = newData;
}
 
data[count] = value;
count++;

Dvě situace dle kapacity:

  • Kapacita stačí - Hodnota se uloží za
  • Kapacita nestačí - Zkopírování prvků → a přidání jednoho prvku

Amortizovaná časová složitost je . Tzn. limitně, kdy počet prvků jde do nekonečna, trvá přidání jednoho prvku konstantní čas.