Rachunek złożoności obliczeniowej

Kurs: Wstęp do programowania
Lekcja 10: Analiza wydajności algorytmów
Temat 1: Rachunek złożoności obliczeniowej

⇓ spis treści ⇓


Rachunek złożoności obliczeniowej jest podstawową koncepcją w informatyce, która pozwala oceniać i porównywać efektywność algorytmów. Analizując algorytmy, często chcemy wiedzieć, jak szybko będą działały oraz ile zasobów będą zużywały, szczególnie w przypadku dużych zbiorów danych. Złożoność obliczeniowa opisuje, jak czas wykonania lub zużycie pamięci algorytmu zmienia się w zależności od rozmiaru danych wejściowych. W tej sekcji zagłębimy się w pojęcie złożoności obliczeniowej, przedstawimy różne klasy złożoności oraz omówimy notacje używane do opisywania wydajności algorytmów, takie jak notacja dużego O (Big O).

Dlaczego złożoność obliczeniowa jest ważna?

W miarę jak rozmiar danych wejściowych rośnie, niektóre algorytmy mogą działać znacznie wolniej niż inne. Zrozumienie złożoności obliczeniowej pozwala na ocenę, czy dany algorytm jest odpowiedni dla problemu, który próbujesz rozwiązać, oraz na wybór najbardziej efektywnego podejścia. W niektórych przypadkach wybór niewłaściwego algorytmu może sprawić, że program stanie się praktycznie bezużyteczny w przypadku dużych zbiorów danych. Złożoność obliczeniowa pozwala przewidzieć, jak algorytm będzie się zachowywał, zanim jeszcze zostanie zaimplementowany, co jest niezwykle ważne przy projektowaniu skalowalnych systemów.

Podstawowe pojęcia złożoności obliczeniowej

Złożoność obliczeniowa algorytmu może być mierzona pod względem dwóch głównych zasobów: czasu i pamięci. Czasowa złożoność obliczeniowa odnosi się do tego, jak długo zajmuje wykonanie algorytmu, podczas gdy złożoność pamięciowa (lub przestrzenna) odnosi się do ilości pamięci potrzebnej do wykonania algorytmu. W obu przypadkach zależność między rozmiarem danych wejściowych a zużyciem zasobów jest kluczowa do oceny wydajności algorytmu.

Złożoność czasowa

Złożoność czasowa to miara, która opisuje, jak długo algorytm będzie się wykonywał w zależności od rozmiaru danych wejściowych. W praktyce używa się notacji asymptotycznej, aby uprościć opis złożoności czasowej, ignorując czynniki stałe i analizując zachowanie algorytmu dla bardzo dużych zbiorów danych. Notacje asymptotyczne pomagają porównywać wydajność algorytmów bez konieczności mierzenia ich dokładnych czasów wykonania.

Złożoność pamięciowa

Złożoność pamięciowa, zwana również złożonością przestrzenną, opisuje, ile pamięci potrzebuje algorytm do wykonania swoich operacji. W niektórych przypadkach algorytm może być szybki, ale bardzo nieefektywny pod względem pamięci, co jest równie istotne do uwzględnienia, zwłaszcza w systemach z ograniczonymi zasobami. Tak jak w przypadku złożoności czasowej, złożoność pamięciowa jest opisywana za pomocą notacji asymptotycznych.

Notacja dużego O (Big O)

Notacja dużego O (Big O) jest najczęściej stosowaną notacją asymptotyczną do opisywania złożoności obliczeniowej algorytmów. Opisuje ona górną granicę tempa wzrostu czasu wykonania lub zużycia pamięci w zależności od rozmiaru danych wejściowych. Dzięki tej notacji możemy porównać algorytmy i zobaczyć, które z nich są bardziej efektywne w przypadku dużych danych. Zrozumienie notacji Big O jest kluczowe dla każdego programisty, który chce projektować i implementować wydajne algorytmy.

Formalna definicja notacji dużego O

Notacja O(f(n)) opisuje, że czas wykonania lub zużycie pamięci algorytmu rośnie co najwyżej w tempie funkcji f(n) dla dużych wartości n. Formalnie, mówimy, że algorytm ma złożoność O(f(n)), jeśli istnieją stałe c > 0 i n₀, takie że dla wszystkich n ≥ n₀:

T(n) ≤ c * f(n)

gdzie T(n) to rzeczywisty czas wykonania algorytmu, a f(n) to funkcja opisująca górną granicę tempa wzrostu.

Klasy złożoności czasowej

Istnieje wiele różnych klas złożoności czasowej, które opisują, jak szybko rośnie czas wykonania algorytmu w miarę wzrostu rozmiaru danych wejściowych. Oto najważniejsze klasy złożoności czasowej, które warto znać:

1. Złożoność stała – O(1)

Złożoność stała oznacza, że czas wykonania algorytmu nie zależy od rozmiaru danych wejściowych. Przykładem może być dostęp do elementu w tablicy za pomocą indeksu. Niezależnie od liczby elementów w tablicy, operacja dostępu trwa tyle samo czasu.

int element = tablica[5]; // O(1)
2. Złożoność liniowa – O(n)

Złożoność liniowa oznacza, że czas wykonania algorytmu rośnie liniowo wraz z rozmiarem danych wejściowych. Przykładem może być iteracja przez wszystkie elementy w tablicy, aby znaleźć maksymalną wartość.

int znajdzMaksymalny(int tablica[], int n) {
    int maks = tablica[0];
    for (int i = 1; i < n; ++i) {
        if (tablica[i] > maks) {
            maks = tablica[i];
        }
    }
    return maks;
}
3. Złożoność logarytmiczna – O(log n)

Złożoność logarytmiczna oznacza, że czas wykonania algorytmu rośnie logarytmicznie wraz z rozmiarem danych wejściowych. Przykładem może być algorytm wyszukiwania binarnego, który dzieli dane wejściowe na pół przy każdym kroku.

int wyszukiwanieBinarne(int tablica[], int lewy, int prawy, int x) {
    while (lewy <= prawy) {
        int srodek = lewy + (prawy - lewy) / 2;
        if (tablica[srodek] == x) return srodek;
        if (tablica[srodek] < x) lewy = srodek + 1;
        else prawy = srodek - 1;
    }
    return -1;
}
4. Złożoność kwadratowa – O(n²)

Złożoność kwadratowa oznacza, że czas wykonania algorytmu rośnie w kwadracie wraz z rozmiarem danych wejściowych. Przykładem może być proste sortowanie bąbelkowe (Bubble Sort), gdzie musimy porównać każdy element z każdym innym elementem.

void bubbleSort(int tablica[], int n) {
    for (int i = 0; i < n - 1; ++i) {
        for (int j = 0; j < n - i - 1; ++j) {
            if (tablica[j] > tablica[j + 1]) {
                std::swap(tablica[j], tablica[j + 1]);
            }
        }
    }
}
5. Złożoność wykładnicza – O(2^n)

Złożoność wykładnicza oznacza, że czas wykonania algorytmu rośnie wykładniczo wraz z rozmiarem danych wejściowych. Algorytmy o złożoności wykładniczej są bardzo nieefektywne dla dużych zbiorów danych. Przykładem może być rozwiązanie problemu komiwojażera metodą brute-force.

Inne notacje asymptotyczne

Oprócz notacji dużego O istnieją inne notacje asymptotyczne, które są używane do opisywania złożoności obliczeniowej:

  • Notacja Omega (Ω): Opisuje dolną granicę tempa wzrostu czasu wykonania algorytmu. Oznacza, że czas wykonania algorytmu nie może być mniejszy niż określona funkcja.
  • Notacja Theta (Θ): Opisuje, że czas wykonania algorytmu rośnie w tempie określonej funkcji, zarówno górną, jak i dolną granicą. Wskazuje na ścisłą równoważność tempa wzrostu.

Podsumowanie

Rachunek złożoności obliczeniowej jest podstawą analizy i projektowania algorytmów. Dzięki zrozumieniu złożoności czasowej i pamięciowej, a także umiejętności posługiwania się notacjami asymptotycznymi, programiści mogą lepiej przewidywać, jak algorytmy będą się zachowywać w rzeczywistych warunkach. W tej lekcji omówiliśmy kluczowe pojęcia związane ze złożonością obliczeniową, różne klasy złożoności oraz jak używać notacji dużego O do opisywania wydajności algorytmów. Zrozumienie tych zasad jest niezbędne do projektowania skalowalnych i efektywnych aplikacji.

Następny temat ==> Koszty czasowe i pamięciowe algorytmów



Spis Treści - Wstęp do programowania

Lekcja 3: Rozwiązywanie problemów i poprawność programów Lekcja 4: Praca z różnymi typami danych Lekcja 5: Obsługa plików i pamięci Lekcja 6: Zaawansowane techniki programistyczne Lekcja 7: Wskaźniki i pamięć dynamiczna Lekcja 8: Struktura kodu i abstrakcja Lekcja 9: Rekurencja i jej zastosowania Lekcja 10: Analiza wydajności algorytmów Lekcja 11: Technika "dziel i zwyciężaj" Lekcja 12: Struktury danych o dynamicznej budowie Lekcja 13: Struktury hierarchiczne: Drzewa Lekcja 14: Struktury danych z bibliotek Lekcja 15: Algorytmy z nawrotami Lekcja 16: Programowanie dynamiczne Lekcja 17: Programowanie zachłanne Lekcja 18: Praca z grafami

Jeśli chciałbyś być poinformowany o następnych kursach to zapisz się do naszego newslettera: