Dataczwartek, 13 sierpnia 2026 Czas18:26:21
← Algorytmy i złożoność

Lekcja 12. Analiza algorytmów — kompletny przewodnik maturalny (notacja Big-O)

Trudny / egzaminacyjny

Po co się tego uczymy?

To podsumowanie całego działu Algorytmy i złożoność — narzędzie do FORMALNEJ oceny każdego algorytmu poznanego w poprzednich 11 lekcjach. Na maturze nie wystarczy napisać, że program "działa" — trzeba umieć powiedzieć, JAK DŁUGO będzie działał przy dużych danych i CZY da się to zrobić szybciej. To dokładnie ta umiejętność.

Teoria

Analiza algorytmu to badanie jego: SZYBKOŚCI działania (jak długo trwa wykonanie) i ZAPOTRZEBOWANIA NA PAMIĘĆ (ile danych/zmiennych tworzy). Nie chodzi o mierzenie czasu zegarem, tylko o POLICZENIE, ile kroków (pętli, porównań, operacji) wykona komputer.

Rodzaje analizy:

Rodzaj analizy Co sprawdzamy Przykład
Czasowa ile kroków wykona program BubbleSort vs QuickSort
Pamięciowa ile pamięci zużyje MergeSort (tablice pomocnicze)
Asymptotyczna jak zachowuje się przy DUŻYCH danych O(n), O(n log n), O(n²)
Przypadków jak działa w różnych warunkach QuickSort: best/worst/average

Notacja Big O — sposób zapisu "jak SZYBKO ROŚNIE liczba operacji" wraz ze wzrostem danych, zamiast konkretnych liczb:

Notacja Znaczenie Przykład
O(1) stała liczba kroków dostęp do elementu listy
O(log n) rośnie wolno wyszukiwanie binarne
O(n) rośnie liniowo wyszukiwanie liniowe
O(n log n) prawie liniowy MergeSort, QuickSort
O(n²) kwadratowy BubbleSort
O(2ⁿ) wykładniczy naiwny rekurencyjny Fibonacci
O(n!) silniowy permutacje — praktycznie nieużywalny

Przypadki działania (best/average/worst) — każdy algorytm może działać różnie w zależności od danych: best case (najkorzystniejszy, np. Insertion Sort na już posortowanej liście), average case (typowy przypadek, np. QuickSort na losowych danych), worst case (najgorszy możliwy układ, np. QuickSort na już posortowanej liście). Na maturze trzeba umieć podać NAJGORSZY przypadek — to on definiuje teoretyczną złożoność algorytmu.

Podsumowanie złożoności WSZYSTKICH algorytmów z tego działu:

Algorytm Czas Pamięć Uwagi
Wyszukiwanie liniowe O(n) O(1) najprostsze
Wyszukiwanie binarne O(log n) O(1) wymaga sortowania
Bubble Sort O(n²) O(1) edukacyjny
Insertion Sort O(n²) / best O(n) O(1) dobry dla małych/prawie posortowanych danych
Merge Sort O(n log n) O(n) stabilny, zawsze przewidywalny
Quick Sort O(n log n) / worst O(n²) O(log n) szybki, ale niestabilny
Algorytm Euklidesa O(log n) O(1) klasyczny
Szybkie potęgowanie O(log n) O(1) dziel i zwyciężaj
BFS / DFS O(V + E) O(V) grafy — V wierzchołków, E krawędzi
Dijkstra O(E log V) O(V) tylko wagi dodatnie

Typowe zadania maturalne — analiza pseudokodu: dla for i in range(n): for j in range(i, n): k += 1 — pętla wewnętrzna wykonuje się n-i razy, sumarycznie 1+2+...+n = n(n+1)/2, czyli O(n²). Dla while n > 0: n = n // 2 — liczba wykonań to log₂ n, czyli O(log n).

Schemat

PORÓWNANIE ZŁOŻONOŚCI (rosnąco):
O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ) < O(n!)
(stały) (wolny wzrost)        (kwadrat)  (wykładniczy) (silniowy)

Dla listy 10 000 elementów - które sortowanie szybsze?
  BubbleSort  → O(n²)     → ~100 000 000 operacji
  MergeSort   → O(n log n)→ ~130 000 operacji
  Odpowiedź: MergeSort — prawie 800 razy mniej operacji!

BEST / AVERAGE / WORST CASE (QuickSort):
  Best/Average: O(n log n)  - pivot dzieli dane w miarę równo
  Worst:        O(n²)       - pivot zawsze skrajny (np. dane już posortowane)

Przykład z życia

Aplikacja bankowa sortująca listę milionów transakcji MUSI używać algorytmu o złożoności O(n log n) (jak MergeSort), a nie O(n²) (jak BubbleSort) — różnica między kilkoma sekundami a kilkoma GODZINAMI przetwarzania. To właśnie analiza złożoności decyduje, który algorytm trafia do prawdziwego, produkcyjnego kodu.

insertion_sort.py

# Sortowanie przez wstawianie (Insertion Sort) - nowy algorytm w tym podsumowaniu
# Każdy element trafia w odpowiednie miejsce w już posortowanej części listy
def insertion_sort(tab):
    for i in range(1, len(tab)):
        klucz = tab[i]
        j = i - 1
        while j >= 0 and tab[j] > klucz:
            tab[j + 1] = tab[j]
            j -= 1
        tab[j + 1] = klucz
    return tab

# Analiza: czas O(n^2), ale best case O(n) gdy dane prawie posortowane
# Pamięć: O(1) - sortuje "na miejscu"
print(insertion_sort([5, 2, 8, 1, 9]))

quick_sort.py

# QuickSort - kolejny algorytm "dziel i zwyciężaj", ale z inną strategią niż MergeSort
def quick_sort(tab):
    if len(tab) <= 1:
        return tab
    pivot = tab[0]                                   # wybierz element "oś"
    mniejsze = [x for x in tab[1:] if x <= pivot]     # wszystko mniejsze/równe pivotowi
    wieksze = [x for x in tab[1:] if x > pivot]       # wszystko większe od pivota
    return quick_sort(mniejsze) + [pivot] + quick_sort(wieksze)

# Analiza: średnio O(n log n), najgorzej O(n^2) - gdy pivot źle dzieli dane
# (np. gdy lista jest już posortowana, a pivot to zawsze pierwszy element)
print(quick_sort([5, 2, 8, 1, 9]))

Komentarz i wyjaśnienie kodu

W insertion_sort() pętla wewnętrzna while j >= 0 and tab[j] > klucz przesuwa elementy WIĘKSZE od aktualnie wstawianego w prawo, robiąc miejsce dla klucz — to jak sortowanie kart w ręku, wsuwając każdą nową kartę na właściwe miejsce między już poukładanymi.

W quick_sort() zwróć uwagę, DLACZEGO może działać wolno: jeśli pivot (zawsze PIERWSZY element listy) trafia na listę JUŻ POSORTOWANĄ, to mniejsze będzie ZAWSZE puste, a wieksze będzie miało prawie WSZYSTKIE pozostałe elementy — algorytm zamiast dzielić dane na pół (jak przy dobrym wyborze pivota), dzieli je na "0 i reszta", co daje O(n²) zamiast O(n log n). To dokładnie pytanie maturalne z teorii tej lekcji.

Ćwiczenie samodzielne

Uruchom insertion_sort() i quick_sort() dla tej samej listy i porównaj wyniki. Następnie uruchom quick_sort() na liście, która jest JUŻ POSORTOWANA (np. [1,2,3,4,5]) i zastanów się, ile porównań wykona w tym najgorszym przypadku.

Zadania do pracy własnej

  1. Dla poniższego pseudokodu określ złożoność obliczeniową: for i in range(n): print(i) oraz for i in range(n): for j in range(n): print(i, j). Uzasadnij odpowiedź, licząc liczbę wykonań instrukcji print.

  2. Dla listy 10 000 elementów oblicz w przybliżeniu, ile operacji wykona: (a) wyszukiwanie liniowe w najgorszym przypadku, (b) wyszukiwanie binarne w najgorszym przypadku, (c) BubbleSort, (d) MergeSort. Przedstaw wyniki w tabeli i skomentuj różnice rzędów wielkości.

  3. Napisz program mierzący RZECZYWISTY czas wykonania (moduł time) dla: BubbleSort, InsertionSort, MergeSort i QuickSort na tej samej losowej liście o rosnącej wielkości (100, 1000, 10 000 elementów). Narysuj (na kartce lub w arkuszu kalkulacyjnym) wykres czasu w zależności od n dla każdego algorytmu i sprawdź, czy kształt wykresów odpowiada teoretycznej złożoności (n² dla Bubble/Insertion, n log n dla Merge/Quick).

Typowe błędy

Podawanie AVERAGE CASE zamiast WORST CASE jako "złożoności algorytmu" — na maturze pytanie o złożoność algorytmu domyślnie dotyczy NAJGORSZEGO przypadku (worst case), chyba że pytanie wyraźnie precyzuje inaczej (np. dla QuickSort trzeba umieć podać OBA: średnio O(n log n), najgorzej O(n²)).

Mylenie złożoności CZASOWEJ z PAMIĘCIOWĄ — algorytm może być szybki czasowo (np. MergeSort, O(n log n)), ale zużywać więcej pamięci (O(n), bo tworzy dodatkowe tablice) niż algorytm wolniejszy czasowo, ale oszczędniejszy pamięciowo (np. BubbleSort, O(1) pamięci).

Zakładanie, że "szybszy w praktyce" zawsze znaczy "lepsza złożoność teoretyczna" — dla MAŁYCH zbiorów danych (kilkanaście elementów) prostszy algorytm (np. Insertion Sort) może być w praktyce szybszy niż teoretycznie "lepszy" MergeSort, ze względu na mniejszy narzut (overhead) na tworzenie dodatkowych struktur.

Nawiązanie do egzaminu zawodowego

To podsumowanie CAŁEGO działu "Algorytmy i złożoność" — każdy z 11 poprzednich tematów (Euklides i Sito, wyszukiwanie binarne, sortowania, bisekcja, szybkie potęgowanie, rekurencja, podejście zachłanne, programowanie dynamiczne, stos/kolejka, lider, grafy) można teraz opisać formalnym językiem złożoności obliczeniowej — dokładnie tak, jak wymaga tego pisemna część matury rozszerzonej z informatyki. Umiejętność analizy złożoności to też fundament dla działu Python — programowanie (poprzedni dział tej ścieżki), gdzie te same algorytmy są implementowane praktycznie.