Dynamiczne struktury danych AiSD_W4 Dynamiczne struktury danych dr inż. Kustra Piotr Opracowanie: Anna Adrian oraz Kustra Piotr
… Tablice dynamiczne t2 t1 nElem program DynamicVec; var t1 : array of integer; t2 : array [1..10] of integer; nElem: integer; BEGIN read(nElem); write('Ile elementów? ', nElem); setlength(t1, nElem); for i:=1 to nElem do begin t1[i]:=0; end; end. t2 t1 … nElem
Macierz dynamiczna sizeY sizeX program DynamicMatrix; var sizeX, sizeY, i, j:integer; m:array of array of integer; begin size:=15; SetLength(m, sizeX, sizeY); for i:=0 to sizeX-1 do for j:=0 to sizeY-1 do m[i,j]:=1; end; readln(); end. sizeY sizeX
Zbiory dynamiczne Zbiory – w matematyce pojęcie fundamentalne, niezmienne; - w informatyce zbiory mogą być zmieniane w wyniku działania algorytmów (powiększać, zmniejszać, albo zmieniać w czasie) – stąd ich nazwa zbiory dynamiczne. Zbiór dynamiczny do którego można dodawać (wstawiać) elementy, usuwać elementy ze zbioru i sprawdzać czy element należy do zbioru nazywane są słownikami. Metoda realizacji zbiorów dynamicznych zależy od operacji, które na konkretnym zbiorze mogą być wykonane.
Elementy zbioru dynamicznego Każdy element zbioru dynamicznego jest reprezentowany przez obiekt, którego pola można odczytać lub modyfikować, jeśli mamy wskaźnik tego obiektu. W pewnych rodzajach zbiorów dynamicznych wyróżnione jest jedno z pól każdego elementu i nazywane kluczem (key) obiektu. Jeśli każdy element zbioru dynamicznego ma własny unikalny (niepowtarzalny) klucz, wtedy zwykle taki zbiór traktujemy jako zbiór kluczy. W niektórych implementacjach zbiorów dynamicznych zakłada się, że zbiór kluczy jest liniowo uporządkowany Obiekt może zawierać też inne dane (dodatkowe dane) zawarte w innych polach, jednak one nie wpływają na realizację zbioru Istnieje możliwość występowania w obiekcie pól zawierających wartości zmieniające się przez operacje na zbiorze (np. wskaźniki do innych obiektów w zbiorze)
Operacje na zbiorach dynamicznych Wyróżnia się dwie grupy operacji na zbiorach dynamicznych, są to: Zapytania– pozwalające uzyskać pewne informacje na temat zbioru: SEARCH, MINIMUM, MAXIMUM, SUCCESSOR(następca), PREDECESSOR(poprzednik) Operacje modyfikujące– umożliwiające wprowadzanie zmian w zbiorze dynamicznym : INSERT i DELETE. Wymienione operacje uznawane są jako typowe. W konkretnych implementacjach zbiorów dynamicznych zwykle znajdują zastosowanie tylko niektóre z nich.
Operacje na zbiorach dynamicznych Zapytania SEARCH(S,k) - dla danego zbioru S i klucza k zwraca wskaźnik x do takiego obiektu w zbiorze S, dla którego key[x]=k albo NIL- gdy w zbiorze S nie znaleziono żadnego obiektu spełniającego podany warunek. MINIMUM(S) – zwraca w wyniku element zbioru S o najmniej -szym kluczu; dotyczy zbiorów liniowo uporządkowanych. MAXIMUM(S) wskazuje element zbioru S o największym kluczu; dotyczy zbiorów liniowo uporządkowanych.
Operacje na zbiorach dynamicznych Zapytania SUCCESSOR (S,x) - dla danego elementu x o kluczu należącym do uporządkowanego zbioru S daje w wyniku następnik elementu x w S, tj najmniejszy element ze zbioru S który jest większy od x. Jeśli x jest największym elementem w zbiorze S wynikiem jest stała NIL PREDECESSOR (S,x) dla danego elementu x o kluczu należącym do uporządkowanego zbioru S daje w wyniku poprzednik elementu x w S, tj największy element ze zbioru S który jest mniejszy od x. Jeśli x jest najmniejszym elementem w zbiorze S wynikiem jest stała NIL
Operacje na zbiorach dynamicznych Operacje modyfikujące INSERT(S,x) – dodaje do zbioru S wskazany element x. Przy tej operacji zakłada się zwykle, że wcześniej zostały zainicjowane wartości wszystkich pól obiektu wskazanego przez x istotne dla realizacji zbioru S DELETE(S,x)- dla danego wskaźnika x do obiektu zawartego w zbiorze S usuwa ten element ze zbioru S. Warto zauważyć, że argumentem tej operacji jest wskaźnik do elementu x a nie wartość jego klucza. Czas wymagany do realizacji operacji na zbiorze wyrażany jest zwykle jako funkcja rozmiaru zbioru.
Elementarne dynamiczne struktury danych Zbiory dynamiczne mogą być realizowane przez różne struktury danych. W dalszym ciągu wykładu przedstawiono podstawy realizacji elementarnych struktur danych, takich jak: Stosy Kolejki Listy Drzewa
Stosy i kolejki Stosy i kolejki są realizacją zbiorów dynamicznych w których element usuwany jest określony jednoznacznie: stos: strategia LIFO (last in first out) –najpóźniej dodany usuwany jest jako pierwszy kolejka: strategia FIFO (first in first out) _ najwcześniej usuwany jest element najstarszy Stosy i kolejki mogą być efektywnie implementowane na wiele sposobów
Stos Stos zawierający nie więcej niż n elementów można zaimplementować w n elementowej tablicy S[1..n] Atrybut top [S] oznacza numer ostatnio wstawionego elementu do stosu. Stos składa się z elementów: S[1..top[S]], gdzie S[1] jest elementem na dnie stosu S[top[S]] jest elementem na wierzchu stosu 1 2 3 4 5 6 7 S 15 9 top [S] = 4
Podstawowe operacje na stosie Sprawdzam czy stos jest pusty: Stack-Empty(S) if top (S) =0 then return TRUE else return FALSE Jeśli top[S] =0 to stos jest pusty. Na próbę zdjęcia elementu ze stosu pustego powinien pojawić się komunikat o wystąpieniu błędu niedomiaru. Błąd przepełnienia występuje wtedy gdy top[S] > n
Podstawowe operacje na stosie Insert – PUSH PUSH (S,x) top [S] top [S] +1 S[ top [S]] x --------------------------------------------------------------------------- PUSH (S,17) PUSH (S,3) 1 2 3 4 5 6 7 S 15 9 17 top [S] = 6
Podstawowe operacje na stosie Delete - POP POP (S) If STACK-EMPTY (S) then error „niedomiar” else top [S] top [S] -1 return S[ top [S] +1] --------------------------------------------------------------------------- 1 2 3 4 5 6 7 S 15 9 17 top [S] = 5
Kolejki Kolejkę zawierającą nie więcej niż n elementów można zaimplementować w n elementowej tablicy Q[1..n] Atrybut head [Q] wskazuje głowę (początek) kolejki Atrybut tail[Q] wyznacza następną wolną pozycję na którą można wstawić do kolejki nowy element. Jeśli head [Q] = tail[Q] to kolejka jest pusta. Początkowo head [Q] = tail[Q] =1 Jeśli kolejka jest pusta i próbujemy usunąć z niej element występuje wtedy błąd niedomiaru Jeśli head [Q] = tail[Q] +1, to kolejka jest pełna i przy próbie wstawienia kolejnego elementu występuje błąd przepełnienia
Kolejki Kolejka zawiera 5 elementów na pozycjach Q [7..11] 2 3 4 5 6 7 8 9 10 11 12 Q 15 head [Q]=7 tail [Q] = 12 Kolejka zawiera 5 elementów na pozycjach Q [7..11] Elementy kolejki zajmują pozycje head [Q], head [Q]+1,……………, tail[Q]-1 Zakładamy, że Tablica Q jest cykliczna, tzn pozycja 1 jest bezpośrednim następnikiem pozycji n .
Podstawowe operacje na kolejkach ENQUEUE (Q,x) Q [tail [Q]] x if tail [Q]=length [Q] then tail [Q] 1 else tail [Q] tail [Q] +1 ------------------------------------------------------------------------------------------------ ENQUEUE (Q,17); ENQUEUE (Q,3); ENQUEUE (Q,5) 1 2 3 4 5 6 7 8 9 10 11 12 Q 15 17 tail [Q] = 3 head [Q]=7
Podstawowe operacje na kolejkach DEQUEUE (Q) DEQUEUE (Q,x) x Q [head[Q]] if head [Q]=length [Q] then head[Q] 1 else head[Q] head [Q] +1 return x ------------------------------------------------------------------------------------------------ DEQUEUE (Q); 1 2 3 4 5 6 7 8 9 10 11 12 Q 15 17 tail [Q] = 3 head [Q]=8
Kolejka dwustronna W stosach wstawianie i usuwanie elementów jest realizowane tylko na jednym końcu, na górze stosu. W kolejce dodajemy elementy na jednym końcu a usuwamy z drugiego końca. Kolejka dwustronna (dwukierunkowa) jest strukturą danych pozwalającą na wstawianie i usuwanie elementów na obu końcach kolejki. Napisać procedury służące do wstawiania elementów na obu końcach i do usuwania z obu końców kolejki zaimplementowanej w tablicy
Listy Lista z dowiązaniami jest prostą i elastyczną strukturą danych, służącą do reprezentowania zbiorów dynamicznych, umożliwiającą wykonanie wszystkich typowych operacji na zbiorach. Elementy listy ułożone są w porządku liniowym. Porządek określają wskaźniki związane z każdym elementem listy, a nie jak w tablicach, gdzie porządek jest wyznaczony przez indeksy.
Atrybuty listy Każdy element listy dwukierunkowej (doubly linked list) jest rekordem składającym się z trzech pól : key[x] –zawierającego klucz elementu x, prev[x]- wskazującego na poprzednika elementu x, Jeśli prev[x]=NIL to element x nie ma poprzednika, x jest pierwszym elementem listy, mówimy, że x jest wtedy głową (head) listy. Jeśli head[L] = NIL to lista jest pusta next[x] – wskazującego na następnika x na liście Jeśli next [x]=NIL to element x nie ma następnika , czyli x jest ostatnim elementem listy, nazywanym ogonem listy
Rodzaje list Lista jest jednokierunkowa (singly linked list) gdy pomijany jest wskaźnik prev Lista jest posortowana gdy kolejność elementów na liście jest zgodna z porządkiem na ich kluczach; - element o najmniejszym kluczu znajduje się w głowie, - element o największym kluczu w ogonie listy. W liście nieposortowanej kolejność elementów jest dowolna. W liście cyklicznej, elementy tworzą pierścień. Pole prev elementu w głowie wskazuje na ogon, a pole next w ogonie na głowę listy cyklicznej.
Operacje na listach - wyszukiwanie Założenie: listy są dwukierunkowe, nieposortowane. Procedura LIST-SEARCH (L,k) wyznacza pierwszy element o kluczu k na liście L za pomocą prostego sortowania. Czas jej działania wynosi (n) LIST-SEARCH (L,k) x head[L] while x NIL i key[x] k do x next[x] return x Po wywołaniu procedury LIST-SEARCH (L,4) na liście otrzymamy wskaźnik do jej trzeciego elementu, a po wywołaniu LIST-SEARCH (L,7) otrzymamy NIL
Operacje na listach – wstawianie nowych elementów Procedura LIST-INSERT(L,x) przyłącza element x, którego pole key zostało wcześniej zainicjowane, na początek listy. Czas działania procedury na liście o n elementach wynosi O(1) LIST-INSERT(L,x) next[x] head[L] if head[L] NIL then prev[head[L] ] x head[L] x prev[x] NIL ------------------------------------------------------------------------------------------------------------------ Po wykonaniu procedury LIST-INSERT(L,x), gdzie key[x]=25 w głowie listy znajdzie się nowy rekord z kluczem 25 pole next zawiera wskaźnik do poprzedniej głowy z kluczem 9
Operacje na listach –usuwanie elementów z list z dowiązaniami Wywołanie procedury LIST-DELETE (L,x) powoduje usunięcie elementu x z listy L. Wycinanie elementu z listy polega na modyfikacji odpowiednich wskaźników. Czas działania procedury na liście o n elementach wynosi O(1) ale pesymistyczny czas usuwania elementu o zadanym kluczu wynosi (n) ponieważ Najpierw musi zostać wywołana procedura LIST-SEARCH, aby wyznaczyć wskaźnik elementu x
Operacje na listach –usuwanie elementów z list 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] ------------------------------------------------------------------------------ Wynik wykonania operacji LIST-DELETE (L,x), gdzie x wskazuje na element o kluczu 4 z poprzedniej listy
Tablicowe reprezentacje struktur wskaźnikowych Reprezentacja wielotablicowa Dana jest lista: Przedstawmy reprezentację tej listy za pomocą trzech tablic: Tablica key zawiera wartości kluczy znajdujących się na liście Tablice next i prev zawierają wskaźniki Wartości key[x], prev[x] i next[x] określają jeden element listy o indeksie x. Indeksy odgrywają rolę wskaźnika w tablicach key, next i prev
Tablicowa reprezentacja listy (danej) W zmiennej L pamiętany jest numer pozycji (7) na której znajduje się głowa listy. Na liście klucz 4 znajduje się bezpośrednio za elementem o kluczu 16
Lista jednostronnie wiązana – implementacja wskaźnikowa Definicja elementu listy info *next val: integer next: stos H NIL type stos = ^node; node=record val: integer; next: stos; end; 10 -6 12 H NIL
Dodawanie do stosu H pom x procedure PUSH (x:integer); NIL var pom:^node; begin new(pom); pom^.val:=x; pom^.next:=head; head:=pom; end; H NIL x pom
Dodawanie do stosu H pom x1 x2 procedure PUSH (x:integer); var pom:^node; begin new(pom); pom^.val:=x; pom^.next:=head; head:=pom; end; x1 H NIL x2 pom
Usuwanie ze stosu H pom 10 -6 12 procedure POP; var pom:^node; begin if head <>NIL then pom:=head; head:= pom^.next; dispose(pom); end; 10 -6 12 H NIL pom
Przeglądanie listy jednostronnie wiązanej procedure ShowList; var pom:^node; begin pom:=head; write('HEAD --> '); while (pom<>NIL) do write(pom^.val, '--> '); pom:=pom^.next; end; writeln ('NIL'); 10 -6 12 H NIL pom HEAD--> 10-->
Przeglądanie listy jednostronnie wiązanej procedure ShowList; var pom:^node; begin pom:=head; write('HEAD --> '); while (pom<>NIL) do write(pom^.val, '--> '); pom:=pom^.next; end; writeln ('NIL'); 10 -6 12 H NIL pom HEAD--> 10--> -6-> 12-->
Przeglądanie listy jednostronnie wiązanej procedure ShowList; var pom:^node; begin pom:=head; write('HEAD --> '); while (pom<>NIL) do write(pom^.val, '--> '); pom:=pom^.next; end; writeln ('NIL'); 10 -6 12 H NIL pom HEAD--> 10--> -6-> 12--> NIL