Złożoność obliczeniowa - Letnie Warsztaty Matematyczno
Transkrypt
Złożoność obliczeniowa - Letnie Warsztaty Matematyczno
Złożoność obliczeniowa Rozwiązywanie zależności rekurencyjnych Złożoność obliczeniową możemy szacować intuicyjnie. Możemy też wyprowadzić ją analitycznie, rozwiązując zależności rekurencyjne. Na przykład, na koszt zsumowania n liczb składa się stały koszt dodania jednej liczby do sumy plus koszt zsumowania n − 1 liczb. Możemy to zapisać jako: TSum (n) ∈ O(1) + TSum (n − 1) Złożoność obliczeniowa Rozwiązywanie zależności rekurencyjnych Teraz chcemy pozbyć się T z prawej strony. Rozwijamy więc według tego samego wzoru: TSum (n) ∈ O(1) + TSum (n − 1) = O(1) + O(1) + TSum (n − 1) = 2O(1) + TSum (n − 1) ··· = (k + 1)O(1) + TSum (n − k) = (n + 1)O(1) + TSum (n − n) = (n + 1)O(1) + TSum (0) Złożoność obliczeniowa Rozwiązywanie zależności rekurencyjnych Przyjmujemy, że T (k) ∈ O(1), jeżeli k jest stałą. Dodatkowo O(f ) · O(g ) = O(fg ). Stąd: TSum (n) ∈ (n + 1)O(1) + TSum (0) = (n + 1)O(1) + O(1) = (n + 2)O(1) = O(n) · O(1) = O(n) Otrzymaliśmy złożoność liniową, tak jak wcześniej. Złożoność obliczeniowa Zadania Trochę matematyki. 5. Udowodnij, że jeżeli f należy do O(k · g (n)) wtedy i tylko wtedy, gdy f należy do O(g (n)). 6. Udowodnij, że dwa logarytmy o różnych podstawach, ale tej samej liczbie logarytmowanej, różnią się zawsze tylko o stałą. 7. Udowodnij, że dwie funkcje wykładnicze o różnych podstawach, ale tym samym wykładniku, różnią się zawsze tylko o stałą. 8. Rozwiąż zależność rekurencyjną T (n) ∈ O(n) + T (n − 1). Letnie Warsztaty Matematyczno-Informatyczne Algorytmy i struktury danych Algorytmy sortowania Algorytmy sortowania Problem sortowania Zanim przejdziemy do omawiania algorytmów rozwiązujących problem sortowania, zdefiniujmy najpierw problem sortowania. Sortowanie polega zamianie kolejności elementów ciągu w taki sposób, aby otrzymać ciąg niemalejący. Innymi słowy: chcemy uporządkować dany zestaw liczb od najmniejszej do największej. Algorytmy sortowania Problem sortowania Proste, prawda? Znów, problem polega na tym, że komputer nie potrafi spojrzeć na wszystkie liczby na raz. Ty też nie potrafisz, jeżeli jest ich kilka milionów. Algorytmy sortowania Sortowanie przez wybieranie Pomysł: wybierz minimum, przenieś je na początek, powtarzaj aż otrzymasz uporządkowany ciąg. Algorytm ten nazywamy sortowaniem przez wybieranie (ang. selection sort). 3 9 5 1 2 7 6 8 Algorytmy sortowania Sortowanie przez wybieranie W pseudokodzie: procedure Selection-Sort(T ) for all i ∈ 0..Length(T ) do m ← Minimum-Index(T [i..Length(T )]) Swap(T [i], T [m + i]) end for return T end procedure Teraz pozostaje jedynie zdefiniować procedurę Minimum-Index, która zwracać będzie indeks minimum w tablicy. 3 9 5 7 8 2 1 6 4 1 9 5 7 8 2 3 6 4 1 2 5 7 8 9 3 6 4 Algorytmy sortowania Sortowanie przez wybieranie Wygląda ona tak: procedure Minimum-Index(T ) minIndex ← 0 for all i ∈ 1..Length(T ) do if T [i] < T [minIndex] then minIndex ← i end if end for return minIndex end procedure Algorytmy sortowania Sortowanie przez wybieranie Jaka jest złożoność obliczeniowa tego algorytmu? Spójrzmy najpierw na Minimum-Index, którego czas wykonania zależy liniowo od długości tablicy. To daje nam O(|T |). Następnie Selection-Sort wywołuje powyższą procedurę |T | razy. W sumie otrzymujemy O(|T |2 ), czyli czas kwadratowy. Algorytmy sortowania Inne algorytmy sortowania w czasie kwadratowym Sortowanie przez wstawianie (ang. insertion sort) — zacznij od pustego ciągu. Wstawiaj do niego kolejne elementy ciągu do posortowania w odpowiednie miejsca. Sortowanie bąbelkowe (ang. bubble sort) — przejdź przez ciąg, porównując każde dwa sąsiadujące elementy. Zamień każdą parę, która jest w złej kolejności. Powtarzaj aż otrzymasz posortowany ciąg. Algorytmy sortowania Sortowanie przez scalanie Rozwiązania wielu problemów algorytmicznych dzielą się na dwie grupy: Łatwe do zrozumienia, ale powolne (sortowanie przez wybieranie). Szybkie, ale trudniejsze do zrozumienia (sortowanie przez scalanie). Mimo wszystko, spróbujmy zrozumieć to drugie. Algorytmy sortowania Sortowanie przez scalanie Zacznijmy od scalania — mając dane dwa posortowane ciągi, scal je w jeden posortowany ciąg. Scalanie możemy wykonać w czasie liniowym. Najmniejszy element obu ciągów znajduje się z przodu jednego lub drugiego z nich. Wybieramy mniejszy z początkowych elementów i umieszczamy go na końcu scalonego ciągu. 1 3 4 8 9 2 5 6 7 Algorytmy sortowania Sortowanie przez scalanie procedure Merge(A, B) M ← Zeros(Length(A) + Length(B)) a ← 0, b ← 0, m ← 0 while a < Length(A) and b < Length(B) do if A[a] < B[b] then M[m] ← A[a], a ← a + 1 else M[m] ← B[b], b ← b + 1 end if m ←m+1 end while while a < Length(A) do M[m] ← A[a], m ← m + 1, a ← a + 1 end while while b < Length(B) do M[m] ← B[b], m ← m + 1, b ← b + 1 end while return M end procedure Algorytmy sortowania Sortowanie przez scalanie Teraz cały algorytm sortowania przez scalanie możemy zapisać jako: 1. Podziel ciąg na dwie części 2. Posortuj obie części 3. Scal je Tylko. . . Jak posortować dwa podciągi? Algorytmy sortowania Sortowanie przez scalanie Teraz cały algorytm sortowania przez scalanie możemy zapisać jako: 1. Podziel ciąg na dwie części 2. Posortuj obie części 3. Scal je Tylko. . . Jak posortować dwa podciągi? Sortowaniem przez scalanie! Algorytmy sortowania Sortowanie przez scalanie procedure Merge-Sort(T ) if Length(T ) � 1 then � Jeżeli ciąg ma 1 element, return T � to jest już posortowany! end if m ← �Length(T )/2� � Dzielimy na pół. L ← Merge-Sort(T [0..m]) � Sortujemy R ← Merge-Sort(T [m..Length(T )]) � obie strony. return Merge(L, R) � Scalamy. end procedure 2 6 4 7 8 9 1 5 Algorytmy sortowania Sortowanie przez scalanie Jaka jest złożoność obliczeniowa sortowania przez scalanie? Spróbujmy rozwiązać zależność rekurencyjną: T (n) = O(n) + 2T (n/2) Dla ułatwienia obliczeń podstawimy n = 2x , otrzymując: T (2x ) = O(2x ) + 2T (2x−1 ) Algorytmy sortowania Sortowanie przez scalanie T (2x ) = O(2x ) + 2T (2x−1 ) Algorytmy sortowania Sortowanie przez scalanie T (2x ) = O(2x ) + 2T (2x−1 ) � = O(2x ) + 2 O(2x−1 ) + 4T (2x−2 ) � Algorytmy sortowania Sortowanie przez scalanie T (2x ) = O(2x ) + 2T (2x−1 ) � = O(2x ) + 2 O(2x−1 ) + 4T (2x−2 ) = O(2x ) + O(2x ) + 4T (2x−2 ) � Algorytmy sortowania Sortowanie przez scalanie T (2x ) = O(2x ) + 2T (2x−1 ) � = O(2x ) + 2 O(2x−1 ) + 4T (2x−2 ) = O(2x ) + O(2x ) + 4T (2x−2 ) = 2O(2x ) + 22 (2x−2 ) � Algorytmy sortowania Sortowanie przez scalanie T (2x ) = O(2x ) + 2T (2x−1 ) � = O(2x ) + 2 O(2x−1 ) + 4T (2x−2 ) = O(2x ) + O(2x ) + 4T (2x−2 ) = 2O(2x ) + 22 (2x−2 ) = 3O(2x ) + 23 (2x−3 ) � Algorytmy sortowania Sortowanie przez scalanie T (2x ) = O(2x ) + 2T (2x−1 ) � = O(2x ) + 2 O(2x−1 ) + 4T (2x−2 ) = O(2x ) + O(2x ) + 4T (2x−2 ) = 2O(2x ) + 22 (2x−2 ) = 3O(2x ) + 23 (2x−3 ) ... � Algorytmy sortowania Sortowanie przez scalanie T (2x ) = O(2x ) + 2T (2x−1 ) � = O(2x ) + 2 O(2x−1 ) + 4T (2x−2 ) = O(2x ) + O(2x ) + 4T (2x−2 ) = 2O(2x ) + 22 (2x−2 ) = 3O(2x ) + 23 (2x−3 ) ... = kO(2x ) + 2k (2x−k ) � Algorytmy sortowania Sortowanie przez scalanie T (2x ) = O(2x ) + 2T (2x−1 ) � = O(2x ) + 2 O(2x−1 ) + 4T (2x−2 ) = O(2x ) + O(2x ) + 4T (2x−2 ) = 2O(2x ) + 22 (2x−2 ) = 3O(2x ) + 23 (2x−3 ) ... = kO(2x ) + 2k (2x−k ) = xO(2x ) + 2x · 20 � Algorytmy sortowania Sortowanie przez scalanie T (2x ) = O(2x ) + 2T (2x−1 ) � = O(2x ) + 2 O(2x−1 ) + 4T (2x−2 ) = O(2x ) + O(2x ) + 4T (2x−2 ) = 2O(2x ) + 22 (2x−2 ) = 3O(2x ) + 23 (2x−3 ) ... = kO(2x ) + 2k (2x−k ) = xO(2x ) + 2x · 20 = O(x2x ) + O(2x ) � Algorytmy sortowania Sortowanie przez scalanie T (2x ) = O(2x ) + 2T (2x−1 ) � = O(2x ) + 2 O(2x−1 ) + 4T (2x−2 ) = O(2x ) + O(2x ) + 4T (2x−2 ) = 2O(2x ) + 22 (2x−2 ) = 3O(2x ) + 23 (2x−3 ) ... = kO(2x ) + 2k (2x−k ) = xO(2x ) + 2x · 20 = O(x2x ) + O(2x ) = O(x2x ) = O(n log n) � Algorytmy sortowania Sortowanie przez scalanie Intuicyjnie złożoność sortowania przez scalanie zrozumieć można tak: Każdy poziom sortowania wprowadza nowy podział. Na poziomie 0 mamy 1 ciąg, na poziomie 1 mamy 2 ciągi, potem 4 ciągi, 8 ciągów itd. Scalenie każdego poziomu zajmuje O(n) czasu (przechodzimy raz po wszystkich elementach), a poziomów mamy O(log n) (bo za każdym razem dzielimy na dwa). Stąd sumaryczna złożoność wynosi O(n log n). Letnie Warsztaty Matematyczno-Informatyczne Algorytmy i struktury danych Sortowanie szybkie Sortowanie szybkie Istota algorytmu Sortowanie szybkie (ang. quick sort przypomina nieco sortowanie przez scalanie). Oba działają na zasadzie: dzielimy ciąg na dwie części, sortujemy obie części i łączymy je z powrotem w jeden ciąg. Sortowanie przez scalanie wykonuje właściwe sortowanie na etapie scalania. Sortowanie szybkie wykonuje właściwe sortowanie na etapie dzielenia. Sortowanie szybkie Istota algorytmu Ogólny zarys algorytmu wygląda tak: 1. Wybierz dowolny element ciągu (piwot). 2. Podziel ciąg na dwie części: jedna z elementami mniejszymi od piwotu, druga z pozostałymi. 3. Posortuj rekurencyjnie obie części. Punkt podziału może wypadać w różnych miejscach! Sortowanie szybkie Pseudokod procedure Quick-Sort(T ) if Length(T ) � 1 then return T end if m ← Partition(T ) L ← Quick-Sort(T [0..m]) R ← Quick-Sort(T [m + 1..Length(T )]) return Concatenate(L, [T [m]], R) end procedure Zauważ: 1. Partition wykonuje podział i zwraca indeks piwotu po podziale. 2. Przy sortowaniu obu części pomijamy piwot! 3. Concatenate łączy listy w jedną (skleja je). 5 2 8 9 1 4 6 7 3 5 2 3 9 1 4 6 7 8 5 2 3 4 1 9 6 7 8 1 2 3 4 5 9 6 7 8 Sortowanie szybkie Pseudokod procedure Partition(T ) pivot ← T [0] p ← 0, q ← Length(T ) − 1 while p < q do while T [p] < pivot do p ←p+1 end while while T [q] � pivot do q ←q−1 end while if p < q then Swap(T [p], T [q]) end if end while Swap(T [0], T [q]) return q end procedure � Szukamy T [p] < pivot � Szukamy T [q] � pivot � Zamieniamy miejscami. � Umieszczamy piwot w środku. Sortowanie szybkie Złożoność obliczeniowa Podobnie jak sortowanie przez scalanie, sortowanie szybkie ma złożoność obliczeniową O(n log n). Ale tylko przy założeniu, że podział jest zawsze mniej-więcej w połowie! Co jeżeli piwot za każdym razem będzie najmniejszym lub największym elementem listy? Wtedy dostaniemy O(n 2 ). Sortowanie szybkie Złożoność obliczeniowa Sortowanie szybkie jest przykładem algorytmu, którego średnia złożoność obliczeniowa (czyli taka, która występuje zazwyczaj), jest różna od złożoności pesymistycznej (czyli najgorszej możliwej). Dzieje się tak dlatego, ponieważ złożoność obliczeniowa sortowania szybkiego zależy nie tylko od wielkości danych, ale i ich rozkładu. Sortowanie szybkie Przyspieszanie sortowania szybkiego Sortowanie szybkie z reguły jest szybsze od sortowania przez scalanie. W przypadku pesymistycznym może być jednak dużo wolniejsze. Możemy zmniejszyć prawdopodobieństwo wystąpienia przypadku pesymistycznego. Sortowanie szybkie Przyspieszanie sortowania szybkiego Sposób 1: wybieramy piwot losowo. Prawdopodobieństwo, że za każdym razem wybierzemy minimum lub maksimum jest bardzo nikłe. Dzięki temu prawdopodobieństwo otrzymania pesymistycznego przypadku też jest nikłe. Sortowanie szybkie Przyspieszanie sortowania szybkiego Sposób 2: wybieramy medianę podciągu. Znalezienie mediany całego ciągu jest kosztowne i wymaga podobnej ilości pracy, co posortowanie połowy ciągu. Możemy jednak wybrać medianę mniejszego podciągu jako piwot, na przykład pierwszych 20 elementów. W ten sposób nie możemy nigdy wybrać ekstremum całego ciągu. W wersji pesymistycznej podział nadal będzie bardzo nierówny, jednak lepszy niż bez tej optymalizacji. Sortowanie szybkie Przyspieszanie sortowania szybkiego Sposób 3: sprawdzamy ułożenie ciągu. Możemy spróbować oszacować jak blisko posortowania znajduje się nasz ciąg liczb w czasie O(n). Jeżeli ciąg jest blisko posortowania, zamiast sortowania szybkiego stosujemy sortowanie przez scalanie, sortowanie bąbelkowe lub w ogóle nie sortujemy, jeżeli ciąg okaże się całkowicie posortowany. Sortowanie szybkie Algorytmy hybrydowe Okazuje się, że sortowanie szybkie (ale także sortowanie przez scalanie), jest wolniejsze od sortowania przez wstawianie dla bardzo krótkich ciągów (kilka-kilkanaście elementów). Możemy sortować algorytmem O(n log n) do pewnego momentu, a dalej stosować sortowanie przez wstawianie.