Algorytmy i ich
poprawność
Poprawność programów
Jeśli uważasz, że jakiś program komputerowy jest
bezbłędny, to się mylisz ….
Po prostu nie zauważyłeś
jeszcze skutków błędu, który jest w
nim zawarty…
Błędy językowe
np. zamiast for i := 1 to N do X[i] := i ;
napisano for i := 1 do N do X[i] := i ;
Powstają w wyniku naruszenia składni języka
(programowania), którego używamy do zapisania
algorytmu.
Błędy językowe
Możliwe skutki i znaczenie:
• zatrzymanie kompilacji lub interpretacji z komunikatem lub bez,
• przerwanie realizacji programu nawet jeśli kompilator błędu nie wykrył,
• błędy nieprzyjemne, ale zwykle niezbyt poważne - są względnie łatwe do poprawienia.
np. sądziliśmy, że po zakończeniu iteracji
for i := 1 to N do X[i] := i
zmienna i ma wartość N a nie N+1
Wynikają z niezrozumienia semantyki używanego
języka programowania.
Błędy semantyczne
Możliwe skutki i znaczenie:
• program nie realizuje poprawnie algorytmu,
• błędy trudne do przewidzenia i potencjalnie groźne, ale są do uniknięcia przy większej wiedzy i starannym sprawdzaniu znaczenia używanych instrukcji.
np. w algorytmie zliczania zdań, w których
występuje słowo “algorytm” nie zauważyliśmy, że
znak “. ” może występować także wewnątrz zdania
(“Na rys. 2 pokazano schemat...”),
a używaliśmy jej do wyszukiwania końca zdania.
Błędy logiczne
Możliwe skutki i znaczenie:
• algorytm przestaje być poprawnym rozwiązaniem zadania algorytmicznego,
• dla pewnych zestawów danych wejściowych algorytm podaje wyniki niezgodne z oczekiwanymi,
• procesor może nie być w stanie wykonać pewnych instrukcji (np. Żądamy dzielenia przez 0),
• błędy bardzo groźne - mogą być trudne do znalezienia i pozostawać długo w ukryciu nawet w trakcie używania programu w postaci kodu.
wynikają z wadliwie skonstruowanych struktur sterujących np.
• niewłaściwych zakresów iteracji,
• niewłaściwych warunków użytych do zatrzymywania iteracji warunkowych
• przeniesienia sterowania w niewłaściwe miejsce
procesu w wyniku zastosowania wyboru
warunkowego (lub instrukcji skoku).
Błędy algorytmiczne
Możliwe skutki i znaczenie:
• algorytm dla pewnych dopuszczalnych danych wejściowych daje niepoprawny wynik,
• wykonanie programu realizującego algorytm jest przerywane w trybie awaryjnym,
• program realizujący algorytm nie kończy w normalnym trybie swego działania.
Przykład błędu algorytmicznego
Pętla nieskończona:
Co to oznacza?
Rozmaitość źródeł błędów różnych typów
Złożone programy wymagają:
• testowania na licznych danych (zestawy testowe) • uruchamiania (badanie wyników końcowych i
pośrednich; DEBUGGING – odpluskwianie)
• Za pomocą debaggingu nie można udowodnić, że nie ma błędów – można jedynie je wykrywać
Obydwie metody nie dają gwarancji wykrycia i usunięcia wszystkich błędów.
Intuicyjnie poprawność = zgodność z zamierzeniami
• Specyfikacja wyraża, co program ma robić, i określa związek miedzy jego danymi wejściowymi i
wyjściowymi.
• Weryfikacja opiera się o sprawdzenie specyfikacji
Weryfikacja poprawności programu
Weryfikacja poprawności programu
Warunek wstępny Warunek końcowy
Warunek wstępny i końcowy
• Warunek wstępny - warunek dotyczący danych wejściowych, który musi być spełniony na początku programu.
• jeżeli dane nie spełniają tego warunku, to nie ma gwarancji że program będzie działał poprawnie ani nawet że się zatrzyma.
• Warunek końcowy - co oblicza program przy założeniu że się zatrzyma.
• warunek końcowy jest zawsze prawdziwy jeżeli zachodzi warunek wstępny.
Weryfikacja poprawności programu
Aby wykazać, że program jest zgodny ze
specyfikacją, trzeba podzielić ją na wiele
kroków, podobnie jak dzieli się na kroki sam
program.
Każdy krok programu ma swój warunek
wstępny i warunek końcowy.
Dowodzimy poprawności specyfikacji przez podstawienie wyrażenia x+1 pod każde wystąpienie x w warunku końcowym.
Otrzymujemy formułę x+1 ≥ 1, która wynika z warunku wstępnego x ≥ 0
Zatem specyfikacja tej instrukcji podstawienia jest poprawna
Weryfikacja poprawności programu
Problem: instrukcja przypisania
warunek wstępny: x ≥ 0
wyrażenie: x = x+1
warunek końcowy: x ≥ 1
, x-zmienna, v-wyrażenie x = v
1. bloki instrukcji wykonywane sekwencyjnie 2. instrukcje wyboru
3. instrukcje powtarzania (pętli) 4. wywołania funkcji
Każdej z tych metod budowania kodu odpowiada metoda przekształcania warunku wstępnego i końcowego w celu wykazania poprawności specyfikacji kroku złożonego
Weryfikacja poprawności programu
Istnieją cztery sposoby łączenia prostych kroków w celu otrzymania kroków bardziej złożonych.
• Aby wykazać prawdziwość specyfikacji instrukcji wyboru, należy dowieść, że warunek końcowy wynika z warunku wstępnego i warunku testu w każdym z przypadków instrukcji wyboru.
• Funkcje też mają swoje warunki wstępne i warunki końcowe; musimy sprawdzić że warunek wstępny funkcji wynika z warunku wstępnego kroku
wywołania, a warunek końcowy pociąga za sobą warunek końcowy kroku wywołania.
• Wykazanie poprawności programowania dla pętli wymaga użycia niezmiennika pętli.
Weryfikacja poprawności programu
Niezmienniki pętli
Niezmiennik pętli określa warunki jakie muszą być zawsze spełnione przez zmienne w pętli, a także przez wartości wczytane lub wypisane (jeżeli takie operacje zawiera).
Warunki te muszą być prawdziwe przed pierwszym wykonaniem pętli oraz po każdym jej obrocie.
(mogą stać się chwilowo fałszywe w trakcie wykonywania wnętrza pętli, ale musza ponownie stać się prawdziwe przy końcu każdej iteracji)
Niezmienniki pętli – przykład
int a=5, b=0;
for (int i=0; i<9; i++) {
b++; }
Zdanie a jest równe 5 jest trywialnym niezmiennikiem pętli.
W praktyce niezmiennik pętli traktowany jest jako założenie indukcyjne, na podstawie którego wykazuje się prawdziwość kroku indukcyjnego.
Niezmienniki pętli – przykład
int NWD(int a, int b) { int c; while (b!=0) { c = a%b; a = b; b = c; } return a; }
Niezmiennikiem pętli, pozwalającym dowieść
poprawność algorytmu NWD jest zdanie:
• Stwierdzenie jest prawdziwe przed pierwszym wykonaniem pętli, ale po dokonaniu całej inicjacji; wynika ono z warunku wstępnego pętli.
• Jeśli założymy, że stwierdzenie jest prawdziwe przed jakimś przebiegiem pętli i że pętla zostanie
wykonana ponownie, a więc warunek pętli jest spełniony, to stwierdzenie będzie nadal prawdziwe po kolejnym wykonaniu wnętrza pętli.
Dowodzenie niezmienników pętli
Proste pętle maja zazwyczaj proste niezmienniki. Pętle dzielimy na trzy kategorie
• pętla z wartownikiem: czyta i przetwarza dane aż do momentu napotkania niedozwolonego elementu • pętla z licznikiem: zawczasu wiadomo ile razy pętla
będzie wykonana
• pętle ogólne: wszystkie inne
Problem „STOP-u”
Aby udowodnić że program się zatrzyma, musimy
wykazać, że zakończą działanie wszystkie pętle programu i wszystkie wywołania funkcji rekurencyjnych.
• Dla pętli z wartownikiem wymaga to umieszczenia w warunku wstępnym informacji, że dane wejściowe zawierają wartownika.
• Dla pętli z licznikiem wymaga to dodefiniowania granicy dolnej (górnej) tego licznika.
Poprawność programów – uwagi
końcowe
Istnieje wiele innych elementów, które muszą być brane pod uwagę przy dowodzeniu poprawności programu:
• możliwość przepełnienia wartości całkowitoliczbowych • możliwość przepełnienia lub niedomiar dla liczb
zmiennopozycyjnych
• możliwość przekroczenia zakresów tablic • prawidłowość otwierania i zamykania plików
Niepoprawność algorytmu
Wykonanie algorytmu rozwiązania zadania dla pewnego zestawu danych zakończy się prawidłowo, ale wyniki są niepoprawne
np. źle sformułowany warunek, powinna być silna nierówność, a jest słaba
Działanie algorytmu dla pewnego zestawu danych zostanie przerwane;
np. dzielenie przez zero, obliczenie pierwiastka kwadratowego z liczby ujemnej
Algorytm dla pewnego zestawu danych działa w nieskończoność (pętle nieskończone);
np. przy przeglądaniu listy wskaźnikowej, wskaźnik nie jest przesuwany do następnego elementu listy
Dowód poprawności algorytmu zawsze składa się z dwóch części:
dowód, że jeśli algorytm się zakończy, to da poprawny wynik,
dowód, że przy poprawnych danych wejściowych algorytm zawsze się zakończy.
Algorytmy poprawne częściowo i
całkowicie
Poprawność częściowa algorytmu
Algorytm Alg działający w strukturze danych S jest częściowo poprawny ze względu na specyfikację <wp, wk>, wtedy i tylko wtedy, gdy dla wszystkich danych spełniających warunek początkowy wp, jeżeli algorytm zatrzyma się, to uzyskane wyniki spełniają warunek końcowy wk.
Powiemy, że algorytm Alg działający w strukturze danych S jest całkowicie poprawny ze względu na specyfikację <wp,wk>, wtedy i tylko wtedy, gdy dla wszystkich danych w strukturze S spełniających warunek początkowy wp, algorytm zatrzymuje się i daje wyniki spełniające warunek końcowy wk.
Poprawność całkowita algorytmu
Niezmienniki pętli
Niezmiennikiem pętli nazywać będziemy
własność
(formułę), która jeśli jest
prawdziwa na początku wykonania pętli, to
jest również prawdziwa po wykonaniu treści
pętli.
Częściowej poprawności algorytmu można dowodzić poprzez:
• wybranie punktów kontrolnych
• związanie z każdym punktem asercji (funkcji logicznej reprezentującej przypuszczenie)
• ustalenie niezmienników w obrębie iteracji
• dowiedzenie, że z prawdziwości jednej asercji wynika prawdziwość następnej, że niezmiennik pozostaje prawdziwy w kolejnych iteracjach i pociąga za sobą prawdziwość ostatniej asercji.
Metoda niezmienników i zbieżników
Całkowitej poprawności algorytmu można dowodzić poprzez:
• dodatkowe ustalenie zbieżnika (wielkości zależnej od zmiennych i danych, która jest zbieżna)
• dowiedzenie, że po skończonej liczbie iteracji algorytm się zatrzyma w ostatnim punkcie kontrolnym.
Przykład zastosowania metody
niezmienników i zbieżników
Algorytm odwracający dowolny napis (procedura odwrócone): odwrócone(“alama2koty”) = “ytok2amala”
• pomocnicze funkcje:
głowa(“alama2koty”) = “a”
ogon(“alama2koty”) = “lama2koty” • operator konkatenacji (złożenia napisów):
“alama” & “2koty” = “alama2koty”
• dla dowolnego napisu T zachodzi: głowa(T) & ogon(T) = T
Przykład zastosowania metody
• Jeśli asercja 1 jest prawdziwa, to 2 też jest prawdziwa (przed rozpoczęciem iteracji)
• Jeśli w pewnym kroku iteracji asercja 2 jest prawdziwa, to w następnym kroku też jest ona prawdziwa (warunek z asercji 2 jest niezmiennikiem iteracji) • Jeśli w ostatnim kroku iteracji asercja 2 jest prawdziwa, to 3 jest też prawdziwa
Aby wykazać częściową poprawność algorytmu należy udowodnić:
Przykład zastosowania metody
• Ad 1.: oczywiście zachodzi równość odwrócone(“”) & T = T • Ad 2.: trzeba sprawdzić czy
odwrócone(Y) & X = odwrócone(głowa(X) & Y) & ogon(X) dla każdego Y i X ≠ “”
Przykład zastosowania metody
• Aby wykazać całkowitą poprawność algorytmu należy jeszcze dodatkowo udowodnić, że dla każdego napisu T punkt kontrolny 2 jest
przechodzony tylko skończoną liczbę razy tzn. 3 punkt kontrolny jest zawsze osiągany.
• Długość napisu X jest zbieżnikiem, który może być w tym celu wykorzystany - w każdej iteracji długość X maleje o jeden znak i nie może stać się mniejsza od 0!
Co z rekurencją?
• Metoda niezmienników i zbieżników może być
zastosowana także dla dowodzenia
poprawności algorytmów rekurencyjnych
• Łatwiej jest skorzystać z tej metody po
Przykład – wieże Hanoi
Danych jest n krążków, umieszczonych w
porządku rosnących średnic, na drążku A.
Zadanie polega na przeniesieniu wszystkich
krążków na drążek B z wykorzystaniem
pomocniczego drążka C (oba drążki B i C są
początkowo puste), ale mniejszy krążek musi
zawsze leżeć na większym.
A B C
procedura przenieś N z X na Y używając Z;
(1) jeśli N = 1, to wypisz “X → Y”;
(2) w przeciwnym razie (tj. jeśli N > 1) wykonaj co
następuje:
(2.1) wywołaj przenieś N - 1 z X na Z używając Y; (2.2) wypisz “X → Y”;
(2.3) wywołaj przenieś N -1 z Z na Y używając X;
(3) wróć do poziomu wywołania
Wieże Hanoi – rekurencyjnie
Algorytm iteracyjny równoważny algorytmowi
rekurencyjnemu!
Ustaw trzy kołki w kółko.
1. powtarzaj co następuje, aż do uzyskania po
kroku 1.1 rozwiązania problemu:
1.1. przenieś najmniejszy z dostępnych krążków z kołka, na którym się znajduje, na kołek następny w kierunku ruchu wskazówek zegara,
1.2. wykonaj jedyne możliwe przeniesienie nie
zmieniające położenia najmniejszego krążka, który został przeniesiony w kroku 1.1.
Złożoność
obliczeniowa
Algorytm jest dokładnie określonym układem
skończonej liczby elementarnych instrukcji wraz z
porządkiem ich wykonywania.
Każda instrukcja ma precyzyjnie określoną interpretację za pomocą podstawowych operacji arytmetycznych i logicznych, a jej wykonanie jest skończone i ma jednoznacznie określony efekt końcowy.
Jako elementy komunikacji ze światem w algorytmie można wyróżnić: dane, na których są wykonywane obliczenia i wyniki, które są oczekiwanym rezultatem działań
Cechy dobrego algorytmu
Każdy dobry algorytm powinien posiadać następujące cechy:
mieć określone operacje podstawowe,
każdy krok jednoznacznie i precyzyjnie zdefiniowany, każdy możliwy przypadek przewidziany,
może korzystać z danych wejściowych,
prowadzi do jednej lub więcej danych wyjściowych, wykorzystuje możliwie jak najmniej pamięci,
wykonuje się w możliwie jak najkrótszym czasie.
Złożoność obliczeniowa
Ten sam problem można rozwiązać różnymi metodami. Problemy mogą więc posiadać kilka alternatywnych algorytmów rozwiązujących je.
Przy porównywaniu algorytmów zwykle bierze się pod uwagę ich efektywność (szybkość działania) i
zapotrzebowanie na zasoby pamięciowe systemu
W celu porównywania algorytmów pod względem ich prędkości działania, wprowadzono pojęcie złożoności obliczeniowej.
Złożoność obliczeniowa (ang. computational
complexity) – miara służąca do określania ilości
zasobów komputerowych potrzebnych do
rozwiązania problemów obliczeniowych.
Złożoność obliczeniowa algorytmu charakteryzuje jak szybko rośnie liczba operacji (elementarnych kroków algorytmu) niezbędnych do znalezienia rozwiązania, gdy wzrasta ilość przetwarzanych danych.
Złożoność obliczeniowa
Złożoność obliczeniowa - cele
Cele, dla których wyznaczamy złożoność
obliczeniową algorytmów są następujące:
można wybrać z grupy algorytmów rozwiązujących ten sam problem, algorytm o najlepszej
(najmniejszej) złożoności.
można zbadać jak zmieni się złożoność, gdy zwiększymy rozmiar danych wejściowych (wpływ wzrost rozmiaru danych wejściowych na czas wykonywania się algorytmu).
Koszt algorytmu
• liczba instrukcji • liczba operacji arytmetycznych • liczba wywołań procedury • liczba zmiennych • ilość miejsca potrzebna dla danychMamy dwa kryteria (miary kosztu) efektywności:
Złożoność obliczeniowa - rodzaje
Złożoność pamięciowa
Wynika z liczby i rozmiaru struktur danych wykorzystywanych w algorytmie
Złożoność czasowa
Wynika z liczby operacji elementarnych wykonywanych w trakcie przebiegu algorytmu
Pamięciowa złożoność obliczeniowa informuje nas o ilości pamięci komputera wyrażanej w liczbie bajtów lub liczbie zmiennych typów elementarnych wymaganej przez dany algorytm dla n przetwarzanych danych.
• Liczba bajtów zależy od liczby zmiennych typów elementarnych użytego języka programowania i reprezentacji zmiennych w pamięci maszyny.
Pamięciowa złożoność
obliczeniowa
Czasowa złożoność obliczeniowa - zależność pomiędzy liczbą operacji elementarnych wykonywanych w trakcie przebiegu algorytmu a rozmiarem danych wejściowych (podawana jako funkcja rozmiaru tych danych)
wielkość złożoności czasowej musi być niezależna od typu maszyny, na której uruchamiamy program
za jednostkę czasowej złożoności obliczeniowej przyjęto wykonanie tzw. operacji dominującej (podstawowej), czyli takiej, wokół której koncentruje się przetwarzanie danych w algorytmie.
Ilość wykonań operacji dominujących jest
proporcjonalna do czasu wykonania algorytmu.
Czasowa złożoność obliczeniowa
Złożoność czasowa podawana jest jako funkcja rozmiaru danych, której wartości podają liczbę operacji, np.
• W algorytmie sortowania bąbelkowego dla listy o długości N:
F(N) = liczba porównań par sąsiednich elementów • W algorytmie rozwiązania wież Hanoi dla N krążków: F(N) = liczba przeniesień pojedynczego krążka z kołka na kołek
Szukamy elementu x na liście o długości N. Dane wejściowe to lista i element do wyszukania.
Algorytm 1
1. weź pierwszy element listy ; 2. wykonuj:
2.1. sprawdź czy bieżący element jest tym szukanym ; 2.2. sprawdź czy osiągnąłeś koniec listy ;
2.3. weź następny element z listy aż znajdziesz lub przejrzysz całą listę
Złożoność obliczeniowa – przykład
Jeśli operacją elementarną jest sprawdź, to F(N)=2N
Szukamy elementu x na liście o długości N. Dane wejściowe to lista i element do wyszukania.
Algorytm 2
1. dopisz szukany element na końcu listy ; 2. weź pierwszy element listy ;
3. wykonuj:
3.1. sprawdź czy bieżący element jest tym szukanym ;
3.2. weź następny element z listy aż znajdziesz ;
4. sprawdź czy jesteś na końcu listy
Złożoność obliczeniowa – przykład
jest mitem stwierdzenie, że komputery są tak szybkie, że czas nie stanowi problemu (rozkład na czynniki pierwsze dużych liczb - 300 cyfr wymaga milionów lat)
potrzebne jest rozwiązywanie coraz większych problemów: komputerowe systemy wspomagania decyzji
komputerowe symulacje i prognozy
muszą funkcjonować systemy komputerowe czasu rzeczywistego
automatyczne sterowanie w złożonych układach
Złożoność czasowa decyduje o tym czy dany algorytm
jest przydatny.
Co to znaczy w praktyce?
Rozróżniamy dwa rodzaje czasowej i
pamięciowej złożoności obliczeniowej:
• Pesymistyczna
złożoność
obliczeniowa
W(n)
• Oczekiwana złożoność obliczeniowa A(n)
Złożoność pesymistyczna i
oczekiwana
Pesymistyczna złożoność obliczeniowa W(n)
dotyczy najgorszego przypadku zestawu danych, zatem określa ona największą ilość operacji dominujących (lub komórek pamięci), która może być wymagana dla najgorszego przypadku n danych wejściowych.
Oczekiwana złożoność obliczeniowa A(n) dotyczy typowego zestawu danych i określa statystycznie średnią ilość operacji dominujących (komórek pamięci), które należy wykonać dla
typowego zestawu danych, aby rozwiązać problem.
Złożoność pesymistyczna i
oczekiwana
Koszt algorytmu Alg dla danych d: t(Alg,d)
Niech Dnbędzie zbiorem danych rozmiaru n dla pewnego problemu P oraz Alg algorytmem rozwiązującym problem P. Pesymistyczna złożoność obliczeniowa:
W(Alg,n) = sup {t(Alg,d) : d ∈Dn}
Oczekiwana złożoność obliczeniowa:
A(Alg,n) = Σ{ p(d) * t(Alg,d) : d ∈Dn},
gdzie p(d) – prawdopodobieństwo wystąpienia danych d.
Złożoność pesymistyczna i
oczekiwana
Rozważmy wyszukiwanie elementu w n elementowym, nieuporządkowanym zbiorze danych.
Operacja dominująca – sprawdzanie, czy i-ty element zbioru jest elementem poszukiwanym, i = 1,2,3,...,n.
Przykład obliczenia złożoności
pesymistycznej i oczekiwanej
Za końcową złożoność obliczeniową danego algorytmu przyjmuje się najczęściej jego złożoność
pesymistyczną.
Przykład - złożoność pesymistyczna
W przypadku najgorszym poszukiwany element jest ostatnim, zatem znaleziony będzie po sprawdzeniu wszystkich elementów tego zbioru.
Również stwierdzenie, iż elementu w zbiorze nie ma, wymaga przeglądnięcia wszystkich elementów.
Przykład - złożoność oczekiwana
Jeśli prawdopodobieństwo wystąpienia poszukiwanego elementu na każdej pozycji w zbiorze jest takie samo, to dla każdego elementu wynosi ono:
Oczekiwana złożoność obliczeniowa jest równa wartości oczekiwanej zmiennej losowej rozkładu prawdopodobieństwa pi, zatem: 1 1 1 1 ( 1) 1 ( ) 2 2 n n i i i n n n A n ip i n n = = + + =
∑
=∑
= ⋅ = 1 , 1, 2,..., i p dla i n n = =Porównywanie czasów działania
algorytmów
Chcemy porównać dwa algorytmy wykonujące to samo zadanie za pomocą jednej pętli ograniczonej, w której liczba iteracji N jest proporcjonalna do rozmiaru danych wejściowych.
Złożoności czasowe tych algorytmów wynoszą: F1(N) = K1+ L1* N
Do porównania algorytmów można wykorzystać iloraz: 1 2 ( ) ( ) ( ) F N s N F N =
s(N) = 1 oznacza jednakową szybkość działania s(N) < 1 oznacza, że algorytm pierwszy jest szybszy
Porównywanie czasów działania
algorytmów
Kiedy zachodzi s(N)=1?
s(N) = 1 wtedy i tylko wtedy, gdy K1= K2i L1= L2
Wniosek: w takim przypadku oba algorytmy są identyczne. 0 0 1 ( ) 1 dla N N s N dla N N ≥ ≤ ⎧ = ⎨< > ⎩
Który algorytm jest lepszy gdy zachodzą następujące nierówności? :
Porównywanie czasów działania
algorytmów
Przyjmuje się, że w celu porównania
algorytmów bada się wartość ilorazu s(N) dla
bardzo dużych wartości N, czyli dla
N
→ ∞
Porównywanie czasów działania
algorytmów
Porównywanie czasów działania
algorytmów
O wyniku porównania czasów działania dwóch algorytmów decyduje wartość:
Jest to tzw. analiza asymptotyczna
1 2
lim ( )
NL
s N
L
→∞=
Porównywanie czasów działania
algorytmów
Asymptotyczna złożoność algorytmu – określenie rzędu wielkości czasu działania algorytmu, tzn. określenie szybkości wzrostu czasu działania algorytmu, gdy rozmiar danych dąży do nieskończoności.
Rząd wielkości funkcji :
• opisuje czas działania algorytmu • charakteryzuje efektywność algorytmu
• za jego pomocą można porównać różne algorytmy
Porównywanie czasów działania
algorytmów
Dwa algorytmy o czasach wykonania F1(N) i F2(N) mają złożoność tego samego rzędu, jeśli:
Algorytm o czasie wykonania F1(N) ma nie wyższy rząd złożoności od algorytmu o czasie wykonania F2(N), jeśli:
1 2
( )
lim
,
0
( )
NF N
C
gdzie
C
F N
→∞=
< < ∞
1 2( )
lim
( )
NF N
C
F N
→∞= < ∞
Porównanie rzędów złożoności
Dopiero zmniejszenie rzędu złożoności obliczeniowej algorytmu jest istotnym ulepszeniem rozwiązania problemu algorytmicznego !
Złożoność asymptotyczna = przybliżona miara efektywności
Z reguły dane nie są uporządkowane i ocena złożoności algorytmu jest rzeczą niełatwą ale bardzo istotna.
Często wystarczają przybliżone, asymptotyczne oszacowania ciągów lub ogólniej funkcji.
Opisują one zachowanie funkcji wraz ze wzrostem argumentu.
Podczas przekształceń rachunkowych celowo ograniczana jest wiedza o funkcji, dzięki czemu łatwiej jest rachować i otrzymać zadowalającą postać przybliżającą.
Notacja O
Notacja Ω (Omega) Notacja Θ (Teta)
Złożoność asymptotyczna
W oszacowaniach asymptotycznych posługujemy się ogólnie przyjętymi symbolami opisującymi asymptotyczne zachowanie jednej funkcji wobec drugiej.
Staramy się wyznaczy złożoność w „przypadku optymistycznym”, „przypadku pesymistycznym” oraz „przypadku średnim”.
Często posługujemy się przybliżeniami opartymi o notacje:
Notacja O
Niech R*=R+∪{0} , oraz niech będą dane funkcje f oraz g:
* *
( ), ( ):
f x g x R →R Definicja:
Mówimy, że funkcja f(x) jest rzędu O(g(x), jeśli istnieje taka stała c>0
oraz , że dla
każdego zachodzi:
(f nie rośnie szybciej niż g). * 0 x ∈R 0 x x≥ ( ) ( ) f x ≤cg x
Notacja O używana jest w analizie górnej granicy asymptotycznej. Służy do szacowania czasu działania algorytmu w przypadku pesymistycznym
Po raz pierwszy tego symbolu użył niemiecki teorio-liczbowiec Paul Bachmann w 1894.
Prace Edmunda Landau'a spopularyzowały tę notację, stąd czasami jest nazywany symbolem Landau'a.
Notacja O
Określenia "złożoność co najwyżej O(g(n))" i "złożoność O(g(n))" są matematycznie równoważne.
Stwierdzenie, ze algorytm sortowania ma złożoność O(n2) oznacza, ze jeśli zastosujemy go do 2-krotnie
dłuższego ciągu danych, czas jego działania wzrośnie mniej więcej 4-krotnie, gdy zaś sortowany ciąg danych wydłuży się 10-krotnie, na uzyskanie wyniku trzeba poczekać około 100 razy dłużej.
Załóżmy, iż oczekiwana złożoność obliczeniowa pewnego algorytmu jest następująca: A(n) = n2+ 3n.
Gdy n → ∞ , czynnik 3n stanie się coraz mniej znaczący w stosunku do czynnika n2. Stąd wnioskujemy, iż jest to
algorytm o złożoności O(n2).
Aby to udowodnić, musimy znaleźć stałą c oraz no, dla
których zachodzi:
n2+ 3n ≤ cn2dla n ≥ n o
Wystarczy przyjąć c = 4 i no= 1:
n2+ 3n ≤ 4n2dla n ≥ 1, co dowodzi, iż podany algorytm
ma rzeczywiście rząd złożoności obliczeniowej równy O(n2).
Notacja O - przykład
Własności notacji O
Własność 1 (przechodniość)
Jeśli f(n) jest O(g(n)) i g(n) jest O(h(n)), to f(n) jest O(h(n)) Własność 2
Jeśli f(n) jest O(h(n)) i g(n) jest O(h(n)), to f(n)+g(n) jest O(h(n)) Własność 3
Funkcja ankjest O(nk)
Własność 4
Funkcja nkjest O(nk+j) dla dowolnego dodatniego j
Wniosek: dowolny wielomian jest O( ) dla n podniesionego do najwyższej w nim potęgi, czyli
f(n) = a
kn
k+ a
Własność 5
Jeśli f(n)=c g(n), to f(n) jest O(g(n)) Własność 6
Funkcja logan jest O(logbn) dla dowolnych a i b większych niż 1 Własność 7
logan jest O(log2n) dla dowolnego dodatniego a
Własności notacji O( )
Niech R*=R+∪{0} , oraz niech będą dane funkcje f oraz g:
* *
( ), ( ):
f x g x R →R Definicja:
Mówimy, że funkcja f(x) jest rzędu Ω(g(x)), , jeśli istnieje taka stała
c>0 oraz , że
dla każdego zachodzi:
(f nie rośnie wolniej niż g). * 0 x ∈R 0 x x≥ ( ) ( ) g x ≤cf x
Notacja Ω
Niech R*=R+∪{0} , oraz niech będą dane funkcje f oraz g:
* *
( ), ( ):
f x g x R →R Definicja:
Mówimy, że funkcja f(x) jest rzędu
Θ
(g(x)), , jeśli istnieją takie stałec1,c2>0 oraz , że dla każdego
zachodzi:
(f rośnie tak samo jak g).
* 0 x ∈R 0 x x≥ 1 ( ) ( ) 2 ( ) c g x ≤ f x ≤c g x
Notacja Θ
Uwagi
2. Można powiedzieć, że
f(n) = Θ(g(n)),
gdy f(n) jest jednocześnie rzędu
Ο
(g(n)) i Ω(g(n)).Notacja Ω jest asymptotyczną granicę dolną – oszacowuje czas działania algorytmu dla najlepszego przypadku Notacja Θ jest asymptotycznie dokładnym oszacowaniem –
ogranicza funkcję od góry i od dołu. 1. Równoważność
Najczęściej algorytmy mają złożoność czasową proporcjonalną do pewnych funkcji:
1 stałe
log n logarytmiczne n liniowe
n log n liniowo-logarytmiczne
n2 wielomianowe (małe wykładniki)
n3 wielomianowe (małe wykładniki)
2n wykładnicza
n! ponadwykładnicza
nn wielomianowa (duże wykładniki)
Złożoność czasowa algorytmów
Złożoność czasowa stała - O(1)
• Algorytm wykonuje stałą ilość operacji dominujących bez względu na rozmiar danych wejściowych.
• W takim algorytmie czas wykonania jest stały i nie zmienia się przy zmianie liczby przetwarzanych elementów.
Złożoność czasowa liniowa - O(n)
• Dla każdej danej algorytm wykonuje stałą ilość operacji dominujących.
• Czas wykonania jest proporcjonalny do liczby n danych wejściowych.
• Czas wykonania rośnie liniowo ze wzrostem liczby elementów.
Złożoność czasowa kwadratowa - O(n2)
• Algorytm dla każdej danej wykonuje ilość operacji dominujących proporcjonalną do liczby wszystkich przetwarzanych danych. • Czas wykonania jest proporcjonalny do kwadratu liczby danych n. • Inne złożoności tego typu O(n3), O(n4)... noszą nazwę
wielomianowych złożoności obliczeniowych.
Złożoność czasowa logarytmiczna - O(log n)
• W algorytmie zadanie rozmiaru n da się sprowadzić do zadania rozmiaru n/2.
• Typowym przykładem jest wyszukiwanie binarne w zbiorze uporządkowanym.
• Sprawdzenie środkowego elementu pozwala określić, w której z dwóch połówek zbioru może znajdować się poszukiwany element.
Rodzaje złożoności
logax = y wtedy i tylko wtedy, gdy ay= x
W określeniu rodzajów złożoności obliczeniowej nie podajemy podstawy logarytmu, ponieważ logarytmy o różnych podstawach z tej samej wartości różnią się od siebie jedynie iloczynem przez stałą: Niech
logax = y oraz logbx = cy Wtedy ay= x, oraz bcy= x Stąd ay= bcy (a)y= (bc)y Zatem a = bcoraz log bx = c logax
Ponieważ podstawy a i b są stałe, to i c musi być stałe. Z kolei z definicji notacji O wynika, iż klasa złożoności dwóch funkcji różniących się jedynie iloczynem przez stałą jest taka sama.
Złożoność czasowa liniowo logarytmiczna - O(n log n)
• Zadanie rozmiaru n daje się sprowadzić do dwóch podzadań rozmiaru n/2 plus pewna ilość operacji, których liczba jest proporcjonalna do ilości danych n.
• Tego typu złożoność obliczeniową posiadają dobre algorytmy sortujące.
Złożoność czasowa wykładnicza - O(2n), O(n!)
• Złożoność obliczeniową O(2n) posiada algorytm, w którym
wykonywana jest stała liczba operacji dla każdego podzbioru n danych wejściowych.
• Złożoność obliczeniową O(n!) posiada algorytm, w którym wykonywana jest stała liczba operacji dla każdej permutacji n danych wejściowych.
Rodzaje złożoności
• Złożoność obliczeniowa wykładnicza jest bardzo niekorzystna, ponieważ czas wykonania rośnie szybko wraz ze wzrostem liczby danych wejściowych.
• Algorytm o takiej złożoności obliczeniowej uważa się za wewnętrznie nierealizowalny i należy go unikać.
Porównanie
1 - komputer wykonujący milion operacji dominujących w ciągu sekundy
2 - superkomputer wykonujący 1000 miliardów operacji dominujących w ciągu sekundy.
5x1031 mld lat 40,17 mld lat 1126 sekund 10-6 sekund Czas wykonania dla
superkomputera 2 5x1037 mld lat 40169423 mld lat 35,7 lat 1,05 sekund Czas wykonania dla komputera 1 200 100 50 20 Rozmiar danych n
Algorytm o złożoności O(2
n
)
Porównanie szybkości wzrostu
funkcji
f(n)= log n f(n)=n f(n) = 2n
Porównanie złożoności
nierealizowalny dla większych n wykładnicza
O(2n), O(n!)
wolny dla większych n sześcienna
O(n3)
wolny dla dużych n kwadratowa O(n2) dosyć szybki liniowo-logarytmiczna O(n log n) szybki liniowa O(n) niesamowicie szybki logarytmiczna O(log n)
działa prawie natychmiast stała
O(1)
Cechy algorytmu Nazwa rodzaju złożoności
obliczeniowej Rodzaj
złożoności czasowej
Znajdowanie złożoności asymptotycznej
Przykład 1: Prosta pętlafor (i=sum=0; i<n; i++) sum+=a[i]; Powyższa pętla powtarza się n razy, podczas każdego jej przebiegu realizuje dwa przypisania:
• aktualizujące zmienną „sum” • zmianę wartości zmiennej „i”
Mamy zatem 2n przypisań podczas całego wykonania pętli.
Asymptotyczna złożoność wynosi O(n)
Przykład 2: Pętla zagnieżdżona
for (i=0; i<n; i++) {
for (j=1, sum=a[0]; j<=i; j++) sum+=a[j]; }
• zmiennej „i” nadawana jest wartość początkowa.
• Pętla zewnętrzna powtarza się n razy, a w każdej jej iteracji wykonuje się wewnętrzna pętla oraz instrukcja przypisania wartości zmiennym „i”, „ j”, „ sum”.
• Pętla wewnętrzna wykonuje się „i” razy dla każdego i∈{1,...,n-1}, a na każdą iteracje przypadają dwa przypisania: jedno dla „sum”, jedno dla„j”.
Przypisania wykonywane w całym programie:
1+3n+2(1+2+...+n-1) = 1+3n+n(n-1) = O(n)+O(n2) = O(n2)
Asymptotyczna złożoność wynosi O(n2).
• Wyznaczenie złożoności asymptotycznej jest trudniejsze jeżeli liczba iteracji nie jest zawsze jednakowa.
Przykład 3
Znajdź najdłuższą podtablice zawierającą liczby uporządkowane rosnąco.
for (i=0; len=1; i<n-1; i++) {
for (i1=i2=k=i; k<n-1 && a[k]<a[k+1]; k++,i2++); if(len < i2-i1+1) len=i2-i1+1;
}
Jeśli liczby w tablicy są uporządkowane malejąco, to pętla zewnętrzna wykonuje się n-1 razy, a w każdym jej przebiegu pętla wewnętrzna wykona się tylko raz.
Złożoność algorytmu jest więc O(n).
Znajdowanie złożoności asymptotycznej
Jeśli liczby w tablicy są uporządkowane rosnąco, to pętla zewnętrzna wykonuje się n-1 razy, a w każdym jej przebiegu pętla wewnętrzna wykona się i razy dla i∈{1,...,n-1}.
Złożoność algorytmu jest więc O(n2).
Przykład 3
Znajdź najdłuższą podtablice zawierającą liczby uporządkowane rosnąco.
for (i=0; len=1; i<n-1; i++) {
for (i1=i2=k=i; k<n-1 && a[k]<a[k+1]; k++,i2++); if(len < i2-i1+1) len=i2-i1+1;
}
Wyszukiwanie elementu maksymalnego
Lista kroków:
krok 1: wmax← d[1]; pmax← 1 krok 2: Dla i = 2,3,...,n:
jeśli wmax< d[i], to wmax← d[i];
pmax← i
krok 3: Zakończ algorytm
Jeśli za operacje dominujące przyjmiemy ilość wykonanych porównań elementów zbioru, to złożoność obliczeniowa algorytmu wyszukiwania elementu maksymalnego jest równa T(n) = n – 1.
Algorytm posiada liniową klasę czasowej złożoności obliczeniowej O(n).
Lista kroków:
krok 1: wn← d[1]; pn← 1; ln← 1
krok 2: Dla i = 1,2,...,n: wykonuj kroki 3...5 krok 3: licznik ← 0
krok 4: Dla j = 1,2,...,n: jeśli d[i] = d[j], to
licznik ← licznik + 1
krok 5: Jeśli licznik > ln, to
wn← d[i];
pn← i;
ln← licznik krok 6: Zakończ algorytm
Wyszukiwanie najczęstszego elementu
• Wybór n elementów wymaga n operacji.
• Po każdym wyborze musimy wykonać n porównań wybranego elementu z elementami zbioru w celu zliczenia ilości jego
wystąpień.
• Daje to wynik: T(n) = n2
Modyfikacja programu przez przeniesienie instrukcji ze środka na zewnątrz pętli.
max = maksimum(t); //zwraca największą wartość w tablicy t
for(i=0; i<100; i++)
t[i] = t[i]* 100/max;
Ulepszenia rzędu wielkości
po ulepszeniu:
max = maksimum(t); //zwraca największą wartość w tablicy t
wspol = 100/max; for(i=0; i<100; i++)
t[i] = t[i] * wspol;
Wyszukanie w liście liniowej L elementu o wartości x. Prosty algorytm zawiera warunek postaci:
1. „czy znaleziono w L wartość x” i
2. „czy został osiągnięty koniec listy L?”
Ulepszenia rzędu wielkości
po ulepszeniu:
Na koniec listy dodawany jest element o wartości x.
Wyszukiwanie binarne dla problemu wyszukiwania elementu w posortowanej liście L o długości N.
Ulepszenia rzędu wielkości
Ulepszenia rzędu wielkości
• Policzenie porównań
• Każde porównanie skraca długość listy wejściowej o połowę
• Proces ten kończy się, gdy lista jest pusta lub znaleziony zostanie poszukiwany element
• Pesymistyczna liczba porównań ilość możliwych operacji dzielenia wielkości N przez 2, zanim osiągnie 0, czyli log2N.
Czy można skonstruować jeszcze lepszy algorytm?
• Każdy poprawny algorytm znajdujący rozwiązanie danego problemu ustanawia dla niego górne ograniczenie złożoności. • Możliwości dalszej poprawy rzędu złożoności algorytmu będącego rozwiązaniem problemu wyznacza dolne ograniczenie!
Taki pogląd funkcjonuje w środowisku programistów - nie określono przecież granicy rozwoju mocy obliczeniowych komputerów.
Mitem jest stwierdzenie, że komputery są tak szybkie, że czas nie stanowi problemu (rozkład na czynniki pierwsze dużych liczb - 300 cyfr wymaga milionów lat)
Należy zdecydowanie przeciwstawiać się przekonaniu o tym, że ulepszenia sprzętowe uczynią pracę nad efektywnymi algorytmami zbyteczną.
Nie przejmuj się efektywnością algorytmu…
• Istnieją problemy których rozwiązanie za pomocą
zasobów komputerowych jest teoretycznie
możliwe, ale praktycznie przekracza możliwości
istniejących technologii.
• Przykładem takiego problemu jest rozumienie
języka naturalnego, przetwarzanie obrazów (do
pewnego stopnia oczywiście) czy “inteligentna”
komunikacja pomiędzy komputerami a ludźmi na
rozmaitych poziomach.
Nie przejmuj się efektywnością algorytmu…
wystarczy poczekać kilka lat
• Kiedy pewne problemy stają się “proste”… Nowa
grupa wyzwań, które na razie można sobie tylko
próbować wyobrażać, wytyczy nowe granice
możliwości wykorzystania komputerów.
Nie przejmuj się efektywnością algorytmu…
Cormen T.H., Leiserson Ch.E., Rivest R.L.,
Stein C.: „Wprowadzenie do algorytmow”.
WNT, Warszawa, 2005
Wroblewski P.: „Algorytmy, struktury danych i
techniki programowania, Wydanie III”, Helion,
Gliwice, 2003
N. Wirth: „Algorytmy + struktury danych =
programy”. WNT, Warszawa, 2004.