Podyplomowe Studium Programowania i Systemów Baz Danych Algorytmy, struktury danych i techniki programowania 15 godz. wykładu / 15 godz. laboratorium dr inż. Paweł Syty, 413GB, sylas@mif.pg.gda.pl, http://sylas.info Literatura T.H. Cormen i inni, Wprowadzenie do algorytmów, PWN 2013 J. Bentley, Perełki oprogramowania, wyd. II, Helion 2012 D. Harel, Rzecz o istocie informatyki. Algorytmika, WNT 2008 P. Wróblewski, Algorytmy, struktury danych i techniki programowania, Helion 2015 N. Wirth, Algorytmy + struktury danych = programy, WNT 2001 Materiały dydaktyczne http://students.sylas.info
Algorytm definicja, cechy, poprawność Obliczenie znalezienie rozwiązania danego zagadnienia w oparciu o dostępne dane i z użyciem algorytmu. Algorytm poddający się interpretacji skończony zbiór instrukcji wykonania zadania mającego określony stan końcowy dla każdego zestawu danych wejściowych. Innymi słowy algorytm jest ciągiem kroków obliczeniowych, prowadzących do przekształcenia danych wejściowych w wyjściowe. Własności algorytmu może korzystać z danych wejściowych prowadzi do jednej lub większej liczby danych wyjściowych wskazana własność ogólności rozwiązanie zawsze osiągnięte i to w skończonej liczbie kroków każdy możliwy przypadek przewidziany każdy krok jednoznacznie i precyzyjnie zdefiniowany korzysta z operacji podstawowych (plus iteracje i struktury warunkowe)
Deterministyczna maszyna Turinga budowa i działanie 1. moduł sterujący, mogący znajdować się w jednym ze skończonej liczby stanów w danej chwili, 2. głowica czytająco-pisząca, 3. taśma, będąca układem pamięciowym podzielonym na jednostki i prawostronnie nieskończonym, może być traktowana jako model każdego obliczenia sekwencyjnego. Każde obliczenie można przedstawić poprzez siedem elementarnych operacji, tworzących tzw. język Turinga Posta mogący realizować dowolne możliwe obliczenia.
Język Turinga Posta (pierwszy język programowania): DRUKUJ-0 (oraz DRUKUJ-1) IDŹ-W-PRAWO IDŹ-W-LEWO IDŹ-DO-KROKU-i-JEŚLI-1 IDŹ-DO-KROKU-i-JEŚLI-0 STOP Instrukcjom przyporządkowane są kody, np. DRUKUJ-0 ma kod 000, DRUKUJ-1 ma kod 001, IDŹ-W-LEWO ma kod 010, STOP ma kod 100 itp.
Przykład programu: 1. DRUKUJ-0 2. IDŹ-W-LEWO 3. IDŹ-DO-KROKU-2-JEŚLI-1 4. DRUKUJ-1 5. IDŹ-W-PRAWO 6. IDŹ-DO-KROKU-2-JEŚLI-1 7. DRUKUJ-1 8. IDŹ-W-PRAWO 9. IDŹ-DO-KROKU-1-JEŚLI-1 10. STOP 5
Poprawność algorytmów Algorytm nazywamy poprawnym, jeżeli dla dowolnych poprawnych danych wejściowych, osiąga on punkt końcowy i otrzymujemy poprawne wyniki. Cechy algorytmu poprawnego Częściowa poprawność. Algorytm nazywamy częściowo poprawnym, gdy prawdziwa jest następująca implikacja: jeżeli algorytm osiągnie koniec dla dowolnych poprawnych danych wejściowych, to dane wyjściowe będą spełniać warunek końcowy. Własność określoności obliczeń. Algorytm posiada tę własność, jeżeli dla dowolnych poprawnych danych wejściowych, działanie algorytmu nie zostanie przerwane. Własność stopu. Algorytm posiada tę własność, jeżeli dla dowolnych poprawnych danych wejściowych, algorytm nie będzie działał w nieskończoność. 6
Dowodzenie poprawności algorytmów metoda niezmienników Floyda wyróżnić newralgiczne punkty w algorytmie określić warunki (niezmienniki), jakie muszą być spełnione w każdym wyróżnionym punkcie udowodnić poprawność kolejnych warunków, zakładając poprawność warunków poprzedzających własność stopu udowodnić np. metodą liczników iteracji lub metodą malejących wielkości 7
Przykłady sformułowania problemów algorytmicznych Sortowanie Wejście: tablica T zawierająca n elementów (a 1, a 2,..., a n ) typu porządkowego. Wyjście: tablica o tych samych elementach, ale uporządkowana niemalejąco (czyli uporządkowana permutacja ciągu wejściowego). Wyszukiwanie Wejście: posortowana, n-elementowa tablica liczbowa T oraz liczba p. Wyjście: liczba naturalna, określająca pozycję elementu p w tablicy T, bądź -1, jeżeli element w tablicy nie występuje. Generowanie podciągu Wejście: dwie liczby całkowite m i n, gdzie m <= n. Wyjście: posortowana lista m losowych liczb całkowitych z przedziału 1...n, wśród których żadna nie powtarza się dwukrotnie. 8
Problem komiwojażera Wejście: n punktów (miast), odległości pomiędzy miastami (d ij i, j = 1, 2,..., n); Wyjście: trasa komiwojażera przez wszystkie miasta (przy czym dopuszczalna jest tylko jedna wizyta w każdym mieście permutacja miast) o najmniejszej sumie odległości. Bardziej formalnie: znaleźć cykl w grafie o minimalnym całkowitym koszcie. Wieże Hanoi Zadanie polega na przeniesieniu wieży z krążków na inny pręt, z zachowaniem następujących reguł: jednorazowo można przenosić tylko jeden krążek dopuszczalne jest umieszczanie tylko mniejszego krążka na większym 9
Typy danych Podstawowe rodzaje obiektów, na których operują algorytmy: liczby (całkowite, zmiennoprzecinkowe, zespolone, dwójkowe itp.) znak (pojedynczy znak alfanumeyczny) tekst (ciąg znaków) wartość logiczna (prawda/fałsz) wskaźnik Zmienna Obszar pamięci, przechowujący dane pod określoną nazwą. O rodzaju i sposobie przechowywania decyduje typ zmiennej. Przykłady w C/C++: int a; // zadeklarowanie zmiennej całkowitej o nazwie a a = 10; // i przypisanie jej wartości 10 float pi = 3.1415926; // deklaracja i przypisanie w jednym 10
Struktury danych Struktura danych (ang. data structure) sposób uporządkowania informacji w komputerze. Na strukturach danych operują algorytmy. Tablica jednowymiarowa (wektor) Poszczególne komórki dostępne są za pomocą kluczy, które najczęściej przyjmują wartości numeryczne. W komórkach można przechowywać zmienne różnego typu. T1 = {1, 4, 5, 12, 24, 10, 0, -4, 12, 15} (T1[2] ma wartość 4, T1[6] ma wartość 10, itp.) wypisz (T1[3] + T1[4]) / 2 T2 = {"poniedziałek", "wtorek",..., "niedziela"} (T2[1] = "poniedziałek" itd.) wypisz "Dzisiaj jest ", T2[6] 11
Tablica wielowymiarowa Przykład tablica 3x3: T = T T T 11 21 31 T T T 12 22 32 T T T 13 23 33 1 = 4 7 2 5 8 3 6 9 T = {{1,2,3},{4,5,6},{7,8,9}} T 12 = T[1][2] = 2 itp. pętla od i = 1 do 3 pętla od j = 1 do 3 T[i][j] = 10 // przypisanie liczby 10 wszystkim elementom Inne struktury danych: rekord lista stos kolejka drzewo i jego liczne odmiany (np. drzewo binarne) graf 12
Programowanie to modyfikowanie, rozszerzanie, naprawianie, ale przede wszystkim tworzenie oprogramowania. Język programowania to usystematyzowany sposób przekazywania komputerowi poleceń do wykonania. Język programowania pozwala programiście na precyzyjne przekazanie maszynie, jakie dane mają ulec obróbce i jakie czynności należy podjąć w określonych warunkach. Języki programowania wiążą się zwykle ze sztywną składnią, dopuszczającą używanie jedynie specjalnych kombinacji wybranych symboli i słów kluczowych. Języki programowania mogą być kompilowane lub interpretowane. Formalna składnia typowego języka programowania zawiera zwykle różne warianty struktur sterujących, wzorce podstawowych instrukcji, sposoby definiowania struktur danych. 13
Struktury sterujące bezpośrednie następstwo wykonaj A, potem B, następnie C pseudokod C C++ Fortran77 Python wczytaj a a = a + 1 wypisz a int a; scanf( %d,&a); a++; printf( %d,a); int a; cin >> a; a++; cout << a; real a read(*,*) a a = a + 1 write(*,*) a a=input() a=a+1 print a (wczytaj liczbę z klawiatury, zwiększ jej wartość o 1, wypisz wynik na ekranie) 14
wybór warunkowy (rozgałęzienie warunkowe) jeżeli Q to wykonaj A, w przeciwnym razie wykonaj B Q warunek logiczny, np. a>0 pseudokod C/C++ Fortran77 Python jeżeli a > 0 a = a + 1 c = a * 3 w przeciwnym wypadku a = a 1 c = a / 3 if (a>0){ a++; c=a*3; } else { a--; c = a/3; } if (a.gt. 0) then a=a+1 c=a*3 else a=a-1 c=a/3 endif if a > 0: a=a+1 c=a*3 else: a=a-1 c=a/3 15
iteracja (pętla) ograniczona wykonaj A dokładnie n razy pseudokod C++ Fortran77 Python pętla od i = 1 do n wypisz i wypisz i*i for (i=1; i<=n;i++) {cout << i; cout << i*i;} do 55 i=1, n write(*,*) i write(*,*) i*i 55 continue for i in range(1:n+1): print i print i*i pętle zagnieżdżone pseudokod pętla od i = 1 do n pętla od j = 1 do i wypisz i + j C++ for (i=1; i<=n; i++){ for (j=1; j<=i; j++){ cout << i + j; } } 16
iteracja (pętla) ograniczona dopóki Q, wykonuj A pseudokod C++ Fortran77 Python i = 1 dopóki i n wypisz i wypisz i*i i = i + 1 wykonuj A, dopóki Q i = 1 wykonuj wypisz i wypisz i*i i = i + 1 dopóki i n+1 i=1; do while (i<=n) {cout << i; cout << i*i; i++;} i=1 15 if (i.ls. n) then write(*,*) i write(*,*) i*i i=i+1 goto 15 endif i=1 while i<=n: print i print i*i i=i+1 17
instrukcja skoku skocz do oznaczonego miejsca w programie i = 1 #G wypisz i wypisz i*i i = i + 1 jeżeli i n skocz do G Uwaga! Instrukcje skoku dopuszczalne są jedynie w wyjątkowych sytuacjach utrudniają bowiem śledzenie przebiegu programu 18
podprogramy fragment algorytmu zapisany w formie osobnej procedury lub funkcji, np. w celu umożliwienia jego wywoływania dla różnych wartości parametrów. silnia(n): jeżeli n == 0 silnia = 1 w przeciwnym wypadku silnia = n * silnia(n-1) dodaj_i_wypisz(a, b): wypisz a + b wywołanie podprogramu wynik = silnia(10) + 1 pętla od n = 1 do 100 wypisz silnia(n) pętla od i = 1 do 100 pętla od j = 1 do 100 dodaj_i_wypisz(i, j) Silnia - przykład w C++ #include <iostream> using namespace std; int silnia(int n){ if (n == 0) return 1; else return n * silnia(n-1); } int main(){ int n; cin >> n; cout << silnia(n); } 19
Przykład algorytmu sortującego sortowanie przez wstawianie efektywny algorytm sortowania niewielkiej liczby elementów działa na zasadzie porządkowania talii kart Schemat działania algorytmu: 1. utwórz zbiór elementów posortowanych i przenieś do niego dowolny element ze zbioru nieposortowanego 2. weź dowolny element ze zbioru nieposortowanego 20
3. wyciągnięty element porównuj z kolejnymi elementami zbioru posortowanego póki nie napotkasz elementu równego lub mniejszego, lub nie znajdziesz się na początku zbioru uporządkowanego 4. wyciągnięty element wstaw w miejsce gdzie skończyłeś porównywać 5. jeśli zbiór elementów nieuporządkowanych jest niepusty, wróć do punktu 2 Pseudokod // wejście: n-elementowa tablica T[1... n] pętla od i = 2 do n // niezmiennik: fragment T[1... i-1] jest posortowany // cel: przesunąć element T[i] w dół na właściwe miejsce pętla od j = i do 2 z krokiem -1 jeżeli T[j] < T[j-1] zamień T[j] z T[j-1] // realizacja zamiany: // tmp = T[j]; T[j] = T[j-1]; T[j-1] = tmp Algorytm jest stabilny (nie zmienia kolejności takich samych liczb w tablicy wynikowej). 21
Analiza algorytmów Czas działania algorytmu (złożoność czasowa) liczba elementarnych operacji (np. podstawienie, porównanie, prosta operacja arytmetyczna), potrzebnych do wykonania algorytmu najczęściej jest funkcją rozmiaru danych wejściowych Rozmiar danych wejściowych przykłady dla sortowania: liczba elementów do posortowania (n) mnożenie liczb całkowitych: całkowita liczba bitów operacje na grafach: liczba wierzchołków 22
Dla sortowania przez wstawianie: 1 pętla od i = 2 do n 2 pętla od j = i do 2 3 jeżeli T[j] < T[j-1] 4 tmp = T[j] 5 T[j] = T[j-1] zamień T[j] z T[j-1] 6 T[j-1] = tmp 7 w przeciwnym wypadku zakończ pętlę wewnętrzną Oznaczmy przez p liczbę porównań w wierszu 3. Maksymalnie będzie ich p=(n-1)(n-1) (bo mamy n-1 wykonań pętli zewnętrznej pomnożone przez, w uproszczeniu, maksymalnie n-1 wykonań pętli wewnętrznej; ten przypadek wystąpi dla pechowego przypadku tablicy wejściowej posortowanej malejąco). Minimalna liczba porównań to p=n-1 (bo mamy n-1 wykonań pętli zewnętrznej pomnożone przez jedno, negatywne porównanie; ten przypadek wystąpi dla tablicy wejściowej już posortowanej rosnąco wewnętrzna pętla w takim przypadku nie musi nic robić, poza jednym porównaniem). 23
Liczba wykonań instrukcji w wierszach 4 6 jest zależna od p. W skrajnym przypadku może to być aż (n-1)(n-1) instrukcji na każdy wiersz, z drugiej strony instrukcje te się mogą się w ogóle nie wykonać, w przypadku wszystkich negatywnych porównań (tablica wstępnie posortowana rosnąco). Załóżmy dla uproszczenia, że pojedyncze instrukcje porównania i przypisania wykonują się w tym samym czasie t. Procedura sortująca wykona się więc maksymalnie w czasie 4t(n-1)(n-1) (przypadek pesymistyczny dane posortowane odwrotnie; funkcja kwadratowa względem n), natomiast minimalny czas wykonania to t(n-1) (przypadek optymistyczny dane posortowane; funkcja liniowa względem n). Przypadek średni (oczekiwany) będzie w tym przypadku zbliżony do przypadku pesymistycznego (też będzie to funkcja kwadratowa). 24
Wymagana pamięć (złożoność pamięciowa) liczba podstawowych komórek pamięci, wykorzystywanych przez algorytm najczęściej jest funkcją rozmiaru danych wejściowych Dla sortowania przez wstawianie: wymagana pamięć wynosi n (elementy tablicy) + 1 (zmienna pomocnicza tmp) zależność liniowa względem n. 25
Oszacowania asymptotyczne Notacja Θ (Theta) Przykład: ½ n 2-3n = Θ(n 2 ). Uzasadnienie: Szukamy stałych c 1 i c 2 oraz n 0 takich, że c 1 n 2 <= ½ n 2-3n <= c 2 n 2 dla każdego n > n 0. Dzieląc przez n 2 otrzymujemy: c 1 <= ½ - 3/n <= c 2. Nierówność ta jest spełniona dla wszystkich n>6, np. gdy c 1 =1/14 i c 2 = ½. Zatem : ½ n 2-3n = Θ(n 2 ). 26
Przykład: 6n 3 Θ(n 2 ). Uzasadnienie: Załóżmy, że istnieją stałe c 2 oraz n 0 takie, że 6n 3 <= c 2 n 2 dla każdego n > n 0. Ale wtedy 6n <= c 2 /6 co nie może być prawdą dla dowolnie dużych n, ponieważ c 2 jest stałą. Notacja Θ asymptotycznie ogranicza funkcję od góry i od dołu. Oszacowania Θ używamy dla określenia pesymistycznej złożoności obliczeniowej algorytmów. Na przykład pesymistyczny czas wykonania sortowania przez wstawianie (czyli pesymistyczna złożoność obliczeniowa tego algorytmu) jest rzędu Θ(n 2 ). 27
Intuicyjnie, składniki niższego rzędu mogą być pominięte, gdyż są mało istotne dla dużych n. Składniki wyższego rzędu są wtedy dominujące. Przykład: dowolna funkcja kwadratowa jest rzędu Θ(n 2 ), tzn. an 2 + bn + c = Θ(n 2 ). d i Ogólnie, dowolny wielomian p(n) = a in = Θ(n d ), o ile a i są stałymi oraz a d > 0. Funkcję stałą określamy jako Θ(n 0 ) lub Θ(1). i = 0 28
Notacja O (dużego O) Przykład: ½ n 2-3n = O(n 2 ), ale również np. 5n +6 = O(n 2 ). Notacja O określa asymptotyczną granicę górną. Korzystamy z niej, żeby oszacować funkcję z góry, z dokładnością do stałego współczynnika. Można powiedzieć, że czas działania algorytmu sortowania przez wstawianie jest rzędu O(n 2 ) czyli algorytm ten nie zostanie nigdy wykonany wolniej niż w czasie kwadratowym (ale może być wykonany szybciej np. w czasie liniowym). 29
Notacja Ω (Omega) Notacja Ω określa asymptotyczną granicę dolną. Można powiedzieć, że czas działania algorytmu sortowania przez wstawianie jest rzędu Ω(n) czyli algorytm ten nie zostanie nigdy wykonany szybciej niż w czasie liniowym. 30
Notacja o (małego o) Przykład: 2n = o(n 2 ), ale n 2 o(n 2 ). Notacja ω (małego omega) Przykład: n 2 /2= ω(n), ale n 2 /2 ω(n 2 ). 31
Własności oszacowań Twierdzenie. Dla każdych dwóch funkcji f(n) i g(n) zachodzi zależność f(n) = Θ(g(n)) wtedy i tylko wtedy, gdy f(n) = O(g(n)) i f(n) = Ω(g(n)). Przykład: Z tego, że ½ n 2-3n = Θ(n 2 ) wynika, że ½ n 2-3n = O(n 2 ) oraz ½ n 2-3n = Ω (n 2 ). Przechodniość: f(n) = Θ(g(n)) i g(n) = Θ(h(n)) implikuje f(n) = Θ(h(n)) f(n) = O(g(n)) i g(n) = O(h(n)) implikuje f(n) = O(h(n)) f(n) = Ω(g(n)) i g(n) = Ω(h(n)) implikuje f(n) = Ω(h(n)) f(n) = o(g(n)) i g(n) = o(h(n)) implikuje f(n) = o(h(n)) f(n) = ω(g(n)) i g(n) = ω(h(n)) implikuje f(n) = ω(h(n)) Zwrotność: f(n) = Θ(f(n)) f(n) = O(f(n)) f(n) = Ω(f(n)) 32
Symetria: f(n) = Θ(g(n)) wtedy i tylko wtedy, gdy g(n) = Θ(f(n)) Symetria transpozycyjna: f(n) = O(g(n)) wtedy i tylko wtedy, gdy g(n) = Ω(f(n)) f(n) = Ω(g(n)) wtedy i tylko wtedy, gdy g(n) = O(f(n)) Notacja asymptotyczna w równaniach Gdy notacja asymptotyczna pojawia się po prawej stronie równania, tak jak do tej pory (np. n = O(n 2 ) ), oznacza to przynależność: n O(n 2 ). Z kolei, np. równanie: 2n 2 + 3n +1 = 2 n 2 + Θ(n) oznacza, że Θ(n) jest pewną anonimową funkcją (o pomijalnej nazwie), tzn, 2n 2 + 3n +1 = 2 n 2 + f(n), gdzie f(n) jest funkcją należącą do zbioru Θ(n). W tym przypadku f(n) = 3n+1 = Θ(n). Użycie notacji asymptotycznej pozwala więc na uproszczenie równań poprzez wyeliminowanie nieistotnych jego składników. 33
Standardowe oszacowania f(n) = O(1) funkcja f(n) jest ograniczona przez funkcję stałą f(n) = O(log k n) funkcja f(n) jest ograniczona przez funkcję logarytmiczną f(n) = O(n) funkcja f(n) jest ograniczona przez funkcję liniową f(n) = O(n log k n) f(n) = O(n k ) funkcja f(n) jest ograniczona przez funkcję potęgową lub wielomian f(n) = O(a n ) funkcja f(n) jest ograniczona przez funkcję wykładniczą f(n) = O(n!) funkcja f(n) jest ograniczona przez silnię UWAGA! W większości przykładów w dalszej części wykładów funkcja log bez podanej podstawy oznacza logarytm o podstawie 2 34