Naucz się tematu Algorytmy i złożoność od zera
1Co mierzy złożoność
Algorytm to skończona lista precyzyjnych kroków rozwiązujących problem, na przykład sortowanie listy albo szukanie nazwiska w książce telefonicznej.
Dwa algorytmy mogą rozwiązywać ten sam problem, ale zajmować bardzo różną ilość czasu. Złożoność opisuje, jak rośnie nakład pracy, gdy dane wejściowe się powiększają.
Rozmiar danych wejściowych oznacza się n: liczba elementów do posortowania, długość listy, liczba wierzchołków grafu.
Wyobraź sobie szukanie nazwiska w książce telefonicznej liczącej 1000 stron. Przeczytanie każdej strony zajmuje do 1000 kroków. Otwieranie jej w połowie i za każdym razem dzielenie na pół zajmuje około 10 kroków.
Złożoność czasowa zlicza kroki. Złożoność pamięciowa zlicza dodatkową pamięć. Obie zapisuje się jako funkcję n.
Zwykle rozważa się przypadek pesymistyczny: największy nakład pracy spośród wszystkich danych o rozmiarze n. Czasem pytają zamiast tego o przypadek średni lub optymistyczny.
Samosprawdzenie Algorytm, którego nakład pracy rośnie jak n², działa 1 sekundę dla n = 1000. Ile mniej więcej czasu zajmie dla n = 2000?
- 1 sekunda
- 2 sekundy
- 4 sekundy
- 8 sekund
Odpowiedź: 4 sekundy. Podwojenie n mnoży n² przez 2² = 4, więc czas rośnie z około 1 do około 4 sekund. 2 sekundy byłyby odpowiedzią dla wzrostu liniowego, a 8 dla n³.
2Notacja dużego O i klasy wzrostu
Notacja dużego O zachowuje tylko to, co ważne dla dużych n. Odrzucasz stałe współczynniki i zostawiasz tylko najszybciej rosnący składnik.
3n² + 5n + 2 ⇒ O(n²)
Dla n = 1000 składnik n² wynosi 3 000 000, a 5n tylko 5000. Mniejsze składniki prawie nie zmieniają sumy.
Te klasy pojawiają się raz po raz, od najwolniejszego do najszybszego wzrostu:
- O(1), stała: odczyt elementu tablicy według indeksu.
- O(log n), logarytmiczna: wyszukiwanie binarne, które w każdym kroku zmniejsza problem o połowę.
- O(n), liniowa: jedno przejście po wszystkich elementach, na przykład szukanie największej wartości.
- O(n log n): dobre algorytmy sortowania, takie jak sortowanie przez scalanie.
- O(n²), kwadratowa: dwie zagnieżdżone pętle po elementach, na przykład sortowanie bąbelkowe.
- O(2ⁿ), wykładnicza: sprawdzanie każdego podzbioru. Szybko staje się bezużyteczna.
Duże O to ograniczenie górne: nakład pracy rośnie co najwyżej tak szybko. Ω (duża omega) to ograniczenie dolne, a Θ (duża theta) to ograniczenie dokładne, zarazem górne i dolne.
Podstawa logarytmu nie ma znaczenia w notacji dużego O, ponieważ logarytmy o różnych podstawach różnią się tylko stałym czynnikiem.
Samosprawdzenie Jaka jest notacja dużego O dla 4n + 2n² + 100?
- O(100)
- O(2n²)
- O(n)
- O(n²)
Odpowiedź: O(n²). Dla dużych n dominuje składnik n², a stały czynnik 2 się odrzuca. O(n) zachowuje zły składnik, a O(2n²) zapomina odrzucić stałą.
3Wyszukiwanie, sortowanie i rekurencja
Kilka algorytmów pojawia się w prawie każdym kursie. Ucz się ich złożoności jako faktów.
- Wyszukiwanie liniowe sprawdza elementy po kolei: O(n). Wyszukiwanie binarne wymaga posortowanej tablicy i w każdym kroku zmniejsza zakres o połowę: O(log n).
- Sortowanie bąbelkowe, przez wstawianie i przez wybieranie porównują i zamieniają elementy w zagnieżdżonych pętlach: O(n²). Sortowanie przez wstawianie ma O(n) dla tablicy, która jest już posortowana.
- Sortowanie przez scalanie dzieli tablicę na połowy, sortuje je i scala: O(n log n) w każdym przypadku, z dodatkową pamięcią O(n).
- Quicksort dzieli tablicę wokół elementu rozdzielającego: O(n log n) średnio, ale O(n²) w przypadku pesymistycznym, na przykład dla posortowanej tablicy, gdy elementem rozdzielającym jest pierwszy element.
- Żadne sortowanie oparte tylko na porównywaniu elementów nie może w przypadku pesymistycznym być lepsze niż Ω(n log n). Sortowanie przez zliczanie jest szybsze, bo nie porównuje elementów.
Funkcja rekurencyjna wywołuje samą siebie dla mniejszych danych, aż dojdzie do przypadku bazowego, na który odpowiada bezpośrednio. Jej czas działania zapisuje się jako równanie rekurencyjne:
- T(n) = T(n/2) + O(1) daje O(log n), jak wyszukiwanie binarne.
- T(n) = 2T(n/2) + O(n) daje O(n log n), jak sortowanie przez scalanie.
- T(n) = T(n − 1) + O(n) daje O(n²).
- T(n) = 2T(n − 1) + O(1) daje O(2ⁿ).
Warto znać trzy idee projektowania. Dziel i zwyciężaj dzieli, rozwiązuje i łączy. Algorytm zachłanny w każdym kroku wybiera lokalnie najlepszą opcję. Programowanie dynamiczne zapamiętuje odpowiedzi na nakładające się podproblemy, więc każdy liczy się tylko raz.
Samosprawdzenie Jaka jest złożoność czasowa quicksorta w przypadku pesymistycznym?
- O(log n)
- O(n log n)
- O(n)
- O(n²)
Odpowiedź: O(n²). Przy złych elementach rozdzielających każdy podział zostawia n − 1 elementów po jednej stronie, co daje O(n²). O(n log n) to jego przypadek średni i przypadek pesymistyczny sortowania przez scalanie.
4Metoda krok po kroku
Aby znaleźć złożoność czasową fragmentu kodu:
- Ustal, czym jest n: rozmiarem danych wejściowych.
- Policz, ile razy działa każda pętla. Pętla, która za każdym razem dodaje 1, działa około n razy. Pętla, która podwaja lub dzieli na pół, działa około log₂ n razy.
- Pętle zagnieżdżone mnoży się. Pętle występujące jedna po drugiej dodaje się.
- Dla funkcji rekurencyjnej zapisz równanie rekurencyjne: ile jest wywołań, na jakim rozmiarze, plus praca w jednym wywołaniu.
- Zachowaj najszybciej rosnący składnik i odrzuć stałe.
- Sprawdź na małej liczbie, na przykład n = 8 albo n = 1024.
W pytaniu teoretycznym najpierw nazwij algorytm lub ideę, którą opisuje, potem przypomnij sobie jego złożoność i warunki (posortowane dane, nieujemne wagi, nakładające się podproblemy).
5Przykład krok po kroku
Przejdź przez każdy krok. Spróbuj przewidzieć następny, zanim go odsłonisz.
Przykład krok po krokuJaka jest złożoność czasowa tego kodu? for i = 1 to n: (j = 1; while j < n: j = j × 2)
1n to liczba, do której biegną pętle.
2i biegnie od 1 do n co 1, więc pętla zewnętrzna wykonuje się n razy.
3j przyjmuje wartości 1, 2, 4, 8, … i kończy, gdy dojdzie do n. Po k rundach j = 2k, więc pętla kończy się po około log₂ n rundach.
4Pętla wewnętrzna znajduje się wewnątrz zewnętrznej, więc liczby się mnoży.
n · log₂ n ⇒ O(n log n)
5Dla n = 1024 pętla wewnętrzna wykonuje się log₂ 1024 = 10 razy, więc ciało pętli wykonuje się 1024 · 10 = 10 240 razy. To znacznie mniej niż n² = 1 048 576.
6Typowe błędy
- Mnożenie pętli, które działają jedna po drugiej. n + n = 2n to nadal O(n).
- Zachowywanie stałych, jak w O(2n) czy O(n/2). Oba to O(n).
- Liczenie pętli z podwajaniem jako n rund. Wykonuje się ona około log₂ n razy.
- Użycie wyszukiwania binarnego na nieposortowanych danych albo na liście wiązanej, w której nie da się dojść do środka jednym krokiem.
- Podanie przypadku średniego quicksorta, gdy pytają o pesymistyczny.
- Myślenie, że duże O oznacza „dokładnie”. O(n²) to ograniczenie górne; Θ(n²) to dokładny wzrost.
Samosprawdzenie Funkcja wykonuje pętlę z n iteracjami, a potem, osobno, kolejną pętlę z n iteracjami. Jaka jest jej złożoność czasowa?
- O(2n)
- O(log n)
- O(n)
- O(n²)
Odpowiedź: O(n). Pętle występujące jedna po drugiej się dodaje: n + n = 2n, a stałą się odrzuca, więc wynik to O(n). O(n²) byłoby poprawne tylko wtedy, gdyby jedna pętla znajdowała się wewnątrz drugiej.
7Jak wyglądają zadania egzaminacyjne
- Łatwe: pojęcia i fakty. Złożoność wyszukiwania binarnego, sortowania przez scalanie lub dostępu do tablicy, znaczenie O, Ω i Θ, czym jest rekurencja.
- Średnie: stosowanie pojęć. Złożoność fragmentu kodu, równanie rekurencyjne do rozwiązania, jak zmienia się czas, gdy n się podwaja, który algorytm pasuje do zadania.
- Trudne: przypadki i analiza. Twierdzenie o rekurencji uniwersalnej, przypadki pesymistyczne quicksorta, algorytmy zachłanne a programowanie dynamiczne oraz klasy P, NP i NP-zupełne.
P zawiera problemy, które można rozwiązać w czasie wielomianowym. NP zawiera problemy, których rozwiązania można sprawdzić w czasie wielomianowym. Każdy problem z P należy do NP; czy P = NP, nadal nie wiadomo.
Ucz się krok po kroku w aplikacjiUcz się krok po kroku w aplikacji