Cel i zakres kursu
Celem kursu jest poznanie metod i technik formułowania problemów i ich rozwiązań w postaci algorytmicznej. Słuchacze zapoznają się z podstawowymi pojęciami z zakresu algorytmiki takimi jak: podstawowe struktury danych,
opisowe i graficzne przedstawianie algorytmów oraz z przykładami algorytmów rozwiązywania typowych zadań numerycznych i logicznych.
Efekty kształcenia - wiedza Student po zakończeniu kursu:
- jest świadomy ograniczeń technik algorytmicznych stosowanych w zadaniach technicznych i technologicznych,
- potrafi wyjaśnić zasadę działania algorytmów wstawiania, usuwania i wyszukiwania elementów w szerokiej klasie dynamicznych struktur danych, takich jak listy, drzewa binarne, B-drzewa czy kolejki priorytetowe,
- zna podstawowe algorytmy rozwiązujące wybrane zadania algorytmiczne (np.
sortowanie, wyszukiwania wzorca w tekście, podstawowe algorytmy grafowe i
teorioliczbowe itp.)
Efekty kształcenia - umiejętności Student po zakończeniu kursu:
- potrafi interpretować schematy blokowe algorytmów oraz opracować je dla prostych zadań algorytmicznych,
- potrafi zaproponować właściwie dobraną technikę algorytmiczną do konkretnego zadania algorytmicznego (np. sortowania, wyszukiwania wzorca w tekście, itp.),
- potrafi zaproponować właściwie dobraną strukturę danych w procesie implementacji zadanego algorytmu.
Literatura podstawowa
Wróblewski P.: Algorytmy, struktury danych i języki programowania, Helion, Gliwice, 2015 Aho A. V., Hopcroft J. E., Ullman J.D.: Algorytmy i struktury danych, Helion, Gliwice, 2003.
Sedgewick R., Wayne K., Algorytmy. Wyd. IV, Helion, Gliwice 2017
Literatura uzupełniająca
Sysło M.M., Algorytmy. Wydawnictwa Szkolne i Pedagogiczne, Warszawa 1997
Wykład 1
1. Algorytm i jego własności, pojęcie problemu algorytmicznego i algorytmu, własności algorytmów, sposoby zapisu algorytmów
2. Podstawowe struktury danych
3. Złożoność algorytmów
Wg M.M.Sysły "Algorytm jest przepisem opisującym krok po kroku rozwiązanie problemu lub osiągnięcie jakiegoś celu".
Inna, klasyczna definicja algorytmu powiada, że jest to sposób rozwiązania pewnej klasy zadań.
Słowo algorytm - od fragmentu nazwiska Muhammad ibn Musa al-Chorezmi, matematyka i astronoma arabskiego (VIII/IX wiek). Był on prekursorem metod obliczeniowych w matematyce, a także twórcą pojęcia algebra.
Za prekursorkę algorytmów "komputerowych" uważa się Adę Augustę (stąd też nazwa jednego ze znanych języków programowania ADA) hrabinę Lovelace
(1815-1852), córkę Byrona.
Algorytmy przedstawia się w postaci:
- Opisu słownego
(1.1)
x x x
f
Przykład zadania: Obliczyć wartość funkcji
dla dowolnego, rzeczywistego x
Specyfikacja tego zadania ma postać:
Dane: Dowolna liczba rzeczywista x.
Wynik: Wartość funkcji podanej powyższym wzorem , jeżeli x jest różne od 0 oraz 0 – w przeciwnym przypadku.
0 x dla 1
0 x dla 0
0 x dla 1
x f
Lista kroków ma postać:
Dane: Dowolna liczba rzeczywista x.
Wynik: Wartość funkcji f(x) określona wzorem (1.2) Krok 1:Wczytaj wartość danej x.
Krok 2: Jeśli x > 0, to f(x) = 1. Zakończ Krok 3: Jeśli x = 0, to f(x) = 0. Zakończ Krok 4: {Przypadek x < 0} f(x) = -1. Zakończ
(1.2)
Metody konstrukcji algorytmów
Budowa algorytmu - proces twórczy nie podlega z góry ustalonym regułom.
Istnieją pewne ogólne metody, które można w określonych sytuacjach zastosować.
Najczęściej spotykane to metody:
Dziel i zwyciężaj– Problem rozmiaru n (o n danych) zostaje podzielony na kilka mniejszych podproblemów, w taki sposób, że z ich rozwiązań wynika rozwiązanie zasadniczego problemu. Naturalną konstrukcją algorytmiczną i programistyczną jest w tym wypadku rekurencja (rekursja).
Przykład - zadanie o tzw. wieżach z Hanoi.
Programowanie dynamiczne– "ulepszenie" metody dziel i zwyciężaj , kiedy wymaga ona wielokrotnego rozwiązywania tych samych problemów. Aby uniknąć powtarzania wyliczania tych samych wartości, zaczynamy proces obliczeń od rozwiązania najmniejszych podproblemów, zapisujemy rozwiązania w tablicy pomocniczej, a następnie używamy tych rozwiązań dla coraz to większych podproblemów, aż do uzyskania rozwiązania pełnego zadania.
Przykład – tzw. "problem plecakowy".
Metoda "zachłanna"- polega na rozpatrywaniu danych w kolejności uporządkowanej, np. posortowane. W danym kroku wybiera się te dane, które są najodpowiedniejsze, np. o najmniejszej wartości. Najczęściej metoda ta prowadzi do otrzymania rozwiązania
przybliżonego, choć istnieją problemy, dla których metoda zachłanna daje rozwiązanie optymalne.
Przykład - problem SST (Shortest Spanning Tree – najkrótsze drzewo spinające).
Pojęcie rekurencji - wieże z Hanoi
Francuski matematyk Edouard Lucas (rok 1883) - łamigłówka, klasyczne wprowadzenie zagadnienia rekurencji [Graham].
Należy przenieść całą wieżę krążków A na jeden z pozostałych prętów.
W każdym ruchu można brać tylko jeden krążek i nie wolno położyć większego krążka na mniejszym. Ile co najmniej ruchów należy wykonać, aby przenieść całą wieżę ?
Niech: Tn - minimalna liczba ruchów potrzebnych do przeniesienia n krążków z jednego pręta na drugi zgodnie z podanymi regułami - oczywiście T0= 0, T1= 1 i T2= 3
Ogólnie metodę przeniesienia n krążków można sformułować w następujący sposób:
- najpierw przenieś n – 1 krążków na pręt środkowy. Wymaga to wykonania Tn – 1ruchów, - następnie przenieś największy krążek na pręt docelowy (1 ruch),
- przenieś n – 1 krążków z pręta pomocniczego na pręt docelowy (znów Tn – 1ruchów).
Do przeniesienia n krążków (n > 0) potrzeba zatem co najwyżej 2Tn – 1+ 1 ruchów:
1 T 2 Tn n1
1 T 2 Tn n1
Próba znalezienia lepszego porównania prowadzi do stwierdzenia, że do przeniesienia n krążków potrzeba co najmniej 2Tn – 1+ 1 ruchów. Jest zatem:
dla n > 0
dla n > 0
Z tych nierówności oraz z rozwiązania trywialnego zadania z 0 krążkami wynika, że T0= 0
Jest to typowy przykład rekurencji (rekursji).
Aby znaleźć liczbę ruchów dla zadania z n krążkami należy wykonać podobne obliczenia kolejno dla 1, 2, . . . , n – 1 krążków.
Rekurencja musi mieć określoną wartość brzegową – tutaj T0 i z równania wyrażającego ogólną wartość z wartości wcześniej obliczonych.
Zalety rekurencji to eleganckie i zwięzłe formuły.
Wady to fakt, że implementacja programowa rekurencji posiada wiele ograniczeń natury technicznej.
Są to między innymi: ograniczona wielkość stosu, dodatkowe narzuty czasowe związane z
koniecznością odkładania na stosie i zdejmowania z niego kolejnych stanów procesora i śladów powrotu.
Wszędzie, gdzie tylko się da należy dążyć do wyeliminowania rekurencji.
W rozwiązaniu zadania o wieżach z Hanoi można wyeliminować rekurencję.
Rozważmy kilka "małych" zadań i spróbujmy obliczyć dla nich wartości Tn. Kolejno:
T3 = 23 + 1 = 7, T4 = 27 + 1 = 15, T5 = 2x15 + 1 = 31, T6 = 2x31 + 1 = 63
W zakresie wartości od 1 do 6 wygląda to jak Tn = 2n - 1 dla n 6.
Metodą indukcji matematycznej można wykazać, że zależność powyższa jest prawdziwa dla dowolnego n nieujemnego.
Bazą dowodu jest zależność dla n = 0: T0= 20- 1 = 0.
Krok indukcyjny dla n > 0 wynika gdy założymy, że zależność jest spełniona jeżeli n zastąpimy przez n – 1:
Podstawowe struktury danych
Do podstawowych struktur danych, na których najczęściej operują algorytmy należą:
• Zbiór,
• lista,
• graf,
• drzewo,
• słownik.
Lista (w matematyce odpowiada jej ciąg) to skończony ciąg elementów
Q = [x1, x2, . . . , xn].
Elementy skrajne x1oraz xnto jej końce - odpowiednio lewy i prawy.
Wielkość Q = n to długość listy. Szczególnym przypadkiem listy jest lista pusta Q = [ ].
Niech będą dane dwie listy: Q = [q1, q2, . . . , qn] i R = [r1, r2, . . . , rm.] oraz niech 0 i j n.
Podstawowe, abstrakcyjne operacje na listach to:
• dostępdo elementu listy Q[i] = qi
• podlista Q[i..j] = [qi, qi+1, . . . , qj]
• złożenie(konkatenacja) Q & R = [q1, . . . , qn, r1, . . . , rm.]
Za pomocą tych trzech podstawowych operacji można definiować inne operacje, przykładowo:
1)insert(Q, x, i)= Q[1. . i] & [x] & Q[i+1. . Q] – wstawienie elementu x za element qina liście Q.
2)front(Q)= Q[1] - pobranie lewego końca listy.
3) rear(Q) = Q[Q] - pobranie prawego końca listy.
4) push(Q, x)= [x] & Q - wstawienie elementu x na lewy koniec listy, 5)pop(Q)= Q[2. . Q] - usunięcie aktualnego, lewego końca listy,
Lista, na której można wykonać wszystkie operacje 2 – 7 to kolejka podwójna.
Jeżeli na liście są zdefiniowane tylko operacje front, pop i push to jest ona stosem.
Lista, na której możemy wykonać tylko operacje front, pop i inject to kolejka.
Implementacje maszynowe listy: tablicowa Q[i] = qi
dowiązaniowa - każdy element listy (jej wierzchołek) zawiera oprócz danej również dowiązanie – adres następnego elementu).
Zbiór - jego elementy S = {x1, x2, . . . , xn}, inaczej niż w przypadku listy nie są ułożone w żadnym ustalonym porządku.
Zakładamy, że rozpatrywane zbiory są skończone.
Moc lub rozmiar Szbioru to liczba n jego elementów.
Podstawowe operacje na zbiorach to:
a) insert(x, S) - wstawienie elementu x do zbioru S: S := S {x}
b) delete(x, S) - usunięcie elementu x ze zbioru S: S := S – {x}
c) member(x, S) - sprawdzenie, czy x jest elementem zbioru S,
funkcja ta ma wartość
d) min(S) - zwraca najmniejszy element w zbiorze S, przy założeniu pewnego ustalonego porządku liniowego
e) max(S) - zwraca największy element w zbiorze S
f) deletemin(S), deletemax(S) - odpowiednio S := S - {min(S)}, S := S - {max(S)}
g) union(S
1, S
2) - oblicza S
1 S
2
S x gdy false
S x gdy true
Najczęściej stosowane implementacje komputerowe zbioru to:
wektor charakterystyczny - jeżeli elementy zbioru S odpowiadają indeksom tablicy C to:
S x gdy false
S x gdy x true
C
implementacja listowa – jeżeli ustawimy elementy zbioru w jakimś porządku, to otrzymamy listę.
Każda implementacja listy może reprezentować zbiór.
Graf jest strukturą złożoną z dwóch zbiorów G = (V, E)
gdzie V jest skończonym zbiorem wierzchołków – w przypadku, gdy graf jest modelem struktury danych elementy zbioru V nazywa się węzłami.
E jest zbiorem krawędzi, tzn. par wierzchołków (węzłów) ze zbioru V.
W grafie zorientowanym zbiór E jest podzbiorem zbioru uporządkowanych par wierzchołków:
x , y : x , y V x y
2 1 n m n
n 1
n m
Rozmiar grafu G jest sumą dwóch liczb: n = |V| oraz m = |E|.
W grafie niezorientowanym krawędzie są nieuporządkowanymi parami wierzchołków.
W grafie niezorientowanym
a w zorientowanym
W algorytmach i programach komputerowych stosuje się dwa rodzaje implementacji grafu:
Listę sąsiedztwa
– dla każdego
x Vbuduje się listę - oznaczoną jako L[x] - wierzchołków y będących sąsiadami x, tzn.
takich że (x, y) E. Obszar pamięci potrzebny do zapamiętania tak przedstawionego grafu
zależy w sposób liniowy od rozmiaru grafu.
Macierz sąsiedztwa – macierz kwadratowa A o n wierszach i n kolumnach, taka że
E y x dla 0
E y x dla y 1
x
A , ,
, , ,
ten model grafu wymaga obszaru pamięci proporcjonalnego do n
2Drzewo to odmiana grafu - jest to graf spójny acykliczny.
Złożoność algorytmów
Jak porównywać ze sobą różne algorytmy rozwiązujące tą samą klasę zadań ? W praktyce do porównywania algorytmów służy miara zwana złożonością.
Mówi się o złożoności czasowej i o złożoności przestrzennej.
W tym drugim przypadku – złożoność przestrzenna pozwala oszacować obszar pamięci potrzebny do przechowania danych i ew. pośrednich wyników obliczeń.
Klasę rozwiązywanych zadań charakteryzuje wielkość zwana rozmiarem zadania.
Jest to przeważnie liczba przetwarzanych danych.
Czasy działania najczęściej spotykanych w praktyce algorytmów są proporcjonalne do jednej z następujących funkcji:
stała 1 - algorytmy, w których większość operacji jest wykonywana tylko raz, bez względu na rozmiar zadania; mówimy, że ich czas działania jest stały.
log n - zależność ma charakter logarytmiczny - wzrost czasu działania w miarę wzrostu rozmiaru zadania jest nieznaczny. Taka zależność występuje w algorytmach, gdzie rozwiązywanie zadań polega na transformacji problemu na szereg podproblemów. Wartość log n podwaja się
dopiero po zwiększeniu n do n2.
n - zależność liczby operacji od rozmiaru zadania ma charakter liniowy; na każdy z n elementów wejściowych potrzebna jest mała ilość czasu przetwarzania. Jest to sytuacja optymalna dla algorytmów przetwarzających n elementów wejściowych.
n log n - algorytmy rozwiązują problem metodą jego dekompozycji na niezależne problemy o małym rozmiarze, a następnie łączą otrzymane rozwiązania. N.p. dla n = 1 000 000 wartość n log n przy podstawie 2 wynosi ok. 20 000 000.
n2 - zależność kwadratowa - w algorytmach, które muszą dokonać przeglądu wszystkich par n elementów (w algorytmie występują dwie "zagnieżdżone" pętle). Podwojenie rozmiaru zadania powoduje czterokrotny wzrost czasu działania.
n3 - zależność sześcienna – algorytmy przegładają wszystkie możliwe trójki danych (potrójna pętla). Algorytmy o takiej złożoności nadają się praktycznie do rozwiązywania tylko niewielkich zadań. Dwukrotne zwiększenie rozmiaru zadania powoduje aż ośmiokrotny wzrost czasu obliczeń.
2n - tylko niewiele algorytmów o złożoności wykładniczej znajduje zastosowanie praktyczne.
Podwojenie n powoduje "kwadratowy" wzrost czasu obliczeń.