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. 1 Algorytm sortowania przez scalanie – rekurencyjny algorytm sortujący. Listę A1,…,An 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: Indukcja: Jeśli lista do posortowania jest pusta lub jednoelementowa, zostaje zwrócona ta sama lista – jest ona już posortowana. Jeżeli lista ma nie mniej niż 2 elementy to podziel listę na dwie. Posortuj każdą z dwóch list i scal. 2 Rekurencyjne dzielenie i scalanie Dzielenie 52461326 5246 2 4 6 26 13 46 52 5 1326 1 3 2 6 3 Rekurencyjne dzielenie i scalanie Scalanie 12234566 1236 2456 5 2 4 6 26 13 46 25 1 3 2 6 Jaka jest procedura scalająca? 4 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 A więc scalenie dwóch ciągów wymaga O(n+m) operacji porównań elementów i wstawienia ich do tablicy wynikowej. 5 Złożoność czasowa 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(n2). Dla małych n algorytm sortowania przez wybieranie jest szybszy niż sortowania przez scalanie. 6 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ę-. 7 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. 8 Przykłady int alfa[20] - tablica zawierająca dane typu integer char wiersz[100] - tablica zawierająca dane typu char indeks liczbowy Tablica asocjacyjna: $dane_osobowe["imie"] = "Jan"; $dane_osobowe["nazwisko"] = "Kowalski"; $dane_osobowe["adres"] = "Polna 1"; indeks znakowy 9 Struktury danych Zbiory są fundamentalnym pojęciem w matematyce, a także w informatyce, ale… …w informatyce interesujące są zbiory dynamiczne czyli zbiory, które mogą się powiększać, zmniejszać lub w jakiś sposób zmieniać w czasie. 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. 10 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). 2 pola 9 11 -3 5 12 key next Jeżeli klucze wszystkich elementów są różne to o zbiorze dynamicznym możemy myśleć jak o zbiorze kluczy. 11 Operacje na zbiorach dynamicznych Operacje na zbiorach dynamicznych można podzielić na dwie grupy: zapytania – operacje pozwalające uzyskać pewne informacje na temat zbioru. Przykłady 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. 12 Operacje na zbiorach dynamicznych cd. operacje modyfikujące – operacje, które pozwalają zmienić zbiór Przykłady 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. 13 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ę). Przykład: stos książek, stos talerzy. Operacje, jakie można wykonywać na stosie: 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). POP - ściągnięcie obiektu ze stosu; może doprowadzić do niedopełnienia stosu (ang. stack underflow). 14 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 15 Implementacja operacji na stosie: PUSH <- top[S] Push(S,x) top[S]<-top[S]+1 S[top[S]]=x POP <- top[S] Pop(S) if Stack-Empty(S) then error „niedomiar” else top[S]<-top[S]-1 return S[top[S]+1] 16 Przykład S 1 2 3 4 15 6 2 9 5 6 7 Push(S,17) top[S]=4 Push(S,3) S 1 2 3 4 5 6 15 6 2 9 17 3 7 top[S]=6 Pop(S) S 1 2 3 4 5 6 15 6 2 9 17 3 7 top[S]=5 17 Kolejka 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). Przykład: kolejka ludzi w sklepie. Operacje, jakie można wykonywać na kolejce: ENQUEUE - czyli wstawienie elementu do kolejki. DEQUEUE - czyli usunięcie elementu z kolejki. 18 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. 19 Implementacja operacji na kolejce przy pomocy tablicy: WSTAWIENIE tail[Q]-1 ENQ(Q,x) Q[tail[Q]]<-x if tail[Q]=length[Q] then tail[Q]<-1 else tail[Q]<-tail[Q]+1 USUNIĘCIE head[Q] DEQ(Q) x<-Q[head[Q]] if head[Q]=length[Q] then head[Q]<-1 else head[Q]<-head[Q]+1 return x 20 UWAGA: Obsługa błędów przepełnienia i niedomiaru pominięta – praca domowa! Przykład Q 1 2 3 4 5 6 7 6 9 8 4 head[Q]=4 8 9 10 Enq(Q,17) tail[Q]=8 Enq(Q,3) Enq(Q,5) Q 1 2 tail[Q]=1 3 4 5 6 7 8 9 10 6 9 8 4 17 3 5 head[Q]=4 Deq(Q) Q 1 2 tail[Q]=1 3 4 5 6 7 8 9 10 6 9 8 4 17 3 5 head[Q]=5 21 Lista 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. Lista jednokierunkowa head[L]-> 11 9 -3 5 prev key 12 next Lista dwukierunkowa head[L]-> 9 4 -2 1 22 Lista 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. 23 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. 24 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. 25 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. 26 Przykład head[L] 9 head[L] 25 head[L] 25 4 -2 1 key[x]=25 List-Insert(L,x) 9 4 -2 1 key[x]=4 List-Delete(L,x) 9 -2 1 27 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. 28 Drzewa 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. n1 (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. n2 n5 n3 n4 n7 n6 (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. 29 Drzewa Reprezentacja - graf korzeń A B krawędź C rodzic F E D dziecko G H I J K L 30 Drzewa (E) Relację rodzic-dziecko w naturalny sposób można rozszerzyć do relacji przodekpotomek. w1 (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. w2 (D) W dowolnym drzewie T, dowolny węzeł n wraz z jego potomkami nazywamy poddrzewem. w5 w3 w4 w7 w6 31 Drzewa Reprezentacja - graf A przodek L B C węzeł wewnętrzny F E D potomek L G H liść I J K L liść 32 Drzewa (E) W drzewie istnieje dokładnie jedna ścieżka pomiędzy węzłem a korzeniem. Przez ścieżkę rozumiemy ciąg krawędzi. (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. (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. 33 Drzewa Reprezentacja struktury drzewiastej - graf ścieżka o długości 3 A B C ścieżka o długości 2 G F E D H I Drzewo o wysokości 3 J K L 34 Inne reprezentacje zbiory zagnieżdżone G I J H E D B nawiasy zagnieżdżone K L F C A (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 35 Liczbę bezpośrednich potomków węzła wewnętrznego nazywamy jego stopniem. Maksymalny stopień węzłów jest stopniem drzewa. 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. w1 w4 w2 w6 w4 w8 w6 w5 w9 w7 w10 36 Drzewa binarne Rekurencyjna definicja drzewa binarnego R A B Podstawa: Drzewo puste jest drzewem binarnym. 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. 37 Drzewo binarne 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. p left key left 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. 38 Drzewa binarne root[T] * Pola key nie zostały uwzględnione. 39 Rekurencja w drzewach Często zdarza się, że musimy wykonać pewną operacje P na każdym z elementów drzewa. 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: (1) Preorder (wzdłużny): R, A, B R (2) Inorder (poprzeczny): A, R, B (3) Postorder (wsteczny): A, B, R A B 40 Przykład * _ + a d / b C * e f (1) Preorder (R,A,B): *+a/bc–d*ef (2) Inorder (A,R,B): a+b/c*d–e*f (3) Postorder (A,B,R): abc/+def*–* 41 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] 2 5 3 2 3 7 7 5 5 8 8 5 42 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 5 Szukamy liczby 8. Zaczynamy od korzenia czyli liczby 5. Ponieważ 5<8 zatem przechodzimy do prawego poddrzewa. 3 2 7 5 8 43 Wyszukiwanie elementu Tree-Search(x,k) if x=NIL lub x – wskaźnik do korzenia drzewa k=key[x] k - klucz then return x if k<key[x] then return Tree-Search(left[x], k) then return Tree-Search(right[x], k) Wstawianie elementu Usuwanie elementu praca domowa 44