Przykłady analizy wydajności

Kurs: Wstęp do programowania
Lekcja 10: Analiza wydajności algorytmów
Temat 6: Przykłady analizy wydajności

⇓ spis treści ⇓


Analiza wydajności algorytmów jest kluczowym krokiem w ocenie efektywności różnych podejść do rozwiązywania problemów. W tej sekcji przyjrzymy się konkretnym przykładom analizy wydajności, aby lepiej zrozumieć, jak różne algorytmy radzą sobie z różnymi problemami i jak ich wydajność zmienia się w zależności od rozmiaru danych wejściowych. Skoncentrujemy się na złożoności czasowej i pamięciowej, analizując przypadki od prostych po bardziej złożone. Przedstawimy również szczegółowe omówienie, jak można oceniać i porównywać wydajność algorytmów w praktycznych zastosowaniach.

Przykład 1: Wyszukiwanie liniowe

Wyszukiwanie liniowe to jeden z najprostszych algorytmów przeszukiwania danych, który polega na sekwencyjnym sprawdzaniu każdego elementu w strukturze danych, aż znajdzie się szukany element lub zakończy się przeszukiwanie całego zbioru.

Analiza złożoności czasowej

Złożoność czasowa wyszukiwania liniowego wynosi O(n), gdzie n to liczba elementów w zbiorze danych. W najlepszym przypadku szukany element znajduje się na początku, co daje złożoność O(1). W najgorszym przypadku algorytm musi przeszukać wszystkie elementy, co daje złożoność O(n). Średnia złożoność również wynosi O(n), ponieważ szukany element może znajdować się w dowolnym miejscu zbioru z równym prawdopodobieństwem.

int wyszukiwanieLiniowe(int arr[], int n, int x) {
    for (int i = 0; i < n; ++i) {
        if (arr[i] == x) return i;
    }
    return -1;
}
Analiza złożoności pamięciowej

Złożoność pamięciowa wynosi O(1), ponieważ algorytm nie używa dodatkowej pamięci niezależnie od rozmiaru danych wejściowych. Wszystkie operacje są wykonywane na oryginalnym zbiorze danych.

Przykład 2: Wyszukiwanie binarne

Wyszukiwanie binarne jest znacznie bardziej wydajnym algorytmem, ale wymaga posortowanego zbioru danych. Działa na zasadzie dzielenia zbioru na pół przy każdym kroku, aż znajdzie się szukany element lub zbiór danych zostanie wyczerpany.

Analiza złożoności czasowej

Złożoność czasowa wyszukiwania binarnego wynosi O(log n), ponieważ przy każdym kroku zmniejszamy zbiór danych o połowę. W najlepszym przypadku element znajduje się na środku, co daje złożoność O(1). W najgorszym przypadku algorytm wykonuje logarytmiczną liczbę kroków, co czyni go bardzo wydajnym dla dużych zbiorów danych.

int wyszukiwanieBinarne(int arr[], int lewy, int prawy, int x) {
    while (lewy <= prawy) {
        int srodek = lewy + (prawy - lewy) / 2;
        if (arr[srodek] == x) return srodek;
        if (arr[srodek] < x) lewy = srodek + 1;
        else prawy = srodek - 1;
    }
    return -1;
}
Analiza złożoności pamięciowej

Złożoność pamięciowa wyszukiwania binarnego wynosi O(1), ponieważ algorytm nie zużywa dodatkowej pamięci. Wszystkie operacje są wykonywane w miejscu.

Przykład 3: Sortowanie bąbelkowe

Sortowanie bąbelkowe (Bubble Sort) to prosty, ale nieefektywny algorytm sortujący, który porównuje sąsiednie elementy i zamienia je, jeśli są w złej kolejności. Proces ten jest powtarzany, aż zbiór danych zostanie posortowany.

Analiza złożoności czasowej

Złożoność czasowa sortowania bąbelkowego w najgorszym i średnim przypadku wynosi O(n²), ponieważ musimy porównać każdy element z każdym innym elementem. W najlepszym przypadku, gdy dane są już posortowane, złożoność wynosi O(n), ponieważ algorytm może zakończyć się po jednym przejściu przez dane.

void bubbleSort(int arr[], int n) {
    for (int i = 0; i < n - 1; ++i) {
        for (int j = 0; j < n - i - 1; ++j) {
            if (arr[j] > arr[j + 1]) {
                std::swap(arr[j], arr[j + 1]);
            }
        }
    }
}
Analiza złożoności pamięciowej

Złożoność pamięciowa wynosi O(1), ponieważ algorytm nie wymaga dodatkowej przestrzeni pamięciowej. Operacje są wykonywane bezpośrednio na oryginalnym zbiorze danych.

Przykład 4: Sortowanie szybkie (Quick Sort)

Sortowanie szybkie (Quick Sort) to jeden z najwydajniejszych algorytmów sortujących. Działa na zasadzie wyboru elementu zwanego pivotem, a następnie dzieli dane na dwie części: mniejsze od pivota i większe od pivota. Następnie sortuje każdą część rekurencyjnie.

Analiza złożoności czasowej

Złożoność czasowa sortowania szybkiego w średnim przypadku wynosi O(n log n). W najgorszym przypadku, gdy pivot jest zawsze najgorszym możliwym wyborem (np. najmniejszy lub największy element), złożoność wynosi O(n²). Istnieją jednak techniki, takie jak wybór losowego pivota, które mogą zmniejszyć ryzyko najgorszego przypadku.

int partition(int arr[], int low, int high) {
    int pivot = arr[high];
    int i = low - 1;
    for (int j = low; j < high; ++j) {
        if (arr[j] < pivot) {
            ++i;
            std::swap(arr[i], arr[j]);
        }
    }
    std::swap(arr[i + 1], arr[high]);
    return i + 1;
}

void quickSort(int arr[], int low, int high) {
    if (low < high) {
        int pi = partition(arr, low, high);
        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}
Analiza złożoności pamięciowej

Złożoność pamięciowa sortowania szybkiego wynosi O(log n) w przypadku rekurencyjnego stosu wywołań, co czyni go bardziej pamięciooszczędnym niż inne algorytmy sortowania rekurencyjnego, takie jak Merge Sort.

Przykład 5: Tablica dynamiczna

Tablica dynamiczna to struktura danych, która automatycznie rozszerza się, gdy zabraknie miejsca na nowe elementy. Operacje dodawania elementów mają złożoność zamortyzowaną O(1), ale w niektórych przypadkach operacje rozszerzenia tablicy mogą mieć złożoność O(n).

Analiza kosztów zamortyzowanych

Jeśli analizujemy łączny koszt wszystkich operacji dodawania, widzimy, że większość operacji ma koszt O(1), a tylko nieliczne mają koszt O(n). Dzięki analizie kosztów zamortyzowanych średni koszt każdej operacji wynosi O(1).

class DynamicArray {
private:
    int* arr;
    int capacity;
    int size;
public:
    DynamicArray() : capacity(1), size(0) {
        arr = new int[capacity];
    }
    
    void add(int value) {
        if (size == capacity) {
            capacity *= 2;
            int* newArr = new int[capacity];
            for (int i = 0; i < size; ++i) {
                newArr[i] = arr[i];
            }
            delete[] arr;
            arr = newArr;
        }
        arr[size++] = value;
    }
    
    ~DynamicArray() {
        delete[] arr;
    }
}

Podsumowanie

Przykłady analizy wydajności algorytmów pokazują, jak różne techniki mogą wpływać na czas wykonania i zużycie pamięci. Omówiliśmy proste algorytmy, takie jak wyszukiwanie liniowe, oraz bardziej złożone, takie jak Quick Sort i tablice dynamiczne. Dzięki tej wiedzy możesz lepiej zrozumieć, jak algorytmy działają w praktyce i jak optymalizować ich wydajność w zależności od specyfiki problemu. Analiza wydajności jest niezbędna do projektowania skalowalnych i efektywnych rozwiązań, co jest kluczowe w dzisiejszym świecie przetwarzania dużych zbiorów danych.

Następna lekcja ==> Technika „dziel i zwyciężaj”



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: