Dynamiczne struktury danych

background image

Dynamiczne struktury danych w języku Turbo Pascal

Rodzaje zmiennych

W języku Pascal zmienne dzielą się na statyczne i dynamiczne.
Zmienna statyczna tworzona jest w trakcie kompilacji programu i istnieje do
momentu jego zakończenia lub też do momentu zakończenia funkcji lub procedury, w
której została zadeklarowana.
Program w momencie uruchamiania otrzymuje od systemu operacyjnego fragment
pamięci o rozmiarze 64KB przeznaczony na dane statyczne - fragment ten nazywamy
segmentem danych. W segmencie tym są umieszczane wszystkie zmienne globalne
programu a także zmienne zadeklarowane lokalnie w procedurach i funkcjach. Łączny
rozmiar tych zmiennych nie może przekroczyć rozmiaru segmentu danych, tj. 65536
bajtów.
Zmienną dynamiczną nazywamy zmienną utworzoną w trakcie wykonywania
programu. Zmienne te tworzone są na tzw. stercie, której rozmiar jest na ogół różny i
może wynosić nawet do 600KB. W odróżnieniu od segmentu danych zmienną
utworzoną na stercie można w dowolnym momencie usunąć, zwalniając w ten sposób
niepotrzebnie zajmowaną pamięć.

Typy wskaźnikowe. Wskaźniki

Do pracy ze zmiennymi dynamicznymi używa się typów wskaźnikowych. Elementami
tych typów są zmienne wskaźnikowe, które krótko nazywamy wskaźnikami. Wskaźnik
jest adresem pewnej zmiennej określonego typu. Mówiąc o wskaźniku zawsze mamy
na myśli zmienną, której adres zawiera wskaźnik.


Dynamiczny przydział pamięci

Wskaźnikowi po zadeklarowaniu można przypisać adres dowolnej istniejącej zmiennej
identycznego typu jak typ wskaźnika. Niezależnie od typu wskaźnika zawsze można
mu przypisać tzw. adres pusty, który w języku Pascal oznaczamy słowem Nil -
wskaźnik o wartości Nil nie wskazuje niczego.
Program w dowolnym momencie może utworzyć nową zmienną i jej adres przypisać
zmiennej wskaźnikowej. Tak powstałą zmienną nazywamy zmienną dynamiczną.

Standardowa procedura

New(Zmienna wskaźnikowa)
tworzy na stercie nową zmienną typu identycznego z typem wskaźnika i jej adres
przypisuje zmiennej wskaźnikowej.
Po poprawnym wykonaniu procedury New zmienna wskaźnikowa zawiera adres
utworzonej zmiennej, jeżeli jednak utworzenie zmiennej dynamicznej nie jest możliwe
z powodu braku pamięci na stercie, to następuje błąd wykonania programu i w
konsekwencji jego przerwanie. W związku z tym należy przed każdą próbą przydziału
pamięci sprawdzać możliwość wykonania tej operacji za pomocą funkcji
bezparametrowej MaxAvail, wartością której jest ilość możliwej do przydzielenia
przez procedurę New pamięci. Sposób postępowania ilustruje poniższy przykład:

background image

Var
Wsk : ^Integer;
Begin
...
If MaxAvail>=SizeOf(Integer) Then New(Wsk)
Else
Begin
obsługa bł
ędu
End;
...
End.

Standardowa funkcja SizeOf zwraca rozmiar zmiennej podanego jako argument typu w
bajtach.


Czas życia zmiennych dynamicznych

Zmienna dynamiczna utworzona procedurą New istnieje do momentu usunięcia jej
przez programistę - w odróżnieniu od zmiennych statycznych zmienne dynamiczne
nigdy nie są usuwane automatycznie.

Usunięcie zmiennej wykonuje się procedurą

Dispose(Zmienna wskaźnikowa)
Należy pamietać, że jeżeli program utworzy zmienną dynamiczną, której adres
zostanie zapamiętany w zmiennej wskaźnikowej, a następnie przed usunięciem tej
zmiennej dokona zmiany wskaźnika, to jej usunięcie nie będzie już możliwe - nie
będzie bowiem znany adres tej zmiennej.
W poniższym przykładzie:

...
New(Wsk);
ReadLn(Wsk^);
If Wsk^=0 Then Wsk:=Nil;
...
tworzona jest zmienna, której adres przypisywany jest zmiennej wskaźnikowej Wsk.
Następnie następuje odczyt wartości liczbowej do tej zmiennej, po czym wskaźnik
Wsk może otrzymać adres pusty Nil. Na skutek wykonania tej instrukcji program
"gubi" adres utworzonej zmiennej - zmienna ta będzie zajmować pamięć do czasu
zakończenia programu.

Procedura GetMem
Pamięć na stercie można również przydzielić procedurą

GetMem(Zmienna wskaźnikowa, Rozmiar)
w której argument rozmiar określa ilość potrzebnej do przydzielenia pamięci. Przed
wykonaniem procedury należy sprawdzić ilość dostępnej pamięci funkcją MaxAvail
lub też jeżeli program obsługuje błąd braku pamięci na stercie, wartość wskaźnika po

background image

wywołaniu procedury.

Pamięć przydzieloną procedurą GetMem zwalnia się za pomocą procedury

FreeMem(Zmienna wskaźnikowa, Rozmiar)
gdzie argument rozmiar powinien być identyczny z argumentem użytym podczas
przydziału pamięci.
Procedur New i Dispose oraz GetMem i FreeMem nie wolno ze sobą mieszać - pamięć
przydzielona przez New musi być zwolniona przez Dispose, analogicznie pamięć
przydzielona przez GetMem należy zwolnić procedurą FreeMem.

Operacje arytmetyczne na wkaźnikach

Wskaźniki można ze sobą porównywać za pomocą operatorów = oraz <>. Dwa
wskaźniki uważamy za równe, gdy wskazują na ten sam obszar pamięci lub gdy oba
mają wartość Nil.
Dozwolona jest również operacja zwiększania i zmniejszania wskaźnika o liczbę
całkowitą za pomocą procedur Inc i Dec. Po wykonaniu instrukcji
Inc(Wsk);

wskaźnik Wsk wskazuje na kolejny element w pamięci typu identycznego z typem
wskaźnika, tzn. jeżeli Wsk jest typu ^Word, to dodanie do wartości wskaźnika liczby 1
powoduje przesunięcie wskaźnika na kolejną liczbę typu Word, czyli o dwa bajty.
Ogólnie, jeżeli Wsk jest typu ^TYP, to zwiekszenie wskaźnika o 1 procedurą Inc
przesuwa wskaźnik o SizeOf(TYP) bajtów.
Analogiczne zasady obowiązują podczas zmniejszania wskaźnika o liczbę całkowitą
procedurą Dec.

Organizacja listy jednokierunkowej


Niech dana będzie definicja typu rekordowego Skladnik:

Type
WSkladnik = ^Skladnik;
Skladnik = Record
X : Integer;
Next : WSkladnik;
End;

Rekord Skladnik zawiera dwa pola: pole X typu Integer oraz pole wskaźnikowe Next
typu WSkladnik, czyli wskaźnik na rekord Skladnik.
Załóżmy, że w programie zadeklarowana jest zmienna Start typu wskaźnik na
Skladnik, czyli Start : WSkladnik. Program może utworzyć dynamicznie zmienną
rekordową, jej adres przypisać zmiennej wskaźnikowej Start i wczytać do tej zmiennej
liczbę typu Integer:

background image


...
New(Start);
Start^.Next:=Nil;
Write('Podaj liczb
ę całkowitą = ');
ReadLn(Start^.X);
...
Ponieważ pole Next rekordu może zawieraż adres rekordu typu Skladnik można
utworzyć dynamicznie kolejny rekord przypisując jego adres polu Next utworzonego
przez chwilą rekordu:

...
New(Start^.Next);
Start^.Next^.Next:=Nil;
Write('Podaj liczb
ę całkowitą = ');
ReadLn(Start^.Next^.X);
...
Powstała w ten sposób struktura zawierająca dwa rekordy, przy czym adres pierwszego
pamiętany jest w zmiennej wskaźnikowej Start, adres drugiego w polu Next
pierwszego rekordu. Wyrażenie Start jest typu WSkladnik i jest adresem pierwszego z
rekordów, wyrażenie Start^.Next również jest typu WSkladnik i jest adresem rekordu
drugiego.
Proces ten można powtórzyć po raz trzeci:

...
New(Start^.Next^.Next);
Start^.Next^.Next^.Next:=Nil;
Write('Podaj liczb
ę całkowitą = ');
ReadLn(Start^.Next^.Next^.X);
...

W ten sposób zostanie utworzony trzeci rekord, którego adres będzie pamiętany w
polu Next drugiego z rekordów. Możemy utworzyć kolejny czwarty rekord wpisując
jego adres w pole Next rekordu trzeciego, rekord piąty wpisując jego adres w pole
Next rekordu czwartego, itd.

Przyjmijmy teraz, że została utworzona tego typu struktura zawierająca 20
połączonych ze sobą rekordów. Każdy z rekordów zawiera jedną liczbę całkowitą typu
Integer. Program może w każdej chwili odwołać się do dowolnej z tych liczb, choć
odwołanie takie może być kłopotliwe - odwołanie do szóstej liczby miałoby postać:
Start^.Next^.Next^.Next^.Next^.Next^.X - chcąc odwołać się do liczby o numerze 20
należałoby podać 19 razy wyrażenie Next^.

Szczególna uwagę należy jednak poświęcić pamięci zajmowanej przez liczby w
segmencie danych. Do zapamiętania owych 20 liczb użyto jednej zmiennej statycznej,
a mianowicie wskaźnika Start, który zajmuje zaledwie 2 bajty segmentu danych.
Oczywistym jest, że zwiększając ilość liczb w utworzonej strukturze np. do 200 nie
zajmujemy żadnego dodatkowego bajtu w segmencie danych - wszystkie te liczby
zostaną zapamietane dzieki użyciu jednego dwubajtowego wskaźnika Start :
WSkladnik.

background image

Tak utworzoną strukturę nazywamy listą jednokierunkową (liniową). Rekord, który
został użyty do utworzenia tej listy nazywamy składnikiem listy, zaś jej pierwszy
składnik początkiem listy.

Bardzo ważnym elementem listy jest pole Next o wartości Nil. Pamiętając adres
pierwszego składnika listy mamy dostęp do wszystkich jej składników musimy jednak
wiedzieć w którym miejscu lista się kończy. Koniec ten jest wyznaczany właśnie przez
pole Next o wartości Nil. Ostatni składnik listy ma pole Next równe Nil, wszystkie
poprzednie składniki przechowują w tym polu adres składnika następnego.
Nazwa lista jednokierunkowa wynika ze sposobu łączenia poszczególnych składników
listy: mając adres N-tego składnika można ustalić adres każdego ze składników o
numerach N+1, N+2, N+3, itd., nie jest natomiast możliwe ustalenie adresu składnika
wcześniejszego np. N-1.
Po liście możemy zatem poruszać się tylko w jednym kierunku - od początku do końca
listy, przejście w drugą stroną nie jest możliwe.

Podstawowe operacje wykonywane na liście jednokierunkowej

Poniższe przykłady zawierają definicje kilku podstawowych funkcji służących do
zarządzania listą jednokierunkową. Za składnik listy przyjęto zdefiniowany powyżej
rekord typu Skladnik.

Przykład
Zdefiniować funkcję LRozmiar wartością której będzie ilość składników listy
utworzonej przy pomocy rekordu typu Skladnik.

Function LRozmiar(Start : WSkladnik) : Word;
Var
Wynik : Word;
Begin
Wynik:=0;
While Start<>Nil Do
Begin
Start:=Start^.Next;
Inc(Wynik);
End;
LRozmiar:=Wynik;
End;


Przykład
Zdefiniować funkcję LDodaj o wartościach logicznych, która doda do listy utworzonej
przy pomocy rekordu Skladnik kolejną liczbę na jej końcu. Funkcja powinna zwrócić
True w przypadku powodzenia operacji, False w przeciwnym wypadku. (dst)

Function LDodaj(Var Start : WSkladnik; Liczba : Integer) : Boolean;
Var
Adr : WSkladnik;
Begin
If MaxAvail<SizeOf(Adr^) Then LDodaj:=False

background image

Else
Begin
New(Adr);
Adr^.X:=Liczba;
Adr^.Next:=Nil;
If Start=Nil Then Start:=Adr
Else LAdres(Start, LRozmiar(Start))^.Next:=Adr;
LDodaj:=True;
End;
End;

Przykład
Zdefiniować procedurę LUsunPierw, która usunie w listy utworzonej przy pomocy
rekordu Skladnik pierwszy jej składnik.

Procedure LUsunPierw(Var Start : WSkladnik);
Var
Adr : WSkladnik;
Begin
If Start=Nil Then Exit;
Adr:=Start;
Start:=Start^.Next;
Dispose(Adr);
End;


Stosy i kolejki

Program może przetwarzać listę jednokierunkową w pewien specyficzny sposób. Jeżeli
program dodaje kolejny składnik zawsze na końcu listy, zaś po przetworzeniu usuwa z
listy jej pierwszy składnik, to taką strukturę nazywamy kolejką. Nazwa kolejki
wywodzi się z kolejki w sklepie, w której obowiązuje zasada, że jako pierwszy
opuszcza kolejkę ten, kto przebywa w niej najdłużej. Zasadę tą często formułuje się
krótko: pierwszy przyszedł, pierwszy wychodzi.

Jeżeli program przetwarzając listę jednokierunkową dodaje do niej kolejne składniki
zawsze na początku, zaś po przetworzeniu usuwa z listy jej pierwszy składnik, to taką
strukturę nazywamy stosem. Stos można sobie wyobrazić jako słupek ułożonych jedna
na drugiej monet - zawsze można dołożyć monetę na szczycie stosu a także można
usunąć tylko monetę znajdującą się na szczycie. W organizacji stosu obowiązuje
zasada ostatni przyszedł, pierwszy wychodzi.

Lista dwukierunkowa

Umieszczenie w rekordzie pola o nazwie Next zawierającego adres kolejnego
składnika listy pozwala na poruszanie się po jej składnikach w jednym kierunku. Jeżeli
oprócz pola Next rekord bedzie zawierał pole o nazwie Last zawierające adres
składnika poprzedniego, to taką strukturę nazywamy listą dwukierunkową.

Program przetwarzając listę dwukierunkową pamięta dwa wskaźniki: adres pierwszego

background image

jej elementu oraz adres elementu ostatniego. Pole Next pozwala na przejście po
wszystkich składnikach od pierwszego do ostatniego, pole Last na przejście od
składnika ostatniego do pierwszego.
Do utworzenia dwukierunkowej listy liczb całkowitych potrzebne są następujące typy
danych:

Type
WSkladnik2 = ^Skladnik2;
Skladnik2 = Record
X : Integer;
Last, Next : WSkladnik2;
End;
Dla listy dwukierunkowej można zdefiniować analogiczne funkcje i procedury jak dla
listy jednokierunkowej. W tym wypadku każda z funkcji i procedur powinna otrzymać
jako argumenty dwa wskaźniki: Start i Koniec wskazujące odpowiednio adres
pierwszego i ostatniego elementu listy.

Przykładowe możliwe zadania do wykonania na laboratorium: (Zadania
b
ędą indywidualne)


Utworzyć następujące struktury danych:
1 – Lista jednokierunkowa
2 – Lista dwukierunkowa
3 – Stos
4 – Kolejka
5 – Tablica list jednokierunkowych / dwukierunkowych / stosów / kolejek

Na strukturach tych powinno być możliwe realizowane następujących operacji:
Każda struktura powinna umożliwiać wyświetlanie umieszczonych w niej elementów a
ponadto:
1 – Lista jednokierunkowa

dodawanie elementu w dowolnym (wybranym przez użytkownika programu)
miejscu listy

-

usuwanie dowolnego elementu listy

2 – Lista dwukierunkowa

-

dodawanie elementu w dowolnym (wybranym przez użytkownika programu)
miejscu listy

-

usuwanie dowolnego elementu listy dwukierunkowej

3 – Stos

-

dodawanie elementu na stos

-

usuwanie elementu ze stosu

-

podawanie ilości elementów na stosie

4 – Kolejka

-

dodawanie elementu do kolejki

-

usuwanie elementu z kolejki

-

podawanie ilości elementów, które znajdują się w kolejce

5 – Tablice list:

-

wybór listy do na której chcemy operować

-

pozostałe operacje jak na zwykłych listach


Wyszukiwarka

Podobne podstrony:

więcej podobnych podstron