Wykład 6 Dynamiczne struktury danych 1
Plan wykładu Ø Wprowadzenie Ø Popularne dynamiczne struktury danych (ADT) Ø stosy, kolejki, listy opis abstrakcyjny Ø Listy liniowe Ø Implementacja tablicowa stosu i kolejki Ø Drzewa Ø Możliwe implementacje 2
Wprowadzenie Ø Do tej pory najczęściej zajmowaliśmy się jedną strukturą danych tablicą. Struktura taka ma charakter statyczny jej rozmiar jest niezmienny. Powoduje to konieczność poznania wymaganego rozmiaru przed rozpoczęciem działań (ewentualnie straty miejsca deklarujemy wystarczająco dużą tablicę). Ø W wielu zadaniach wygodniejsza jest struktura o zmiennym rozmiarze (w zależności od aktualnych potrzeb) struktura dynamiczna. Ø Potrzebujemy struktury pozwalającej na przechowywanie elementów niezależnie od ich fizycznego położenia. logicznie fizycznie 2 0 5 3 4 1 3 2 4 1 0 5 3
Wprowadzenie Ø Przykładowe operacje dla struktur danych: Insert(S, k): wstawianie nowego elementu Delete(S, k): usuwanie elementu Min(S), Max(S): odnajdowanie najmniejszego/największego elementu Successor(S,x), Predecessor(S,x): odnajdowanie następnego/ poprzedniego elementu Ø Zwykle przynajmniej jedna z tych operacji jest kosztowna czasowo (zajmuje czas O(n)). Czy można lepiej? 4
Abstrakcyjne typy danych (Abstract Data Types ADT ) Ø Abstrakcyjnym typem danych nazywany formalną specyfikację sposobu przechowywania obiektów oraz zbiór dobrze opisanych operacji na tych obiektach. Ø Jaka jest różnica pomiędzy strukturą danych a ADT? à struktura danych (klasa) jest implementacją ADT dla specyficznego komputera i systemu operacyjnego. 5
Popularne dynamiczne ADT Ø Listy łączone Ø Stosy, kolejki Ø Drzewa z korzeniem (rooted trees), binarne, BST, czerwonoczarne, AVL itd. Ø Kopce i kolejki priorytetowe Ø Tablice z haszowaniem 6
Listy Ø Lista L jest liniową sekwencją elementów. Ø Pierwszy element listy jest nazywany head, ostatni tail. Jeśli obydwa są równe null, to lista jest pusta Ø Każdy element ma poprzednik i następnik (za wyjątkiem head i tail) Ø Operacje na liście: Successor(L,x), Predecessor(L,x) List-Insert(L,x) List-Delete(L,x) List-Search(L,k) 2 0 2 3 0 1 head x tail 7
Listy łączone Ø Rozmieszczenie fizyczne obiektów w pamięci nie musi odpowiadać ich logicznej kolejności; wykorzystujemy wskaźniki do obiektów (do następnego/poprzedniego obiektu) Ø Manipulując wskaźnikami możemy dodawać, usuwać elementy do listy bez przemieszczania pozostałych elementów listy Ø Lista taka może być pojedynczo lub podwójnie łączona. head a1 a2 a3 an tail null null 8
Węzły i wskaźniki Ø Węzłem nazywać będziemy obiekt przechowujący daną oraz wskaźnik do następnej danej i (opcjonalnie dla listy podwójnie łączonej) wskaźnik do poprzedniej danej. Jeśli nie istnieje następny obiekt to wartość wskaźnika będzie null Ø Wskaźnik oznacza adres obiektu w pamięci Ø Węzły zajmują zwykle przestrzeń: Θ(1) key data next prev struct node { } key_type key; data_type data; struct node *next; struct node *prev; 9
Wstawianie do listy (przykład operacji na liście) wstawianie nowego węzła q pomiędzy węzły p i r: p r p q r a1 a3 a1 a2 a3 a2 next[q]ß r next[p] ß q 10
Usuwanie z listy usuwanie węzła q p q r p r a1 a2 a3 a1 a3 next[p]ß r next[q]ß null a2 q null 11
Operacje na liście łączonej List-Search(L, k) 1. x ß head[l] 2. while x null and key[x] k 3. do x ß next[x] 4. return x List-Insert(L, x) 1. next[x] ß head[l] 2. if head[l] null 3. then prev[head[l]] ß x 4. head[l] ß x 5. prev[x] ß null List-Delete(L, x) 1. if prev[l] null 2. then next[prev[x]] ß next[x] 3. else head[l] ß next[x] 4. if next[l] null 5. then prev[next[x]] ß prev[x] 12
Listy podwójnie łączone head x a1 a2 a3 a4 tail null null Listy cykliczne: łączymy element pierwszy z ostatnim 13
Stosy Ø Stosem S nazywany liniową sekwencję elementów do której nowy element x może zostać wstawiony jedynie na początek, analogicznie element może zostać usunięty jedynie z początku tej sekwencji. Ø Stos rządzi się zasadą Last-In-First-Out (LIFO). Ø Operacje dla stosu: Stack-Empty(S) Pop(S) Push(S,x) Push 2 0 1 5 Pop head null 14
Kolejki Ø Kolejka Q jest to liniowa sekwencja elementów do której nowe elementy wstawiane są na końcu sekwencji, natomiast elementy usuwane są z jej początku. Ø Zasada First-In-First-Out (FIFO). Ø Operacje dla kolejki: Queue-Empty(Q) EnQueue(Q, x) DeQueue(Q) DeQueue EnQueue 2 0 2 3 0 1 head tail 15
Implementacja stosu i kolejki Ø Tablicowa Wykorzystujemy tablicę A o n elementach A[i], gdzie n jest maksymalną ilością elementów stosu/kolejki. Top(A), Head(A) i Tail(A) są indeksami tablicy Operacje na stosie/w kolejce odnoszą się do indeksów tablicy i elementów tablicy Implementacja tablicowa nie jest efektywna Ø Listy łączone Nowe węzły tworzone są w miarę potrzeby Nie musimy znać maksymalnej ilości elementów z góry Operacje są manipulacjami na wskaźnikach 16
Implementacja tablicowa stosu Push(S, x) 1. if top[s] = length[s] 2. then error overflow 3. top[s] ß top[s] + 1 4. S[top[S]] ß x Pop(S) 1. if top[s] = -1 2. then error underflow 3. else top[s] ß top[s] 1 4. return S[top[S] +1] 0 1 2 3 4 5 6 1 5 2 3 top Kierunek wstawiania Stack-Empty(S) 1. if top[s] = -1 2. then return true 3. else return false 17
Implementacja tablicowa kolejki Dequeue(Q) 1. x ß Q[head[Q]] 2. if head[q] = length[q] 3. then head[q] ß 1 4. else head[q] ß (head[q]+1) mod n 5. return x 1 5 2 3 0 tail head Enqueue(Q, x) 1. Q[tail[Q]] ß x 2. if tail[q] = length[q] 3. then tail[q] ß x 4. else tail[q] ß (tail[q]+1) mod n 18
Abstrakcyjny typ danych dla kolejki priorytetowej Ø Kolejka priorytetowa przechowuje dowolne obiekty Ø Każdy z obiektów jest parą (klucz, element) Ø Podstawowe metody dla kolejki priorytetowej: insertitem(k, o) dodaje obiekt o kluczu k i elemencie o removemin() usuwa element kolejki o najmniejszym kluczu 19
Abstrakcyjny typ danych dla kolejki priorytetowej Ø Dodatkowe metody minkey(k, o) zwraca (ale nie usuwa) najmniejszą wartość klucza minelement() zwraca (ale nie usuwa) element o najmniejszym kluczu size(), isempty() Ø Zastosowania: Algorytmy grafowe Systemy aukcyjne Kodowanie Systemy giełdowe 20
Relacja porządku Ø Elementy w kolejce priorytetowej pochodzą ze zbioru uporządkowanego Ø Dwa rozróżnialne obiekty mogą mieć te samą wartość klucza Ø Relacja porządku Zwrotna: x x Antysymetryczna: x y y x x = y Przechodnia: x y y z x z 21
Sortowanie z wykorzystaniem kolejki priorytetowej Ø Łatwo wykorzystać kolejkę priorytetową do sortowania obiektów: Wstawiamy obiekty do kolejki priorytetowej operacje insertitem(e, e) dla każdego obiektu e Usuwamy obiekty z kolejki poprzez sekwencję operacji removemin() Ø Złożoność obliczeniowa zależna od sposobu implementacji kolejki priorytetowej Algorithm PQ-Sort(S, C) Input sequence S, comparator C for the elements of S Output sequence S sorted in increasing order according to C P priority queue with comparator C while!s.isempty () e S.remove (S. first ()) P.insertItem(e, e) while!p.isempty() e P.minElement() P.removeMin() S.insertLast(e) 22
Implementacja sekwencyjna Ø Implementacja w postaci nieposortowanej sekwencji Wstawiamy elementy do listy liniowej w porządku w jakim się pojawiają Ø Implementacja w postaci posortowanej sekwencji Wstawiamy elementy do listy liniowej tak aby pozostawała ona posortowana Ø wydajność: insertitem zajmuje czas O(1) (wstawianie na początek listy) removemin, minkey i minelement zajmuje czas O(n) ponieważ wymaga przejścia przez całą listę w celu wyznaczenia minimalnego klucza Ø wydajność: insertitem zajmuje czas O(n) (wymaga trawersowania listy) removemin, minkey i minelement zajmuje czas O(1) ponieważ element o minimalnym kluczu znajduje się na początku listy 23
Selection-Sort Ø Sortowanie przez wybór może być rozumiane jako wariacja PQ-sort z wykorzystaniem nieposortowanej sekwencji Ø Czas działania: 1. Wstawianie do kolejki to n operacji insertitem co zabiera czas O(n) 2. Usuwanie n elementów z kolejki to ciąg operacji removemin o czasie: n + (n -1) + + 1 Ø Daje to łączny czas działania O(n 2 ) 24
Insertion-Sort Ø Sortowanie przez wstawianie odpowiada PQsort przy wykorzystaniu implementacji kolejki priorytetowej poprzez posortowaną sekwencję elementów Ø Czas działania: Wstawianie elementów zajmuje odpowiednio czas proporcjonalny do: 1 + 2 + + n czyli O(n 2 ) Usuwanie elementów to sekwencja n operacji removemin co zajmuje czaso(n) Daje to łączny czas działania O(n 2 ) 25
Drzewa z korzeniem Ø Drzewem z korzeniem T nazywamy ADT dla którego elementy są zorganizowane w strukturę drzewiastą. Ø Drzewo składa się z węzłów przechowujących obiekt oraz krawędzi reprezentujących zależności pomiędzy węzłami. Ø W drzewie występują trzy typy węzłów: korzeń (root), węzły wewnętrzne, liście Ø Własności drzew: Istnieje droga z korzenia do każdego węzła (połączenia) Droga taka jest dokładnie jedna (brak cykli) Każdy węzeł z wyjątkiem korzenia posiada rodzica (przodka) Liście nie mają potomków Węzły wewnętrzne mają jednego lub więcej potomków (= 2 à binarne) 26
Drzewa z korzeniem A 0 B C D 1 E F G H I J 2 K L M N 3 27
Terminologia Ø Rodzice (przodkowie) i dzieci (potomkowie) Ø Rodzeństwo (sibling) potomkowie tego samego węzła Ø Relacja jest dzieckiem/rodzicem. Ø Poziom węzła Ø Ścieżka (path): sekwencja węzłów n 1, n 2,,n k takich, że n i jest przodkiem n i+1. Długością ścieżki nazywamy liczbę k. Ø Wysokość drzewa: maksymalna długość ścieżki w drzewie od korzenia do liścia. Ø Głębokość węzła: długość ścieżki od korzenia do tego węzła. 28
Drzewa binarne Ø Drzewem binarnym T nazywamy drzewo z korzeniem, dla którego każdy węzeł ma co najwyżej 2 potomków. A B C A B C D E F D E F G Porządek węzłów jest istotny!!! G 29
Drzewa pełne i drzewa kompletne Ø Drzewo binarne jest pełne jeśli każdy węzeł wewnętrzny ma dokładnie dwóch potomków. Ø Drzewo jest kompletne jeśli każdy liść ma tę samą głębokość. A A B C B C D E D E F G F G pełne kompletne 30
Własności drzew binarnych Ø Ilość węzłów na poziomie d w kompletnym drzewie binarnym wynosi 2 d Ø Ilość węzłów wewnętrznych w takim drzewie: 1+2+4+ +2 d 1 = 2 d 1 (mniej niż połowa!) Ø Ilość wszystkich węzłów: 1+2+4+ +2 d = 2 d+1 1 Ø Jak wysokie może być drzewo binarne o n liściach: (n 1)/2 Ø Wysokość drzewa: 2 d+1 1= n à log (n+1) 1 log (n) 31
Tablicowa implementacja drzewa binarnego 1 A 2 3 B C 4 5 6 7 D E F G Poziom 0 1 2 Na każdym poziomie d mamy 2 d elementów 1 2 3 4 5 6 7 A B C D E F G 2 0 2 1 2 2 Kompletne drzewo: parent(i) = floor(i/2) left-child(i) = 2i right-child(i) = 2i +1 32
Listowa implementacja drzewa binarnego root(t) A B C Każdy węzeł zawiera Dane oraz 3 wskaźniki: przodek lewy potomek prawy potomek D E F G H data 33
Listowa implementacja drzewa binarnego (najprostsza) root(t) A B C Każdy węzeł zawiera Dane oraz 2 wskaźniki: lewy potomek prawy potomek D E F G H data 34
Listowa implementacja drzewa (n-drzewa) root(t) A D B E C D F G H I Każdy węzeł zawiera Dane oraz 3 wskaźniki: przodek lewy potomek prawe rodzeństwo J K 35
Przykład zastosowania - Algorytm kodowania Huffmana Ø David Huffman (1952) wymyślił sprytną metodę konstrukcji optymalnego kody prefixowego (prefix-free) o zmiennej długości słów kodowych Kodowanie opiera się o częstość występowania znaków Ø Optymalny kod jest przedstawiony w postaci drzewa binarnego Każdy węzeł wewnętrzny ma 2 potomków Jeśli C jest rozmiarem alfabetu to ma ono C liści i C -1 węzłów wewnętrznych 36
Algorytm kodowania Huffmana Ø Ø Ø Budujemy drzewo od liści (bottom-up) Zaczynamy od C liści Przeprowadzamy C -1 operacji łączenia Niech f [c] oznacza częstość znaku c w kodowanym tekście Wykorzystamy kolejkę priorytetową Q, w której wyższy priorytet oznacza mniejszą częstotliwość znaku: GET-MIN(Q) zwraca element o najniższej częstości i usuwa go z kolejki 37
Algorytm Huffmana wejście: alfabet C i częstości f [ ] wyjście: drzewo kodów optymalnych dla C HUFFMAN(C, f ) n C Q C for i 1 to n-1 z New-Node( ) x z.left GET-MIN(Q) y z.right GET-MIN(Q) f [z] f [x] + f [y] INSERT(Q, z) return GET-MIN(Q) Czas wykonania O(n lg n) 38
Kody Huffmana Ø Kodowanie Huffmana Jest adaptowane dla każdego tekstu Ø Przykład kodowania Huffmana: tekst: Składa się z m a n a m a m a p a Słownika, mapującego każdą literę tekstu na ciąg binarny Kod binarny (prefix-free) t i p i t i p i Ø Prefix-free Korzysta się z łańcuchów o zmiennej długości s 1,s 2,...,s m, takich że żaden z łańcuchów s i nie jest prefixem s j znak częstość kod a 5 10 i 4 01 p 3 111 m 2 000 t 2 001 Zakodowany tekst: n 2 110 000 10 110 10 000 10 000 10 111 10 001 01 111 01 001 01 111 01 m a n a m a m a p a t i p i t i p i 39
Budowanie kodów Huffmana Ø Znajdujemy częstości znaków Ø Tworzymy węzły (wykorzystując częstości) Ø powtarzaj Stwórz nowy węzeł z dwóch najrzadziej występujących znaków (połącz drzewa) Oznacz gałęzie 0 i 1 Ø Zbuduj kod z oznaczeń gałęzi znak kod a 10 i 01 p 111 m 000 t 001 n 110 1 1 5 0 10 znak częstość a 5 i 4 p 3 m 2 t 2 n 2 1 0 18 0 1 3 2 5 4 2 2 8 0 4 1 0 p n a i t m 111 110 10 01 001 000 40