Podyplomowe Studium Programowania i Systemów Baz Danych · liczby (całkowite, zmiennoprzecinkowe,...

34
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, [email protected], 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

Transcript of Podyplomowe Studium Programowania i Systemów Baz Danych · liczby (całkowite, zmiennoprzecinkowe,...

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, [email protected], 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.

5

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

6

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ść.

7

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

8

Przykłady sformułowania problemów algorytmicznych

Sortowanie

Wejście: tablica T zawierająca n elementów (a1, a2, . . . , an) 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.

9

Problem komiwojażera

Wejście: n punktów (miast), odległości pomiędzy miastami.

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

10

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”

11

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]

12

Tablica wielowymiarowa

Przykład – tablica 3x3:

T =

987

654

321

TTT

TTT

TTT

333231

232221

131211

T = {{1,2,3},{4,5,6},{7,8,9}}

T12 = 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

13

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.

14

Struktury sterujące

bezpośrednie następstwo

wykonaj instrukcję/polecenie A, potem B, następnie C

pseudokod C C++ Fortran77 Python zadeklaruj a

wczytaj a

a = a + 1

wypisz a

int a;

scanf(”%d”,&a);

a++;

printf(”%d”,a);

int a;

cin >> a;

a++;

cout << a;

integer a

read(*,*) a

a = a + 1

write(*,*) a

a=input()

a=a+1

print a

(zadeklaruj zmienną typu całkowitego, wczytaj liczbę całkowitą z klawiatury,

zwiększ jej wartość o 1, wypisz wynik na ekranie)

15

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

16

iteracja (pętla) o określonej liczbie przebiegów

wykonaj instrukcje 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 C++ pętla od i = 1 do n

pętla od j = 1 do i

wypisz i + j

for (i=1; i<=n; i++){

for (j=1; j<=i; j++){

cout << i + j;

}

}

17

iteracja (pętla) o liczbie przebiegów określonej warunkiem

dopóki Q, wykonuj A

pseudokod C++ Fortran77 Python i = 1

dopóki i ≤ n

wypisz i

wypisz i*i

i = i + 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

wykonuj A, dopóki Q

i = 1

wykonuj

wypisz i

wypisz i*i

i = i + 1

dopóki i ≤ n+1

18

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

19

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);

}

20

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

21

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).

22

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

23

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

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).

24

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).

25

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.

26

Oszacowania asymptotyczne

Notacja Θ (Theta)

Przykład: ½ n

2 - 3n = Θ(n

2).

Uzasadnienie:

Szukamy stałych c1 i c2 oraz n0 takich, że c1 n2 <= ½ n

2 - 3n <= c2 n

2 dla

każdego n > n0.

Dzieląc przez n2 otrzymujemy: c1 <= ½ - 3/n <= c2.

Nierówność ta jest spełniona dla wszystkich n>6, np. gdy c1=1/14 i c2= ½.

Zatem : ½ n2 - 3n = Θ(n

2).

27

Przykład: 6n3 ≠ Θ(n

2).

Uzasadnienie:

Załóżmy, że istnieją stałe c2 oraz n0 takie, że 6n3 <= c2 n

2 dla każdego n > n0.

Ale wtedy 6n <= c2/6 co nie może być prawdą dla dowolnie dużych n, ponieważ

c2 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 Θ(n2).

28

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 Θ(n2),

tzn. an2 + bn + c = Θ(n

2).

Ogólnie, dowolny wielomian p(n) =

d

i

i

in0

a = Θ(nd), o ile ai są stałymi oraz ad > 0.

Funkcję stałą określamy jako Θ(n0) lub Θ(1).

29

Notacja O (dużego O)

Przykład: ½ n2 - 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(n2) – czyli

algorytm ten nie zostanie nigdy wykonany wolniej niż

w czasie kwadratowym (ale może być wykonany

szybciej – np. w czasie liniowym).

30

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.

31

Notacja o (małego o)

Przykład: 2n = o(n2), ale n

2 ≠ o(n

2).

Notacja ω (małego omega)

Przykład: n2/2= ω(n), ale n

2 /2 ≠ ω(n

2).

32

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 ½ n2 - 3n = Θ(n

2) wynika, że ½ n

2 - 3n = O(n

2)

oraz ½ n2 - 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))

33

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(n2) ), oznacza to przynależność: n O(n

2).

Z kolei, np. równanie: 2n2 + 3n +1 = 2 n

2 + Θ(n) oznacza, że Θ(n) jest pewną

anonimową funkcją (o pomijalnej nazwie), tzn. 2n2 + 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.

34

Standardowe oszacowania

f(n) = O(1) – funkcja f(n) jest ograniczona przez funkcję stałą

f(n) = O(logkn) – funkcja f(n) jest ograniczona przez funkcję logarytmiczną

f(n) = O(n) – funkcja f(n) jest ograniczona przez funkcję liniową

f(n) = O(n logkn)

f(n) = O(nk) – funkcja f(n) jest ograniczona przez funkcję potęgową lub

wielomian

f(n) = O(an) – 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