STRUKTURY DANYCH Janusz Marecki Struktury danych 2 Słowo wstępne Słowo wstępne Jednym z istotnych zadań informatyki jest przetwarzanie i przesyłanie informacji. Realizacja tych zadań wymaga coraz bardziej skomplikowanych programów komputerowych. Duże znaczenie ma przy tym opracowanie ogólnych zasad, struktur i języków programowania. Książka STRUKTURY DANYCH Pana Janusza Mareckiego, którą mam przyjemność Państwu przedstawić dotyczy aktualnej w informatyce problematyki. Autor w sposób logiczny i systematyczny opisuje kolejne struktury danych: listy, stosy, kolejki, odwzorowania, drzewa, grafy, zbiory i słowniki. Implementację tych struktur przedstawiono jasno w języku Pascal. Ponadto każdy rozdział jest zakończony przykładem zastosowaniia omawianej struktury danych. STRUKTURY DANYCH mogą być z powodzeniem wykorzystywane w przedmiotach: podstawy informatyki, podstawy programowania lub bazy danych i wiedzy. Dobrym ćwiczeniem może być implementacja przedstawionych struktur w innych językach programowania. Niniejsza publikacja stanowi dobrą pomoc dydaktyczną dla studentów Wyższej Szkoły Informatyki i Zarządzania w Bielsku-Białej. Może być również pomocny dla studentów informatyki innych uczelni. Prof. zw. dr hab. inż. Andrzej Grzywak Dyrektor Instytutu Informatyki Politechniki Śląskiej w Gliwicach 3 Struktury danych 4 Słowo wstępne Spis treści 1 2 3 4 5 Listy .....................................................................................................................................9 1.1 Wprowadzenie......................................................................................................................... 9 1.2 Operacje ADT LIST ............................................................................................................. 12 1.3 Tablicowa implementacja ADT LIST................................................................................. 15 1.4 Wskaźnikowa implementacja ADT LIST........................................................................... 19 1.5 Porównanie implementacji tablicowej i wskaźnikowej ..................................................... 23 1.6 Implementacja kursorowa ADT LIST. Symulacja pamięci komputera.......................... 24 1.7 Przykład zastosowania ADT LIST...................................................................................... 30 Stosy...................................................................................................................................31 2.1 Wprowadzenie....................................................................................................................... 31 2.2 Operacje ADT STACK ........................................................................................................ 31 2.3 Tablicowa implementacja ADT STACK ............................................................................ 33 2.4 Wskaźnikowa implementacja ADT STACK ...................................................................... 35 2.5 Przykład zastosowania ADT STACK ................................................................................. 38 Kolejki................................................................................................................................39 3.1 Wprowadzenie....................................................................................................................... 39 3.2 Operacje ADT QUEUE ........................................................................................................ 40 3.3 Wskaźnikowa implementacja ADT QUEUE ..................................................................... 41 3.4 Implementacja kolejki cyklicznej........................................................................................ 44 3.5 Przykład zastosowania ADT QUEUE................................................................................. 47 Odwzorowania...................................................................................................................48 4.1 Wprowadzenie....................................................................................................................... 48 4.2 Operacje ADT MAPPING ................................................................................................... 48 4.3 Tablicowa implementacja ADT MAPPING....................................................................... 49 4.4 Listowa implementacja ADT MAPPING ........................................................................... 50 4.5 Przykład zastosowania ADT MAPPING............................................................................ 51 Drzewa ...............................................................................................................................52 5.1 Wprowadzenie....................................................................................................................... 52 5.2 Operacje ADT TREE ........................................................................................................... 55 5.3 Tablicowa implementacja ADT TREE ............................................................................... 56 5.4 Implementacja ADT TREE za pomocą list dzieci ............................................................. 59 5.5 Implementacja ADT TREE przez „sąsiada i dziecko”...................................................... 62 5 Struktury danych 5.6 Implementacja ADT TREE przez „sąsiada, dziecko i rodzica” ....................................... 66 5.7 Drzewa binarne ..................................................................................................................... 67 5.8 Przykład zastosowania ADT TREE .................................................................................... 70 6 Grafy skierowane.............................................................................................................. 72 6.1 Wprowadzenie....................................................................................................................... 72 6.2 Operacje ADT GRAPH ........................................................................................................ 74 6.3 Macierzowa implementacja Grafu ...................................................................................... 75 6.4 Implementacja grafu przez listy sąsiedztwa ....................................................................... 77 6.5 Przykład zastosowania ADT GRAPH................................................................................. 81 7 Zbiory ................................................................................................................................ 82 7.1 Wprowadzenie....................................................................................................................... 82 7.2 Operacje ADT SET............................................................................................................... 82 7.3 Bitowo – Wektorowa implementacja ADT SET ................................................................ 85 7.4 Implementacja ADT SET przez listy uporządkowane ...................................................... 86 7.5 Przykład zastosowania ADT SET........................................................................................ 92 8 Słowniki ............................................................................................................................ 93 8.1 Wprowadzenie....................................................................................................................... 93 8.2 Operacje ADT DICTIONARY ............................................................................................ 93 8.3 Tablicowo-Kursorowa implementacja ADT DICTIONARY ........................................... 94 8.4 Implementacja ADT DICTIONARY przez Haszowanie Otwarte ................................... 95 8.5 Implementacja ADT DICTIONARY przez Haszowanie Zamknięte ............................... 99 8.6 Przykład zastosowania ADT DICTIONARY ................................................................... 102 9 10 6 Pytania kontrolne ........................................................................................................... 103 Literatura..................................................................................................................... 106 Słowo wstępne Przedmowa Na wstępie chciałbym podziękować wszystkim, którzy zdecydowali się skorzystać z tego właśnie skryptu. Omawiane tutaj zagadnienia starałem się przedstawić możliwie przejrzyście i zrozumiale. Algorytmy i implementacje przedstawione są w języku Pascal. Mimo, iż wielu programistów uważa, że jest to język, którego złoty wiek już się skończył, moim zdaniem, do prezentacji problemów informtycznych nadaje się on doskonale. Zanim zagłębimy się w same struktyry danych, chciałbym w tym miejscu podziękować wyszystkim, którzy w mniejszym, bądź większym stopniu pomogli mi przenieść zebraną podczas nauki wiedzę na kartki tego skryptu. Nie sposób tutaj kogokolwiek faworyzować, dlatego uchylę się od monotonnej listy nazwisk. Pragnę po prostu okazać moją wdzięczność i prosić Ciebie, drogi czytelniku o wyrozumiałość ! Czym są struktury danych? Każdy program komputerowy korzysta z zasobów komputera, głównie z pamięci operacyjnej i procesora. Procesor, mimo iż może wykonywać skomplikowane obliczenia na liczbach zapisanych w systemie dwójkowym, nie potrafi interpretować wszystkich typów danych, które spotykamy w językach programowania. Dopiero kompilatory tłumaczą program oparty na liczbach rzeczywistych, ciągach napisów i typach wyliczeniowych na kod maszynowy kierujący pracą procesora. Dzięki temu programy stają się bardziej zrozumiałe, nawet dla samego programisty. Kompilatory zwykło się uważać za pierwsze ogniwo w łańcuchu rozwoju oprogramowania. Kolejnym etapem w tymże rozwoju było stworzenie nowych, bardziej intuicyjnych typów danych, typów, z którymi mamy kontakt na co dzień. Tak powstały struktury danych, będące poniekąd zestawieniem danych niższego poziomu, czyli znajdujących się we wcześniejszych ogniwach omawianego łańcucha. Dla przykładu weźmy omawianą w tym skrypcie strukturę kolejki. W informatyce ma ona takie samo znaczenie, jak w rzeczywistości. Przeważnie widząc kolejkę zadajemy pytanie: „kto stoi na jej czele?”, chcemy także wiedzieć kiedy ktoś dochodzi do kolejki, albo zostaje obsłużony. Tak sformułowane pytania będziemy kierowali bezpośrednio do struktury danych, nie przejmując się, w jaki sposób zostanie to przetłumaczone na język procesora. Komunikacja z naszą strukturą będzie więc bardzo prosta i intuicyjna. Dla kogo są struktury danych ? 7 Struktury danych Starałem się ten skrypt napisać tak, by mógł z niego skorzystać każdy użytkownik komputera, który miał kontakt z programowaniem. Niewątpliwie dużym ułatwieniem dla czytelnika jest znajomość Pascala, w którym napisane są wszystkie zamieszczone tutaj programy. Osobiście polecam ten skrypt studentom, którzy mają za sobą wstęp do programowania i nie gubią się w typie wskaźnikowym. „Programistą staje się ten, kto pisze programy”, dlatego uważam, że dobrym ćwiczeniem, które pomogłoby każdemu zaznajomić się z omawianym w tym skrypcie materiałem, byłoby „przetłumaczenie” go na inny język programowania. Jak czytać ten skrypt ? Cały skrypt podzielony jest tematycznie na 8 części, w skład których wchodzą: listy, stosy, kolejki, odwzorowania, drzewa, grafy, zbiory i słowniki. Na początku każdego rodziału, który rozpoczyna się krótkim wprowadzeniem, wypisane są wszystkie operacje, które będą charakterystyczne dla omawianej struktury danych (w przypadku kolejki, będą to operacje opisujące dodanie elementu na koniec kolejki, sprawdzenie co znajduje się na początku kolejki itp.) . W dalszej części rozdziału podane są co najmniej dwa różne sposoby implementacji w Pascalu danej struktury danych wraz z jej operacjami. Każdy rozdział kończy się krótkim przykładem ilustrującym zastosowanie omawianej struktury danych. Osobiście zalecam każdemu czytelnikowi zaznajomienie się z pierwszym rozdziałem, czyli z listami, nim przejdzie on do bardziej skomplikowanych struktur danych. Ponadto dla poprawnego zrozumienia rozdziału 6, opisującego grafy, należy koniecznie przeczytać rozdział 5, związany z drzewami. Jedną z istotnych cech sztuki programowania jest przyjęcie, że żaden napisany program nie jest doskonały, zawsze można go ulepszyć i poprawić. To samo dotyczy wszystkich moich programów, które wcale nie uważam za całkowicie doskonałe. Przeciwnie, za wszelkie uwagi na ich temat będę Tobie, Drogi Czytelniku, szczerze wdzięczny ! Janusz Marecki Bielsko-Biała, wrzesień 1999 roku 8 Listy 1 Listy 1.1 Wprowadzenie Z punktu widzenia programisty, listy są doskonałym sposobem na zorganizowanie i ułożenie danych w dobry porządek. Wynika z tego, że każdą niepustą listę charakteryzują następujące wyróżniki: • Posiada element pierwszy • Posiada element ostatni • Każdy element listy może posiadać tylko jednego poprzednika • Każdy element listy może posiadać tylko jednego następnika Trudno sobie wyobrazić dzisiejsze oprogramowanie bez istnienia list. W każdym środowisku graficznym mamy przecież do czynienia z różnego rodzaju suwakami, rozwijanymi menu, nakładającymi się kartami itp. Wykorzystanie list można bardzo wyraźnie zauważyć w systemach, które prezentują swoje dane w oknach. Stosuje się tutaj odpowiednią listę kolejności, która zawiera odnośniki do poszczególnych okien. Program, który ma za zadanie odświeżyć obraz, wywołuje funkcję rysowania dla kolejnych elementów tej listy, a użytkownik ma wrażenie, że okna są na wierzchu, lub pod spodem. W momencie kliknięcia na jakiekolwiek okno, jego odnośnik wędruje na koniec listy kolejności i wywoływany jest program odświeżający obraz. Każdy edytor tekstu jest zorganizowany przez wiele różnego rodzaju list. Kolejne linijki zapisane słowami są w rzeczywistości elementami listy, dlatego program wyświetlający tekst na ekranie musi posiadać odnośnik do pierwszego obiektu listy (w tym wypadku pierwszej linii tekstu). Przemieszczając się po tekście, aktualizujemy ten odnośnik, co wywołuje program odświeżania obrazu. W obu tych przykładach, w których stosuje się listy zaskakuje nas funkcjonalność oraz wyjątkowo prosta implementacja. Każda lista składa się z połączonych ze sobą obiektów. Z kolei każdy obiekt przechowuje dwie informacje: • Merytoryczną, widoczną na zewnątrz systemu, czyli współpracującą z jego użytkownikiem np.: tekst, kolor itp. • Organizacyjną, widoczną wewnątrz systemu, którą jest odnośnik (bądź kilka odnośników) do innego elementu danej listy. 9 Struktury danych Powyższa struktura obiektu kryje jednak w sobie dosyć istotne rozszerzenie, które jest powszechnie stosowane w programach komputerowych. Wyobraźmy sobie listę obiektów, których informacją merytoryczną są odnośniki do innych list (niekoniecznie tego samego typu) . Powoduje to, że pozornie jednowymiarowa lista zawiera w sobie podlisty, które mogą z kolei dalej się rozszerzać. Powstaje w ten sposób wielowymiarowa struktura, w której można się jednak dosyć sprawnie poruszać. Przykładem może być tutaj wielopoziomowe rozwijane menu, które spotykamy w systemach WINDOWS. Gdy informacją organizacyjną jest jeden odnośnik (do poprzednika lub następnika), wówczas mówimy o liście jednokierunkowej. Gdy występują dwa odnośniki (do poprzednika i następnika) wtedy mamy do czynienia z listą dwukierunkową. Istnieje także wiele innych list, których obiekty posiadają co najmniej 2 różne odnośniki. Są to listy wielokierunkowe, lub wielokrotnie wiązane. Mówimy wtedy o powstawaniu bardziej słożonych struktur informatycznych, jak np.: drzewa lub grafy, które omówione będą później. Warto w tym miejscu zaznaczyć, że istnieje różnica między listą dwukierunkową, a listą podwójnie wiązaną. Dla każdego obiektu listy dwukierunkowej zachodzi bowiem reguła: Następnik Poprzednika = Poprzednik Następnika = Ten sam obiekt Listy jednokierunkowe posiadają jedną, dosyć ważną wadę. Jeśli jest to lista, której obiekty posiadają odnośniki do następników, wówczas każdorazowe wyznaczenie poprzednika danego obiektu wymaga przejścia niekiedy nawet całej listy, co z kolei znacznie spowalnia całą operację. Tak więc kosztem zaoszczędzenia pamięci komputera (w przypadku list jednokierunkowych ich obiekty muszą pamiętać tylko jeden odnośnik) wydłużamy czas niektórych operacji. Przy dalszych zagadnieniach dotyczących list musimy wprowadzić dwa nowe pojęcia: • Head – głowa listy. • Tail – ogon listy. Głowa i Ogon listy są bez wątpienia jej obiektami, jednak posiadają one pewne specyficzne cechy: • Ich informacja merytoryczna jest nieistotna. • Głowa listy nie posiada odnośnika do swojego poprzednika, a Ogon do następnika 10 Listy Rysunek 1 Schemat jednokierunkowej listy wskaźnikowej z głową i ogonem Jeśli założymy, że nasza lista posiada zarówno Głowę jak i Ogon, wówczas nawet lista pusta będzie się składała z dwóch obiektów, bowiem Head i Tail nie mogą zostać z niej usunięte. Wprowadzenie Head i Tail znacznie upraszcza implementację listy i zwiększa jej niezawodność, gdyż nie musimy się przejmować w którym miejscu dodajemy obiekt, bądź też z którego miejsca go usuwamy. Przy tworzeniu większych aplikacji dużą wygodą jest posiadanie gotowego zestawu funkcji operujących na listach. W dalszej części tego rozdziału stworzymy nowy typ danych – typ listowy: ADT LIST (Abstract Data Type LIST). 11 Struktury danych 1.2 Operacje ADT LIST Każdy nowy typ danych posiada zestaw procedur i funkcji, które go obsługują i modyfikują. Ponadto zmienne nowego typu posiadają szereg parametrów, bez których nie mogłyby istnieć. Na początku należy wprowadzić dwa typy: • ElementType – jest to rodzaj informacji merytorycznej zawartej w obiektach listy • Position – jest to pozycja w liście, która w zależności od rodzaju implementacji może być typu wskaźnikowego lub wyliczeniowego Należy jeszcze zapoznać się z pojęciem Pozycji „za ostatnim elementem”. Jeśli pozycja ostatniego elementu jest dla nas zrozumiała, to jej następnik jest właśnie „Pozycją za ostatnim elementem”. W implementacji znajdują się także dwie wartości stałe: • EmptyElement typu ElementType – jest to pusty element (jego informacja merytoryczna nie istnieje) • NullPosition typu Position – jest to oznaczenie nieistniejącej pozycji. Teraz możemy już wyróżnić podstawowe funkcje ADT LIST: function _END(L : List) : Position; Funkcja ta zwraca pozycję elementu za ostatnim w liście L, która jest różnie zdefiniowana w zależności od implementacji ADT LIST function INSERT(X : ElementType; P : Position; var L : List) : Position; Funkcja wstawia do listy L element X na pozycję określaną przez P. Jeśli operacja się powiodła, zwraca wskaźnik do dodanego elementu w liście L. Jeśli operacja się nie powiodła, zwraca wskaźnik do elementu za ostastnim w liście L. function DELETE(P : Position; var L : List) : Position; Funkcja usuwa element wskazywany przez P w liście L. Jeśli operacja się powiowła, zwracany jest wskaźnik do głowy listy L. Jeśli operacja się nie powiodła, zwracany jest wskaźnik do elementu za ostatnim w liście L. function FIRST(var L : List) : Position; Jeśli lista L istnieje, funkcja ta zwraca wskaźnik do jej głowy. Jeśli lista L nie istnieje (co oznacza, że nie posiada głowy), wówczas zwracana jest NullPosition. function NEXT(P : Position; L : List) : Position; Funkcja zwraca wskaźnik do elementu następnego w stosunku do wskazywanego przez P w liście L, jeśli oczywiście następnik istnieje. Jeśli P wskazuje na element ostatni, wówczas zwracana jest pozycja zo ostatnim. 12 Listy function PREVIOUS(P : Position; var L : List) : Position; Funkcja zwraca wskaźnik do elementu poprzedniego w stosunku do wskazywanego przez P w liście L, jeśli oczywiście poprzednik istnieje. Jeśli P jest głową listy L, wówczas zwracana jest NullPosition. function LOCATE(X : ElementType; L : List) : Position; Funkcja ta znajduje element X w liście L. Jeśli element X występuje, wówczas zwracana jest jego pozycja w liście. Jeśli elementu X nie ma, funkcja zwraca pozycję za ostatnim. function RETRIEVE(P : Position; L : List) : ElementType; Funkcja zwraca element znajdujący się na pozycji P w liście L. Jeśli pozycja P jest pozycją pustą (nie zawiera informacji merytorycznej) wtedy funkcja zwraca EmptyElement. function INIT_LIST(var L : List) : Position; Funkcja ta jest odpowiedzialna za inicjację listy. Tworzy ona głowę listy L i zwraca jej pozycję. Gdy w komputerze występuje brak pamięci, funkcja ta zatrzymuje wykonywanie programu. procedure MAKENULL_LIST(var L : List); Procedura ta jest odpowiedzialna za usunięcie wszystkich elementów listy L oprócz jej głowy. procedure DESTROY_LIST(var L : List); Procedura ta usuwa wszystkie elementy listy L razem z jej głową. Po jej wywołaniu zwalniana jest cała pamięć, którą zajmowała lista L. Oprócz wymienionych wcześniej procedur i funkcji często stosuje się funkcje pomocnicze: function PRINTLIST(P : Position; L : List) : Position; Funkcja wypisuje element listy L znajdujący się na pozycji P i zwraca pozycję poprzednika P. W przypadku, gdy element na pozycji P nie istnieje, zwracana jest EmptyPosition. function INLIST(X : ElementType; L : List) : Integer; Funkcja ta zwraca ilość wystąpień elementu X w liście L. W dalszej części tego rozdziału pokazane zostaną trzy podstawowe implementacje typu listowego: • Tablicowa • Wskaźnikowa • Kursorowa 13 Struktury danych Rysunek 2 - Połączenie listy wskaźnikowej i kursorowej. Pierwsza lista zawiera kursory do pierwszego i ostatniego elementu drugiej listy 14 Listy 1.3 Tablicowa implementacja ADT LIST Główną wadą tej implementacji jest to, że niezależnie od rozmiaru listy, zajmuje ona zawsze tą samą ilość pamięci. W nagłówku programu należy zdefiniować stałą MaxLenght, która określa maksymalny rozmiar listy. W implemantacji tablicowej lista ma postać rekordu. Rekord ten składa się z: • Tablicy „Elements” zawierającej „MaxLength” elementów typu ElementType • Zmiennej „Last” typu integer, która jest ostatnią pozycją listy ement ement ........ ........ t) element xLength ściwa lista wykorzystana pamięć W tej implementacji pozycja jest kursorem do danego elementu. Nie stosuje się więc głowy listy, a pozycja za ostatnim elementem jest równa Last+1. Nagłowek naszej implementacji wygląda następująco: const MaxLength = 1000; EmptyElement = ''; type Position = integer; ElementType = String[10]; List = Record Elements : array[1..MaxLength] of ElementType; Last : Position; end; Teraz przyjrzyjmy się głównym funkcjom w tej implementacji: function _END(L : List) : Position; begin _END := L.Last+1; end; function INSERT(X : ElementType; P : Position; L : List) : Position; VAR Q : Position; begin if L.Last >= MaxLength then begin write('Lista pelna !'); 15 Struktury danych INSERT := _END(L); end else if (P < 1) or (P > L.Last+1) then begin write('Pozycja nie istnieje ! (INSERT)'); INSERT := _END(L); end else begin for Q := L.Last downto P do L.Elements[Q+1] := L.Elements[Q]; L.Elements[P] := X; L.Last := L.Last+1; INSERT := P; end; end; {Przesunięcie elementów w } {prawo} {Wstawienie elementu X na pozycję P} {Zwiększenie długości listy} {INSERT} function DELETE(P : Position; var L : List) : Position; var Q : Position; begin if (P < 1) or (P>L.Last) then begin write('Pozycja nie istnieje (DELETE)'); DELETE := _END(L); end else begin Q := P; while Q < L.Last do begin L.Elements[Q] := L.Elements[Q+1]; {Przesunięcie elementów w } {lewo} Q := Q+1; {Element na pozycji Q zostanie zamazany} end; L.Elements[L.Last] := EmptyElement; {usunięcie niepotrzebnego } {elementu} L.Last := L.Last-1; {skrócenie listy} DELETE := 1; end; end; {DELETE} function FIRST(var L : List) : Position; begin if L.Last = 0 then FIRST := _END(L) else FIRST := 1; 16 {Lista jest pusta, jej długość = 0 } Listy end; function NEXT(P : Position; var L : List) : Position; begin if P >= L.Last then NEXT := _END(L) else NEXT := P+1; end; {FIRST} {P nie posiada następnika} function PREVIOUS(P : Position; var L : List) : Position; begin if P <= 1 then {P nie posiada poprzednika} PREVIOUS := _END(L) else PREVIOUS := P-1; end; function LOCATE(X : ElementType; var L : List) : Position; var Q : Position; begin LOCATE := _END(L); for Q:=1 to L.Last do begin {Sprawdzenie wszystkich elementów listy} if L.Elements[Q] = X then {Element został znaleziony} LOCATE := Q; end; end; function RETRIEVE(P : Position; L : List) : ElementType; begin RETRIEVE := EmptyElement; if (P >= 1) and (P <= L.Last) then {Jeśli pozycja znajduje się wewnątrz } {listy} RETRIEVE := L.Elements[P] else write('Pozycja nie istnieje (RETRIEVE)'); end; procedure INIT_LIST(var L : List); begin L.Last := 0; {Lista nie ma głowy, więc nie posiada żadnych } {elementów} end; {INIT} function MAKENULL_LIST(var L : List) : Position; 17 Struktury danych var Q : Position; begin Q := 1; while Q <= L.Last do {pętla dopóki istnieje pierwszy element} DELETE(Q,L); {usunięcie pierwszego elementu listy} MAKENULL := _END(L); end; {MAKENULL_LIST} W implementacji tablicowej nie stosuje się funkcji DESTROY_LIST, bowiem nie jest możliwe zwolnienie pamięci, która jest przydzielana w momencie uruchamiania programu. 18 Listy 1.4 Wskaźnikowa implementacja ADT LIST Jest to w pełni dynamiczna implementacja, dzięki czemu pamięć przydzielana jest obiektom listy w trakcie działania programu. Będzie zatem konieczne użycie typu wskaźnikowego. W tej implementacji listą jest wskaźnik do głowy. Nim jednak zdefiniujemy wszystkie funkcje na bazie listy dwukierunkowej z głową, wprowadzimy pewną zmianę do pojęcia Pozycji. W naszej implementacji pozycją elementu X będzie wskaźnik do elementu poprzedniego. Tak więc: • Głowa listy nie będzie miała określonej pozycji • Jako pozycję pierwszego elementu listy (posiadającego informację merytoryczną) będziemy rozumieli wskaźnik do głowy • Wskaźnik do ostatniego elementu listy (posiadającego informację merytoryczną) będzie zarazem pozycją za ostatnim elementem. Rysunek 3 - Lista dwukierunkowa z głową Na rysunku nr 3 pokazany jest prosty przykład. Pozycją elementu „A” jest wskaźnik p, elementu „B” jest wskaźnik q, itd. Chcąc usunąć element „C” będziemy usuwali element na pozycji r. Pozycją za ostatnim elementem jest s. Ponadto wskaźnik „Next” będzie określał następnika danego elementu, a wskaźnik „Previous” poprzednika danego elementu. Dlatego p = q^.Previous s = r^.Next p^.Previous = Nil itp. Niech ElementType będzie typu integer. Nagłówek implementacji wskaźnikowej wygląda następująco: const EmptyElement=0; NullPosition = nil; type ElementType = integer; Position = ^CellType; CellType = record Element : ElementType; Next,Previous : Position; 19 Struktury danych end; List = Position; {Lista jest wskaźnikiem do głowy} Zdefiniujemy teraz wszystkie operacje ADT LIST: function _END(var L : List) : Position; var Q : Position; begin Q := L; {Ustawiamy wskaźnik Q na głowę listy L} while Q^.Next <> nil do {Przesuwamy się aż do ostatniego elementu} Q := Q^.Next; _END := Q; end; function INSERT(X : ElementType; P : Position; var L : List) : Position; {Wstawiamy element X za pozycją P} VAR Q : Position; begin new(Q); if Q = nil then begin writeln('Blad przydzialu pamieci'); INSERT := nil; end else begin Q^.Element := X; {Przypisujemy wartość X nowemu obiektowi} Q^.Next := P^.Next; {Aktualizujemy wskaźniki „Next” } P^.Next := Q; {dla obiektów P oraz Q } Q^.Next^.Previous := Q; {Aktualizujemy wskaźniki „Previous” } Q^.Previous := P; {dla obiektów Q^.Next oraz Q } INSERT := P; {Zwracamy pozycje X} end; end; function DELETE(P : Position; var L : List) : Position; VAR Q : Position; begin Q := P^.Next; {Kasujemy element na pozycji P, wskazywany przez Q} if Q <> nil then begin P^.Next := P^.Next^.Next; {Na pozycji P będzie teraz następnik Q} if Q^.Next <> nil then Q^.Next^.Previous := P; {Jeśli usuwany element nie był ostatni, } {, to jego następnik będzie posiadał nowego poprzednika} dispose(Q); {usunięcie elementu z pozycji P} 20 Listy DELETE := L; end else begin DELETE := nil; end end; {funkcja zwróci głowę nowej listy} {Usunięcie elementu nie jest możliwe} function FIRST(var L : List) : Position; begin FIRST := L; end; function NEXT(P : Position; L : List) : Position; begin if P^.Next <> nil then NEXT := P^.Next else begin NEXT := nil; end; end; function PREVIOUS(P : Position; L : List) : Position; begin if P^.Previous <> nil then PREVIOUS := P^.Previous else begin PREVIOUS := nil; end; end; {Jeśli P ma następnika} {Jeśli P ma poprzednika} function LOCATE(X : ElementType; L : List) : Position; var Q : Position; begin LOCATE := _END(L); Q := L; {Zaczynamy poszukiwania od pierwszej pozycji, czyli głowy} while Q^.Next <> nil do begin {Sprawdzamy wszystkie pozycje} if Q^.Next^.Element = X then {Jeśli X jest na pozycji Q} LOCATE := Q; Q := Q^.Next; end; end; function RETRIEVE(P : Position; L : List) : ElementType; begin if (P^.Next = nil) or (P = nil) then {Sprawdzamy poprawność pozycji} 21 Struktury danych RETRIEVE := EmptyElement else RETRIEVE := P^.Next^.Element; end; {Pobieramy element z pozycji P} function INIT_LIST(var L : List) : Position; begin new(L); {Dynamiczne stworzenie głowy listy L} if L = nil then writeln('Blad inicjacji listy'); L^.Next := nil; {Głowa listy L nie posiada następnika} L^.Previous :=nil; {Głowa listy L nie posiada poprzednika} INIT_LIST := L; end; procedure MAKENULL_LIST(var L : List); begin while L^.Next <> nil do {Jeśli głowa listy L będzie miała jeszcze } {następnika} DELETE(L,L); {Usuwamy element z pierwszej pozycji, jaką jest } end; { głowa listy} procedure DESTROY_LIST(var L : List); begin MAKENULL_LIST(L); {Usunięcie elementów z wszystkich pozycji} dispose(l); {Usunięcie głowy listy} end; function INLIST(X : ElementType; L : List) : Integer; var P : Position; Temp : Integer; begin Temp :=0; {Ustawiamy licznik występowania X w liście L na „0”} P := FIRST(L); while P <> _END(L) do begin if RETRIEVE(P,L) = X then {Sprawdzamy, czy X jest na pozycji P} Inc(Temp); P := NEXT(P,L); {Przesuwamy się na następną pozycję} end; INLIST := Temp; end; 22 Listy 1.5 Porównanie implementacji tablicowej i wskaźnikowej Z zależności od potrzeb użytkownika, jak również możliwości sprzętowych należy zawsze wybrać jedną z dwóch przedstawionych implementacji typu listowego. W poniższej tabeli zestawiono główne czynniki przemawiające za i przeciw każdej z implementacji: lementacja tablicowa lementacja wskaźnikowa ZALETY • Operacje PREVIOUS oraz • Wykorzystujemy zawsze tyle _END zajmują stały czas pamięci, ile potrzebuje dana lista niezależnie od wielkości listy • Nie ma potrzeby określania ilości • Dla globalnej tablicy nie potrzebnej pamięci przed musimy się obawiać uruchomieniem programu przypadkowego naruszenia listy przez inne funkcje • Nie trzeba pamiętać wskaźników WADY • Konieczność określenia • W miarę powiększania się rozmiaru tablicy przed rozmiarów listy, operacje uruchomieniem programu PREVIOUS i _END zajmują coraz więcej czasu (dla list • Może prowadzić do jednokierunkowych) marnotrawienia pamięci, gdyż liczba elementów listy • Zachodzi niebezpieczeństwo bywa czasem mniejsza od naruszenia struktury listy liczby elementów, dla których przy korzystaniu z tych wcześniej zarezerwowaliśmy samych wskaźnikow na pamięć zewnątrz i wewnątrz funkcji • Szybkość operacji INSERT • Jeśli informacja merytoryczna oraz DELETE maleje wraz ze zajmuje mało pamięci, wzrostem ilości elementów wówczas na rozmiar listy listy wpływa pamięć zajmowana przez wskaźniki WNIOSKI lementacja idealna dla dużych list, jeśli lementację tę należy stosować w iemy jakich będą one rozmiarów. Nieażdym przypadku, kiedy nie wiemy ależy jej stosować, jeśli wykorzystujemy kich rozmiarów może być lista. Należy ylko część z góry zadeklarowanejamiętać, że wskaźniki także zajmują blicy. amięć, której w trakcie wykonywania ogramu może nagle zabraknąć. 23 Struktury danych 1.6 Implementacja kursorowa ADT LIST. Symulacja pamięci komputera Implementacja kursorowa, to w rzeczywistości symulacja wskaźników w tablicy. Każdą listę możemy więc utożsamiać z ciągiem indeksów (kursorów) pewnej tablicy globalnej – w tym przypadku tablicy SPACE, a w przypadku ogólnym pamięci komputera. Każda komórka tablicy SPACE przechowuje dwie informacje: • Merytoryczną – czyli istotną dla użytkownika programu • Organizacyjną, którą jest pojedynczy kursor (indeks) do innej komórki tablicy SPACE. Lista jest więc kursorem do komórki tablicy SPACE. W implementacji będziemy stosowali listy z głowami, dlatego komórka, która będzie wskazywana przez głowę będzie posiadała bezużyteczną informację merytoryczną. Z kolei komórka, która zamiast adresu swojego następnika będzie przechowywała adres 0 (zerowy) będzie oznaczała, że w tym miejscu kończy się jakaś lista. Ponadto w przestrzeni SPACE będzie zawsze istniała lista komórek wolnych, której głową będzie kursor Avialable. Wszystkie te prawidła przedstawia rysunek nr 4. Rysunek 4 - Przestrzeń dla komórek różnych list Łatwo zauważyć, że w przedstawionym modelu pamięci występują tylko trzy wolne komórki pamięci. Są to trzy dowolnie wybrane kursory z listy Avialable. 24 Listy Podobnie jak w przypadku wskaźnikowej implementacji listy, także tutaj stosujemy pojęcie pozycji elementu jako kursora do poprzednika tego elementu. W kursorowej implementacji listy istnieje elementarna funkcja: Function MOVE(var P,Q : Cursor) : boolean Która powoduje oderwanie pierwszej komórki wskazywanej przez kursor P i dołączenie jej na początek listy wskazywanej przez kursor Q. Sytuację tą przedstawia rysunek nr 5. Rysunek 5 - Funkcja MOVE dla przestrzeni komórek SPACE łówek kursorowej implementacji ADT demonstracji obszaru pamięci SPACE IST wygląda następująco: rzyjmujemy zmienne globalne: Const MaxLength = 1000; EmptyElement = ''; Type ElementType = string[10]; Cursor = integer; CellType = record Element : ElementType; Next : Cursor; end; List = Cursor; SPACE : ARRAY[1..MaxLength] of CellType; Avialable,L1,L2 : List; oraz L2 będą przykładowymi listami. Pozostaje nam zdefiniować wszystkie potrzebne funkcje: procedure INITIALIZE; var k : Cursor; begin {Inicjalizacja obszaru pamięci SPACE} 25 Struktury danych for k := MaxLength-1 downto 1 do SPACE[k].Next := k+1; {Łączenie komórek w jedną listę Avialable} SPACE[MaxLength].Next := 0; {Ustalenie końca listy Avialable } Avialable := 1; {Ustalenie głowy listy Avialable} end; function MOVE(var P,Q : Cursor) : boolean; var TempCursor : Cursor; {Tymczasowy kursor, który zachowa wartość Q} begin if P = 0 then begin writeln('Komorka nie istnieje !'); MOVE := false; end else begin TempCursor := Q; Q := P; {Aktualizacja kursora Q} P := SPACE[P].Next; {Aktualizacja kursora P} SPACE[Q].Next := TempCursor; {Aktualzacja następnika kursora Q} MOVE := true; end; end; function INIT_LIST(var L : List) : boolean; begin L := 0; INIT := false; if MOVE(Avialable,L) = false then writeln('Blad przy inicjacji listy : Brak wolnej pamieci'); else INIT := true; {Zmiana kursora L występuje przy jednoczesnym przesunięciu głowy } {listy Avialable na wskazywany przez nią kursor} end; function FIRST(L : List) : Cursor; begin FIRST := L; end; function NEXT(P : Cursor; L : List) : Cursor; begin if SPACE[P].Next = 0 then begin {komórka wskazuje koniec listy} writeln('Blad przy funkcji NEXT : Wyjscie poza zakres listy'); NEXT := 0; end else NEXT := SPACE[P].Next; 26 Listy end; function PREVIOUS(P : Cursor; var L : List) : Cursor; var C : Cursor; begin C := FIRST(L); PREVIOUS := 0; if P = C then {Głowa nie posiada poprzednika} writeln('Blad przy funkcji PREVIOUS : Wyjscie poza zakres listy') else begin {Przemieszczamy się kursorami począwszy od głowy} while (SPACE[C].Next <> P) and (C<>0) do { listy, aż następnikiem } C := NEXT(C,L); {będzie P} PREVIOUS := C; end; end; function _END(var L : List) : Cursor; var C : Cursor; begin C := L; {Ustawiamy kursor C na głowie listy L} while SPACE[C].Next <> 0 do C := NEXT(C,L); {Przesuwamy się na } _END := C; {koniec listy L} end; function INSERT(X : ElementType; P : Cursor; var L : List) : Cursor; VAR C : Cursor; {C jest kursorem do następnika P} begin if P = _END(L) then C := 0 {Wstawianie X na koncu listy L} else C := SPACE[P].Next; {Wstawianie X nie na koncu listy L} if MOVE(Avialable,C) then begin {C będzie nową pustą komórką} SPACE[C].Element := X; {Przypisanie X do komórki wskazywanej } {przez C} SPACE[P].Next := C; {Połączenie listy L w jedną całość} INSERT := P; end else begin writeln('Blad przy wstawianiu do listy : Brak Pamieci'); INSERT := 0; end; end; {INSERT} 27 Struktury danych function DELETE(P : Cursor; L : List) : Cursor; begin if P <> _END(L) then {Usuwany element nie jest „za ostatnim”} MOVE(SPACE[P].Next,Avialable); DELETE := FIRST(L); end; procedure MAKENULL_LIST(var L : List); begin while L <> _END(L) do DELETE(L,L); {Usuwamy zawsze pierwszy } {element listy L, pozostaje jedynie głowa listy L} end; procedure DESTROY_LIST(var L : List); begin MAKENULL(L); MOVE(L,Avialable); end; {Usuwamy głowę listy L} function LOCATE(X : ElementType; var L : List) : Cursor; var Q : Cursor; begin LOCATE := _END(L); Q := L; while Q <> _END(L) do begin {Przeszukujemy całą listę od pierwszej pozycji} if SPACE[SPACE[Q].Next].Element = X then LOCATE := Q; {X znajduje się na pozycji Q} Q := NEXT(Q,L); {Przejście na następną pozycję} end; end; function RETRIEVE(P : Cursor; L : List) : ElementType; begin if P = _END(L) then begin writeln('Blad odczytu listy : element nie istnieje'); RETRIEVE := EmptyElement; end else RETRIEVE := SPACE[SPACE[P].Next].Element; {Pobranie elementu znajdującego się na pozycji P} end; 28 Listy Dynamiczne tworzenie list w kursorowej implementacji ADT LIST przedstawia przykład: ................. INITIALIZE; {Inicjalizacja przestrzeni komórek SPACE} INIT_LIST(L1); {Inicjalizacja pierwszej listy} INIT_LIST(L2); {Inicjalizacja drugiej listy} INSERT('Jan',_END(L1),L1); INSERT('Marek',FIRST(L1),L1); {Modyfikacje list} INSERT('Danuta',_END(L2),L2); INSERT('Ewa',NEXT(FIRST(L1),L1),L1); DESTROY_LIST(L1); {Usunięcie pierwszej listy} DESTROY_LIST(L2); {Usunięcie drugiej listy} .................. 29 Struktury danych 1.7 Przykład zastosowania ADT LIST Przygotowanie biblioteki zawierającej operacje na listach procentuje, gdyż pozwala programiście zaoszczędzić wiele czasu. Podczas dalszych prac nad programem może on posługiwać się stabilnym i przejrzystym zestawem instrukcji bez wnikania w szczegóły implementacji. Poniżej znajduje się funkcja, która usuwa z listy duplikaty: procedure PURGE(var L : List); var P,Q : Position; begin P := FIRST(L); while P <> _END(L) do begin Q := NEXT(P,L); while Q <> _END(L) do if RETRIEVE(P,L) = RETRIEVE(Q,L) then DELETE(Q,L) else Q := NEXT(Q,L); P := NEXT(P,L); end; end; Jak widać jest ona przejrzysta i zrozumiała. 30 Stosy 2 Stosy 2.1 Wprowadzenie Stos (Stack) jest elementarną strukturą przechowywania danych. W językach niskiego poziomu (np. Assembler) spotykamy instrukcje, które bezpośrednio się do niego odwołują. Każdy program napisany w języku proceduralnym potrzebuje do działania pewnej podręcznej pamięci, w tym wypadku właśnie stosu. Przy zagłębianiu się w procedurę na stosie umieszczane są wartości zmiennych występujących w programie, oraz aktualna zawartość rejestrów mikroprocesora. Po obsłużeniu takiej procedury, ze stosu w odwrotnej kolejności pobierane są stare wartości i program wykonywany jest dalej. Warto przy tym dodać, że ilość pamięci, która zostanie zarezerwowana dla stosu jest ustalana przez programistę. Gdy podczas działania programu pamięci tej zabraknie, wówczas wystąpi przepełnienie stosu i przerwanie działania programu. Każdy stos jest odmianą kolejki LIFO – Last In First Out co oznacza, że ostatni element, który umieścimy na stosie będzie pierwszym elementem, którego będziemy mogli z niego zdjąć. Sytuaję tą przedstawia rysunek nr 6. Rysunek 6 - Odwrócenie kolejności przy użyciu Stosu Stos służy także do zapamiętywania ciągu poleceń dla linii tekstu. Możemy bowiem przyjąć dwa specjalne symbole: # kasuje znak poprzedzający @ kasuje wszystkie znaki poprzedzające w linii Wówczas umieszczony na stosie ciąg znaków „a@bc#de#a” oznacza „bda”. Za takie przetworzenie danych będzie odpowiadać specjalna procedura: EDIT. W tym rozdziale zdefiniujemy nowy typ danych ADT STACK, z którego będziemy mogli korzystać w późniejszych programach. 2.2 Operacje ADT STACK 31 Struktury danych Każda prawidłowa implementacja stosu powinna zawierać takie funkcje jak: function EMPTY(S : pointer) : boolean; Funkcja zwraca wartość logiczną, która mówi czy stos S jest pusty. function TOP(var S : pointer) : ElementType; Funkcja zwraca wartość elementu znajdującego się na górze stosu S. Element ten jednak pozostaje nienaruszony. procedure POP(var S : pointer); Procedura ta zdejmuje (usuwa) ze stosu pierwszy element, jeśli oczywiście stos nie jest pusty. procedure PUSH(X : ELEMENTTYPE; var S : pointer); Procedura umieszcza na stosie S element X. Jeśli wystąpi przepełnienie stosu S, program powinien się zatrzymać. procedure MAKENULL_STACK(var S : pointer); Procedura ta usuwa ze stosu S wszystkie jego elementy. Sama informacja o istnieniu stosu pozostaje. Ponadto w implementacji wskaźnikowej zdefiniujemy kilka innych procedur: procedure INIT_STACK(var S : pointer); Procedura inicjująca stos S. W pamięci tworzony jest ogon nowego stosu. procedure DESTROY_STACK(var S : pointer); Procedura, która fizycznie usuwa z pamięci wszystkie elementy związane ze stosem S (zniszczeniu ulega także ogon stosu). Z uwagi na fakt, że przy operacji na stosie często występują programowe „wyjątki” zdefiniujemy także globalny rejestr flagowy FlagRegister, jako 3 elementową tablicę, o wartościach 1 - błąd oraz 0 – wszystko w porządku. FlagRegister[1] - Operacja TOP(S), gdy stos jest pusty FlagRegister[2] - Operacja POP(S), gdy stos jest pusty FlagRegister[3] - Operacja PUSH(X,S), gdy stos jest pelny 32 Kolejki 2.3 Tablicowa implementacja ADT STACK W tablicowej implementacji Stos jest Rekordem zawierającym tablicę Elements typu ElementType, oraz zmienną Top typu integer, która określa numer pierwszego elementu stosu. Zapełnianie tej tablicy (przez odkładanie na stos kolejnych elementów) odbywa się począwszy od Top = MaxLength, aż do Top = 1. Dla Top = 1 stos będzie pełny. Gdy wystąpi Top > MaxLength, wtedy stos będziemy traktowali jako pusty. Sytuację tą przedstawia poniższy schemat: ............ ............ xLength ... wszy element gi element ... ... atni element zar wolny zar zajęty Nagłówek tablicowej implementacji stosu wygląda następująco: const MaxLength = 1000; {Maksymalna wielkość stosu} type FlagRegister = array[1..3] of byte; ElementType = integer; {Typ informacji merytorycznej} STACK = record Top : integer; Elements : array[1..MaxLength] of ElementType; end; var FR : FLAGREGISTER; {Definiowany globalnie rejestr flagowy} Poniżej przedstawiona jest implementacja wszystkich funkcji ADT STACK. procedure MAKENULL_STACK(var S : STACK); begin S.Top := MaxLength+1; {Stos staje się pusty, Top wskazuje na } end; {element poza stosem} function EMPTY(S : STACK) : boolean; begin EMPTY := S.Top > MaxLength; {Porównanie „Top” oraz „MaxLength”} end; procedure POP(var S : STACK); 33 Struktury danych begin if EMPTY(S) then begin FR[2] := 1; ERROR; end else S.Top := S.Top+1; end; {Modyfikacja rejestru flagowego} {Przerwanie wykonywania programu} {Top zbliżył się o 1 do wartości MaxLength} function TOP(var S : STACK) : ElementType; begin if EMPTY(S) then begin FR[1] := 1; {Z pustego stosu nie możemy pobrać żadnej wartości} ERROR; end else TOP := S.Elements[S.Top]; {Pobranie elementu z góry stosu} end; procedure PUSH(X : ElementType; var S : STACK); begin if S.Top = 1 then begin {Stos jest pełny} FR[3] := 1; ERROR; end else begin S.Top := S.Top-1; {Zmienna Top zbliża się do 1} S.Elements[S.Top] := X; {Umieszczenie X na odpowiednim miejscu } {tablicy Elements} end; end; 34 Kolejki 2.4 Wskaźnikowa implementacja ADT STACK Implementacja ta jest całkowicie dynamiczna, dzięki czemu stos zajmuje tylko tyle pamięci operacyjnej komputera, ile jest aktualnie potrzebne. Wyróżniamy tutaj wyraźnie ogon stosu, jako stały element stosu nie posiadający informacji merytorycznej. Przy inicjacji stosu tworzony jest więc nowy obiekt, którego wskaźnik jest nil. W miarę odkładania na stos kolejnych elementów, ich wskaźniki łączą się szeregowo. Wskaźnik do stosu jest zarazem wskaźnikiem do szczytu stosu, co przedstawia rysunek nr 7. Rysunek 7 - Wskaźnikowa implementacja stosu Nagłówek dla tej implementacji ADT STACK wygląda następująco: type ElementType = integer; FlagRegister = array[1..3] of byte; pointer = ^CELL; {typ pointer jest wskaźnikiem do stosu} Stack = pointer; CELL = record Next : pointer; Element : ElementType; end; var FR : FLAGREGISTER; {Definiowany globalnie rejestr flagowy} Poniżej przedstawione są funkcje dla wskaźnikowej implementacji stosu. function EMPTY(S : pointer) : boolean; var pom : pointer; 35 Struktury danych begin pom := S; {Ustawiamy wskaźnik pomocniczy pom na górę stosu} while pom^.Next <> nil do {Przechodzimy wgłąb stosu aż do ogona} pom := pom^.Next; EMPTY := pom = S; {Jeśli wskaźnik stosu pokrywa się z ogonem, to } end; {stos jest pusty} procedure POP(var S : pointer); var pom : pointer; begin if EMPTY(S) then begin FR[2] := 1; ERROR; end else begin pom := S; S := S^.Next; dispose(pom); end; end; {jeśli stos jest pusty, sygnalizuj błąd} {Na szczycie stosu jest teraz następnik S} {Usuwamy z pamięci stary szczyt stosu} procedure PUSH(X : ElementType; var S : pointer); var pom : pointer; begin new(pom); {Alokacja pamięci dla nowego obiektu} if pom = nil then begin {brak wolnej pamieci, stos jest już pelny} FR[3] := 1; ERROR; end else begin pom^.Next := S; {Następnikiem nowego elementu będzie } {dotychczasowy szczyt stosu S} pom^.Element := X; {Przypisanie wartości X nowemu elementowi } {pom} S := pom; {Element „pom” staje się nowym szczytem stosu} end; end; function TOP(var S : pointer) : ElementType; begin if EMPTY(S) then begin {Stos jest pusty, nie można pobrać elementu z } FR[1] := 1; {jego szczytu} ERROR; end else TOP := S^.Element; {Pobranie elementu ze szczytu stosu S} end; 36 Kolejki procedure INIT_STACK(var S : pointer); begin new(S); {Stworzenie ogona stosu, który jest aktualnie jego szczytem} S^.Next := nil; {Ogon stosu nie posiada następnika} end; procedure MAKENULL_STACK(var S : pointer); begin while not EMPTY(S) do POP(S); {Dopóki stos nie będzie pusty,będziemy} { z niego zdejmowali kolejne elementy, pozostanie tylko ogon} end; procedure DESTROY_STACK(var S : pointer); begin MAKENULL_STACK(S); {Zdjęcie wszystkich elementów ze stosu S} dispose(S); {Usunięcie ogona stosu S} end; 37 Struktury danych 2.5 Przykład zastosowania ADT STACK Przy pomocy zdefiniowanych operacji ADT STACK możemy zbudować procedurę EDIT omawianą na początku tego rozdziału. procedure EDIT(var S : STACK; Str : EditString); {typ „EditString” jest łańcuchem znaków} var{Str jest wejściowym ciągiem znaków} L : integer; C : char; begin MAKENULL_STACK(S); for L := 1 to Length(Str) do {Sprawdzamy kolejno wszystkie znaki ciągu „Str”} begin C := Str[L]; if C = '#' then POP(S) {Wymazujemy ostatni znak} else if C = '@' then MAKENULL_STACK(S) {Wymazujemy wszystkie dotychczasowe znaki} else PUSH(C,S); {Umieszczamy na stosie S znak C } end; end; 38 Kolejki 3 Kolejki 3.1 Wprowadzenie Przy wyjaśnianiu zasady funkcjonowania kolejki pomocna okazuje nam się intuicja. Kolejkę możemy traktować jako listę jednokierunkową z głową i ogonem. W tym przypadku głowę będziemy traktować jak przód kolejki i nazywać Front, a ogon jako tył i nazywać Rear. Dołączając do kolejki nowy element, doczepiamy go do aktualnego Rear; gdy element opuszcza kolejkę, Front wskazuje się element „następny w kolejce”. Każda kolejka jest więc typu LILO – Last In Last Out, co przedstawia poniższy rysunek. Rysunek 8 - Kolejka LILO Kolejki znajdują szerokie zastosowanie w przedmiocie Systemy Obsługi Masowej. Zmieniającą się kolejkę i strumień przepływający przez nią można wiarygodne symulować przy pomocy odpowiedniego oprogramowania. W dajszej części tego rozdziału stworzymy kolejkowy typ danych: ADT QUEUE. Zaimplementujemy go na dwa sposoby: • Dynamicznie – przy użyciu wskaźników do głowy i ogona • Statycznie – tworząc kolejkę cykliczną umieszczoną w tablicy. 39 Struktury danych 3.2 Operacje ADT QUEUE Podobnie jak w przypadku stosu zastosujemy globalny rejestr flagowy FlagRegister. Jego flagi będą odpowiednio określały: FlagRegister[1] - Operacja FRONT(Q), gdy kolejka jest pusta. FlagRegister[2] - Operacja ENQUEUE(X,Q), gdy kolejka jest pelna. FlagRegister[3] - Operacja DEQUEUE(Q), gdy kolejka jest pusta. function EMPTY(Q : QUEUE) : boolean; Funkcja zwraca wartość logiczną, mówiącą czy kolejka Q jest pusta. procedure DEQUEUE(var Q : QUEUE); Procedura usuwa z kolejki Q pierwszy element, zmieniając przy tym Rear. Gdy kolejka jest pusta, procedura powinna ustalić FlagRegistger[3] = 1. procedure ENQUEUE(X : ELEMENTTYPE; var Q : QUEUE); Procedura doczepia na koniec kolejki Q nowy element X, aktualizując jednocześnie Rear. W przypadku przepełnienia kolejki, procedura powinna ustalić FlagRegister[2] = 1. function FRONT(var Q : QUEUE) : ElementType; Funkcja zwraca wartość pierwszego elementu kolejki Q. Sam element pozostaje nienaruszony. procedure INIT_QUEUE(var Q : QUEUE); Procedura inicjująca kolejkę Q. Niezależnie od implementacji po jej wykonaniu Front kolejki pokrywa się z jej Rear. procedure MAKENULL_QUEUE(var Q : QUEUE); Procedura usuwająca z kolejki Q wszystkie elementy. Po jej wykonaniu Front pokrywa się z Rear. procedure DESTROY_QUEUE(var Q : QUEUE); Procedura stosowana jest tylko we wskaźnikowej implementacji ADT QUEUE. Oprócz usunięcia wszystkich elementów kolejki Q, zwalnia całą pamięć, która dotychczas była zarezerwowana przez Front i Rear. 40 Kolejki 3.3 Wskaźnikowa implementacja ADT QUEUE We wskaźnikowej implementacji, Kolejka jest rekordem składającym się z dwóch wskaźników: Front i Rear. Odpowiednio Rear wskazuje na ostatni element w kolejce, który posiada informację merytoryczną - jego następnikiem jest nil. Front wskazuje z kolei na element, który nie posiada informacji merytorycznej. Sytuację tą przedstawia rysunek nr 9. Rysunek 9 - Wskaźnikowa implementacja kolejki Przy takiej implementacji kolejki zwykłe zamienienie ze sobą wskaźników Front i Rear nie odwróci kolejkości kolejki. Nagłówek tej implementacji wygląda następująco: type ElementType = char; FlagRegister = array[1..3] of byte; pointer = ^CELLTYPE; {Wskaźnik do pojedyńczego elementu kolejki} CELLTYPE = record {Element kolejki} Next : pointer; {Wskaźnik do następnika danego elementu} Element : ElementType; {Informacja merytoryczna elementu kolejki} end; QUEUE = record {Rekord będący właściwą kolejką} Front, Rear : pointer; {Wskaźniki do Głowy i Ogona kolejki} end; var FR : FlagRegister; {Definiowany globalnie rejestr flagowy} Poszczególne operacje mają postać: function EMPTY(Q : QUEUE) : boolean; begin EMPTY := Q.Front = Q.Rear; {Kolejka jest pusta, gdy „Front” porywa } {się z „Rear”} end; 41 Struktury danych procedure DEQUEUE(var Q : QUEUE); var pom : pointer; begin if EMPTY(Q) then begin {Kolejka jest pusta, nie można z niej usunąć } {żadnego elementu} FR[3] := 1; ERROR; end else begin pom := Q.Front; {Zapamiętanie starego „Rear” kolejki Q} Q.Front := Q.Front^.Next; {Nową głową stanie się następnik } {dotychczasowej głowy} dispose(pom); {Zwolnienmie pamieci zajmowanej przez stary } {„Front” kolejki Q} end; end; procedure ENQUEUE(X : ElementType; var Q : QUEUE); begin new(Q.Rear^.Next); {Stworzenie nowego elementu dołączonego do } {„Rear” kolejki Q} if Q.Rear^.Next = nil then begin {brak pamieci, kolejka Q jest pelna} FR[2] := 1; ERROR; end else begin Q.Rear := Q.Rear^.Next; {Ustalenie nowego „Rear” kolejki Q} Q.Rear^.Element := X; {Przypisanie nowemu elementowi wartości X} Q.Rear^.Next := nil; {Nowy „Rear” nie może posiadać następnika} end; end; function FRONT(var Q : QUEUE) : ElementType; begin if EMPTY(Q) then begin {Kolejka jest pusta} FR[1] := 1; ERROR; end else FRONT := Q.Front^.Next^.Element; {Zwrócenie wartości pierwszego} {elementu, który jest następnikiem „Front” (rys. nr 8) } end; procedure INIT_QUEUE(var Q : QUEUE); begin new(Q.Front); {Stworzenie nowego elementu kolejki, który nie będzie} Q.Front^.Next := nil; { posiadał informacji merytorycznej. Będą na } {niego wskazywały zarówno „Front” jak i „Rear”. } 42 Kolejki Q.Rear := Q.Front; end; {Element ten nie będzie posiadał następnika} procedure MAKENULL_QUEUE(var Q : QUEUE); begin while not EMPTY(Q) do DEQUEUE(Q); {Usunięcie wszystkich elementów} {kolejki, które zawierają informację merytoryczną } end; procedure DESTROY(var Q : QUEUE); begin MAKENULL(Q); {Usunięcie wszystkich elementów, zawierających } {informację merytoryczną, oraz głowy (Front) kolejki } dispose(Q.Front); end; 43 Struktury danych 3.4 Implementacja kolejki cyklicznej Kolejka cykliczna to kolejka, której elementy umieszczone są w tablicy, którą możemy interpretować jako podzielony na części pierścień. Ilość tych części (MaxLength) jest wielkością tablicy, czyli pamięcią, w której są przechowywane elementy. Umownie możemy przyjąć, że wydłużanie się kolejki (dołączanie nowych elementów) postępuje zgodnie z ruchem wskazówek zegara. Sytuację tą przedstawia rysunek nr 10. Rysunek 10 - Schemat kolejki cyklicznej Aby rozróżnić sytuację, kiedy kolejka będzie pusta bądź pełna musimy poświęcić w tablicy jeden element, który będzie głową. Tak więc: • Dla kolejki pustej Front = Rear • Dla kolejki pełnej (Front + 1) mod MaxLength = Rear. Dla kolejki cyklicznej musimy zdefiniować funkcję cyklicznego przejścia po kolejnych elementach tablicy. function ADDONE(I : integer) : integer; begin ADDONE := (I + 1) mod MaxLength; end; {ADDONE(MaxLength) = 1} Zatem pierwszy element tablicy, zawierający informację merytoryczną będzie się znajdował na pozycji ADDONE(Front), a ostatni na pozycji Rear. Nagłówek tej implementacji ADT QUEUE będzie wyglądał następująco: const MaxLength = 1000; 44 {Maksymalna wielkość tablicy – iość podziałów na} Kolejki {kole} type ElementType = char; FlagRegister = array[1..3] of byte; QUEUE = record {Kolejka jest tutaj rekordem} Elements : array[1..MaxLength] of ElementType; {Tablica, która} {zawiera elementy kolejki} Front, Rear : integer; {Przód i tył cyklicznej kolejki} end; var FR : FlagRegister; {Definiowany globalnie rejestr flagowy} Poniżej przedstawione są wszystkie funkcje dla tej implementacji kolejki. function EMPTY(Q : QUEUE) : boolean; begin EMPTY := Q.Front = Q.Rear; end; procedure DEQUEUE(var Q : QUEUE); begin if EMPTY(Q) then begin FR[3] := 1; ERROR; end else Q.Front := ADDONE(Q.Front); {Przesuwamy głowę kolejki zgodnie z} {ruchem wskazówek zegara } end; procedure ENQUEUE(X : ELEMENTTYPE; var Q : QUEUE); begin if ADDONE(Q.Rear) = Q.Front then begin {brak pamieci, kolejka Q jest } FR[2] := 1; {pelna} ERROR; end else begin Q.Rear := ADDONE(Q.Rear); {Przesuwamy ogon} Q.Elements[Q.Rear] := X; {Przypisujemy odpowiedniej komórce } {tablicy wartość X} end; end; function FRONT(var Q : QUEUE) : ElementType; begin if EMPTY(Q) then begin FR[1] := 1; ERROR; 45 Struktury danych end else FRONT := Q.Elements[ADDONE(Q.Front)]; {Pobieramy pierwszy element z kolejki} end; procedure INIT_QUEUE(var Q : QUEUE); begin Q.Front := 1; {Dla poprawnej inicjacji kolejki wystarczy jedynie, aby } {„Front=Rear” } Q.Rear := Q.Front; end; procedure MAKENULL_QUEUE (var Q : QUEUE); begin Q.Front = Q.Rear; {Usunięcie wszystkich elementów kolejki} end; 46 Kolejki 3.5 Przykład zastosowania ADT QUEUE Możemy zademonstrować działanie ADT AUEUE na prostym przykładzie. INIT_QUEUE(Q); writeln('Podaj początkowe wejście do funkcji EDIT : '); readln(ED); EDIT(Q,ED); EDIT(Q,ED); {Powtórne wywołanie EDIT(Q), aby sprawdzić, czy} { działa operacja MAKENULL(Q) na niepustej kolejce {wypisanie elementow kolejki wraz z ich usuwaniem} write('Elementy kolejki: '); while not EMPTY(Q) do begin write(FRONT(Q)); DEQUEUE(Q); end; readln; DESTROY(Q); 47 Struktury danych 4 Odwzorowania 4.1 Wprowadzenie Odwzorowanie (pamięć asocjacyjna) jest funkcją przyporządkowującą elementom jednego typu, elementy drugiego typu. Będziemy przyjmować oznaczenia: • Domain type – typ dziedziny • Range type – typ przeciwdziedziny W tym rozdziale zaimplementujemy na dwa sposoby typ danych reprezentujący odwzorowanie: ADT MAPPING. 4.2 Operacje ADT MAPPING Do podstawowych operacji na typie reprezentującym odwzorowanie należą: Procedure MAKENULL_MAPPING(var M : MAPPING); Procedura ta czyni odwzorowanie M odwzorowaniem pustym. Procedure ASSIGN(var M : MAPPING; d : DomainType; r : RangeType); Procedura definiuje M(d) równe r, bez względu na to, czy M(d) było wcześniej zdefiniowane. Function COMPUTE(var M : MAPPING; d : DomainType; var r : RangeType) : boolean Funkcja zwraca wartość true i nadaje paramertowi r wartość M(d), gdy M(d) jest zdefiniowane; w przeciwnym wypadku zwraca wartość false; 48 Odwzorowania 4.3 Tablicowa implementacja ADT MAPPING W tej implementacji odwzorowaniem jest Tablica. Jej rozmiar jest stały, zależny od dziedziny, dlatego musimy przyjąć dwie stałe typu wyliczeniowego, które będą ograniczały dziedzinę od góry i dołu. Ponadto jeśli odwzorowanie M nie jest zdefiniowane dla jakiejś wartości „d” z dziedziny, wówczas musimy przypisać M(d) = Undefined, czyli zachodzi potrzeba zdefiniowania stałej Undefinied typu RangeType, które nie będzie nigdy wartością odwzorowania. Nagłówek implementacji będzie zatem wyglądał następująco: const FirstValue = -50; LastValue = 10; Undefined = ‘’; type MAPPING = array[DomainType] of RangeType; , natomiast operacje ADT MAPPING mają postać: procedure MAKENULL_MAPPING(var M : MAPPING); var i : DomainType; begin for i := FirstValue to LastValue do {Pętla dla całej dziedziny} M[i] := Undefined; end; Procedure ASSIGN(var M : MAPPING; d : DomainType; r : RangeType); begin M[d] := r; end; Function COMPUTE(var M : MAPPING; d : DomainType; var r : RangeType) : boolean begin If M[d] = Undefined then COMPUTE := false; else begin r := M[d]; COMPUTE := true; end; end; 49 Struktury danych 4.4 Listowa implementacja ADT MAPPING W tej implementacji odwzorowanie jest listą par: (d1,r1), ... , (dk,rk) , gdzie di – są argumentami dla odwzorowania M, a wartościami: ri = M(di). Lista może być zaimplementowana w dowolny sposób. Wykorzystamy tutaj gotowe operacje ADT LIST. Nagłówek listowej implementacji odwzorowania wygląda następująco: type ElementType = record; Domain : DomainType; Range : RangeType; end; Warto w tym miejscu zaznaczyć, że powyższy typ jest informację merytoryczną w liście reprezentującej odwzorowanie. Operacja MAKENULL_MAPPING jest równoznaczna operacji MAKENULL_LIST, dlatego pozostaje nam zdeniniować: Procedure ASSIGN(var M : MAPPING; d : DomainType; r : RangeType); var X : ElementType; P : Position; {P jest użyte do przechodzenia od pierwszej do ostatniej } {pozycji w Odwzorowaniu M} begin X.Domain := d; {Przypisanie elementowi X wartości dziedziny i } X.Range := r; {przeciwdziedziny} P := FIRST(M); while P <> _END(M) do {Przejście po wszystkich argumentach } {odwzorowania} if RETRIEVE(P,M).Domain = d then {Jeśli odwzorowanie M było } {zdefiniowane dlaargumentu „d”, to} DELETE(P,M) { usuń cały element z listy} else P := NEXT(P,M) INSERT(X,FIRST(M),M); {Dodaj do listy reprezentującej odwzorowanie} {„M” nowy element X } end; function COMPUTE(var M : MAPPING; d : DomainType; r : RangeType) : boolean; var P : Position; begin COMPUTE := false; {Początkowy rezultat funkcji COMPUTE} 50 Odwzorowania P := FIRST(M); while P <> _END(M) do begin if RETRIEVE(P,M).Domain = d then begin r := RETRIEVE(P,M).Range; COMPUTE := true; end; P := NEXT(P,M); end; end; {Jeśli wartość M(d) jest } {określona} {Przypisz r wartość M(d) } 4.5 Przykład zastosowania ADT MAPPING Na poniższym przykładzie podany jest fragment programu wypisującego odwzorowanie: var d : DomainType; r : RangeType; M : Mapping; begin for d := FirstValue to LastValue do begin COMPUTE(M,d,r); writeln('Wartość odwzorowania dla argumentu ',d,' : '); end; end; 51 Struktury danych 5 Drzewa 5.1 Wprowadzenie Drzewo (Tree) jest zbiorem elementów zwanych wierzchołkami (nodes), spośród których jeden wyróżniony nazywamy korzenieniem (root). W zbiorze wierzchołków jest określona relacja „bycia rodzicem”, która nakłada hierarchiczną stukturę na ten zbiór. Każdy wierzchołem ma przy tym dokładnie jednego rodzica. Możemy podać jednak bardziej formalną definicję, do której w tym rozdziale bezpośrednio się odniesiemy: Jeśli pojedynczy węzeł jest drzewem, wówczas jest równocześnie jego korzeniem. Załóżmy, że n jest węzłem, a T1,T2, ... , Tk są drzewami. Możemy skonstruować nowe drzewo przez uczynienie n rodzicem węzłów n1,n2, ... ,nk. W takim drzewie n jest korzeniem, a T1,T2, ... , Tk są poddrzewami (subtrees) korzenia. Węzły n1,n2, ... ,nk są nazywane dziećmi węzła n. Za przykład może nam posłużyć Spis treści książki. Książka C1 S1.1 S1.2 C2 S2.1 S2.1.1 S2.1.2 S2.2 S2.3 C3 Rysunek 11 - Poddrzewa drzewa głownego Relacja bycia rodzicem jest zobrazowana za pomocą kresek łączących węzły. Przy czym węzeł leżący wyżej jest rodzicem węzła leżącego niżej. 52 Drzewa Jeśli n1,n2, ... ,nk jest takim ciągiem węzłów w drzewie, że ni jest rodzicem węzła ni+1 dla k > i > 0, to taki ciąg nazywamy ścieżką (path) od węzła n1 do węzła nk. Długość (length) ścieżki jest o 1 mniejsza od liczby węzłów, które ją tworzą, stąd ścieżka od danego węzła do niego samego ma długość 0, a ścieżka od rodzica do właściwego dziecka ma długość 1. Jeśli istnieje ścieżka z węzła a do b, to a nazywamy przodkiem (ancestor) węzła b, natomiast b potomkiem (descendent) węzła a. Każdy węzeł jest zarówno swoim przodkiem i potomkiem. Przodek (potomek) danego węzła różny od niego samego jest nazywany właściwym przodkiem (potomkiem) danego węzła. W drzewie jedynym węzłem, który nie ma przodków właściwych jest korzeń. Węzeł, który nie ma potomków właściwych nazywamy liściem. Poddrzewem w danym drzewie nazywamy dowolny węzeł wraz z jego wszystkimi potomkami. Wysokością węzła w drzewie nazywamy najdłuższą drogę od węzła do liścia w drzewie. Wysokością drzewa jest wysokość jego korzenia. Głębokością węzła jest długość ścieżki od korzenia do węzła (istnieje dokładnie jedna taka ścieżka). Poddrzewa C1, C2, C3 przedstawione na rysunku nr 11 mają kolejno wysokości 1, 2 oraz 0. Dzieci danego węzła są zazwyczaj uporządkowane od lewej do prawej. Możemy porządek wśród dzieci rozszerzyć na dwa dowolne węzły drzewa według zasady: „Jeśli a i b są rodzeństwem i a występuje na lewo od b to wszyscy potomkowie „a” są na lewo od potomków b.” Są 3 podstawowe sposoby wypisywania porządków w drzewach: Rysunek 12 - Droga przeglądu drzewa • PREORDER – Przesuwamy się drogą przeglądu drzewa od korzenia. Wypisujemy węzeł, gdy napotkamy go po raz pierwszy. Dla rysunku nr 12 wypiszemy: 1,2,5,6,3,4,7. • POSTORDER – Przesuwamy się drogą przeglądu drzewa od korzenia. Wypisujemy węzeł, gdy napotykamy go po raz ostatni, przechodząc w górę do jego rodzica. Dla rysunku nr 12 wypiszemy: 5,6,2,3,7,4,1. 53 Struktury danych • INORDER – Przesuwamy się drogą przeglądu drzewa od korzenia. Liście drzewa wypisujemy przy pierwszym napotkaniu, a pozostałe węzły przy drugim napotkaniu. Dla rysunku nr 12 wypiszemy: 5,2,6,1,3,7,4. Niektóre z omawianych przez nas drzew będą etykietowane tzn. każdy węzeł oprócz swojego numeru (informacja organizacyjna) będzie posiadał etykietę (Label) będącą informacją merytoryczną. W dalszej części rozdziału zdefiniujemy nowy typ danych ADT TREE i podamy kilka sposobów jego implementacji. 54 Drzewa 5.2 Operacje ADT TREE Do podstawowych operacji na drzewach należą: function PARENT(N : NODE; T : TREE) : NODE; Funkcja zwraca rodzica węzła „N” w drzewie „T”. Jeśli „N” jest korzeniem, zwraca EMP – węzeł pusty sygnalizujący wyjście poza drzewo. function LEFTMOST_CHILD(N : NODE; T : TREE) : NODE; Funkcja zwraca pierwsze z lewej dziecko węzła „N” w drzewie „T”. Gdy „N” jest liściem, wówczas funkcja zwraca EMP. function RIGHT_SIBLING(N : NODE; T : TREE) : NODE; Funkcja zwraca pierwszego z prawej sąsiada (Sąsiad – węzeł mający tego samego rodzica) węzła „N” w drzewie „T”. Gdy „N” nie posiada takiego sąsiada, funkcja zwraca EMP. function _LABEL(N : NODE; T : TREE) : LAB; Funkcja zwraca etykietę (typu LAB) węzła „N” w drzewie „T”. function ROOT(T : TREE) : NODE; Funkcja zwraca węzeł, który jest korzeniem drzewa „T” lub EMP, gdy drzewo „T” jest puste. procedure MAKENULL_TREE(var T : TREE); Procedura czyni „T” drzewem pustym. Dla niektórych implementacji możliwe jest także zdefiniowanie rodziny operacji CREATEi(V, T1,T2, ... ,Ti) Każda taka operacja tworzy nowy węzeł „N” o etykiecie „V”, a następnie podpinane są do niego poddrzewa T1,T2, ... ,Ti. Dotychczasowe korzenie drzew T1,T2, ... ,Ti stają się teraz dziećmi węzła N. On sam staje się korzeniem nowego drzewa. 55 Struktury danych 5.3 Tablicowa implementacja ADT TREE W tej implementacji wszystkie węzły są reprezentowane przez indeksy tablicy liczb całkowitych „TREE”. Wartości w tej tablicy, także będące liczbami całkowitymi odpowiadają rodzicom kolejnych węzłów. Dlatego więc: TREE[i] = j gdy węzeł „j” jest rodzicem węzła „i”. TREE[i] = 0 gdy węzeł „i” jest korzeniem drzewa. Dla przykładowego drzewa pokazanego na rusunku nr 12 tablica TREE będzie wyglądała następująco: • Pierwszą wadą tej implementacji jest to, że musimy wiedzieć jaka będzie liczba wierzchołków naszego drzewa, przez co nie jest możliwa zmiana jego struktury podczas działania programu. • Drugą istotną wadą jest niejednoznaczność operacji LEFTMOST_CHILD oraz RIGHT_SIBLING. Łatwo zauważyć, że w obu tych przypadkach o strukturze drzewa decyduje numer jego wierzchołka, nie zaś jego położenie (gdybyśmy w drzewie z rusunku nr 12 zamienili miejscami 5 i 6 wierzchołek, tablica TREE pozostałaby bez zmian). Etykiety (typu LAB) wierzchołków drzewa będziemy także przechowywać w tablicy TREE, której każdy element będzie rekordem. Nagłówek tablicowej implementacji drzewa wygląda następująco: const MaxNodes = 10; EMP = 0; EMPTYLABEL = ''; {Ilość węzłów w drzewie} {Pusty węzeł} {Pusta etykieta} type NODE = integer; {Typ liczbowy reprezentujący węzły} LAB = string; {Typ etykiety} TREE = array[1..MaxNodes] of record {Typ drzewa} N : NODE; {Numer węzła} L : LAB; {Etykieta} end; Poniżej zdefiniowane są wszystkie operacje dla tej implementacji: 56 Drzewa function PARENT(N : NODE; T : TREE) : NODE; begin if (N > 0) and (N < MaxNodes+1) then {Jeśli „N” jest indeksem tablicy } {TREE} PARENT := T[N].N else PARENT := EMP; {„N” jest poza tablicą TREE} end; function LEFTMOST_CHILD(N : NODE; T : TREE) : NODE; var Pom : NODE; {Pomocniczy węzeł} begin LEFTMOST_CHILD := EMP; {Ustalenie początkowej wartości dla funkcji} Pom := 1; while (Pom < MaxNodes+1) do begin {Sprawdzamy kolejne elementy } {tablicy TREE} if T[Pom].N = N then begin {Znaleziono pierwsze dziecko węzła „N” } {w drzewie „T”} LEFTMOST_CHILD := Pom; {Ustalenie końcowaj wartości funkcji} Pom := MaxNodes {Przypisanie wymuszające wyjscie z petli} end else inc(Pom); {Przejście do następnego elementu tablicy TREE} end; end; function RIGHT_SIBLING(N : NODE; T : TREE) : NODE; var Pom,i : NODE; {pomocnicze węzły} begin RIGHT_SIBLING := EMP; Pom := PARENT(N,T); {Pobranie ojca dla węzła „N”} i:=N+1; {Sprawdzamy tylko węzły o większym indeksie } {(leżące „na prawo” od „N”)} while i < MaxNodes+1 do begin if T[i].N = Pom then begin {Znaleziono najbliższego sąsiada dla „N”} RIGHT_SIBLING := i; i := MaxNodes; {Przypisanie wymuszające wyjscie z petli} end; inc(i); end; end; function _LABEL(N : NODE; T : TREE) : LAB; begin 57 Struktury danych if (N > 0) and (N < MaxNodes+1) then _LABEL := T[N].L; else _LABEL := EMPTYLABEL; end; {Węzeł „N” jest w tablicy TREE} {Pobranie etykiety dla węzła „N”} function ROOT(T : TREE) : NODE; var Pom : NODE; begin ROOT := EMP; for Pom :=1 to MaxNodes do {Sprawdzamy rodziców dla każdego węzła} if T[Pom].N = EMP then ROOT := Pom; {Parent dla korzenia = EMP} end; procedure MAKENULL_TREE(var T : TREE); var N : Node; begin for N:=1 to MaxNodes do begin {Pętla dla wszystkich węzłów drzewa} T[N].N := EMP; {Usuwamy informację o rodzicach} T[N].L := EMPTYLABEL; {Czyścimy etykiety} end; end; 58 Drzewa 5.4 Implementacja ADT TREE za pomocą list dzieci Wykorzystamy ADT LIST, który był omawiany w Rozdziale 1. W tej implementacji, dla każdego węzła drzewa jest tworzona lista jego dzieci. Wszystkie listy umieszczone są w tablicy indeksowanej numerami węzłów. Drzewo jest tutaj rekordem składającym się z: • Tablicy list zaimplementowanych w dowolny sposób, które przechowują numery węzłów. Węzły te są kolejnymi dziećmi węzła wskazywanego przez indeks tablicy. • Tablicy etykiet dla poszczególnych węzłów. • Numeru węzła, który jest korzeniem drzewa. Powyższą sytuację ilustruje rysunek nr 13 (Zapis drzewa z rysunku nr 12). Rysunek 13 - Implementacja drzewa za pomocą list dzieci Dla tej implementacji nagłówek wygląda następująco: const MaxNodes = 50; {Maksymalna liczba węzłów w drzewie} EMP = 0; EMPTYLABEL = ''; type LABELTYPE = string[10]; NODE = Integer; TREE = record Header : array[1..MaxNodes] of List; {Tablica list} Labels : array[1..MaxNodes] of LABELTYPE; {Tablica etykiet} Root : NODE; {Numer węzła, który jest korzeniem} end; Poszczególne operacje są zdefiniowane następująco: 59 Struktury danych function PARENT(N : NODE; T : TREE) : NODE; var i : integer; begin if N = T.Root then PARENT := EMP {Korzeń nie posiada rodzica} else for i:=1 to MaxNodes do begin {Sprawdzamy wszystkie elementy tablicy} {Header} if INLIST(N,T.Header[i]) > 0 then {Sprawdzamy, czy węzeł „N” } PARENT := i; { wystąpił w liście dzieci węzła „i”} end; end; function LEFTMOST_CHILD(P : NODE; T : TREE) : NODE; var L : List; begin L := T.Header[P]; {„L” staje się listą dzieci węzła „P”} LEFTMOST_CHILD := RETRIEVE(FIRST(L),L); {Pierwszy element na liście } {dzieci jest pierwszym dzieckiem } end; { EmptyElement dla Listy = EMP dla drzewa} function RIGHT_SIBLING(P : NODE; T : TREE) : NODE; var i : integer; L : List; begin for i:=1 to MaxNodes do begin {Przeglądamy wszystkie węzły w } {poszukiwaniu ojca „P”} L := T.Header[i]; if INLIST(P,L) > 0 then {Jeśli „P” jest dzieckiem „i”} RIGHT_SIBLING := RETRIEVE(NEXT_LIST(LOCATE(P,L),L),L); {Następny element na liście sąsiedztwa węzła „i” } end; { jest najbliższym sąsiadem węzła „P”} end; function _LABEL(P : NODE; T : TREE) : LABELTYPE; begin _LABEL := T.Labels[P]; {Pobranie etykiety dla węzła „P” } {w drzewie „T” } end; function ROOT(var T : Tree) : NODE; begin ROOT := T.Root; 60 Drzewa end; procedure MAKENULL_TREE(var T : Tree); var i : integer; begin for i:= 1 to MaxNodes do begin MAKENULL_LIST(T.Header[i]); T.Labels[i] := EMPTYLABEL; end; end; {Pętla wykonywana dla każdego } {węzła drzewa „T”} {Usunięcie listy dzieci węzła „i”} {Usunięcie etykiety węzła „i” } Główną wadą tej implementacji jest dość długi czas wykonania funkcji RIGHT_SIBLING, oraz trudno definiowana rodzina operacji CREATEi. Aby zbudować drzewo pokazane na rysunku nr 12 należy napisać instrukcje: for I := 1 to MaxNodes do INIT_LIST(T.Header[i]); {Inicjacja wszystkich list} {sąsiedztwa} INSERT(2,_END(T.Header[1]),T.Header[1]); {Doczepianie kolejnych węzłów} {na koniec list sąsiedztwa węzłow 1,2,4} INSERT(3,_END(T.Header[1]),T.Header[1]); INSERT(4,_END(T.Header[1]),T.Header[1]); INSERT(5,_END(T.Header[2]),T.Header[2]); INSERT(6,_END(T.Header[2]),T.Header[2]); INSERT(7,_END(T.Header[4]),T.Header[4]); T.Root :=1; {Ustalenie korzenia dla drzewa „T” } 61 Struktury danych 5.5 Implementacja ADT TREE przez „sąsiada i dziecko” W tej implementacji mamy do czynienia z przestrzenią komórek (węzłów) CellSpace, w których możemy umieścić wiele różnych drzew. Również zdefiniowanie rodziny operacji CREATEi jest stosunkowo proste. Każda komórka przestrzeni CellSpace posiada 3 informację: • Numer węzła dla operacji RIGHT_SIBLING. • Numer węzła dla operacji LEFTMOST_CHILD. • Etykietę komórki. W takiej implementacji drzewo z rysunku nr 12, mogłoby zająć początkową część przestrzeni CellSpace w następujący sposób: umer węzła EFTMOST_CHILD IGHT_SIBLING ................ ................ ................ abel ...... ...... ...... ...... ...... ...... ...... ...... Widocznie w tej tabeli wartości 0 oznaczają EMP, czyli występują, gdy dany węzeł nie ma sąsiada lub dziecka. Ponadto w przestrzeni CellSpace zawsze występuje lista komórek pamięci wolnej Avialable, które są ze sobą powiązane polem RIGHT_SIBLING. Ostatnia wolna komórka pamięci posiada zarówno RIGHT_SIBLING = EMP oraz LEFTMOST_CHILD = EMP. Dlatego inicjalizacja przestrzeni komórek polega na utworzeniu listy Avialable przy jednoczesnym wyzerowaniu wszystkich pól RIGHT_SIBLING. Nagłówek tej implementacji ADT TREE wygląda następująco: const MaxNodes = 50; EMP = 0; EMPTYLABEL = ''; {pusty węzeł} {pusta etykieta} type NODE = integer; LABELTYPE = string[10]; CELL = record {Pojedyncza komórka (rekord) przestrzeni CellSpace } 62 Drzewa LB : LABELTYPE; {LB oznacza Label} RS,LC : NODE {RS oznacza RightSibling, LC oznacza LeftMostChild} end; SPACE = array[1..MaxNodes] of CELL; {typ dla przestrzeni komórek} TREE = NODE; {Drzewo jest pojedynczym węzłem i zarazem } {korzeniem} var CellSpace : SPACE; {Przestrzeń wolnych komórek} Avialable : NODE; {Lista komórek wolnych, nie zajętych przez żadne } {drzewa} T1,T2,T3,T4 : TREE; {Przykładowe drzewa} L : LIST; {Lista potrzebna do wywołania funkcji CREATEi} Dalej przedstawione są operacje w tej implementacji. procedure INITIALIZE_SPACE; {Inicjalizacja komórej przestrzeni CellSpace} var i : integer; begin for i:=1 to MaxNodes do begin {Pętla dla wszystkich komórek } {przestrzeni CellSpace} CellSpace[i].RS := i+1; {Łączenie komórek przez kolejne } {pola RIGHT_SIBLING} CellSpace[i].LC := EMP; {Zerowanie pola LEFTMOST_CHILD} CellSpace[i].LB := EMPTYLABEL; {Czyszczenie etykiety komórki } end; CellSpace[MaxNodes].RS := EMP; {Zakończenie listy wolnych komórek } {Avialable} Avialable := 1; {Ustawienie początku listy wolnych komórek Avialable} end; function PARENT(N : NODE; T : TREE) : NODE; var i,j : NODE; begin PARENT := EMP; {Ustalenie początkowej wartości funkcji} i := 1; {„i” będzie oznaczało kolejnych rodziców} while i <= MaxNodes do begin {Sprawdzamy po kolei rodziców} j:= CellSpace[i].LC; {Niech „j” będzie pierwszym dzieckiem węzła „i”} while j <> EMP do begin {Sprawdzamy pozostałe dzieci węzła „i” w} {poszukiwaniu węzła „N”}} if N = j then begin PARENT := i; i := MaxNodes; {Przypisanie pozwalające na opuszczenie } {pierwszej pętli} 63 Struktury danych j := EMP; {Przypisanie pozwalające na opuszczenie } {drugiej pętli} end else j := CellSpace[j].RS; {Przejście do kolejnego dziecka węzła „i”} end; inc(i); end; end; {Przejście do kolejnego rodzica} {PARENT} function LEFTMOST_CHILD(N : NODE; T : TREE) : NODE; begin LEFTMOST_CHILD := CellSpace[N].LC; end; function RIGHT_SIBLING(N : NODE; T : TREE) : NODE; begin RIGHT_SIBLING := CellSpace[N].RS; end; function _LABEL(N : NODE; T : TREE) : LABELTYPE; begin _LABEL := CellSpace[N].LB; end; function ROOT(T : TREE) : NODE; begin ROOT := T; end; Procedura usuwająca drzewo „T” jest rekurencyjna. Jeśli nie możemy usunąć pierwszego z lewej dziecka, wówczas usuwamy pierwszego z prawej sąsiada. procedure MAKENULL_TREE(var T : TREE); var N : Node; begin {Rekurencja „w dół”} if CellSpace[T].LC <> EMP then MAKENULL(CellSpace[T].LC); {Rekurencja „w prawo”} if CellSpace[T].RS <> EMP then MAKENULL(CellSpace[T].RS); CellSpace[T].RS := Avialable; {Podpięcie komórki na początek } {listyAvialable} Avialable := T; {Ustalenie nowego początku listy Avialable} end; function CREATEi(v : LABELTYPE; L : LIST) : NODE; {L - lista korzeni drzew} var {, które staną się dziećmi nowego węzła o etykiecie v} N : NODE; {Funkcja zwraca korzeń nowego drzewa, lub EMP gdy brak } 64 Drzewa {jest wolnych komórek w przestrzeni CellSpace} {Pozycja w liście korzeni drzew} P : Position; begin if CellSpace[Avialable].RS <> EMP then begin {Jeśli są wolne komórki } {przestrzeni CellSpace} N := Avialable; {Ustalenie korzenia nowego drzewa „N”} Avialable := CellSpace[Avialable].RS; {Przesunięcie w prawo } {listy Avialable} CellSpace[N].LB := v; {Przypisanie korzeniowi etykiety v} CellSpace[N].RS := EMP; {Korzeń „N” nie posiada sąsiada} P := FIRST(L); {Ustalenie LEFTMOST_CHILD dla węzła „N”. Będzie nim pierwszy} CellSpace[Avialable].LC := RETRIEVE(P,L); { korzeń z listy L} while RETRIEVE(NEXT(P,L),L) <> EMP do begin {Łączenie korzeni z listy L polem RIGHT_SIBLING} CellSpace[RETRIEVE(P,L)].RS := RETRIEVE(NEXT(P,L),L); P := NEXT(P,L); end; CREATEi := N; {Funkcja zwraca korzeń nowego drzewa} end else CREATEi := EMP; {Brak wolnej pamięci} end; Wykorzystanie funkcji CREATEi ilustruje poniższy przykład: INITIALIZE; INIT(L); T1 := CREATEi('puste1..',L); T2 := CREATEi('puste2..',L); T3 := CREATEi('puste3..',L); {Lista korzeni „L” jest pusta} {Tworzenie trzech pustych drzew} INSERT(T1,_END(L),L); {Tworzenie listy korzeni dla drzew T1,T2,T3} INSERT(T2,_END(L),L); INSERT(T3,_END(L),L); T4 := CREATEi('drzewo123 ',L); {T1,T2,T3 stają się dziećmi T4 – nowego } {drzewa} 65 Struktury danych 5.6 Implementacja ADT TREE przez „sąsiada, dziecko i rodzica” Jak łatwo się domyślić główną wadą implementacji drzewa omawianej w punkcie 5.5 była operacja PARENT. Można podać dwa różne sposoby ominięcia tego problemu. Pierwszym sposobem jest dodanie do pojedynczej komórki CELL informacji o rodzicu węzła z danej komórki. CELL = record LB : LABELTYPE; RS,LC,Parent : NODE; end; Wówczas funkcja PARENT ma postać: function PARENT(n,T) : NODE; begin PARENT := CellSpace[n].Parent; end; Ta korzystna zmiana odbywa się jednak kosztem poświęcenia dużej ilości pamięci na przechowywanie informacji „Parent”. Inną modyfikacją jest dodanie do komórki CELL jednego bitu. Jeśli ma on wartość 1, wówczas przyjmujemy, że RIGHT_SIBLING jest w rzeczywistości wartością PARENT. Dla bitu o wartości 0 RIGHT_SIBLING wskazuje zwyczajnie na pierwszego sąsiada z prawej. CELL = record LB : LABELTYPE; RS,LC : NODE; BIT : 0..1; end; function PARENT(n,T) : NODE; begin while CellSpace[n].BIT <> 1 do n := CellSpace[n].RS; PARENT := CellSpace[n].RS; end; {Dopóki BIT = 0 odwiedzamy} { kolejnych sąsiadów „n”} {Teraz BIT = 0 } {, a RIGHT_SIBLING = PARENT} Kosztem drobnej straty pamięci zdecydowanie przyspieszamy operację PARENT. Wszystkie pozostałe operacje są w obu przypadkach takie same, jak w przypadku implementacji przez „sąsiada i dziecko” 66 Drzewa 5.7 Drzewa binarne Drzewa binarne (Binary Trees) mają w informatyce szerokie zastosowanie. Możemy podać ich nieformalną definicję: Drzewo binarne może być drzewem pustym, albo drzewem, w którym każdy węzeł spełnia jedno z kryteriów: • Nie ma dzieci • Ma tylko lewe dziecko • Ma tylko prawe dziecko • Ma zarówno lewe, jak i prawe dziecko. Rysunek 14 - Przykładowe drzewo binarne Wyróżniamy dwie reprezentacje drzewa binarnego: Reprezentacja kursorowa W tej reprezentacji występuje przestrzeń komórek CellSpace. Drzewo jest więc reprezentowane przez pojedynczą komórkę tej przestrzeni, która jest jego korzeniem. Poszczególne komórki tablicy CellSpace składają się z dwóch (dla drzew etykietowanych – z trzech) informacji: • Numeru lewego dziecka • Numeru prawego dziecka Brak dziecka jest oznaczany numerem 0. Nagłówek tej reprezentacji ma więc postać: var CellSpace : array[1..MaxNodes] of record LeftChild : Node; RightChild : Node; end; Zapis drzewa z rysunku nr 14 wygląda więc następująco: 67 Struktury danych Child htChild Przeważnie spotyka się jednak drugą reprezentację drzewa binarnego Reprezentacja wskaźnikowa Drzewo jest tutaj wskaźnikiem do korzenia. Cała struktura drzewa składa się z połaczonych ze sobą dynamicznie utworzonych obiektów, które przechowują po trzy wskaźniki do obiektów – odpowiednio lewego i prawego dziecka oraz rodzica oraz informację merytoryczną. Nagłówek tej reprezentacji drzewa binarnego ma więc postać Type Ptr : ^Node; Node = record LeftChild, RightChild, Parent : Ptr; Data : ElementType; end; {Informacja merytoryczna} Możemy łatwo napisać funkcję, która łączy dwa osobne drzewa w jedno drzewo o wspólnym korzeniu, którego zwraca. function CREATE(LeftTree,RightTree : Ptr) : Ptr; {LeftTree i RightTree są } {wskaźnikami do korzeni łączonych drzew} var Root : Ptr; {Root jest korzeniem nowego drzewa} begin new(Root); Root^.LeftChild := LeftTree; {Dołączenie dzieci do nowego korzenia} Root^.RightChild := RightTree; Root^.Parent := nil; LeftTree^.Parent := Root; {Ustalenie wspólnego rodzica dla dzieci} 68 Drzewa RightTree^.Parent := Root; CREATE := Root; end; {CREATE} Przez odpowiednią konstrukcję drzew binarnych możemy utworzyć drzewa przeszukiwań. Dla każdego węzła w takim drzewie zachodzi reguła: „informacja merytoryczna zawarta w lewym dziecku, rodzicu i prawym dziecku ustawiona jest w rosnący (malejący) ciąg”. Po wywołaniu procedury PREORDER (lub POSTORDR) informacja merytoryczna zostaje posortowana. Powszechnie stosuje się także drzewa „kopce” – wykorzystywane w sortowaniu stogowym, oraz zrównoważone drzewa AVL wykorzystywane w algorytmach kompresji danych. 69 Struktury danych 5.8 Przykład zastosowania ADT TREE Omawiane na początku tego rozdziału trzy sposoby przeglądu drzewa możemu już teraz zapisać przy wykorzystaniu operacji ADT TREE. procedure PREORDER(N : NODE; T : TREE); var P : NODE; begin Writeln(_LABEL(N,T)); {Wypisanie węzła} P := LEFTMOST_CHILD(N,T); {Próba przejścia do pierwszego dziecka } {węzła „N”} while P <> EMP do begin PREORDER(P,T); {Wywołanie rekurencyjne} P := RIGHT_SIBLING(P,T); {Próba przejścia do prawego sąsiada } {węzła „P”} end; end; procedure POSTORDER(N : NODE; T : TREE); var P : NODE; begin P := LEFTMOST_CHILD(N,T); {Próba przejścia do pierwszego dziecka } {węzła „N”} while P <> EMP do begin POSTORDER(P,T); {Wywołanie rekurencyjne} P := RIGHT_SIBLING(P,T); {Próba przejścia do prawego sąsiada } {węzła „P”} end; Writeln(_LABEL(N,T)); {Wypisanie węzła} end; procedure INORDER(N : NODE; T : TREE); var P : NODE; begin P := LEFTMOST_CHILD(N,T); {Próba przejścia do pierwszego dziecka } {węzła „N”} if P = EMP then Writeln(_LABEL(N,T)) {Jeśli „P” jest liściem, to go wypisz} else begin INORDER(P,T); {Wywołanie rekurencyjne} 70 Drzewa Writeln(_LABEL(N,T)); P := RIGHT_SIBLING(P,T); {Wypisanie węzła} {Próba przejścia do prawego sąsiada } {węzła „P”} while P <> EMP do begin INORDER(P,T); P := RIGHT_SIBLING(P,T); {Próba przejścia do prawego sąsiada} {węzła „P”} end; end; end; 71 Struktury danych 6 Grafy skierowane 6.1 Wprowadzenie Graf skierowany (d-graf) „G” składa się ze zbioru wierzchołków (Vertices) „V” oraz zbioru łuków (Arcs) „E” . Powszechne stało się więc oznaczenie G = (V,E). Każdy łuk jest uporządkowaną parą wierzchołków. (V,W) oznacza łuk od wierzchołka V, do wierzchołka W. Jeśli mówimy, że wierzchołek W sąsiaduje z V, to znaczy że w zbiorze „E” istnieje łuk (V,W). Na rysunku poniżej przedstawiony jest graf o 7 wierzchołkach i 11 łukach. Rysunek 15 - Przykładowy Graf Skierowany Ścieżką (path) w d-grafie jest ciąg wierzchołków V1,V2, ... ,Vn taki, że w zbiorze E istnieją łuki: (V1,V2), (V2,V3), ... ,(Vn-1,Vn). Długością ścieżki nazywamy ilość łuków na niej występujących, czyli w tym przypadku n-1. Szczególnym przypadkiem jest ścieżka Vi, która ma długość 0 i reprezentuje przejście od wierzchołka Vi do Vi. Każdy wierzchołek grafu „G” posiada swój własny indeks. Informacją merytoryczną oprócz samej struktury grafu może być: • Etykieta przechowywana w każdym wierzchołku • Etykieta przechowywana w każdym łuku. Graf G=(V,E) nazywamy spójnym, gdy dla każdych dwóch wierzchłków Va,Vb grafu istnieje ścieżka je łącząca. Wierzchołkiem wiszącym „V” nazywamy taki wierzchołek, dla którego nie da się znaleźć innego wierzchołka „W” aby w zbiorze „E” istniał łuk (V,W). 72 Grafy skierowane Grafem o składowych niespójnych nazywamy taki Graf, który posiada przynajmniej jeden wierzchołek wiszący. Łuk „e” należący do zbioru „E” nazywamy mostem, gdy po jego usunięciu powstaną dwie składowe niespójne. Wierzchołek „v” należący do zbioru „V” nazywamy rozwidlającym, jeśli po usunięciu jego samego i wszystkch powiązanych z nim łuków otrzymamy dwie składowe niespójne. W tym rozdziale stworzymy nowy typ danych: ADT GRAPH. Każdy graf będziemy rozpatrywali w pewien specyficzny sposób. Podczas kompilacji programu będzie tworzona globalna tablica wierzchołków, których liczba w trakcie działania programu nie będzie ulegała zmianie. Na wierzchołki będą mogły być nakładane różne grafy. Nasze operacje ADT GRAPH będą więc operować wyłącznie na łukach poszczególnych grafów. Zgromadzona w wierzchołkach informacja merytoryczna będzie wspólna dla wszystkich grafów. Sytuację tę przedstawia rysunek nr 16. Rysunek 16 Grafy oparte na wspólnych wierzchołkach 73 Struktury danych 6.2 Operacje ADT GRAPH Dla ADT GRAPH elementerne jest przeglądanie sąsiadów danego wierzchołka V, czyli operacje które zdefiniujemy muszą pozwalać na wykonanie instrukcji: For każdy wierzchołek W sąsiadujący z wierzchołkiem V Pewna akcja dla W do Wygodnie jest użyć pojęcia indeksu dla ponumerowania wszystkich wierzchołków grafu. W wierzchołku (Vertex) będziemy przechowywali dwie informacje: • Indeks wierzchołka • Kolor wierzchołka – jako przykład informacji merytorycznej Wyróżnimy trzy podstawowe operacje: function FIRST_VERTEX(V : Vertex) : Index; Funkcja zwraca indeks pierwszego wierzchołka sąsiadującego z V, lub EMP (indeks pusty), gdy V nie ma sąsiadów. function NEXT_VERTEX(V : Vertex; I : Index) : Index; Funkcja zwraca indeks następnego po „I” spośród wierzchołków sąsiadujących z „V”, lub EMP, gdy „I” jest indeksem ostatniego sąsiada V. function GET_VERTEX(V : Vertex; I : Index) : Vertex Funkcja zwraca wierzchołek sąsiadujący z „V” o indeksie „I”. Dla jednej z implementacji grafu zdefiniujemy także zestaw operacji pozwalających na jego modyfikację. Będą to funkcje: function IS_EDGE(V1,V2 : Vertex) : boolean; Funkcja określa, czy istnieje łuk (V1,V2) function CREATE_EDGE(V1,V2 : Vertex) : boolean; Funkcja tworzy łuk (V1,V2), jeśli oczywiście wcześniej nie istniał. function DELETE_EDGE(V1,V2 : Vertex) : boolean; Funkcja usuwa z grafu łuk (V1,V2), jeśli oczywiście istnieje. procedure DELETE_VERTEX(V : Vertex); Procedura usuwa wszystkie łuki związane z wierzchołkiem V. procedure INITIALIZE; Procedura inicjalizuje graf. 74 Grafy skierowane procedure MAKENULL; Procedura usuwa wszystkie łuki grafu. procedure DESTROY; Procedura usuwa graf z pamięci komputera. 6.3 Macierzowa implementacja Grafu W macierzowej implementacji graf jest reprezentowany przez odpowiednią macierz sąsiedztwa „T. Jest to macierz kwadratowa posiadająca „MaxVertex” wierszy. Jeśli T[i,j] = true (gdzie „i” jest numerem wiersza a „j” numerem kolumny) wówczas istnieje łuk łącząca wierzchołek o indeksie „i” z wierzchołkiem o indeksie „j”; Jeśli T[i,j] = false, wtedy takiej krawędzi nie ma. Nagłówek macierzowej implementacji będzie miał więc postać: type EMP = 0; {pusty index} Index : Integer; T : array[1 .. MaxVertex,1 .. MaxVertex] of boolean; {macierz sąsiedztwa} Vertex = record Id : Index; Color : Integer; {informacja merytoryczna wierzchołka} end; var Ver : array[1 .. MaxVertex] of Vertex; {Przestrzeń wierzchołków} , a operacje ADT GRAPH: function FIRST_VERTEX(V : Vertex) : Index; var i : Index; begin FIRST_VERTEX := EMP; for i := 1 to MaxVertex do {Sprawdzamy wszystkie wierzchołki } {z tablicy „Ver”} if T[Ver.Id,i] = true then FIRST_VERTEX := i; {Znaleziono sąsiada wierzchołka „V”} end; {FIRST_VERTEX} 75 Struktury danych function NEXT_VERTEX(V : Vertex; I : Index) : Index; var j : integer; begin NEXT_VERTEX := EMP; for j := I+1 to MaxVertex do {Zaczynamy poszukiwanie od } {wierzchołka „I+1”} if T[Ver.Id,j] = true then NEXT_VERTEX := j; {Znaleziono następnego sąsiada} end; function GET_VERTEX(V : Vertex; I : Index) : ^Vertex; var {funkcja zwraca wskaźnik do wierzchołka} t : Index; begin t := FIRST_VERTEX(V); {Poszukujemy pierwszego sąsiada } {wierzchołka „V”} while t <> I do t := NEXT_VERTEX(V,t); {Przesuwamy się do sąsiada o indeksie „I”} GET_VERTEX := @Ver[t]; {Zwracamy wskaźnik do wierzchołka} end; 76 Grafy skierowane 6.4 Implementacja grafu przez listy sąsiedztwa Przy tej implementacji odniesiemy się bezpośrednio do implementacji drzewa przez listy dzieci. Możemy bowiem przyjąć, że wierzchołki grafu odpowiadają węzłom drzewa, natomiast kolejni sąsiedzi danego wierzchołka są kolejnymi dziećmi danego rodzica. Dlatego też wykorzystamy gotowe operacje ADT TREE. Możemy więc dla przykładu rozpatrzyć rysunek nr 13, który będzie teraz przedstawiał odpowiednie listy sąsiedztwa (Adjacency lists). Dla czytelności tej implementacji przyjmijmy, że graf jest obiektem. Nagłówek listowej implementacji będzie więc miał postać: uses trees, lists; {Bezpośrednio korzystamy z operacji ADT TREE oraz ADT LIST} const MaxVertex = MaxNodes; {Maksymalna liczba wierzchołków jest } EmpVertex = EMP; {maksymalną liczbą węzłów, } {pusty wierzchołek oznaczamy jako EMP} type Index = node; {typ indeksowy dla grafu} pVertex =^Vertex; {Wskaźnik do wierzchołka grafu} Vertex = record {Rekord reprezentujący wierzcholek grafu} Color : integer; {Przykładowa informacja merytoryczna wierzchołka} Id : Index; {Indeks wierzchołka} end; GRAPH = object {Graf jest tutaj obiektem} Edg : Tree; {Za łuki grafu odpowiada drzewo „Edg” } function function function function function function {Nagłówki operacji wykonywanych na obiekcie - grafie} FIRST_VERTEX(V : Vertex) : Index; NEXT_VERTEX(V : Vertex;I : Index) : Index; GET_VERTEX(V : Vertex; I : Index) : pVertex; CREATE_EDGE(V1,V2 : Vertex) : boolean; DELETE_EDGE(V1,V2 : Vertex) : boolean; IS_EDGE(V1,V2 : Vertex) : boolean; procedure DELETE_VERTEX(V1 : Vertex); procedure INITIALIZE; procedure DESTROY; procedure MAKENULL; end; procedure InitSpaceOfVertex; {Procedura inicjująca przestrzeń wierzchołków} Musimy określić przestrzeń wierzchołków: var 77 Struktury danych Ver : array[1..MaxVertex] of Vertex; {Przestrzeń wierzchołków, które } {będą dowolnie wykorzystywane przez grafy} Dalej zdefiniujemy wszystkie operacje dla tej implementacji grafu: function GRAPH.FIRST_VERTEX(V : Vertex) : Index; begin FIRST_VERTEX := LEFTMOST_CHILD(V.Id,Edg); {Pierwszy sąsiad w grafie } {jest odpowiednikiem pierwszego dziecka w drzewie} end; function GRAPH.NEXT_VERTEX(V : Vertex;I : Index) : Index; begin NEXT_VERTEX := RIGHT_SIBLING(V.Id,Edg); { Następny sąsiad w grafie } {jest odpowiednikiem następnego dziecka w drzewie} end; function GRAPH.GET_VERTEX(V : Vertex; I : Index) : pVertex; var T : Index; begin T := GRAPH.FIRST_VERTEX(V); {T będzie pierwszym sąsiadem wierzchołka „V”} while T <> I do T := GRAPH.NEXT_VERTEX(V,T); {Przejście do sąsiada „V” o indeksie „I” } GET_VERTEX := @Ver[T]; {Zwrócenie wskaźnika do odpowiedniego wierzchołka} end; function GRAPH.CREATE_EDGE(V1,V2 : Vertex) : boolean; var L : List; begin if GRAPH.IS_EDGE(V1,V2) then CREATE_EDGE := false {Gdy łuk (V1,V2) już w grafie istnieje} else begin L := Edg.Header[V1.Id]; {L wskazuje na odpowienią listę sąsiadów wierzchołka V1} INSERT(V2.Id,_END(L),L); {Dodanie na koniec listy „L” indeksu wierzchołka V2} CREATE_EDGE := true; end; end; 78 Grafy skierowane function GRAPH.IS_EDGE(V1,V2 : Vertex) : boolean; begin {Jeśli w liście sąsiedztwa wierzchołka V1 znajduje się} {indeks wierzchołka V2, wówczas funkcja zwróci wartośc „true”} IS_EDGE := INLIST(V2.Id, Edg.Header[V1.Id]) <> 0; end; function GRAPH.DELETE_EDGE(V1,V2 : Vertex) : boolean; var T : Index; L : List; begin L := Edg.Header[V1.Id]; {Ustawienie wlasciwej listy sąsiedztwa „L” dla wierzchołka V1} DELETE_EDGE := false; T:=GRAPH.FIRST_VERTEX(V1); {T będzie pierwszym sąsiadem V1} while T <> EMP do {Przeszukujemy wszystkich sąsiadów V1} begin if T = V2.Id then begin {Jeśli sąsiadem „T” jest wierzchołek „V2” } DELETE( LOCATE(T,L) ,L); {Usunięcie wierzchołka V2 z listy sąsiedztwa V1} DELETE_EDGE := true; end; T := GRAPH.NEXT_VERTEX(V1,T); {Przejście z „T” do następnego sąsiada wierzchołka V1} end; end; procedure GRAPH.DELETE_VERTEX(V1 : Vertex); var I : Index; begin MAKENULL_LIST(Edg.Header[V1.Id]); {Wyczyszczenie listy sąsiedztwa wierzchołka V1} for I := 1 to MaxVertex do {Pętla dla wszystkich list sąsiedztwa grafu} GRAPH.DELETE_EDGE(Ver[I],V1); {Usunięcie łuków, które dochodzą do wierzchołka „V1”} end; procedure GRAPH.MAKENULL; var I : Index; begin for I := 1 to MaxVertex do 79 Struktury danych GRAPH.DELETE_VERTEX(Ver[I]); {Usunięcie wszystkich łuków grafu} end; procedure GRAPH.INITIALIZE; var I : Index; begin for I := 1 to MaxVertex do begin INIT_LIST(Edg.Header[I]); end; end; {Zainicjowanie list sąsiedztwa dla } {wszystkich wierzchołków grafu} procedure GRAPH.DESTROY; var I : Index; begin for I := 1 to MaxVertex do begin DESTROY_LIST(Edg.Header[I]); {Usunięcie z pamięci wszystkich list sąsiedztwa dla danego grafu} end; end; procedure InitSpaceOfVertex; var I : Index; begin for I := 1 to MaxVertex do begin {Pętla dla każdego wierzchołka z przestrzeni wierzchołków, } Ver[I].Id := I; {która ustala jego indeks oraz } Ver[I].Color := I*I; {przykładową informację merytoryczną} end; end; 80 Grafy skierowane 6.5 Przykład zastosowania ADT GRAPH Jest kilka sposobów przeglądania grafów. Jednym z nich jest algorytm przeglądania wgłąb DFS (Deep First Search). Główną cechą tego algorytmu jest to, że stara się on zawsze zagłębić w strukturę grafu tzn. jeśli to możliwe, odwiedzać w pierwszej kolejności sąsiadów. Jeśli więc algorytm ten zadziałałby na grafie z rysunku nr 15, poczynając od wierzchołka 1 wówczas kolejność odwiedzonych wierzchołków wyglądałaby: 1,2,3,5,4,6,7 . Dla algorytmu DFS istotna jest tablica odwiedzonych wierzchołków MARK. Jeśli algorytm odwiedza wierzchołek „i” wówczas MARK[i] := true . Przed rozpoczęciem działania algorytmu DFS musimu ustalić MARK[k] := false dla k = 1 ... MaxVertex. Procedurę DFS wywołujemy dla początkowego wierzchołka – wierzchołka startowego, zatem chcąc odwiedzić wszystkie wierzchołki grafu (pamiętajmy, że istnieją grafy nie posiadające żadnych łuków !) musimy procedurę DFS wywołać dla każdego wierzchołka. Poniżej przedstawiona jest najprostsza wersja procedury DFS: procedure DFS(V : Vertex); var Id : Index; {pomocniczy index wierzcholka} begin MARK[V.Id] := true; {Zaznaczenie wierzchołka „V” jako odwiedzony} Do_Something_with_vertex(V); {Wykonanie dowolnej operacji na odwiedzanym wierzchołku „V”} Id := G.FIRST_VERTEX(V); {„Id” ustawiamy na pierwszego sąsiada wierzchołka „V”} while Id <> EmpVertex do begin {Pętla dla wszystkich sąsiadów wierzchołka „V”} if MARK[Id] = false then {Jeśli sąsiad „Id” nie został jeszcze odwiedzony} DFS(ver[Id]); {Wywołanie rekurencyjne dla wierzchołka „Id”} Id := G.NEXT_VERTEX(V,Id); {Przejście do następnego sąsiada wierzchołka „V”} end; end; Aby odwiedzić wszystkie wierzchołki grafu wystarczy zatem napisać: for i:=1 to MaxVertex do MARK[i] := false; {Początkowe ustalenie tablicy MARK} for i:=1 to MaxVertex do {Pętla dla wszystkich wierzchołków grafu} if MARK[i] = false then DFS(ver[i]); {Zagłębiamy się w nieodwiedzone wierzchołki} 81 Struktury danych 7 Zbiory 7.1 Wprowadzenie Zbiory, którymi będziemy się zajmowali w tym rozdziale będą odpowiednikami zbiorów matematycznych, dlatego nie będziemy się szczegółowo wgłębiać w ich definicje. Mimo, iż we współczesnych językach programowania istnieje typ danych reprezentujący zbiór, to jednak posiada on jedną podstawową wadę. Wyobraźmy sobie zbiór A, posiadający „k” elementów. Komputer, chcąc wygenerować wszystkie podzbiory zbioru A, musi zarezerwować pamięć dla P(A) zbiorów, czyli dla 2k elementów. To ograniczenie wpływa znacząco na maksymalny rozmiar zbioru A, który wynosi log2M , gdzie M jest ilością wolnej pamięci komputera. Nowy typ danych ADT SET, który stworzymy, będzie pozwalał na wykorzystanie całej dostępnej pamięci komputera na zapamiętanie elementów zbioru. 7.2 Operacje ADT SET Część operacji, które za chwilę zdefiniujemy, jest nam doskonale znana – są to operacje czysto matematyczne. Pozostałe operacje będą potrzebne do implementacji typu zbiorowego. Typ zbiorowy będzie oznaczany jako PtrSet. Operacje matematyczne procedure UNION(A,B : PtrSet; var C : PtrSet); Procedura odpowiada sumie mnogościowej zbiorów A i B. Zbiór wynikowy oznaczany jest przez C. procedure INTERSECTION(A,B : PtrSet; var C : PtrSet); Procedura odpowiada iloczynowi mnogościowemu zbiorów A i B. Zbiór wynikowy oznaczany jest przez C. procedure DIFFERENCE(A,B : PtrSet; var C : PtrSet); Procedura odpowiada różnicy zbiorów A i B. Zbiór wynikowy oznaczany jest przez C. 82 Zbiory procedure MERGE(A,B : PtrSet; var C : PtrSet); Procedura odpowiada różnicy symetrycznej zbiorów A i B. Zbiór wynikowy oznaczany jest przez C. function MEMBER(X : ElementType; A : PtrSet) : boolean; Funkcja zwraca wartość logiczną true, gdy element „X” znajduje w zbiorze „A”. W przeciwny wypadku funkcja zwraca wartość false. się function MIN(A : PtrSet) : ElementType; Funkcja zwraca minimalny element zbioru A. Gdy Zbiór jest pusty, zwraca EMPTYELEMENT. function MAX(A : PtrSet) : ElementType; Funkcja zwraca maksymalny element zbioru A. Gdy Zbiór jest pusty, zwraca EMPTYELEMENT. function EQUAL(A,B : PtrSet) : boolean; Gdy zbiory A i B są równe, funkcja zwraca wartość true, w przeciwnym wypadku wartość false. procedure INSERT_TO_SET(X : ElementType; var A : PtrSet); Procedura dodaje do zbioru „A” element „X”. procedure DELETE_FROM_SET(X : ElementType; var A : PtrSet); Procedure usuwa ze zbioru „A” element „X”. procedure MAKENULL_SET(var A : PtrSet); Procedura czyni zbiór „A” zbiorem pustym procedure ASSIGN_SET(var A : PtrSet; B : PtrSet); Procedura czyni zbiór „A” równy zbiorowi polecenia A := B „B”. Jest to odpowiednik function FIND_IN_SET(X : ElementType) : PtrSet; Funkcję tą stosuje się tylko wtedy, gdy mamy do czynienia z pewną rodziną zbiorów. Jeśli element „X” znajdzie się w jednym z tych zbiorów, wówczas funkcja zwraca ten zbiór. Dla różnego rodzaju zbiorów nie da się określić typu jej elementów, dlatego też nie da się jednoznacznie określić relacji mniejszości (większości) dla wszystkich zbiorów. W tym rozdziale zdefiniujemy więc operację porównawczą: 83 Struktury danych function COMPARE(a,b : ELEMENTTYPE) : integer; Funkcja zwraca: 0 - gdy elementy „a” i „b” są równe 1 - gdy wiekszy jest element „a” 2 - gdy wiekszy jest element „b”. 84 Zbiory 7.3 Bitowo – Wektorowa implementacja ADT SET To, która implementacja ADT SET jest najlepsza zależy od: • Rodzaju operacji, które chcemy na zbiorach wykonywać. • Rozmiaru zbiorów, na których będziemy pracować. Jeśli wiemy jakich rozmiarów będzie nasz zbiór, wówczas stosuje się implementację bitowo wektorową. Każdy zbiór jest tutaj reprezentowany przez tablicę: PtrSet = array[1..MaxSize] of boolean; gdzie MaxSize jest dopuszczalną ilością elementów zbioru. Z kolei każdy element jest reprezentowany przez odpowiedni wektor, który jest liczbą naturalną z przedziału 1...MaxSize. Jeśli więc element „i” jest w zbiorze „A” (typu PtrSet), wówczas A[i] = true. Jeśli typ elementów zbioru nie jest liczbą naturalną z przedziału 1...MaxSize, wtedy stosujemy odwzorowanie , choćby przy pomocy ADT MAPPING. DomainType = 1 .. MaxSize; RangeType = „biały” , „czerwony” , „żółny” , ... , „niebieski” {Dowolny inny typ} Operacje MEMBER, INSERT_TO_SET oraz DELETE_FROM_SET są tutaj natychmiastowe i nie będziemy ich definiowali. Wyjątkowo prosto prezentuje się operacja UNION. Procedure UNION(A,B : PtrSet; var C : PtrSet); var f : integer; begin for i := 1 to MaxSize do C[i] := A[i] or(*) B[i]; end; Definiując operacje INTERSECTION oraz DIFFERENCE wystarczy odpowiednio w miejsce (*) wstawić and i and not . Operacja MAKENULL_SET przypisuje wszystkim elementom tablicy reprezentującej zbior wartość false. Operacja ASSIGN_SET przepisuje wartości wszystkich elementy jednego zbioru do drugiego zbioru. Operacje MIN i MAX są po prostu odpowiedzialne za znalezienie największego elementu w tablicy. 85 Struktury danych 7.4 Implementacja ADT SET przez listy uporządkowane W tej implementacji każdy zbiór będzie reprezentowany przez listę elementów uporządkowanych rosnąco (malejąco). Dla ułatwienia implementacji będziemy zajmowali się listami „z głową”. Warto tutaj zaznaczyć, że w danej liście reprezentującej zbiór nigdy nie będzie duplikatów. W tej implementacji zbiór zajmuje w pamięci tylko tyle miejsca, ile potrzebne jest do zapamiętania elementów, co wpływa jednak niekorzystnie na szybkość operacji matemetycznych, które są nad nim wykonywane. Również implementacja jest tutaj bardziej skomplikowana. Rysunek 17- Dwa zbiory reprezentowane przez uporządkowane listy W tej implementacji zdefiniujemy dodatkowo procedurę inicjującą nowy zbiór (posiadający tylko głowę) w pamięci komputera oraz procedurę usuwającą ten zbiór z pamięci. Nagłówek dla tej implementacji zbioru wygląda następująco: const EMPTYELEMENT = MaxInt; {Określenie pustego elementu} type ElementType = integer; {Przykładowy typ elementów zbioru} PtrSet = ^celltype; {Wskaźnik do zbioru - Pointer to Set} celltype = record {Pojedynczy obiekt w zbiorze} Element : ELEMENTTYPE; Next : PtrSet; {Wskaźnik do następnego obiektu w zbiorze} end; Na początku zdefiniujemy funkcję porównującą elementy zbioru: function COMPARE(a,b : ElementType) : integer; begin if a=b then COMPARE := 0 86 Słowniki else if a>b then COMPARE := 1 else COMPARE := 2; end Dla uproszczenia zapisu napiszmy pomocniczą procedurę Attach, która dołączy nowy obiekt z wartością X do obiektu wskazywanego przez „Ptr”. procedure Attach(var Ptr : PtrSet,X : ElementType); begin new(Ptr^.Next); Ptr := Ptr^.Next; Ptr^.Element := X; Ptr^.Next := nil; end; Teraz zdefiniujemy operacje matematyczne. procedure UNION(A,B : Pointer; var C : PtrSet); var Acurrent,Bcurrent,Ccurrent : PtrSet; begin Acurrent := A^.Next; Bcurrent := B^.Next; new(C); {Utworzenie głowy nowej listy C} Ccurrent := C; while (Acurrent <> nil) or (Bcurrent <> nil) do begin {Pętla wykonywana do momentu osiągnięcia końca list A i B} if (Acurrent = nil) and (Bcurrent <> nil) then begin {koniec listy A} Attach(Ccurrent,Bcurrent^.Element); Bcurrent := Bcurrent^.Next; end else if (Acurrent <> nil) and (Bcurrent = nil) then begin {koniec listy B} Attach(Ccurrent,Acurrent^.Element); Acurrent := Acurrent^.Next; end else {Acurrent i Bcurrent nie wskazują końców list A i B} begin if COMPARE(Acurrent^.Element,Bcurrent^.Element)=0 then begin Attach(Ccurrent,Acurrent^.Element); Acurrent := Acurrent^.Next; {Doczepiamy element } Bcurrent := Bcurrent^.Next; { występujący w obu zbiorach} end else if COMPARE(Acurrent^.Element,Bcurrent^.Element)=1 then begin Attach(Ccurrent,Bcurrent^.Element); {Doczepiamy element } Bcurrent := Bcurrent^.Next; { występujący w zbiorze B} 87 Struktury danych end else if COMPARE(Acurrent^.Element,Bcurrent^.Element)=2 then begin Attach(Ccurrent,Acurrent^.Element); {Doczepiamy element } Acurrent := Acurrent^.Next; { występujący w zbiorze A} end; end end; end; procedure INTERSECTION(A,B : PtrSet; var C : PtrSet); var Acurrent,Bcurrent,Ccurrent : PtrSet; begin Acurrent := A^.Next; Bcurrent := B^.Next; new(C); Ccurrent := C; while (Acurrent <> nil) and (Bcurrent <> nil) do begin {Pętla wykonywana} { do momentu osiągnięcia końca listy A lub końca listy B} if COMPARE(Acurrent^.Element,Bcurrent^.Element)=0 then begin Attach(Ccurrent,Acurrent^.Element); {Dołączamy element } Acurrent := Acurrent^.Next; { występujący w obu listach} Bcurrent := Bcurrent^.Next; end else if COMPARE(Acurrent^.Element,Bcurrent^.Element)=1 then Bcurrent := Bcurrent^.Next else if COMPARE(Acurrent^.Element,Bcurrent^.Element)=2 then Acurrent := Acurrent^.Next; end; end; procedure DIFFERENCE(A,B : PtrSet; var C : PtrSet); var Acurrent,Bcurrent,Ccurrent : PtrSet; begin Acurrent := A^.Next; Bcurrent := B^.Next; new(C); Ccurrent := C; while (Acurrent <> nil) do begin {Pętla aż do osiągnięcia końca listy A} if Bcurrent = nil then begin {koniec listy B} {, kolejne elementy z A dodajemy do C} Attach(Ccurrent,Acurrent^.Element); Acurrent := Acurrent^.Next; end else 88 Słowniki if COMPARE(Acurrent^.Element,Bcurrent^.Element)=0 then begin {usuwamy element, czyli nie doczepiamy go do C} Acurrent := Acurrent^.Next; Bcurrent := Bcurrent^.Next; end else if COMPARE(Acurrent^.Element,Bcurrent^.Element)=2 then begin Attach(Ccurrent,Acurrent^.Element); {Dodajemy element ze zbioru A do zbioru C} Acurrent := Acurrent^.Next; {gdyż nie ma go w zbiorze B} end else if COMPARE(Acurrent^.Element,Bcurrent^.Element)=1 then Bcurrent := Bcurrent^.Next; {Przesuwamy się w zbiorze B, aby } {kontynuować poszukiwanie elementu wskazywanego przez Acurrent} end; end; procedure MERGE(A,B : PtrSet; var C : PtrSet); var C1,C2 : PtrSet; begin UNION(A,B,C1); INTERSECTION(A,B,C1); DIFFERENCE(C1,C2,C); end; function MEMBER(X : ElementType; A : PtrSet) : boolean; var Temp : PtrSet; begin Temp := A^.Next; {ustawiamy Temp na najmniejszym elemencie w zbiorze A} MEMBER := false; while Temp <> nil do begin {Przeszukujemy cały zbiór A} if Temp^.Element = X then MEMBER := true; Temp := Temp^.Next; {Przechodzimy do następnego elementu w zbiorze A} end; end; function MIN(A : PtrSet) : ElementType; begin if A^.Next = nil then MIN :=EMPTYELEMENT; else MIN := A^.Next^.Element; {Najmniejszy element może } { być tylko na początku rosnąco uporządkowanej listy } end; function MAX(A : PtrSet) : ElementType; var 89 Struktury danych Current : PtrSet; begin Current := A; while Current^.Next <> nil do Current := Current^.Next; if Current = A then MAX := EMPTYELEMENT {Największy element może } {być tylko na końcu rosnąco uporządkowanej listy} else MAX := Current^.Element; end; function EQUAL(A,B : PtrSet) : boolean; var Acurrent,Bcurrent : PtrSet; begin EQUAL := true; Acurrent := A; Bcurrent := B; {Rozpatrujemy część wspolną zbiorów ..} while (Acurrent <> nil) and (Bcurrent <> nil) do begin if COMPARE(Acurrent^.Element,Bcurrent^.Element) <> 0 then EQUAL := false; Acurrent := Acurrent^.Next; Bcurrent := Bcurrent^.Next; end; {Gdyby jeden ze zbiorów był zawarty w drugim ..} if (Acurrent <> nil) or (Bcurrent <> nil) then EQUAL := false; end; Oprócz operacji matemetycznych należy jeszcze zapisać operacje modyfikujące zbiory. Należą do nich: procedure INSERT_TO_SET(X : ElementType; var A : PtrSet); var Current,NewPtr : PtrSet; begin Current := A; If MEMBER(X,A) = false then begin {Jeśli X nie ma w zbiorze A} If A^.Next = nil then Attach(A,X) {Zbiór A jest pusty} else begin {zbiór A nie jest pusty} Current := A; while COMPARE(Current^.Next^.Element,X) = 1 do {Ustalenie pozycji, na którą} Current := Current^.Next; {wstawimy nowy element X} NewPtr := Current^.Next; new(Current^.Next); {Dodanie nowego elementu} Current := Current^.Next; 90 Słowniki Current^.Next := NewPtr; end; end; end; procedure DELETE_FROM_SET(X : ElementType; var A : PtrSet); var Current,Temp : PtrSet; begin Current := A; while Current^.Next <> nil then begin {Poszukujemy elementu X} if COMPARE(X,Current^.Next^.Element)=0 then begin Temp := Current^.Next; Current^.Next := Current^.Next^.Next; {Przepięcie wskaźników} dispose(Temp); {Usunięcie elementu X} end else Current := Current^.Next; {Przejście do następnego elementu} end; {w celu znalezienia X} end; procedure INIT_SET(var A : PtrSet); begin new(A); A^.Next := nil; end; procedure DESTROY_SET(var A : PtrSet); begin if A^.Next <> nil then DESTROY_SET(A^.Next); dispose(A); end; procedure MAKENULL_SET(var A : PtrSet); begin DESTROY_SET(A); INIT_SET(A); end; procedure ASSIGN_SET(var A : PtrSet; B : PtrSet); var Acurrent, Bcurrent : PtrSet; begin MAKENULL_SET(A); Acurrent := A; 91 Struktury danych Bcurrent := B^.Next; {BCurrent wskazuje na najmniejszy element zbioru B} while Bcurrent <> nil do begin Attach(Acurrent,Bcurrent^.Element); {Dołączenie do A kolejnego elementu z B} Bcurrent := Bcurrent^.Next; end; end; 7.5 Przykład zastosowania ADT SET Aby zademonstrować kilka operacji na zbiorach, musimy na początku określić zmienne globalne: var k : integer; Tab : Array[1..MaxSets] of PtrSet; Teraz stworzymy 3 nowe zbiory. for k:=1 to 3 do INIT_SET(Tabl[k]); Dodamy do nich elementy. INSERT(1,Tab[1]); INSERT(2,Tab[1]); INSERT(3,Tab[1]); INSERT(3,Tab[2]); INSERT(4,Tab[2]); INSERT(5,Tab[2]); {1---> 1,2,3} {2---> 3,4,5} I zastosujemy operacje matematyczne: UNION(Tab[1],Tab[2],Tab[3]); {1v2} DIFFERENCE(Tab[1],Tab[2],Tab[4]); {1\2} INTERSECTION(Tab[1],Tab[2],Tab[5]); {1^2} 92 {Rodzina zbiorów} Słowniki 8 Słowniki 8.1 Wprowadzenie Słowniki można interpretować jako pewnego rodzaju zbiory. Istotną jednak cechą jest tutaj dostępność do danych, czyli czas, po jakim możemy odszukać element w słowniku. W tym celu często stosuje się tzw. kontenery (buckets), które są odpowiednikiem podziału każdego słownika na części według pierwszej litery. Im więcej takich kontenerów słownik posiada, tym szybciej można się dostać do jego elementów. Niestety, odbywa się to kosztem zwiększonego zapotrzebowania na pamięć . W tym rozdziale podamy trzy sposoby implementacji słowników - ADT DICTIONARY. 8.2 Operacje ADT DICTIONARY Typ danych przechowywanych w słowniku będziemy oznaczali jako NameType. Do podstawowych operacji, które wykonyje się na słownikach należą: procedure INSERT_TO_DICTIONARY(X : NameType; var A : Dictionary); Procedura powodująca wstawienie do słownika A elementu X. Gdy element X już wcześniej występował w słowniku A, wówczas nie dzieje się nic. procedure DELETE_FROM_DICTIONARY(X : NameType; var A : Dictionary); Po wywołaniu tej procedury element X nie będzie się już znajdował w słowniku A. function MEMBER(X : NameType; var A : Dictionary) : boolean; Funkcja zwraca wartość logiczną true, gdy element w słowniku A. W przeciwnym wypadku funkcja zwraca false; X znajduje się Procedure MAKENULL_DICTIONARY(var A : Dictionary); Procedura usuwa wszystkie elementy ze słownika A. 93 Struktury danych 8.3 Tablicowo-Kursorowa implementacja ADT DICTIONARY W tej implementacji słownik jest rekordem składającym się z: • Tablicy elementów typu NameType o stałym 1...MaxElements. • Kursora do ostatniego zajętego elementu w tablicy rozmiarze Główną wadą tej implementacji jest nieefektywne wykorzystanie pamięci przy zbiorach o rozmiarach mniejszych niż maksymalna pojemność słownika oraz jak się później okaże stosunkowo długi czas wykonania operacji wstawiania i usuwania elementu ze słownika. W tej implementacji nagłówek wygląda następująco: const MaxSize = 50; {Maksymalny rozmiar słownika} type NameType = String; {Typ elementów w słowniku} Dictionary = record Last : integer; {Kursor do ostatniego zajętego elementu} Data : array[1..MaxSize] of NameType; {Tablica dla elementów słownika} end; Podobnie jak w przypadku ADT SET z uwagi na różnorodność typu NameType musimy zdefiniować funkcję COMPARE function COMPARE(A,B : NAMETYPE) : integer; {Zwraca: Numer wiekszego argumentu} begin if A=B then COMPARE :=0 else if B>A then COMPARE := 2 else COMPARE := 1; end; Operacje ADT DICTIONARY mają postać: procedure DELETE_FROM_DICTIONARY(X : NameType; var A : Dictionary); var i : integer; begin if A.Last > 0 then begin {Słownik nie jest pusty} i := 1; {Przesunięcie "i" na własciwą pozycję} while (i < A.Last) and (COMPARE(X,A.Data[i]) <> 0) do inc(i); if COMPARE(A.Data[i],X)=0 then begin {Jeśli element „i” jest równy X} A.Data[i] := A.Data[A.Last]; {W miejsce usuwanego elementu } 94 Słowniki {wstawiamy ostatni w słowniku i zmniejszamy rozmiar słownika} dec(A.Last); end; end; end; procedure INSERT_TO_DICTIONARY(X : NameType; var A : Dictionary) begin if A.Last < MaxSize then begin {Jeśli słownik nie jest pełny} inc(A.Last); {Zwiększenie rozmiaru słownika} A.Data[A.Last] := X; {Ostatnim zajętym elementem w tablicy jest X} end; end; function MEMBER(X : NameType; var A : Dictionary) : boolean; var i : integer; IsIn : boolean; begin IsIn := false; i:=1; {Przeszukujemy słownik aż do napotkania X lub wyjścia } while (i < A.Last+1) and (IsIn = false) do begin { poza kursor A.Last} if COMPARE(A.Data[i],X) = 0 then IsIn := true; inc(i); {Przejście do następnego elementu} end; MEMBER := IsIn; end; procedure MAKENULL_DICTIONARY(var A : Dictionary); begin A.Last := 0; {Kursor do ostatniego elementu przyjmuje wartość 0} end; 8.4 Implementacja ADT DICTIONARY przez Haszowanie Otwarte Haszowanie Otwarte (Open Hashing) polega na podzieleniu słownika na specjalne klasy (kontenery), w których przechowywane są elementy. Im więcej takich klas występuje, tym szybsze staje się odszukiwanie pozycji w słowniku. 95 Struktury danych Każda klasa jest zarazem początkiem listy bez głowy, w której umieszczone są nieuporządkowane elementy danej klasy. Przydzielanie odpowiednikch klas elementom odbywa się dzięki charakterystycznej funkcji haszującej, która w zależności od różnorodności haseł w słowniku może mieć różną postać. Teoretycznie najlepsza funkcja haszująca rozmieści hasła w słowniku równomiernie, przez co średni czas dostępu do nich będzie najkrótszy. Przykładowy słownik pokazany jest na rysunku nr 18. Rysunek 18 - Słownik przy haszowaniu otwartym Tablica nagłówków klas zawiera wskaźniki do odpowiednich list elementów. Jeśli w danej klasie (kontenerze) nie ma żadnego hasła, wówczas odpowiedni wskaźnik dla klasy ma wartość nil. Dodawanie nowego elementu do słownika ma następujący przebieg: • Ustalenie odpowiedniego kontenera, dla dodawanego elementu przy pomocy funkcji haszującej • Pobranie nagłowka odpowiedniej listy • Wstawienie na początek tej listy nowego elementu. Usunięcie elementu ze słownika przebiega według schematu: • Ustalenie odpowiedniego kontenera dla usuwanego elementu przy pomocy funkcji haszującej • Pobranie nagłówka odpowiedniej listy 96 Słowniki • Znalezienie i usunięcie elementu z listy. Znajdowanie elementu w słowniku jest częścią procesu usuwania elementu ze słownika. Słownik jest tablicą nagłówków klas, co pokazuje nagłówek dla tej implementacji: const B = 50; {Ilość kontenerów} type Range = 0..B-1; {Typ indeksowy dla kontenerów} NameType = string[10]; Ptr = ^Cell; {Wskaźnik do rekordu w liście elementów} Cell = record Next : Ptr; {Wskaźnik do następnego ebiektu w liście elementów} Element : NameType; {Pojedyncze hasło w słowniku} end; DICTIONARY = array[Range] of Ptr; {Słownik jest tablicą nagłówków dla list elementów} Poniżej przedstawione są operacje dla implementacji ADT DICTIONARY przez haszowanie otwarte. function h(X : NAMETYPE) : Range; {Przykładowa funkcja haszująca, która} {elementowi przyporządkowuje sumę numerów znaków} { występujących w wyrazie} var i,r : integer; begin r := 0; for i:=1 to Length(X) do r := r + ORD(X[i]); h := r mod B; {Wartość musi mieścić się w type „Range”} end; function MEMBER(X : NameType; A : Dictionary) : boolean; var Current : Ptr; begin MEMBER := false; Current := A[h(X)]; {Ustalenie listy dla elementu X} while Current <> nil do begin {Przeglądane całej listy} {w celu znalezienia elementu X} if Current^.Element = X then MEMBER := true {Element X został znaleziony} else Current := Current^.Next; {Przejście do następnego elementu w liście} 97 Struktury danych end; end; procedure INSERT_TO_DICTIONARY(X : NameType; var A : Dictionary); var Bucket : Range; NewPtr : Ptr; begin if not MEMBER(X,A) then begin {Jeśli elementu X nie ma} {jeszcze w słowniku A} Bucket := h(X); {Ustalenie kontenera dla elementu X} new(NewPtr); if NewPtr <> nil then begin {Jeśli nowy obiekt został utworzony} NewPtr^.Element := X; {Przypisanie nowemu obiektowi wartości X} NewPtr^.Next := A[Bucket]; {Ustalenie nowej głowy dla listy } A[Bucket] := NewPtr; { elementów w danym kontenerze} end else writeln(' Operacja nie powiadla sie. Slownik jest pelny !'); end; end; procedure DELETE_FROM_DICTIONARY(X : NameType; var A : Dictionary); var Current,Temp : Ptr; Bucket : Range; begin Bucket := h(X); {Ustalenie listy, w której może znajdować się } {element przeznaczony do usunięcia} Current := A[Bucket]; if Current <> nil then begin {Jeśli lista zawiera elementy ze słownika} if Current^.Element = X then begin {Jeśli poszukiwany element } {znajduje się na pierwszej pozycji w liście} A[Bucket] := A[Bucket]^.Next; dispose(Current); {Usunięcie elementu} Current := nil; {Wyjście z pętli} end else {Usuwany element nie jest pierwszy w liście} while Current^.Next <> nil do begin if Current^.Next^.Element = X then begin Temp := Current^.Next; Current^.Next := Current^.Next^.Next; {Odczepienie elementu z listy} dispose(Temp); {Usunięcie elementu z pamięci} Current := nil; {Wyjście z pętli} end; end; end; 98 Słowniki end; procedure MAKENULL_DICTIONARY(var A : Dictionary); var i : Range; begin for i:=0 to B-1 do begin while A[i]<>nil do {Dopóki kontener nie będzie pusty} DELETE_FROM_DICTIONARY(A[i]^.Element,A); {Usunięcie pierwszego elementu} end; {w kontenerze} end; 8.5 Implementacja ADT DICTIONARY przez Haszowanie Zamknięte W haszowniu zamkniętym (closed hashing) zamknięta tablica haszująca (closed hash table) nie zawiera nagłówków klas elementów, lecz sama elementy słownika. W rezultacie każda klasa zawiera tylko 1 element. Gdy chcemy umieścić element X w tablicy, a h(X) wskazuje klasę, która jest zajęta (kolizja), to stosujemy rehaszowanie , czyli poszukiwanie alternatywnych lokalizacji h1(X), h2(X), ... w tablicy haszującej aż do znalezienia miejsca, lub stwierdzenia, że tablica jest pełna i element nie może być umieszczony. Aby dopuścić usuwanie elementów nie komplikując operacji MEMBER, rozróżniamy między wartością pusty (empty), a usunięty (deleted) i podczas usuwania wstawiamy na usunięte miejsce wartość „deleted”. Wtedy funkcja poszukująca element w słowniku sprawdza kolejne lokalizacje h 1(X), h2(X), ... aż do natrafienia na „empty”. Podczas wstawiania elementy „deleted” mogą być w razie braku wolnego miejsca użyte. Dla przykładu weźmy 4 hasła a,b,c,d w słowniku, których klucze mają wartość h(a)=3 h(b)=0 h(c)=4 h(d)=3. Stosujemy strategię rehaszowania liniowego wedle schematu: hi(X) = (h(X)+i) mod B, w takiej sytuacji jeśli chcemy wstawić „a” i 3 komórka tablicy jest zajęta, to szukamy kolejnych lokalizacji, czyli 4,5,6, ... B-1. Początkowo całą tablicę wypełniamy wartością „empty”. Przykładowa tablica może wyglądać następująco: ks tablicy tość ty 99 Struktury danych ty ted Operacja MEMBER przeszukuje kolejne lokalizacje aż do znalezienia elementu, lub napotkania wartości „empty”. Przykładowo jeśli chcemy znaleźć element „e” , a h(e)=6 wtedy widzimy, że komórka 6 ma martość „deleted”. Szukamy więc dalej i znajdujemy element „e” dla klucza h1(e)=7. Funkcja INSERT_TO_DICTIONARY powinna znaleźć pierwszą wolną lokalizaję w tablicy (czyli taką, dla której wartość jest „empty” lub „deleted”) i tam umieścić nowy element. Gdy taka lokalizacja nie znajduje się w tablicy, oznacza to, że słownik jest pełny. W implementacji zastosujemy dwie funkcje lokalizujące. function LOCATE(X : NameType; A : Dictionary) Przegląda słownik od klasy h(X) do momentu aż natrafi na X lub klasę pustą (empty), lub obejdzie tałą tablicę stwierdzając, że X nie ma w słowniku. Funkcja zwraca numer klasy, na której się zatrzyma (dla „empty” znajdzie puste miejsce). function LOCATE1(X : NameType; A : Dictionary) Działa tak jak LOCATE, jednak dodatkowo zatrzymuje się na „deleted”. Będzie przez nas użyta do efektywnego wstawiania nowych elementów do słownika. Funkcja h(X) będzie taka sama jak w przypadku haszowania otwartogo. Nagłówek dla implementacji ADT DICTIONARY przez Haszowanie Zamknięte ma postać: const Empty = ' '; {Oznaczenie pustej wartości w słowniku} Deleted = '**********'; {Onaczenie usuniętej wartości w słowniku} B = 10; {Ilość klas} type Range = 0..B-1; NameType = string[10]; Dictionary = array[Range] of NameType; , a poszczególne operacje: procedure MAKENULL_ DICTIONARY (var A : Dictionary); 100 Słowniki var i : Range; begin for i:=0 to B-1 do A[i]:=Empty; {Przypisanie wszystkim elementom } {tablicy wartości „empty”} end; function LOCATE(X : NameType; A : DICTIONARY) : Range; var initial,i : Range; begin initial := h(X); {Początkowy numer klasy} i := 0; {Przyrost dla początkowego numeru klasy} while (i<B) and (A[initial+i] <> empty) and (A[initial+i] <> X) do inc(i); LOCATE := (initial+i) mod B; end; function LOCATE1(X : NameType; A : DICTIONARY) : Range; var initial,i : Range; begin initial := h(X); i := 0; while (i<B) and (A[initial+i] <> Empty) and (A[initial+i] <> X) and (A[initial+i] <> Deleted) do inc(i); {Pętla zatrzymuje się na wartości „deleted”} LOCATE1 := (initial+i) mod B; end; function MEMBER(X : NameType; A : Dictionary) : boolean; begin MEMBER := A[ LOCATE(X,A) ]=X; {Jeśli funkcja LOCATE zwróci miejsce } {, na którym występuje element X, to znaczy, że jest on w słowniku} end; procedure INSERT_TO_DICTIONARY(X : NameType; var A : Dictionary); var Bucket : Range; begin if not MEMBER(X,A) then begin {Jeśli elementy nie ma jeszcze w słowniku} Bucket := LOCATE1(X,A); {Znalezienie pierwszego wolnego miejsca} if (A[Bucket] = Empty) or (A[Bucket] = Deleted) then A[Bucket] := X {Dodanie do słownika nowego elementu} else writeln(' Operacja nie powiadla sie. Slownik jest pelny !'); 101 Struktury danych end; end; procedure DELETE_FROM_DICTIONARY(X : NameType; var A : Dictionary); var Bucket : Range; begin Bucket := LOCATE(X,A); {Pobranie numeru klasy, w której wystepuje X} if A[Bucket] = X then A[Bucket] := Deleted; {Usunięcie elementu X ze słownika} end; 8.6 Przykład zastosowania ADT DICTIONARY Na poniższym przykładzie przedstawiono słownik wykorzystujący operacje ADT DICTIONARY. var D : Dictionary; begin MAKENULL_DICTIONARY(D); INSERT_TO_DICTIONARY('ala',D); INSERT_TO_DICTIONARY ('ma',D); INSERT_TO_DICTIONARY ('duzego',D); INSERT_TO_DICTIONARY ('szarego',D); INSERT_TO_DICTIONARY ('kota',D); DELETE_FROM_DICTIONARY ('szarego',D); if MEMBER('ala',D) then writeln('w slowniku jest ala'); end; 102 Słowniki 9 Pytania kontrolne 1. Czy usuwając element z listy w implementacji tablicowej ADT LIST zwalniamy pamięć zajmowaną przez informację organizacyjną tego elementu? 2. Czy wadą wskaźnikowej implementacji ADT list jest szybkość wykonania operacji LOCATE()? 3. Czy operacja DELETE() wykonuje się szybciej dla tablicowej implementacji ADT LIST w stosunku do implementacji wskaźnikowej? 4. Jakie są potencjalne wady wskaźnikowej implementacji list jednokierunkowych w stosunku do tablicowej implementacji list jednokierunkowych? 5. Czy usunięcie wszystkich elementów listy jest szybsze w tablicowej implementacji ADT LIST w stusunku do implementacji kursorowej? 6. W kursorowej implementacji ADT LIST znalazł się błąd – istnieje komórka, której kursor wskazuje na tą samą komórkę, np. komórka nr.7 posiada kursor wskazujący na komórkę nr 7. Czy oznacza to, że lista zawierająca omawianą komórkę jest pusta? 7. Czy możliwe jest wykonanie procedury INITIALIZE, jeśli przestrzeń komórek w kursorowej implemententacji ADT LIST zawiera błędne wartości? 8. Czy stosem w tablicowej implementacji ADT STACK jest rekord składający się ze stałej wielkości tablicy elementów i ogranicznika stosu Top? 9. Czy funkcja TOP( ) różni się implementacjach ADT STACK od funkcji POP( ) tym, że pobiera wartość ze szczytu stosu bez jego usuwania? 10. Czy kolejkę we wskaźnikowej implementacji ADT QUEUE możemy utożsamiać z listą jednokierunkową? 11. Czy po wywołaniu MAKENULL_QUEUE( ) we wskaźnikowej implementacji ADT QUEUE wskaźniki Front i Rear wskazują na to samo? 12. Dlaczego w tablicowej implementacji kolejki cyklicznej nie wykorzystuje się wszystkich komórek tablicy na elementy kolejki? 13. Jeśli wartości Front i Rear w tablicowej implementacji kolejki cyklicznej różnią się o 1 (modulo wielkość tablicy kolejki), to czy kolejka może być pełna? 14. Czy we wskaźnikowej implementacji odwzorowania ADT MAPPING zbiory dziedziny i przeciwdziedziny (Range i Domain) muszą być równoliczne? 15. Czy we wskaźnikowej implementacji odwzorowania ADT MAPPING procedura ASSIGN() działa tak samo długo jak procedura COMPUTE()? 16. Czy procedura INORDER() dla drzewa 2←1→3→4 wypisze jego węzły w kolejności 2,1,3,4? 17. Procedura PREORDER() wypisała węzły drzewa w kolejności 1,2,3,4. Jakie jest prawdopodobieństwo, że chodzi o drzewo 2←1→3→4? 103 Struktury danych 18. Czy w tablicowej implementacji ADT TREE możemy jednoznacznie określić kolejność dzieci danego węzła? 19. Czy funkcja PARENT() wykonuje się szybciej dla tablicowej implementacji ADT TREE niż dla implementacji przez „listy dzieci” 20. Czy w implementacji ADT TREE przez „listy dzieci” musimy explicite podawać, który węzeł jest korzeniem drzewa? 21. Czy drzewem w implementacji ADT TREE przez „listy dzieci” jest rekord składający się z tablicy numerów węzłów i wskaźnika do korzenia? 22. Czy w implementacji ADT TREE przez „sąsiada i dziecko” operacje LEFTMOST_CHILD i RIGHT_SIBLING wykonują się w tym samym czasie? 23. Czy w implementacji ADT TREE przez „sąsiada i dziecko” można zapisywać drzewa binarne? 24. Czy w implementacji ADT TREE przez „sąsiada i dziecko” istnieje możliwość łatwego łączenie wielu drzew w jedno drzewo o wspólnym nowym korzeniu? 25. Jaki jest cel stosowania modyfikacji w implementacji ADT TREE przez „sąsiada i dziecko”? 26. Czy każdy graf skierowany jest drzewem? 27. Czy graf, którego macierz sąsiedztwa jest symetryczna może być skierowany? 28. Jak łatwo sprawdzić, czy graf reprezentowany przez macierz sąsiedztwa posiada pętle (łuki zaczynające i kończące się w tym samym wierzchołku)? 29. Czy w implementacji ADT GRAPH przez „listy sąsiedztwa” funkcja FIRST_VERTEX działa szybciej niż w implementacji macierzowej? 30. Czy graf kompletny (każdy wierzchołek połączony z każdym innym) może posiadać puste listy sąsiedztwa? 31. Czy zbiorem w Bitowo-Wektorowej implementacji ADT SET jest tablica wielkości MaxSize zawierająca elementy zbioru? 32. Czy w implementacji ADT SET przez listy uporządkowane na uporządkowanej liście mogą pojawić się duplikaty? 33. Czy w implementacji ADT SET przez listy uporządkowane operacje MIN( ) i MAX( ) zajmują taki sam czas obliczeń? 34. Czy w implementacji ADT SET przez listy uporządkowane operacje UNION( ) i DIFFERENCE( ) działają szybciej niż w implementacji Bitowo-Wektorowej? 35. Czy w Tablicowo -Kursorowej implementacji ADT DICTIONARY operacja INSTERT_TO_DICTIONARY jest szybsza niż w innych implementacjach? 36. Czy w implementacji ADT DICTIONARY przez Haszowanie Otwarte ilość kontenerów wpływa na pojemność słownika? 37. Czy w implementacji słownika przez Haszowanie Otwarte ilość kontenerów wpływa na szybkość działania funkcji INSERT_TO_DICTIONARY? 38. Jaka jest idea rehaszowania? 39. Jaki jest cel oznaczania niektórych pól słownika w implementacji przez Haszowanie Zamknięte jako „deleted” a innych jako „empty”? 40. Jaki jest pesymistyczny czas wyszukiwania słowa w słowniku dla implementacji przez Haszowanie Zamknięte? 104 Zakończenie Zakończenie Dzięki strukturom danych programy stają się stabilniejsze i bardziej przejrzyste, a programista nie musi nawet wiedzieć w jaki sposób zostały zaimplementowane. Pozwala mu to zaoszczędzić czas dla zagadnień ważniejszych, czyli dla samej istoty problemu. Pewnej analogii możemy się tutaj doszukać dla programowania w środowisku graficznym Windows. Żaden programista nie wgłębia się w to, jak są skonstruowane okna i rozwijane menu, tylko używa ich w swoich programach. W środowisku programistów mówi się, że czas jaki pozostaje do zakończenia pisania programu jest wielkością stałą i nie zależy od stopnia zaawansowania programu. Wydaje się, że struktury danych zmienią ten pogląd. Czas, który poświęcimy na przygotowanie odpowiednich bibliotek, już wkrótce okaże się nie być czasem straconym, a kod źródłowy programu będzie bardziej niezawodny i zrozumiały. Umiejętne dopasowywanie struktur danych do problemów informatycznych rozbije dany problem na zagadnienia prostsze i mniej uwikłane w myśl zasady „Dziel i Zwyciężaj”. 105 Struktury danych 10 Literatura 1. Aho A. V., Hopcroft J. E., Ullman J. D. – „Data structures and algorithms” Addison – Wesley, 1983. 2. Banachowski L., Diks K., Rytter W. – „Algorytmy i struktury danych” Warszawa 1996. 3. Bondy J. A., Murty U.S.R. – „Graph theory with applications” American Alsevier, 1976. 4. Cormen T. H., Leiserson E. C., Rivest R. L. – „Wprowadzenie do algorytmów” Warszawa 1998. 5. Kuzak T. – „Metody Programowania” (niepublikowane wykłady) Uniwersytet Jagielloński 1999. 6. Marecki J. – „Struktury Danych” WSIiZ, Bielsko-Biała 1999. 106