Zbiory dynamiczne Zbiór dynamiczny Zbiór dynamiczny to zbiór wartości pochodzących z pewnego określonego uniwersum, którego zawartość zmienia się w trakcie działania programu. Elementy zbioru dynamicznego musimy co najmniej umieć porównać pod względem identyczności (czy dwa elementy są równe albo różne). Dynamiczne struktury danych Dynamiczna struktura danych, to struktura danych pozwalająca na przechowywanie zbioru dynamicznego; rozmiar tej struktury dostosowuje się do rozmiaru danych. W zbiorze dynamicznym musimy umieć realizować operację dodania nowego elementu do zbioru i usunięcia ze zbioru wskazanego elementu. Tablica dynamiczna jest dynamiczną strukturą danych. Zastosowanie: przechowywanie pewnego zbioru danych, którego zawartość będzie się zmieniać w trakcie pracy programu. Słownik Słownik (ang. dictionary) to struktura danych pozwalająca efektywnie realizować następujące operacje: ◦ insert(x) – dodanie nowego elementu x do zbioru dynamicznego, ◦ delete(x) – usunięcie elementu o wartości x ze zbioru dynamicznego, ◦ search(x) – sprawdzenie czy w zbiorze dynamicznym znajduje się element o wartości x. Multizbiór to zbiór dynamiczny, w którym mogą się powtarzać elementy o takich samych wartościach. Struktura danych jest homogeniczna, jeśli składa się z elementów tego samego typu. Listy Lista Lista (ang. list) to homogeniczna struktura danych służąca do reprezentowania zbioru dynamicznego, w której elementy ułożone w ciąg. Element listy nazywa się węzłem (ang. node); każdy węzeł zawiera pole info służące do przechowywania wartości z pewnego określonego uniwersum oraz pole next ze wskaźnikiem na następny element listy (ostatni element listy ma w polu next wpisany wskaźnik pusty). Pierwszy węzeł listy jest nazywany głową (ang. head) albo początkiem listy. Dostęp do elementów listy jest sekwencyjny – a więc dojście do elementu k-tego wymaga przejścia przez kolejne elementy listy od pierwszego do docelowego. Zastosowanie: lista najlepiej nadaje się do danych, które będą przetwarzane sekwencyjnie. Lista Lista jednokierunkowa (ang. single linked list) to lista, po której można się poruszać tylko od głowy do ogona – w każdym węźle jest tylko wskaźnik do następnika. Lista dwukierunkowa (ang. double linked list) to lista, po której można się poruszać w obu kierunkach: w stronę głowy i w stronę ogona – w każdym węźle są dwa wskaźniki next do następnika i prev do poprzednika. Lista Lista cykliczna to lista, w której ostatni węzeł posiada wskaźnik do pierwszego węzła. Lista dwukierunkowa może być cykliczna. Lista z wartownikiem to lista, w której na końcu umieszczony jest węzeł zwany wartownikiem – wartownik nie przechowuje danych, pełni rolę pomocniczą w nawigacji po liście. Lista z wartownikiem może być cykliczna lub dwukierunkowa. Gdy dane pochodzą z uniwersum z porządkiem liniowym, to dane w liście można przechowywać w sposób uporządkowany – mamy w tedy do czynienia z listą uporządkowaną. Lista Wyszukiwanie wartości w liście – wersja iteracyjna Search(wezeł *w, x) -> boolean { while (w.info != x) { if (w.next != null) w := w.next; else return false; } return true; } Czas: O(n) gdzie n to ilość elementów na liście Pamięć: O(1) Lista Wyszukiwanie wartości w liście – wersja rekurencyjna Search(wezeł *w, x) -> boolean { if (w == null) return false; if (w.info == x) return true; return Search(w.next, x); } Czas: O(n) gdzie n to ilość elementów na liście Pamięć: O(n) zależy od liczby wywołań rekurencyjnych Lista Wyszukiwanie wartości w liście posortowanej – wersja rekurencyjna Search(wezeł *w, x) -> boolean { if (w == null) return false; if (w.info == x) return true; if (w.info > x) return false; return Search(w.next, x); } Czas: O(n) gdzie n to ilość elementów na liście Pamięć: O(n) zależy od liczby wywołań rekurencyjnych Lista Wstawianie elementu do listy nieuporządkowanej: ◦ na początek listy, ◦ na koniec, ◦ na zadaną pozycję. Wstawianie elementu do listy uporządkowanej Usuwanie elementu z listy: ◦ usuwanie elementu z zadanej pozycji, ◦ usuwanie elementu o zadanej wartości. Listy Technika zwracania wskaźnika do struktury po zmodyfikowaniu. Przykład: wstawienie elementu na zadaną pozycję: insert(węzeł *w, x, pos) -> węzeł* { if (pos < 0) error; if (w == null and pos > 0) error; if (pos > 0) { w.next := insert(w.next, x, pos-1); return w; } else return new węzeł(x, w); } wywołanie: head := insert(head, x); Listy Operacje słownikowe na liście nelementowej wymagają: ◦ czasu O(n), ◦ pamięci O(1) gdy używamy iteracji albo O(n) gdy korzystamy z rekurencji. Listy Zadanie: podział listy na dwie równe podlisty (z dokładnością do 1 elementu). Rozwiązanie: dwa wskaźniki, jeden robi podwójne skoki, drugi pojedyncze; po dotarciu na koniec listy pierwszego wskaźnika, drugi wskazuje na węzeł środkowy. split(węzeł *h) -> (węzeł*, węzeł*) { węzeł *p = head; węzeł *q = head; if (p == null) return (null, null); while (q != null) { q := q.next; if (q == null) break; q := q.next; if (q != null) p := p.next; } q := p.next; p.nex; := null; return (head, q); } Listy Zadanie: należy scalić dwie posortowane lisy . Rozwiązanie: do końca listy wynikowej doczepiamy mniejszy spośród głów pozostałych list. merge(węzeł *g, węzeł *h) -> węzeł* { if (g == null) return h; if (h == null) return g; węzeł *r = g; if (g.info < h.info) g := g.next; else { r := h; h := h.next; } węzeł *s = r; while (g != null and h != null) if (g.info < h.info) { r.next := g; r := r.next; g := g.next; } else { r.next := h; r := r.next; h := h.next; } if (g != null) r.next := g; else r.next := h; return s; } Zadanie 1 Zdefiniuj klasę reprezentującą węzeł listy jednokierunkowej wraz z operacjami wstawiania nowego elementu na zadaną pozycję, usuwania węzła z zadanej pozycji i sprawdzania czy określony element znajduje się na liście (operacje te zaimplementuj rekurencyjnie). Węzeł listy zdefiniuj jako szablon przy pomocy template<typename T>. Zadanie 2 Zdefiniuj klasę reprezentującą listę jako opakowanie dla struktury zbudowanej na węzłach. W klasie tej opakuj metodę wstawiającą do listy, usuwającą z listy i sprawdzającą czy element występuje na liście. Dodatkowo dopisz metody: wstawiająca element na początek listy, na koniec listy, usuwającą pierwszy i usuwającą ostatni element na liście. Listę zdefiniuj jako szablon przy pomocy template<typename T>. Zadanie 3 Zdefiniuj klasę reprezentującą węzeł listy jednokierunkowej uporządkowanej wraz z operacjami wstawiania nowego elementu, usuwania elementu i sprawdzania czy określony element znajduje się na liście. Zdefiniuj też klasę reprezentującą listę jako opakowanie dla struktury zbudowanej na węzłach. Węzeł listy oraz listę zdefiniuj jako szablony przy pomocy template<typename T>. Drzewa Drzewa Drzewo (ang. tree) to zbiór węzłów powiązanych wskaźnikami, bez cykli. Drzewo ukorzenione – posiada wyróżniony węzeł początkowy zwany korzeniem (ang. root). Drzewo jest strukturą hierarchiczną – analogia do polskich jabłek na rozgałęzionej jabłoni. Korzeń węzła znajduje się na poziomie 0; numer poziomu danego węzła w drzewie jest wyznaczony odległością krawędziową od korzenia. Liściem w drzewie (ang. leaf) jest węzeł bez żadnego następnika. Węzeł wewnętrzny posiada co najmniej jednego następnika. Drzewa Wysokość drzewa – długość najdłuższej ścieżki od korzenia do liścia (liczba węzłów) – inaczej liczba poziomów na których zapisane jest drzewo. Głębokość węzła – długość ścieżki od korzenia do tego węzła (liczba węzłów) – inaczej numer poziomu na którym znajduje się węzeł. Uwaga: poziomy w drzewie numerujemy od 0; korzeń znajduje się na poziomie 0. Drzewo Drzewo uporządkowane ma ponumerowanych (oznaczonych) następników – ich kolejność w takim drzewie jest istotna. Drzewo k-arne to drzewo uporządkowane, które posiada co najwyżej k następników. Drzewo binarne – może posiadać dwóch następników: lewego i prawego. Reprezentacja drzew uporządkowanych „na lewo syn na prawo brat” T.root / / / / / / / / / / / / / Drzewo BST Drzewo BST to drzewo binarne, w którym przechowujemy elementy z pewnego uniwersum z porządkiem liniowym i w drzewie tym jest zachowany porządek symetryczny. W drzewie binarnym jest zachowany porządek symetryczny, gdy elementy mniejsze od korzenia znajdują się w lewym poddrzewie, elementy większe od korzenia w prawym poddrzewie oraz w lewym i w prawym poddrzewie też jest zachowany porządek symetryczny. Drzewo BST Wyszukiwanie w drzewie BST – działa tak jak w wyszukiwaniu binarnym serach (węzeł *w, x) -> boolean { if (w == null) return false; if (x < w.info) return search(w.left, x); else if (w.info < x) then return search(w.right, x); else return true; } Drzewo BST Element minimalny znajduje się najbardziej po lewej stronie w drzewie BST – od korzenia poruszamy się ciągle w lewo, ostatni węzeł na lewej ścieżce zawiera minimum. Element maksymalny znajduje się najbardziej po prawej stronie w drzewie BST – od korzenia poruszamy się ciągle w prawo , ostatni węzeł na prawej ścieżce zawiera maksimum. Drzewo BST Wstawienie nowego elementu do drzewa BST – znajdujemy mu miejsce tak jak w wyszukiwaniu binarnym (aż dojdziemy do wskaźnika pustego) insert (węzeł *w, x) -> node* { if (w == null) return new node(x); if (x < w.info) w.left := insert(w.left, x); else if (w.info < x) then w.right := insert(w.right, x); else w.info := x; return w; } Drzewo BST Usunięcie elementu z drzewa BST – znajdujemy miejsce tego elementu tak jak w wyszukiwaniu binarnym (jak dojdziemy do wskaźnika pustego to elementu nie ma w drzewie): ◦ gdy element jest w liściu, to odcinam liść; ◦ gdy element jest w węźle z jednym następnikiem, to usuwam ze ścieżki ten węzeł; ◦ gdy element jest w węźle z dwoma synami, to z prawego poddrzewa usuwamy minimum (albo z lewego poddrzewa usuwamy maksimum) i przenosimy je do tego węzła. Przeglądanie drzew BST In-order – najpierw jest przeglądane i przetwarzane lewe poddrzewo w porządku in-order, potem korzeń a na końcu prawe poddrzewo w porządku in-order. Przeglądanie drzewa BST w porządku inorder gwarantuje, że węzły tego drzewa zostaną przetworzone w kolejności od najmniejszej do największej wartości. Przeglądanie in-order można wykorzystać do sortowania danych. Przeglądanie drzew BST Pre-order – najpierw jest przeglądany i przetwarzany korzeń drzewa, potem lewe poddrzewo w porządku pre-order a na końcu prawe poddrzewo w porządku pre-order. Przeglądanie drzewa BST w porządku pre-order gwarantuje, że na początku będą przetworzone węzły z lewej ścieżki tego drzewa w kolejności od korzenia do węzła o najmniejszej wartości. Post-order – najpierw jest przeglądane i przetwarzane lewe poddrzewo w porządku post-order, potem prawe poddrzewo w porządku post-order a na końcu korzeń drzewa. Przeglądanie drzewa BST w porządku post-order gwarantuje, że na końcu będą przetworzone węzły z prawej ścieżki tego drzewa w kolejności od węzła o największej wartości do korzenia. Struktura drzew BST Wysokość drzewa – liczba poziomów zajmowanych przez drzewo (korzeń znajduje się na poziomie 0, na ostatnim poziomie znajdują się tylko liście) – albo długość wierzchołkowa najdłużej ścieżki od korzenia do liścia . Głębokość węzła – numer poziomu na którym znajduje się dany węzeł – albo krawędziowa odległość od korzenia. Wysokość drzewa n-elementowego jest nie mniejsza niż log(n) i nie większa niż n. Struktura drzew BST Drzewo regularne to drzewo binarne, w którym każdy węzeł wewnętrzny na dwóch synów. Drzewo pełne to drzewo regularne, w którym wszystkie liście są na tym samym poziomie. Drzewo pełne o wysokości h ma 2h-1 węzłów. Drzewo zupełne to drzewo pełne, z którego usunięto część liści z prawej strony. Drzewo zupełne o wysokości h ma od 2h-1 do 2h-1 węzłów. Rotacje w drzewie BST W dowolnym wewnętrznym węźle drzewa BST można zrobić rotację (w lewo albo w prawo) i nie zostanie zniszczony porządek symetryczny w tym drzewie. Zadanie 1 Zdefiniuj klasę reprezentującą węzeł drzewa BST wraz z operacjami wstawiania nowego elementu, usuwania elementu i sprawdzania czy określony element znajduje się w drzewie. Węzeł drzewa zdefiniuj jako szablon przy pomocy template<typename T>. Zadanie 2 Zdefiniuj klasę reprezentującą drzewo BST jako opakowanie dla struktury zbudowanej na węzłach. W klasie tej opakuj metodę wstawiającą do drzewa, usuwającą z drzewa i sprawdzającą czy element występuje w drzewie. Dodatkowo dopisz metody: wyznaczające oraz usuwające najmniejszy i największy element w drzewie. Drzewo BST zdefiniuj jako szablon przy pomocy template<typename T>.