dr inż. Paweł Myszkowski Politechnika Białostocka Wydział Elektryczny Elektronika i Telekomunikacja, semestr II, studia stacjonarne I stopnia Rok akademicki 2015/2016 Wykład nr 11 (11.05.2016)
Plan prezentacji: dynamiczne struktury danych i ich obsługa lista lista jednokierunkowa lista dwukierunkowa lista cykliczna drzewo drzewo binarne
Lista (list) listą nazywamy liniową sekwencję elementów powiązanych ze sobą tak, że każdy element "zna" co najmniej jednego ze swoich sąsiadów nowy element można dodać w dowolnym miejscu listy (na początku, w środku lub na końcu) z listy możemy usunąć także dowolny jej element zasady zarządzania: stos LIFO (Last In First Out) kolejka - FIFO (First In First Out) lista brak zasad
Lista jednokierunkowa każdy element "zna" tylko jednego ze swoich sąsiadów ostatni element nie "zna" żadnego sąsiada istnieje konieczność "pamiętania" pierwszego elementu (head) lista jednokierunkowa różni się od kolejki tylko obsługą można stwierdzić, że kolejka to szczególny przypadek listy jednokierunkowej kolejka - FIFO (First In First Out) lista jednokierunkowa brak zasad
Lista (list) Graficzna prezentacja listy jednokierunkowej in out NULL NULL NULL HEAD
Implementacja listy jednokierunkowej w języku C struct lista int ; struct lista *wsk; ; //definicja pojedynczej struktury // mogą być innego typu, może być też więcej pól //wskaźnik wskazujący na następny element struct lista *head; //deklaracja elementu na początku listy ("głowa") // jest to zmienna globalna // nie ma konieczności pamiętania "ogona" // wygodnie jest jednak pamiętać "głowę", "ogon" // i element bieżący
/* funkcja dodaje nowy element za elementem podanym jako parametr */ void in (int data, struct lista *elem) struct lista *tmp; //zmienna pomocnicza elem tmp = malloc(sizeof(struct lista); tmp-> = data; if (head!= NULL) //jeśli lista nie była pusta if (elem!= NULL) //wstawiamy w środku tmp->wsk = elem->wsk; elem->wsk = tmp; tmp
else //wstawiamy na początku tmp->wsk = head; head = tmp; else //jeśli zaś lista była pusta head = tmp; //powstaje nowa "głowa" head->wsk = NULL; tmp HEAD tmp HEAD HEAD NULL
/* funkcja usuwa z listy element podany jako parametr */ void out (struct lista *elem) elem struct lista *tmp; HEAD if ((head!= NULL) && (elem!= NULL)) //jeśli lista nie była pusta //i podano prawidłowy element if (elem == head) //jeśli to "głowa" ma być usunięta tmp= head->wsk; //zmienna tymczasowa wskazuje na element za "głową" free(head); //"głowa" jest usuwana zwolnienie pamięci head = tmp; //zmienna tymczasowa staje się nową "głową" tmp HEAD
else //jeśli inny element niż głowa tmp=head; //zaczynamy od głowy while(tmp->wsk!= elem) //przejście do elementu wskazującego na elem tmp=tmp->wsk; tmp->wsk = elem->wsk; //zmiana powiązania free(elem); //zwolnienie pamięci usunięcie elementu else printf("nie mogę usunąć lista pusta lub brak pogo elementu"); tmp elem
/* funkcja dodaje nowy element do listy tak, aby była uporządkowana rosnąco według pola z danymi */ void in_order(int data) struct lista *tmp, *poprzedni; tmp = head; while (tmp-> < data) poprzedni = tmp; tmp = tmp->wsk; in(data,poprzedni); //zmienne pomocnicze //zaczynamy od początku listy //warunek pętli może dotyczyć innego pola //zapamiętanie elementu //przejście do następnego elementu //dodanie elementu w odpowiednim miejscu
void print_list(void) //analogicznie jak w przypadku kolejki struct list *tmp; tmp = head; while(tmp!= NULL) printf("element listy: %d", tmp->); tmp = tmp->wsk; void empty_list(void) //usunięcie wszystkich elementów listy while (head!= NULL) out(head); //w kolejce "głowa" jest usuwana domyślnie //w liście nie
Lista jednokierunkowa cykliczna sąsiadem ostatniego elementu jest element pierwszy funkcję elementu head może pełnić dowolny element w przypadku listy jednoelementowej, element wskazuje sam na siebie
Lista dwukierunkowa każdy element, oprócz elementów skrajnych, "zna" obu swoich sąsiadów elementy skrajne mają i "znają" tylko jednego sąsiada nie ma konieczności "pamiętania" pierwszego (head) lub ostatniego (tail) elementu listę dwukierunkową można łatwo przeszukiwać w obu kierunkach należy jednak "pamiętać" jeden z jej elementów
Lista dwukierunkowa Graficzna prezentacja listy dwukierunkowej in out NULL NULL NULL
Lista dwukierunkowa cykliczna Graficzna prezentacja listy dwukierunkowej cyklicznej
Implementacja listy dwukierunkowej w języku C struct lista int ; struct lista *pop; struct lista *nast; ; struct lista *head; //definicja pojedynczej struktury // mogą być innego typu, może być też więcej pól //wskaźnik wskazujący na poprzedni element //wskaźnik wskazujący na następny element //deklaracja elementu na początku listy ("głowa") // jest to zmienna globalna // zamiast "głowy" można pamiętać dowolny element // nie ma konieczności pamiętania "ogona" // wygodnie jest jednak pamiętać "głowę", "ogon" // i element bieżący
/* funkcja dodaje nowy element za elementem podanym jako parametr */ void in (int data, struct lista *elem) elem elem struct lista *tmp; tmp = malloc(sizeof(struct lista); tmp tmp-> = data; if (head!= NULL) //jeśli lista nie jest pusta if ((elem!= NULL) && (elem->nast!= NULL)) tmp->nast = elem->nast; elem->nast = tmp tmp->pop = elem; elem = tmp->nast; elem->pop = tmp; //wstawiamy w środku
if ((elem!= NULL) && (elem->nast == NULL)) //wstawiamy na końcu elem tmp tmp->nast = NULL; tmp->pop = elem; elem->nast = tmp; if (elem == NULL) //wstawiamy na początku HEAD tmp HEAD NULL tmp->nast = head; tmp->pop = NULL; head->pop = tmp; head = tmp; elem else NULL HEAD //jeśli lista była pusta head = tmp; //powstaje nowa "głowa" head->nast = head->pop = NULL; NULL NULL
/* funkcja usuwa z listy element podany jako parametr */ void out (struct lista *elem) struct lista *tmp; if ((head!= NULL) && (elem!= NULL)) //jeśli lista nie była pusta //i podano prawidłowy element if (elem == head) tmp= head->nast; free(head); head = tmp; head->pop=null; /jeśli to "głowa" ma być usunięta //ustawiamy tmp na element za "głową" //"głowa" jest usuwana zwolnienie pamięci //zmienna tymczasowa staje się nową "głową" //"głowa" nie ma elementu poprzedzającego
if (elem->nast == NULL) //jeśli jest to ostatni element tmp=elem->pop; //ustawiamy tmp na poprzedni element free(elem); //zwolnienie pamięci usunięcie elementu tmp->nast = NULL; //ostatni element nie ma następnika else //jeśli to element w środku listy tmp=elem->pop; //ustawiamy tmp na poprzedni element tmp->nast = elem->nast; //wiążemy tmp z kolejnym elementem free(elem); //zwolnienie pamięci usunięcie elementu elem = tmp->nast; //używamy zmiennej elem jako pomocniczej elem->pop = tmp; //aby powiązać ze sobą elementy else printf("nie mogę usunąć lista pusta lub brak pogo elementu");
Lista dwukierunkowa z jednym wskaźnikiem taka realizacja jest możliwa, gdy: wskaźnik można traktować jako liczbę i wykonywać na niej działania bitowe wskaźnik pusty równy jest zero w takim przypadku pojedynczy wskaźnik zawiera różnicę symetryczną (alternatywę rozłączną) [funkcja xor] wartości liczbowej wskaźników na element poprzedni i następny podczas przechodzenia listy należy przechowywać adres poprzednio odwiedzonego elementu na podstawie własności A (A B) = B można z zakodowanej liczby wyciągnąć poprzedni lub następny element, w zależności od kierunku przeglądania listy
Drzewo (tree) z matematycznego punktu widzenia drzewo jest to graf spójny i acykliczny spójny z każdego wierzchołka drzewa można dotrzeć do każdego innego wierzchołka acykliczny bez cykli; dowolne dwa wierzchołki można połączyć tylko w jeden sposób Przykłady drzew:
Drzewo (tree) w informatyce jest to struktura danych, reprezentująca drzewo matematyczne w naturalny sposób odzwierciedla hierarchię danych drzewa ułatwiają i przyspieszają wyszukiwanie danych oraz ułatwiają zarządzanie posortowanymi danymi zastosowanie: bazy danych, grafika komputerowa, przetwarzanie tekstu, telekomunikacja
Budowa drzewa drzewo składa się z węzłów (node) oraz krawędzi (branch), które je łączą węzeł początkowy (najwyższy) jest korzeniem drzewa (root) węzeł nie mający potomków (węzłów podrzędnych) nazywany jest liściem (leaf) węzeł nie będący ani korzeniem ani liściem nazywamy węzłem wewnętrznym ciąg krawędzi łączących węzły nazywamy ścieżką (path) wysokość drzewa to długość najdłuższej ścieżki prowadzącej od korzenia do liścia
Podstawowe operacje na drzewie dodanie nowego elementu w określonym miejscu drzewa usunięcie określonego elementu z drzewa wyliczenie (wypisanie) wszystkich elementów drzewa wyszukanie określonego elementu w drzewie Metody przechodzenia po drzewie preorder najpierw rodzic, następnie potomkowie postorder najpierw potomkowie, następnie rodzic inorder potomek, rodzic, kolejny potomek (drzewa binarne)
Drzewo binarne drzewo binarne to drzewo, którego każdy węzeł ma co najwyżej dwóch potomków drzewo binarne jest dobrym narzędziem wspomagającym sortowanie oraz wyszukiwanie danych przy użyciu rekurencji drzewo binarne może być: pełne każdy węzeł ma dokładnie dwóch potomków kompletne każdy liść ma tę samą głębokość
W kompletnym drzewie binarnym mamy węzłów na poziomie d (d>=0): 2 d węzłów wewnętrznych: 2 d -1 (1+2+4+ +2 d-1 ) wszystkich węzłów: 2 d+1-1 (1+2+4+ +2 d ) Implementacja drzewa za pomocą tablicy: 4 2 1 3 5 6 7 0 1 2 1 2 3 4 5 6 7 2 0 2 1 2 2 rodzic(i)=floor(i/2) lewy_syn(i)=2*i prawy_syn(i)=2*i+1
Implementacja drzewa za pomocą struktury: wskaźniki do rodzica i potomków struct drzewo int ; struct drzewo *przodek; struct drzewo *lewy_syn; struct drzewo *prawy_syn; ; wskaźniki tylko do potomków struct drzewo int ; struct drzewo *lewy_syn; struct drzewo *prawy_syn; ; Zwykle wariant z dwoma wskaźnikami jest wystarczający.
Rekurencyjne przeglądanie drzew binarnych preorder (KLP: korzeń-lewe-prawe; sposób wzdłużny) void KLP(struct drzewo *node) if (node!= NULL) printf("dane z węzła: %d", node->); KLP(node->lewy_syn); KLP(node->prawy_syn);
Rekurencyjne przeglądanie drzew binarnych inorder (LKP: lewe-korzeń-prawe; sposób poprzeczny) void LKP(struct drzewo *node) if (node!= NULL) LKP(node->lewy_syn); printf("dane z węzła: %d", node->); LKP(node->prawy_syn);
Rekurencyjne przeglądanie drzew binarnych postorder (LPK: lewe-prawe-korzeń; sposób wsteczny) void LPK(struct drzewo *node) if (node!= NULL) LPK(node->lewy_syn); LPK(node->prawy_syn); printf("dane z węzła: %d", node->);
Drzewo binarne zorganizowane (uporządkowane) drzewo, w którym dla każdego węzła wszystkie klucze jego lewego poddrzewa są mniejsze od klucza go węzła, a prawego poddrzewa - większe ten typ drzewa znacznie przyspiesza wyszukiwanie danych (dużo mniejsza złożoność obliczeniowa)
Zastosowanie drzew binarnych do obsługi ONP wyrażenie w notacji nawiasowej (c + b) * a + (e / d) wyrażenie w notacji ONP a b c + * d e / + Drzewo można wykorzystać do "rozpięcia" na nim wyrażenia w notacji ONP, a następnie obliczenia wartości tego wyrażenia. + Wyrażenie "rozpięte" na drzewie * / a + d e b c
Algorytm "rozpinania" wyrażenia w ONP na drzewie Elementy czytamy kolejno od końca 1. Pierwszy pobrany element stanowi korzeń drzewa 2. Kolejny element stanowi jego potomka z prawej strony 3. Jeżeli wstawiany element jest: a) znakiem należy powtórzyć punkt 2 b) cyfrą (liczbą) lub literą (zmienną) należy wrócić do najbliższego węzła (cofnąć się o jeden poziom) i sprawdzić, czy ma on już potomka z lewej strony * jeśli nie ma należy go utworzyć i powtórzyć punkty 2 i 3 * jeśli ma wrócić do najbliższego węzła (wyżej) nie posiadającego dwóch potomków i powtórzyć punkty 2 i 3 Wszystkie operacje wykonujemy do wyczerpania elementów.
Obliczanie wartości wyrażenia w ONP "rozpiętego" na drzewie 1. zaczynamy od najniższego poziomu (poddrzewa) lewego potomka korzenia 2. wartość poddrzewa obliczamy od prawej do lewej, czyli wartość prawego potomka, znak operacji w węźle i wartość lewego potomka da nam wyrażenie arytmetyczne do obliczenia 3. usuwamy obu potomków węzła, a jako nową wartość węzła wstawiamy wynik operacji 4. przechodzimy na kolejny (wyższy) poziom 5. gdy dojdziemy do potomka korzenia, powtarzamy punkty 1-4 dla prawego potomka korzenia 6. na koniec obliczamy wartość dla operacji, której znak zapisany jest w korzeniu
Przykład "rozpinania" na drzewie i obliczania wartości wyrażenia w Odwrotnej Notacji Polskiej wyrażenie w notacji ONP 4 2 3 + * 1 5 - + rozpinanie + * - obliczanie + 24 * 20-4 4 + 1 5 4 + 5 1 5 2 3 2 3
Zastosowanie drzewa do kodowania algorytmem Huffmana w roku 1952 Dawid Huffman opracował metodę konstrukcji kodu o zmiennej długości słów kodowych kodowanie opiera się na częstości występowania znaków kod przedstawiony jest w postaci drzewa binarnego jest to drzewo pełne i kompletne jeżeli A jest rozmiarem alfabetu, to drzewo ma A liści i A -1 węzłów wewnętrznych drzewo budujemy od dołu, od liści każdy liść oznaczony jest cyfrą 0 lub 1, a jego umiejscowienie zależy od częstości występowania znaku, który przechowuje
Przykład: kodujemy tekst "eleteleenerget" Częstości poszczególnych liter: e 7 l 2 t 2 n 1 r 1 g 1 Procedura: a) po określeniu częstości znaków tworzymy dla każdego znaku węzeł b) następnie powtarzamy: - tworzymy nowy węzeł dla najrzadziej występujących znaków - oznaczamy gałęzie jako 0 i 1 1 14 0 9 5 1 0 0 1 2 3 1 0 1 0 r 1 1 7 2 1 2 g e l n t
Przykład: kodujemy tekst "eleteleenerget" Kody poszczególnych liter (odczytane wzdłuż ścieżek w drzewie): e 10 l 01 t 000 n 001 r 111 g 110 Zakodowany tekst wejściowy: 100110000100110100011011111010000
Dziękuję za uwagę. Kolejny wykład: 1 czerwca 2016 Zapraszam!