Page 1
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
Page 2
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)
Page 3
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.
Page 4
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.
Page 5
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
Page 6
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ść.
Page 7
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
Page 8
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.
Page 9
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
Page 10
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”
Page 11
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]
Page 12
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
Page 13
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.
Page 14
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)
Page 15
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
Page 16
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;
}
}
Page 17
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
Page 18
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
Page 19
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);
}
Page 20
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
Page 21
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).
Page 22
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
Page 23
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).
Page 24
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).
Page 25
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.
Page 26
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).
Page 27
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).
Page 28
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).
Page 29
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).
Page 30
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.
Page 31
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).
Page 32
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))
Page 33
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.
Page 34
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