1 Strategia dziel i zwyciężaj Wiele ważnych algorytmów ma strukturą rekurencyjną. W celu rozwiązania rozwiązania problemu algorytm wywołuje sam siebie przy rozwiązywaniu podobnych podproblemów. W metodzie dziel i zwyciężaj: (1) problem dzielony jest na kilka mniejszych podproblemów podobnych do początkowego problemu. (2) podproblemy rozwiązywane są rekurencyjnie (3) rozwiązania wszystkich problemów są łączone w celu utworzenia rozwiązania całego problemu.
2 Algorytm sortowania przez scalanie – rekurencyjny algorytm sortujący. Listę A 1,…,A n dzielimy na dwie listy o dwukrotnie mniejszych rozmiarach. Następnie obie listy są sortowane osobno. Aby zakończyć proces sortowania oryginalnej listy n-elementów, obie listy zostają scalone przy pomocy specjalnego algorytmu. Podstawa: Jeśli lista do posortowania jest pusta lub jednoelementowa, zostaje zwrócona ta sama lista – jest ona już posortowana. Indukcja: Jeżeli lista ma nie mniej niż 2 elementy to podziel listę na dwie. Posortuj każdą z dwóch list i scal.
3 Rekurencyjne dzielenie i scalanie Dzielenie
4 Rekurencyjne dzielenie i scalanie Scalanie Jaka jest procedura scalająca?
5 A więc scalenie dwóch ciągów wymaga O(n+m) operacji porównań elementów i wstawienia ich do tablicy wynikowej. Procedura scalania Oznaczmy przez A[0…n] i B[0…m] ciągi, które chcemy scalić do ciągu C[0..m+n]. Procedura scalania jest następująca: (1) Utwórz wskaźniki na początki ciągów A i B -> i=0, j=0 (2) Jeżeli A[i]<=B[j] wówczas wstaw A[i] do C i zwiększ i o jeden. W przeciwnym przypadku wstaw B[j] do C i zwiększ j o jeden (3) Powtarzaj krok 2 aż wszystkie wyrazy A i B trafią do C
6 Można pokazać ze algorytm sortowania przez scalanie zachowuje się jak O(n log n) (przypomnijmy, że algorytm sortowania przez wybieranie zachowuje się jak O(n 2 ). Dla małych n algorytm sortowania przez wybieranie jest szybszy niż sortowania przez scalanie. Złożoność czasowa
7 Elementarne struktury danych Standardowe typy proste – typy danych, które w większości maszyn cyfrowych występują jako możliwości wbudowane. Należą do nich: zbiór liczb całkowitych, zbiór wartości logicznych i zbiór znaków drukarki. integer, Boolean, char Standardowo w komputerach można również skorzystać z liczb ułamkowych (real) oraz z prostych operatorów (+,-,*,/). Typ Boolean ma dwie wartości: true i false. Do operatorów boolowskich zaliczamy: koniunkcję-, alternatywę- i negację-.
8 Tablica Tablica jest strukturą jednorodną – jest złożona ze składowych tego samego typu zwanego typem podstawowym. Tablica jest strukturą o dostępie swobodnym tzn. wszystkie składowe mogą być wybrane w dowolnej kolejności i są jednakowo dostępne. W celu wybrania pojedynczej składowej nazwę tablicy uzupełnia się tzw. indeksem wybierającym składową. Indeks ten powinien być pewnego typu zwanego typem indeksującym tablicy.
9 int alfa[20] - tablica zawierająca dane typu integer char wiersz[100] - tablica zawierająca dane typu char Przykłady Tablica asocjacyjna: $dane_osobowe["imie"] = "Jan"; $dane_osobowe["nazwisko"] = "Kowalski"; $dane_osobowe["adres"] = "Polna 1"; indeks liczbowyindeks znakowy
10 Struktury danych …w informatyce interesujące są zbiory dynamiczne czyli zbiory, które mogą się powiększać, zmniejszać lub w jakiś sposób zmieniać w czasie. Zbiory są fundamentalnym pojęciem w matematyce, a także w informatyce, ale… Algorytmy działają na zbiorach danych. Dynamiczny zbiór danych, na którym można wykonać operację wstawiania elementu, usuwania elementu oraz sprawdzania, czy dany element należy do zbioru nazywamy słownikiem.
11 Każdy element zbioru jest reprezentowany przez obiekt, którego pola można odczytywać i modyfikować. Wartości niektórych pól mogą być zmieniane przez operacje na zbiorze; pola te mogą zawierać np. wskaźniki do innych elementów zbioru. W niektórych rodzajach zbiorów dynamicznych zakłada się, że jedno z pól każdego obiektu wyróżnione jest jako jego klucz (ang. key). Jeżeli klucze wszystkich elementów są różne to o zbiorze dynamicznym możemy myśleć jak o zbiorze kluczy key next 2 pola
12 Operacje na zbiorach dynamicznych Operacje na zbiorach dynamicznych można podzielić na dwie grupy: zapytania – operacje pozwalające uzyskać pewne informacje na temat zbioru. Search(S,k) – zapytanie, które dla danego zbioru S oraz wartości klucza k, daje w wyniku wskaźnik x do takiego elementu w zbiorze, że key[x]=k lub NIL, jeżeli żaden taki element do zbioru nie należy. Minimum(S) – zapytanie dotyczące liniowo uporządkowanego zbioru S, które daje w wyniku element S o najmniejszym kluczu. Przykłady
13 Operacje na zbiorach dynamicznych cd. operacje modyfikujące – operacje, które pozwalają zmienić zbiór Insert(S,x) – operacja modyfikująca, która do danego zbioru S dodaje element wskazywany przez x. Zakładamy, że wartości wszystkich pól elementu wskazywanego przez x istotnych dla realizacji zbioru zostały już zainicjowane. Delete(S,x) – operacja modyfikująca, która z danego zbioru S usuwa element wskazywany przez x. Przykłady
14 Stos Liniowa struktura danych. Dane dokładane są na wierzch stosu, również z wierzchołka są ściągane (stosuje się też określenie LIFO (ang. Last In First Out), oddające tę samą zasadę). PUSH -czyli odłożenie obiektu na stos; rozmiar sosu zwiększa się o 1. Jeżeli przekroczony zostaje maksymalny rozmiar stosu następuje jego przepełnienie (ang. stack overflow). Przykład: stos książek, stos talerzy. Operacje, jakie można wykonywać na stosie: POP -ściągnięcie obiektu ze stosu; może doprowadzić do niedopełnienia stosu (ang. stack underflow).
15 Implementacja stosu za pomocą tablicy Stos S zawierający nie więcej niż n-elementów można zaimplementować w tablicy S[1…n]. Z tablicą taką związany jest dodatkowy atrybut top[S], którego wartość jest numerem ostatnio wstawionego elementu. Stos składa się z elementów S[1],…,S[top[S]], gdzie S[1] jest elementem na dnie stosu, a S[top[S]] jest elementem na wierzchołku stosu. Jeżeli top[S]=0 wówczas stos jest pusty. Do sprawdzenia czy stos S jest pusty używamy operacji Stack-Empty. Stack-Empty(S) if top[S]=0 then return True else return False
16 Implementacja operacji na stosie: PUSH Push(S,x) top[S]<-top[S]+1 S[top[S]]=x <- top[S] POP Pop(S) if Stack-Empty(S) then error niedomiar else top[S]<-top[S]-1 return S[top[S]+1] <- top[S]
S top[S]=4 Push(S,17) Push(S,3) S top[S]=6 Pop(S) S top[S]=5 Przykład
18 Liniowa struktura danych. Kiedy wstawiamy nowy element do kolejki, zostaje on umieszczony na końcu kolejki (w ogonie). Element może zostać usunięty z kolejki tylko wtedy gdy znajduje się na początku kolejki (w głowie). W przypadku kolejki stosuje się też określenie LIFO (ang. Last In First Out). Operacje, jakie można wykonywać na kolejce: ENQUEUE - czyli wstawienie elementu do kolejki. DEQUEUE - czyli usunięcie elementu z kolejki. Kolejka Przykład: kolejka ludzi w sklepie.
19 Implementacja kolejki za pomocą tablicy Kolejkę Q o co najwyżej n-1 elementach można zaimplementować za pomocą tablicy Q[n…1]. Atrybut head[Q] takiej kolejki wskazuje na jej głowę, tj. na początek, natomiast atrybut tail[Q] wyznacza następną wolną pozycję, na którą możemy wstawić do kolejki nowy element. Elementy kolejki znajdują się na pozycjach head[Q], head[Q]+1, …, tail[Q]-1 Załóżmy, że tablica Q jest cykliczna, tzn, pozycja o numerze 1 jest bezpośrednim następnikiem pozycji o numerze n. Jeżeli head[Q]=tail[Q] to kolejka jest pusta. Początkowo head[Q]=tail[Q]=1 to kolejka jest pusta. Jeżeli head[Q]=tail[Q]+1 to kolejka jest pełna. Jeżeli kolejka jest pusta wówczas próba usunięcia elementu z kolejki jest sygnalizowana jako błąd niedomiaru. Próba wstawienia nowego elementu do pełnej kolejki sygnalizowana jest jako błąd przepełnienia.
20 Implementacja operacji na kolejce przy pomocy tablicy: WSTAWIENIE ENQ(Q,x) Q[tail[Q]]<-x if tail[Q]=length[Q] then tail[Q]<-1 else tail[Q]<-tail[Q]+1 tail[Q]-1 USUNIĘCIE DEQ(Q) x<-Q[head[Q]] if head[Q]=length[Q] then head[Q]<-1 else head[Q]<-head[Q]+1 return x head[Q] UWAGA: Obsługa błędów przepełnienia i niedomiaru pominięta – praca domowa!
Q head[Q]=4 tail[Q]= Q head[Q]=5 tail[Q]=1 Enq(Q,17) Enq(Q,3) Enq(Q,5) Q tail[Q]=1 head[Q]=4 Deq(Q) Przykład
22 Lista – struktura danych w których elementy są ułożone w liniowym porządku. Porządek na liście określają wskaźniki związane z każdym elementem listy. prev next key Lista dwukierunkowa head[L]-> Lista jednokierunkowa head[L]-> Lista
23 next[x] – następnik elementu x. Jeżeli next[x]=NIL to x nie ma następnika, jest więc ostanim elementem listy (tzw. ogon). prev[x] – poprzednik elementu x. Jeżeli prev[x]=NIL to x nie ma poprzednika, jest więc pierwszym elementem listy (tzw. głowa). head[L] – pierwszy element listy L. Jeżeli head[x]=NIL to lista jest pusta. Lista
24 Wstawianie do listy z dowiązaniami List-Insert(L,x) next[x]<-head[L] if head[L]!=NIL then prev[head[L]]<-x head[L]<-x prev[x]<-NIL Procedura List-Insert(L,x) przyłącza element x (dla którego pole key zostało wcześniej zainicjowane) na początek listy.
25 Usuwanie z listy z dowiązaniami List-Delete(L,x) if prev[x]!=NIL then next[prev[x]]<-next[x] else head[L]<-next[x] if next[x]!=NIL then prev[next[x]]<-prev[x] Procedura List-Delete(L,x) usuwa element x z listy L.
26 Wyszukiwanie na listach z dowiązaniami List-Search(L,k) x<-head[L] while x!=NIL and key[x]!=k do x<-next[x] return x Procedura List-Search(L,k) wyznacza pierwszy element o kluczu k na liście L.
head[L] key[x]=25 List-Insert(L,x) head[L] -2 key[x]=4 List-Delete(L,x) head[L] -2 Przykład
28 Rekurencyjna definicja listy Listę można zdefiniować w następujący rekursyjny sposób: Lista o typie podstawowym T jest albo: (1) pustą listą, albo (2) konkatenacją (połączeniem) elementu typu T i listy o typie podstawowym T W podobny sposób można zdefiniować strukturą drzewiastą (drzewo). Jest ona albo: (1) strukturą pustą, albo (2) węzłem typu T ze skończoną liczbą dowiązań rozłącznych struktur drzewiastych o typie podstawowym T, nazywanych poddrzewami.
29 n1 n2 n3 n4 n6 n7 n5 Drzewa są zbiorami punktów, zwanych węzłami lub wierzchołkami, oraz połączeń, zwanych krawędziami. (A) Krawędź łączy dwa różne węzły. (B) W każdym drzewie wyróżniamy jeden węzeł zwany korzeniem – n1 (C) Każdy węzeł n nie będący korzeniem jest połączony krawędzią z innym węzłem zwanym rodzicem. Węzeł n nazywamy dzieckiem. (D) jeżeli rozpoczniemy analizę od węzła n nie będącego korzeniem i przejdziemy do rodzica tego węzła, osiągniemy w końcu korzeń. Mówimy, że drzewo jest spójne. Drzewa
30 Drzewa Reprezentacja - graf A B C D G E H IJ F KL korzeń rodzic dziecko krawędź
31 w1 w2 w3 w4 w6 w7 w5 (E) Relację rodzic-dziecko w naturalny sposób można rozszerzyć do relacji przodek- potomek. (F) Liściem nazywamy węzeł drzewa który nie ma potomków. (G) Węzeł nazywamy wewnętrznym jeżeli ma jednego lub większą liczbę potomków. (D) W dowolnym drzewie T, dowolny węzeł n wraz z jego potomkami nazywamy poddrzewem. Drzewa
32 Drzewa Reprezentacja - graf A B C D G E H IJ F KL przodek L potomek L liść węzeł wewnętrzny liść
33 (E) W drzewie istnieje dokładnie jedna ścieżka pomiędzy węzłem a korzeniem. Przez ścieżkę rozumiemy ciąg krawędzi. (G) Z kolei wysokość drzewa definiujemy jako długość najdłuższej ścieżki wychodzącej z korzenia. (H) Głębokość węzła to długość ścieżki od korzenia do tego węzła. Drzewa (F) Liczba krawędzi w ścieżce jest nazywana długością (lub głębokością) – liczba o jeden większa określa poziom węzła.
34 Reprezentacja struktury drzewiastej - graf A B C D G E H IJ F KL ścieżka o długości 3 Drzewa ścieżka o długości 2 Drzewo o wysokości 3
35 zbiory zagnieżdżone A B C D E F G H I J K L nawiasy zagnieżdżone (A(B(D(G),E(I,J,H)),C(F(K,L)))) wcięcia A B D G E I J H C F K L Inne reprezentacje
36 Liczbę bezpośrednich potomków węzła wewnętrznego nazywamy jego stopniem. Maksymalny stopień węzłów jest stopniem drzewa. w1 w2 w4 w6 w7 w2 w6 w5w4 w8 w9 w10 Drzewo nazywamy uporządkowanym jeżeli gałęzie każdego węzła są uporządkowane. Drzewa uporządkowane o stopniu 2 nazywamy drzewami binarnymi.
37 Drzewa binarne Krok: Jeśli R jest węzłem oraz A, B są drzewami binarnymi to istnieje drzewo binarne z korzeniem R, lewym poddrzewem A i prawym poddrzewem B. Korzeń drzewa A jest lewym dzieckiem węzła R, chyba że A jest drzewem pustym. Podobnie korzeń drzewa B jest prawym dzieckiem węzła R, chyba że B jest drzewem pustym. Rekurencyjna definicja drzewa binarnego Podstawa: Drzewo puste jest drzewem binarnym. R A B
38 W każdym węźle drzewa binarnego T znajduje się wskaźnik do ojca oraz lewego i prawego syna, odpowiednio w polach p, left, right. Jeżeli p[x]=NIL to węzeł x jest korzeniem drzewa. Jeżeli left[x]=NIL (right[x]=NIL) to węzeł x nie ma lewego (prawego) syna. Atrybut root[T] zawiera wskaźnik do korzenia. Jeżeli root[T]=NIL to znaczy, że drzewo jest puste. Drzewo binarne p left key
39 root[T] Drzewa binarne * Pola key nie zostały uwzględnione.
40 Rekurencja w drzewach Często zdarza się, że musimy wykonać pewną operacje P na każdym z elementów drzewa. (1) Preorder (wzdłużny): R, A, B P stanowi wówczas parametr ogólniejszego zadania – odwiedzenia wszystkich węzłów – nazywanego przeglądaniem drzewa. Przeglądanie odbywa się zgodnie z pewnym porządkiem. Istnieją trzy podstawowe uporządkowania: R A B (2) Inorder (poprzeczny): A, R, B (3) Postorder (wsteczny): A, B, R
41 * + _ / * a b C d e f Przykład (1) Preorder (R,A,B): * + a / b c – d * e f (2) Inorder (A,R,B): a + b / c * d – e * f (3) Postorder (A,B,R): a b c / + d e f * – *
42 Drzewa przeszukiwań binarnych Drzewa poszukiwań binarnych (ang. binary search trees - BST) to drzewa binarne posiadające następującą własność: Niech x będzie węzłem drzewa BST. Jeżeli y jest węzłem znajdującym się w lewym poddrzewie węzła x, to key[y] key[x] Jeżeli y jest węzłem znajdującym się w prawym poddrzewie węzła x, to key[x] key[y]
43 Wyszukiwanie elementu Podstawa Jeśli drzewo T jest puste, to na pewno nie zawiera elementu x. Jeśli T nie jest puste i szukana wartość x znajduje się w korzeniu, drzewo zawiera x. Krok Jeśli T nie jest puste, ale nie zawiera szukanego elementu x w korzeniu, niech y będzie elementem w korzeniu drzewa T. Jeżeli x<y, szukamy dalej tego elementu tylko w lewym poddrzewie korzenia; Jeżeli x>y, szukamy wartości x tylko w prawym poddrzewie korzenia y Przykład Szukamy liczby 8. Zaczynamy od korzenia czyli liczby 5. Ponieważ 5<8 zatem przechodzimy do prawego poddrzewa.
44 Tree-Search(x,k) if x=NIL lub k=key[x] then return x if k<key[x] then return Tree-Search(left[x], k) then return Tree-Search(right[x], k) x – wskaźnik do korzenia drzewa k - klucz Wyszukiwanie elementu Wstawianie elementu Usuwanie elementu praca domowa