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 2 0 5 3 4 1 fizycznie 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 a1 a3 a2 p q a1 a2 r a3 next[q]ß r next[p] ß q 10 Usuwanie z listy usuwanie węzła q p q a1 a2 r a3 p r a1 a3 q next[p]ß r next[q]ß null a2 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 x head a1 null a2 a3 a4 tail 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 Pop 2 0 1 5 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 1 5 2 3 4 5 6 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 tail 2 3 0 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) Ø wydajność: – insertItem zajmuje czas O(n) (wymaga trawersowania listy) – removeMin, minKey i minElement zajmuje czas O(n) ponieważ wymaga przejścia przez całą listę w celu wyznaczenia minimalnego klucza – 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(n2) 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(n2) – Usuwanie elementów to sekwencja n operacji removeMin co zajmuje czasO(n) Daje to łączny czas działania O(n2) 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 0 A B C E K F L M D 1 G H I J N 2 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 n1, n2, … ,nk takich, że ni jest przodkiem ni+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 A B D C E G ≠ F Porządek węzłów jest istotny!!! B D C E F 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 B D A C E F B D G pełne C E F G kompletne 30 Własności drzew binarnych Ø Ilość węzłów na poziomie d w kompletnym drzewie binarnym wynosi 2d Ø Ilość węzłów wewnętrznych w takim drzewie: 1+2+4+…+2d–1 = 2d –1 (mniej niż połowa!) Ø Ilość wszystkich węzłów: 1+2+4+…+2d = 2d+1 –1 Ø Jak wysokie może być drzewo binarne o n liściach: (n –1)/2 Ø Wysokość drzewa: 2d+1 –1= n à log (n+1) –1 ≤ log (n) 31 Tablicowa implementacja drzewa binarnego 1 A Poziom 0 2 B 5 E 4 D 1 2 A B 20 3 C 21 3 C 6 F 4 D 1 7 G 5 E 2 6 F Na każdym poziomie d mamy 2d elementów 7 G Kompletne drzewo: parent(i) = floor(i/2) left-child(i) = 2i right-child(i) = 2i +1 22 32 Listowa implementacja drzewa binarnego root(T) A B D Każdy węzeł zawiera Dane oraz 3 wskaźniki: • przodek • lewy potomek • prawy potomek C E F G H data 33 Listowa implementacja drzewa binarnego (najprostsza) root(T) A B D Każdy węzeł zawiera Dane oraz 2 wskaźniki: • lewy potomek • prawy potomek C E F G H data 34 Listowa implementacja drzewa (n-drzewa) root(T) A B C D E F D G J H I Każdy węzeł zawiera Dane oraz 3 wskaźniki: • przodek • lewy potomek • prawe rodzeństwo 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 Ø Przykład kodowania Huffmana: – tekst: Ø Kodowanie Huffmana – Jest adaptowane dla każdego tekstu – Składa się z • Słownika, mapującego każdą literę tekstu na ciąg binarny • Kod binarny (prefix-free) Ø Prefix-free – Korzysta się z łańcuchów o zmiennej długości s1,s2,...,sm , takich że żaden z łańcuchów si nie jest prefixem sj m a n a m t i a m p i a p t i znak częstość kod a 5 10 i 4 01 p 3 111 m 2 000 t 2 001 n 2 110 Zakodowany tekst: a p i 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 znak częstość a 5 i 4 p 3 m 2 t 2 n 2 1 Ø Zbuduj kod z oznaczeń gałęzi 10 znak kod a 10 i 01 p 111 m 000 t 001 n 110 1 0 18 8 0 0 1 5 1 3 p 111 0 2 n 110 1 5 4 a 10 2 4 0 2 i t m 01 001 000 40