Strunktura danych

advertisement
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
Download