Złożoność obliczeniowa
Transkrypt
Złożoność obliczeniowa
Złożoność obliczeniowa algorytmu – ilość zasobów komputera jakiej potrzebuje dany algorytm. Pojęcie to wprowadzili J. Hartmanis i R. Stearns. Najczęściej przez zasób rozumie się czas oraz pamięć – dlatego też używa się określeń "złożoność czasowa" i "złożoność pamięciowa". Za jednostkę złożoności pamięciowej przyjmuje się pojedyncze słowo maszyny (np. Bajt). W przypadku złożoności czasowej nie można podać bezpośrednio jednostki czasu, np. milisekundy, bowiem nie wiadomo na jakiej maszynie dany algorytm będzie wykonywany. Dlatego też wyróżnia się, charakterystyczną dla algorytmu, operację dominującą1. Liczba wykonań tej operacji jest proporcjonalna do wykonań wszystkich operacji. Rozróżnia się dwa rodzaje złożoności: złożoność pesymistyczna określa złożoność w "najgorszym" przypadku, dla najbardziej złośliwych danych. złożoność oczekiwana określa złożoność średnią czyli wartość oczekiwaną. Złożoność obliczeniowa nie jest wygodna w stosowaniu, bowiem operacja dominująca na jednym komputerze może wykonywać się błyskawicznie, na innym zaś musi być zastąpiona szeregiem instrukcji. Dlatego też częściej stosuje się złożoność asymptotyczną, która mówi o tym jak złożoność kształtuje się dla bardzo dużych, granicznych rozmiarów danych wejściowych. Do opisu złożoności obliczeniowej asymptotycznej stosuje się trzy notacje: 1. Notacja wielkie O (omikron) 2. Notacja Ω(Omega) 3. Notacja Ɵ(Teta) 1 Operacja elementarna (dominująca) – w teorii obliczeń operacja charakterystyczna dla danego algorytmu, na ogół zajmująca w nim najwięcej czasu. 1 O (omikron) – przykłady (dalej będą kolejne ;-) ): O( n ) Algorytm o liniowej zależności czasu wykonania od ilości danych. Dwukrotny wzrost liczby przetwarzanych danych powoduje dwukrotny wzrost czasu wykonania. O( n2 ) Algorytm, w którym czas wykonania rośnie z kwadratem liczby przetwarzanych elementów. Dwukrotny wzrost liczby danych powoduje czterokrotny wzrost czasu wykonania. O( n log2n ) Dobre algorytmy sortujące mają taką właśnie złożoność obliczeniową. Czas wykonania przyrasta dużo wolniej od wzrostu kwadratowego. O( n! ) O( an ) Bardzo pesymistyczne algorytmy, czas wykonania rośnie bardzo szybko ze wzrostem liczby elementów wejściowych, czyli znalezienie rozwiązania może zająć najszybszym komputerom całe wieki lub tysiąclecia. Takich algorytmów należy unikać jak ognia ! np.: jeżeli mamy jakiś ciąg o długości n, i przechodzimy go dwiema pętlami for(int i=0;i<n;i++) for(int j=0;j<n;j++) to złożoność wynosi n^2, jeżeli jedna pętla to złożoność n, natomiast gdy masz wyszukiwanie binarne ciągu uporządkowanego, czyli przy każdym przejściu odrzucasz połowę ciągu to złożoność wynosi log(n) przy podstawie z 2 2 Zapis O(...) określamy mianem klasy złożoności obliczeniowej algorytmu. Złożoność obliczeniowa umożliwia porównywanie wydajności różnych algorytmów sortujących. Z reguły proste algorytmy posiadają wysoką złożoność obliczeniową - długo dochodzą do wyniku końcowego. Algorytmy bardziej skomplikowane posiadają mniejszą złożoność obliczeniową - szybko dochodzą do rozwiązania. Złożoność obliczeniowa wszystkich algorytmów sortujących została dokładnie oszacowana. Przykład Załóżmy, iż mamy program sortujący dane zbudowany na bazie algorytmu sortującego o klasie złożoności obliczeniowej O(n2). Sto elementów jest sortowane w czasie 1 sekundy. Ile czasu zajmie posortowanie za pomocą tego programu zbioru o tysiącu elementach? Odpowiedź brzmi - 100 sekund. Ponieważ ilość danych wzrosła 10 razy, to czas obliczeń wzrósł 10 2, czyli 100 razy. Poniżej przedstawiamy odpowiednią tabelkę. Lp. 1. 2. 3. 4. Ilość elementów 100 1.000 10.000 100.000 5. 1.000.000 6. 10.000.000 Czas obliczeń 1 sekunda 100 sekund = 1 minuta 40 sekund 10.000 sekund = 2 godziny 46 minut 40 sekund 1.000.000 sekund = 11 dni 13 godzin 46 minut 40 sekund 100.000.000 sekund = 3 lata 2 miesiące 9 godzin 46 minut 40 sekund 1x1010 sekund = 317 lat 1 miesiąc 4 dni 17 godzin 46 minut 40 sekund Widzimy więc, iż algorytm ten spisuje się dobrze tylko przy niewielkiej liczbie elementów. Gdy liczba sortowanych elementów jest duża, czas oczekiwania na rozwiązanie może być nie do zaakceptowania. Dlatego właśnie informatycy poświęcili tak dużo swojego wysiłku na opracowanie odpowiednio szybkich algorytmów sortujących (te najszybsze mają klasę złożoności O(n log2n) ). 3 UWAGA: Notacja „O” eliminuje składniki wolniej rosnące oraz zaniedbuje czynniki stałe, przez które pomnożone są funkcje. Przykład: f(n) = 5n2 – 2*n + 2 = O(n2) Ponieważ: 5n2 - 2n + 2 5n2 (dla każdego…) n N Przykłady określania złożoności czasowej: 1. Przeszukiwanie elementów w celu znalezienia danego elementu x i:=0; Repeat i:=i+1 until (a[i]=x) or (i=n); złożoność O(n) 4 2. Jeżeli elementy są wcześniej posortowane, stosujemy metodę połowienia przedziałów (metoda bisekcji lub przeszukiwania połówkowego) i:=1; j:=n; repeat k:=(i+j) div 2 if x>a[k] then i:= k+1 else j:= k-1 until (x=a[k]) or (i>j); złożoność O(log n) 3. Znajdowanie min i max metodą iteracyjną min:=a[1]; max:=a[1]; for i:=2 to n do begin if min >a[i] then min:=a[i]; if max <a[i] then max:=a[i] end; złożoność O(n) 5 4. wyszukiwanie wartości maksymalnej w ciągu nieposortowanym Załóżmy, że masz n liczb. Potrzebujesz przejrzeć każdą z nich po to, by określić która z nich jest największa. Potrzebujesz zatem n operacji ("spojrzeń) - liczba potrzebnych operacji jest proporcjonalna do rozmiaru ciągu - więc złożoność liniowa, O(n). 5. wyszukiwanie danej liczby w ciągu posortowanym Pomyśl jakąś liczbę od 1 do 1000. Teraz ja zgadnę jaką pomyślałeś, zadając Ci maksymalnie 10 pytań: Czy ta liczba jest mniejsza od 500? Jeśli tak, to czy jest mniejsza od 250? Jesli nie, to czy jest mniejsza od 375? Jeśli nie, to czy jest mniejsza od 438? Jeśli nie, to czy jest mniejsza od 469? Jeśli tak, to czy jest mniejsza od 443? Jeśli tak, to czy jest mniejsza od 440? Jeśli tak, to tą liczbą jest 439. Idea jest taka, że dzięki temu, że ciąg jest posortowany, cały czas pomniejszasz sobie zakres, w którym masz szukać o połowę - dzięki temu potrzebujesz tylko ok. log21000 operacji - a więc masz złożoność O(log2n) - logarytmiczną. 6 6. sortowanie przez wybieranie Masz nieposortowany ciąg o n elementach i posortowany o 0 elementach. Szukasz najmniejszego elementu w ciągu nieposortowanym i wstawiasz go na koniec posortowanego ciągu, tyle razy, aż posortowany ciąg będzie miał n elementów, a nieposortowany 0. Taki stan uzyskasz wtedy, gdy wszystkie n elementów z nieposortowanego przerzucisz w posortowany. Musisz zatem n razy wyszukać najmniejszy element w ciągu - a wyszukiwanie najmniejszego elementu, jak wiemy z 1. przykładu, ma złożoność O(n) (liniową). Zatem wykonanie n razy operacji o złozoności n wymaga n*n operacji, czyli ma złożoność O(n*n) = O(n2) - kwadratową. 7 Z grubsza złożoności można posortować następująco (od najlepszej (najmniejszej)) : log(n) n nlog(n) n^2 n^3 ... 2^n 3^n ... n! Na załączonym wykresie widać jak czas wykonania programu rośnie w zależności od wielkości rozwiązywanego problemu. 8