Typ wskaźnikowy, organizacja list
Charakterystyka ogólna
Dotychczas były omawiane statyczne struktury danych takie, jak tablice, rekordy, pliki i zbiory. Oprócz cech charakterystycznych każdego typu, mają one pewne cechy wspólne. Na przykład maksymalna liczba elementów tablicy, rekordu i zbioru jest ograniczona. Jest ona określona za pomocą deklaracji i nie ulega zmianom podczas pracy programu. Ponadto, co dotyczy także plików, poszczególne elementy tych struktur są umieszczone w pamięci, w stałej kolejności względem siebie, (stała numeracja elementów tablicy, pliku i zbioru, stała kolejność pól rekordu). Spójny obszar pamięci na te zmienne, stanowiące zmienne globalne programu, jest przydzielany podczas kompilacji. Jest on umieszczany w obszarze pamięci statycznej (segment danych - ang. data segment). Jego rozmiar nie może jednak wynosić więcej niż 65520 bajtów, na wszystkie zmienne statyczne (w tym dla pliku tylko na nagłówek pliku). Zmienne lokalne, deklarowane w procedurach, są umieszczane w segmencie stosu - (ang. stack), którego rozmiar może zawierać się pomiędzy 1024 i 65520 bajtów. Standardowy rozmiar segmentu stosu jest równy 16384 bajty. Pamięć dla zmiennych lokalnych jest przydzielana przy wywołaniu procedury i zwalniana po jej zakończeniu. Rozmiar pamięci stosu dla programu można określić za pomocą pierwszego parametru dyrektywy kompilatora {$M...}.
Te cechy statycznych struktur danych uniemożliwiają zastosowanie ich do pewnego rodzaju aplikacji, w których dynamicznie tworzy się grupy elementów danych, o nieokreślonej z góry i często zmieniającej się liczbie elementów. Pewne aplikacje wymagają stosowania list, których elementy zajmują niespójny obszar pamięci i są połączone logicznie za pomocą specjalnych łączników (wiązań, wskaźników). Dla tych struktur danych, podczas kompilacji są przydzielane obszary na zmienne reprezentujące ich nagłówki. Natomiast dla danych, umieszczanych w elementach list, oraz samych elementów, pamięć jest przydzielana i zwalniana dynamicznie, na żądanie.
Zmienne dynamiczne są umieszczane w pamięci dynamicznej nazywanej stertą (ang. heap). Jej rozmiary - minimalny i maksymalny, określa się za pomocą dyrektywy kompilatora {$M...}, dla której stanowią odpowiednio drugi i trzeci parametr. Standardowy rozmiar sterty jest taki jak dla stosu.
Zasady organizacji struktur listowych
Jeżeli pamięć może być przydzielana zmiennym dynamicznym na żądanie, podczas wykonywania programu, to podczas kompilacji, w pamięci statycznej rezerwuje się tylko miejsce, na przechowywanie adresu obszaru pamięci, przydzielanego tym zmiennym. Aby jednak nie przechowywać adresu każdej zmiennej generowanej dynamicznie, zmienne dynamiczne łączy się w grupy, których elementy są dostępne za pomocą adresu, wskazującego na pierwszy element grupy.
Struktury listowe służą do grupowania zmiennych dynamicznych jednego typu. Podstawową ideą listy jest to, że każdy jej element, stanowiący zmienną dynamiczną, oprócz danych zawiera łącznik, który wskazuje, gdzie znajduje się następny element listy. Każdy element może być do listy dołączony, lub z niej usunięty, bez zmiany położenia innych elementów listy w pamięci. Liczba elementów listy nie jest z góry ograniczona. Praktycznie takie ograniczenie wynika z rozmiarów: elementu listy i pamięci sterty.
Listą prostą (ang. linked list, linear list) jest nazywana struktura danych, w której każdy element ma tylko jeden bezpośredni następnik i tylko jeden bezpośredni poprzednik. (W odróżnieniu od drzewa, stanowiącego strukturę hierarchiczną, w którym każdy element może mieć jeden bezpośredni poprzednik i wiele bezpośrednich następników).
Struktury danych wykorzystujące ideę list różnią się dwoma cechami:
sposobem łączenia elementów, (organizacją fizyczną listy)
sposobem dołączania i usuwania elementów, czyli organizacją logiczną listy
Ze względu na sposób łączenia rozróżnia się
listy jednokierunkowe,
listy dwukierunkowe,
listy cykliczne.
Lista jednokierunkowa składa się z elementów, które oprócz danych elementu, zawierają jeden łącznik, wskazujący na następny element listy. Dostęp do elementów listy jest sekwencyjny, przez przeglądanie ich w kolejności od pierwszego do ostatniego. Do tego celu wykorzystuje się:
wskaźnik początku listy, który jest łącznikiem do pierwszego elementu,
oraz wskaźnik bieżący, przemieszczający się po łącznikach, wskazujących na kolejne elementy listy.
Lista dwukierunkowa składa się z elementów, które oprócz danych elementu listy, zawierają dwa łączniki, jeden do następnego, a drugi do poprzedniego elementu listy. Dostęp do elementów listy dwukierunkowej jest także sekwencyjny. Przy pamiętaniu wskaźnika końca listy, lista taka może być przeglądana także w porządku odwrotnym, czyli od ostatniego do pierwszego elementu.
Lista cykliczna zawiera dodatkowy element listy stanowiący jej nagłówek, który wskazuje na pierwszy i ostatni element listy. Ten element jest poprzednikiem pierwszego elementu listy i następnikiem ostatniego elementu listy. Lista cykliczna może być zrealizowana w oparciu o listę jedno- lub dwukierunkową. Praktyczne zastosowania mają listy cykliczne dwukierunkowe.
Ze względu na organizację logiczną nałożoną na fizyczną strukturę listy wyróżnia się
struktury danych uporządkowane według:
kolejności dołączania elementów,
kolejności wynikających z atrybutów danych umieszczonych w elemencie.
Według kolejności dołączania elementów rozróżnia się struktury danych, w których elementy są uporządkowane w kolejności:
zgodnej z kolejnością dołączania - struktura danych typu kolejka.
odwrotnej do kolejności dołączania - struktura danych typu stos.
Struktury danych uporządkowanych według jednego, lub wielu atrybutów nazywane są listami uporządkowanymi.
Operacje na listach jednokierunkowych będą omawiane w p. 13.4, na listach dwukierunkowych w p. 13.5 i cyklicznych w p. 13.7. Zasady organizacji różnych rodzajów stosów, kolejek, list uporządkowanych i innych dynamicznych struktur danych będą rozważane w rozdziale 14. Jest możliwe, aby element listy w swoich atrybutach danych, zawierał wskaźnik początku swojej podlisty, lub dodatkowe łączniki za pomocą, których mógłby być dołączony do innej listy. Są to struktury danych bardziej złożone od list prostych. Jednak zasady ich organizacji, a tym samym zasady ich programowania, wykorzystują zasady dotyczące list prostych. Wszystkie dynamiczne struktury danych są tworzone w oparciu o typ wskaźnikowy.
Typ wskaźnikowy
Pascal wzorcowy i Turbo Pascal definiują typ wskaźnikowy, który umożliwia organizację dynamicznych struktur danych dowolnego typu, przy stosowaniu zbioru tych samych zasad.
Przy stosowaniu zmiennych dynamicznych wyróżnia się dwa rodzaje zmiennych:
zmienne wskazujące (zmienne wskaźnikowe) - (ang. pointer variables)
zmienne wskazywane (ang. referenced variables).
Zmienne wskaźnikowe są zmiennymi statycznymi - nazywanymi też referencyjnymi, które wskazują na zmienne dynamiczne. Postać definicji typu wskaźnikowego jest następująca:
TYPE Nazwa_Typu_Wskaźnikowego=^Nazwa_Typu;
Definicja typu wskaźnikowego, np. PT wiąże go więc z typem wskazywanym np. T:
PT = ^T;
Każdy typ wskaźnikowy, związany z innym typem wskazywanym, jest różny, mimo że zmienne każdego typu wskaźnikowego służą do przechowywania adresu zmiennej wskazywanej.
Deklaracja typu
Typ wskaźnikowy może być związany z różnymi typami danych:
TYPE
PI=^INTEGER;
PR=^REAL;
PB=^BYTE;
a także z typem plikowym
plik=FILE OF Element;
PPlik = ^plik;
Te typy jednak mają małe praktyczne zastosowanie. Praktyczne zastosowanie do organizacji dynamicznych struktur danych mają typy wskaźnikowe związane z rekordami, a także typ wskaźnikowy związany z typem napisowym:
PS=^STRING;
Typem wskazywanym, wykorzystywanym do organizacji list, musi być typ rekordowy zawierający pole reprezentujące dane, przechowywane w elemencie listy, oraz jedno lub dwa pola łączników typu wskaźnikowego. Mogą to być pola o nazwach np.: Nast i Poprz. Pole Nast, zarówno w liście jedno- jak i dwukierunkowej - wskazuje na następny element listy,. Pole Poprz - wskazuje na poprzedni element listy w liście dwukierunkowej. Definicja typu rekordowego T, służącego do organizacji listy jednokierunkowej i jego typu wskaźnikowego PT, może mieć następującą postać:
TYPE
PT=^T;
T =RECORD
Dane:Typ_Danych;
Nast:PT;
END;
Ta definicja zawiera strukturalne pole, zawierające dane elementu listy typu Typ_Danych, który musi być wcześniej zdefiniowany. Dane elementu listy mogą być także deklarowane bezpośrednio, jako pola elementu listy, bez użycia pomocniczego typu rekordowego.
Wyłącznie przy definicji typu wskaźnikowego używa się nazwy typu wskazywanego, przed jego zdefiniowaniem. Położenie pól wskaźnikowych w rekordzie jest nieistotne. Na rysunkach poniżej, dla ich uproszczenia przyjęto, że pole zawierające łącznik do elementu listy jednokierunkowej, jest pierwszym polem rekordu dynamicznego.
Deklaracje zmiennych wskaźnikowych
Deklaracja zmiennej wskaźnikowej:
VAR AdrT:PT;
podczas kompilacji programu rezerwuje pamięć na przechowywanie adresu, który może wskazywać na obszar pamięci, zawierający rekord typu T. Adres ten nie jest określony po kompilacji programu, a tym samym pamięć na rekord typu T, nie jest jeszcze przydzielona. Wartość nieokreślonego adresu jest oznaczana za pomocą stałej NIL. Zmiennym wskaźnikowym, które w programie będą podlegały badaniu, przed ich modyfikacją, powinno się nadawać wartości początkowe:
AdrT:=NIL;
Dla każdej listy implementowanej w programie, będącej listą globalną, należy zadeklarować zmienną reprezentującą adres początku tej listy np.:
VAR pocz:PT;
Dla struktur danych listowych, w których przewiduje się dopisywanie nowych elementów na końcu listy, należy także zadeklarować zmienną reprezentującą koniec listy np.:
VAR kon:PT;
Te zmienne powinny być przekazywane jako parametry do procedur wykonujących działania na liście. W tych procedurach należy deklarować zmienne wskaźnikowe lokalne, np. na nowy element dołączany do listy lub na adres bieżącego elementu listy przeglądanej listy.
Zmienne dynamiczne, w odróżnieniu od zmiennych statycznych, nie mają nazwy, są identyfikowane za pomocą adresu wskazującego na początek obszaru zajmowanego przez zmienną. Wielkość tego obszaru (równa SIZEOF(T)) i kolejność pól rekordu dynamicznego są określone przez typ zmiennej wskazywanej. Nazwę "posiada" tylko zmienna dynamiczna, na którą aktualnie wskazuje zmienna wskaźnikowa. Adres zmiennej dynamicznej jest wpisywany do zmiennej wskaźnikowej podczas jej generacji i programista tym adresem jawnie nie manipuluje. Wartość adresu można jednak odczytać podczas wykonywania programu, na zasadzie zwykłego podglądu zmiennych, udostępnianego przez program uruchamiający debugger.
Zbliżony do typu wskaźnikowego, w sensie przeznaczenia, jest predefiniowany typ adresowy POINTER. Zmienne typów wskaźnikowych oraz zmienne typu POINTER zajmują 4 bajty.
Zmienne
VAR
AdrT:PT;
adr :POINTER;
mogą być przypisywane, przy czym zarówno przypisanie:
adr:=adrT;
jak i przypisanie
adrT:=adr;
są poprawne w Turbo Pascalu 7.0. oraz w BP. W starszych wersjach TP drugie przypisanie wymagało konwersji typu:
adrT:=PT(adr);
W TP7 zdefiniowano dodatkowy typ wskaźnikowy PCHAR, dostępny przy opcji rozszerzonej składni {$X+}, służący do operowania długimi napisami (z zakresu 255..65535 znaków) o zmiennej i nieokreślonej długości. Napis tego typu nie zawiera licznika długości, ale kończy go wyróżniony znak CHR(0). W module Strings, zdefiniowano zbiór procedur do obsługi tego typu, podobnych do procedur działających na napisach, rozszerzony o dodatkowe możliwości.
Przydzielanie i zwalnianie pamięci dla zmiennych dynamicznych.
Generowanie i kasowanie zmiennych dynamicznych
Zmienne dynamiczne nie są deklarowane tak jak zmienne statyczne, ponieważ są generowane i kasowane dynamicznie, podczas działania programu. Istnieją w programie od chwili wygenerowania do chwili skasowania. W programie deklaruje się zmienne, wskazujące na zmienne dynamiczne. Deklaracja zmiennej typu wskaźnikowego ma postać:
VAR Nazwa_Zmiennej_Wskazującej=Nazwa_Typu_Wskaźnikowego;
Deklaracja zmiennej wskaźnikowej p, która będzie wskazywać na element typu T:
VAR p:PT;
rezerwuje obszar pamięci na adres zmiennej dynamicznej, a nie na samą zmienną. Przydzielenie pamięci na zmienną dynamiczna, o rozmiarze odpowiednim do typu zmiennej wskazywanej, jest realizowane za pomocą instrukcji NEW. Może ona być wywoływana w dwóch wersjach: jako procedura:
NEW(p);
lub jako funkcja:
p:=NEW(PT);
Wykonanie instrukcji NEW, oprócz przydziału pamięci, powoduje przypisanie adresu początkowego przydzielonego obszaru pamięci do zmiennej wskaźnikowej p, użytej w instrukcji. Generowanie nowej zmiennej dynamicznej jest wykonywane wówczas, gdy program potrzebuje dodatkowego miejsca, na nowe dane. Dopiero po wygenerowaniu zmiennej dynamicznej można nadać wartości, polom rekordu dynamicznego. Na przykład po wykonaniu NEW(p), można nadać wartości polom rekordu typu T, wskazywanego przez zmienną p.
Powiązanie zmiennej wskaźnikowej ze zmienną dynamiczną pokazuje rys. 13.1. Na tym i dalszych rysunkach przyjęto, że adresy kolejno generowanych rekordów dynamicznych będą oznaczone literami:
A, B, C, D...
Rys. 13.1 Powiązanie zmiennej wskaźnikowej ze zmienną dynamiczną
A- umowny adres obszaru pamięci, przydzielonego na zmienną dynamiczną przez instrukcję NEW(p).
Zwolnienie pamięci zajmowanej przez zmienną dynamiczną, wskazywaną przez p, realizuje instrukcja:
DISPOSE(p);
Instrukcję DISPOSE wykonuje się po ostatnim wykorzystaniu danych ze zmiennej dynamicznej. Dane, które stały się zbędne dla programu, są w ten sposób kasowane, w celu zwolnienia obszaru pamięci sterty na dalsze zmienne dynamiczne. Kolejność zwalniania pamięci jest niezależna od kolejności jej przydziału. Zwalniany jest obszar, na który wskazuje zmienna wskaźnikowa. Zwolnienie obszaru już zwolnionego - jest błędem. Odwoływanie się do pól rekordu zwolnionego nie jest wykrywane, ale może doprowadzić do błędu wykonania. Może on spowodować zawieszenie się programu, ponieważ dane mogą zostać przypadkowo wpisane pod niepożądany adres i zniszczyć, znajdujący się tam fragment programu.
Każda wykonanie instrukcji:
NEW(p);
przydziela nowy obszar na zmienną dynamiczną typu T i zapamiętuje adres początkowy przydzielonego obszaru w zmiennej p. Adres przechowywany w zmiennej przed wykonaniem kolejnego NEW, zostanie zniszczony. Aby zmienne dynamiczne nie gubiły się i nie zaśmiecały sterty, muszą zostać zwolnione lub dołączone do struktury danych, grupującej zmienne dynamiczne, przed wykonaniem kolejnej instrukcji NEW, z nazwą tej samej zmiennej. O tym jak będą łączone rekordy dynamiczne, będzie decydował typ struktury danych, którą programista chce zastosować w programie.
Zwalnianie pamięci dla grupy zmiennych dynamicznych
Jeżeli pamięć dynamiczną przydziela się i zwalnia jednocześnie dla grupy rekordów, można całą pamięć zwolnić jedną instrukcją. W tym celu należy zarejestrować adres początku sterty przed pierwszym przydzieleniem przez NEW(p), w pewnej zmiennej, np. V typu POINTER przez wykonanie instrukcji:
MARK(V);
Procedura RELEASE(V), zwalnia całą pamięć dynamiczną przydzieloną programowi, w czasie od wykonania MARK do wykonania RELEASE.
Pamięć na stercie jest przydzielana blokami, stanowiącymi wielokrotność 8 bajtów. Wielkość pamięci, przydzielana dla rekordu, jest więc zaokrąglana do liczby bloków, która może pomieścić wszystkie bajty. Ponieważ zwalnianie na ogół nie jest realizowane w tej samej kolejności co przydzielanie, wolna pamięć sterty składa się z bloków różnej wielkości.
Jest dostępna funkcja
MaxAvail typu LONGINT
która podaje rozmiar największego bloku sterty.
a także funkcja:
MemAvail typu LONGINT
która podaje wielkość wolnej pamięci na stercie, będącą sumą rozmiarów wolnych bloków sterty.
Operacje na blokach pamięci
Są także dostępne procedury przydziału i zwalniania bloku pamięci, o rozmiarze określonym w bajtach. Ten przydział pamięci nie jest związany z konkretnym typem, ale wielkość przydzielonego bloku zależy oczywiście od rozmiaru danych, które chce się w nim przechować.
Wielkość żądanego bloku rozmiar i zmienna typu POINTER, przechowująca adres tego bloku są parametrami obydwu procedur, których nagłówki i funkcje są następujące:
GETMEM(VAR adres:POINTER; rozmiar:WORD);
- przydziela blok pamięci,
FREEMEM(VAR adres:POINTER; rozmiar:WORD);
- zwalnia blok pamięci.
Operacje na blokach pamięci są wykorzystywane do przydzielania pamięci na napisy. Napisy nawet okrojone, np. typu:
TYPE st20=STRING[20];
zajmują zawsze o jeden bajt więcej, niż zadeklarowano w deklaracji typu, bez względu na to z ilu znaków, składa się napis, wprowadzony do zmiennej napisowej. W programach wykorzystujących wiele napisów można uzyskać dużą oszczędność pamięci, jeżeli zamiast zmiennych napisowych, będzie się wykorzystywało wskaźniki do napisów. Przydział pamięci na ten napis powinien uwzględniać rozmiar konkretnego napisu. Na przykład dla deklaracji:
TYPE PS=^STRING;
VAR
s :STRING;
ws:POINTER;
d :BYTE;
po nadaniu wartości pomocniczej zmiennej napisowej s, można :
odczytać długość napisu:
d:=LENGTH(s);
przydzielić pamięć na dynamiczny napis:
GETMEM(ws,d);
oraz wpisać napis ze zmiennej pomocniczej s do przydzielonego obszaru pamięci.
PS(ws)^:=s;
Odwołania do pól rekordów dynamicznych
Dla typu T, w którym zadeklarowano pole nazwisko oraz typu wskaźnikowego PT:
TYPE
PT=^T;
T =RECORD
nazwisko:st20;
Nast :PT;
END;
oraz zmiennej p
VAR p:PT;
selektor pola nazwisko rekordu dynamicznego, ma postać:
p^.nazwisko
Operator '^' (w Pascalu wzorcowym Wirtha znak '↑ ') jest nazywany operatorem wyłuskiwania. Jeżeli do procedury, która ma wczytać (lub wyświetlić) rekord, zostanie przekazany parametr typu wskaźnikowego, przekazujący adres rekordu, tak jak w nagłówku procedury:
PROCEDURE Wprowadz_Rek(VAR adres:PT);
to instrukcja wiążąca używana w procedurze, odnosząca się do rekordu dynamicznego, wskazywanego przez ten parametr, także używa tego operatora:
WITH adres^
BEGIN
WRITE('Podaj nazwisko ');
READLN(nazwisko);
END;
Zapis adres^ jest synonimem nazwy zmiennej typu rekordowego i dotyczy tego rekordu, na który zmienna wskaźnikowa aktualnie wskazuje. Zasady budowy selektorów dla strukturalnych pól rekordu dynamicznego, łączą w sobie zasady odwołania do rekordu dynamicznego i pól statycznych.
Przypisywanie adresów i zmiennych dynamicznych
Jeżeli zadeklarujemy dwie zmienne wskaźnikowe tego samego typu:
VAR p,q:PT;
przypisanie
p:=q;
jest przypisaniem adresów. Po jego wykonaniu, zmienne p i q będą wskazywać na ten sam rekord dynamiczny, do tej pory wskazywany przez q. Wskaźnik p straci dostęp do rekordu, na który wskazywał przed tym przypisaniem. Rekord, wskazywany dotąd przez zmienną p, stanie się po tej operacji niedostępny.
Jeżeli sekwencja instrukcji:
NEW(p);
p^.nazwisko:='Kowalski';
NEW(q);
q^.nazwisko:='Nowak'
zostanie zakończona instrukcją przypisania:
p:=q;
to przypisanie zmiennych wskaźnikowych, spowoduje, że zmienna p straci dostęp do rekordu Kowalskiego, natomiast dwa adresy p i q będą wskazywać na rekord Nowaka. Stan zmiennych przed i po wykonaniu powyższej instrukcji, pokazano na rys. 13.2.
a) b)
Rys. 13.2 Powiązanie pomiędzy zmiennymi wskaźnikowymi i rekordami dynamicznymi :
a) przed i b) po wykonaniu instrukcji przypisania wskaźników p:=q.
Natomiast przypisanie:
p^:=q^;
jest przypisaniem rekordów dynamicznych, czyli przypisaniem zmiennych wskazywanych. Realizuje ono skopiowanie treści rekordu dynamicznego, wskazywanego przez zmienną q, do obszaru pamięci wskazywanego przez zmienną p. Po jego wykonaniu obszar pamięci, wskazywany przez p, będzie zawierał dokładnie to samo, co obszar pamięci, wskazywany przez q, czyli że każdemu polu rekordu, wskazywanego przez p, zostanie przypisane odpowiednie pole z rekordu, wskazywanego przez q.
Wykonanie instrukcji:
p^:=q^;
gdy p wskazuje na rekord Kowalskiego, a q na rekord Nowaka, spowoduje, że dane Nowaka wypełnią dwa rekordy, a dane Kowalskiego zginą. Stan zmiennych wskaźnikowych oraz wskazywanych przez nie obszarów pamięci, przed i po omawianym przypisaniu rekordów dynamicznych, pokazano na rys. 13.3.
Rys. 13.3 Powiązanie pomiędzy zmiennymi wskaźnikowymi i rekordami dynamicznymi -
a) przed i b) po wykonaniu instrukcji przypisania zmiennych dynamicznych p^:=q^.
Oczywiście można także przypisywać sobie, zgodne co do typu pola rekordów, w tym także pola wskaźnikowe. Przypisanie:
p^.nazwisko:=q^.nazwisko;
tak, jak dla rekordów statycznych, zmienia wartość pola nazwisko w rekordzie, wskazywanym przez p, na nazwisko z rekordu, wskazywanego przez q, pozostawiając pozostałe pola rekordu, wskazywanego przez p, bez zmiany.
Poniżej pokazano przykłady deklaracji typów i zmiennych oraz poprawnych przypisań, wykorzystujących dwa różne typy wskaźnikowe: typ PT, wskazujący na rekord i typ PStr, wskazujący na napis. Sekwencja instrukcji pokazuje jak można przypisać polu typu napisowego (nazwisko), należącego do dynamicznego rekordu, wartość dynamicznego napisu, wskazywanego przez zmienną wskaźnikową, wskazującą na napis.
TYPE
PStr=^STRING;
VAR
AdrElem:PT;
zn :PStr;
el :T;
BEGIN
NEW(zn);
zn^:='Nowak';
el.Nazwisko:='Kowalski';
AdrElem := @el; {lub adrelem :=Addr(el);}
WRITELN(AdrElem^.nazwisko);
AdrElem^.Nazwisko:=zn^;
WRITELN(AdrElem^.Nazwisko);
READLN;
END;
Pierwsza instrukcja WRITELN z tej sekwencji, wyprowadzi nazwisko 'Kowalski', a druga nazwisko 'Nowak'.
Łącznik rekordu, który nie jest połączony z żadnym innym rekordem, powinien wskazywać na wartość NIL. Ustala się to przez przypisanie:
p^.Nast:=NIL;
Dla rekordów wypełnionych danymi Kowalskiego i Nowaka, wskazywanych odpowiednio przez zmienne wskaźnikowe: p -wskazującą na rekord Kowalskiego i q -wskazującą na rekord Nowaka przypisanie:
p^.Nast:=q;
spowoduje, że do rekordu wskazywanego przez p, w polu Nast zostanie wpisany adres rekordu, który był przechowywany w zmiennej wskaźnikowej q. Łącznik w rekordzie wskazywanym przez q, powinien wskazywać na wartość NIL, ponieważ do tego rekordu nie jest dołączony żaden rekord.
q^.Nast:=NIL;
Po wykonaniu operacji połączenia rekordów za pomocą pola łącznika, zmienna p wskazuje na początek dwuelementowej listy rekordów z nazwiskami, która jest pokazana na rys. 13.4.
Rys. 13.4 Dwuelementowa lista rekordów zawierających dane Kowalskiego i Nowaka
Po wygenerowaniu i wypełnieniu nowego rekordu:
a) NEW(q);
b) q^.nazwisko:='Ros';
rekord Nowaka nie został zgubiony i możemy do niego dołączyć rekord z danymi Rosa.
Przypisanie:
c) p^.Nast^.Nast:=q;
spowoduje utworzenie trzyelementowej listy rekordów, uporządkowanych w kolejności dołączania, zawierającej dane Kowalskiego, Nowaka i Rosa, pokazanej na rys. 13.5:
Rys. 13.5 Trzyelementowa lista rekordów zawierających dane Kowalskiego, Nowaka i Rosa.
Operacje a) i b) można powtarzać, wpisując nowe dane do rekordów. Operacja c) dołączania na końcu wymagałaby coraz dłuższej sekwencji odwołań do pól łączników, pozwalającej sięgnąć do ostatniego rekordu listy, lub należałoby przeglądać listę, aż do znalezienia ostatniego elementu. Przy dołączaniu elementów na końcu listy, wygodniej jest dodatkowo pamiętać adres ostatniego elementu listy np. k
VAR k:PT;
W tym przypadku dołączenie nowego elementu na końcu listy wymaga:
wygenerowania rekordu dynamicznego wskazywanego przez q:
NEW(q);
wpisania wartości pustej do pola wskaźnikowego:
q^.Nast:=NIL;
dołączenia nowego elementu jako pierwszego elementu listy, gdy lista jest pusta lub dołączenia go do ostatniego elementu listy:
IF p=NIL
THEN p:=q
ELSE k^.Nast:=q;
przestawienia wskaźnika końca listy na nowy element:
k:=q;
Pomimo dołączania elementu do końca listy, w jednym przypadku określono także nowy początek listy, ponieważ przy dołączaniu do pustej listy przy p=NIL, dołączany element staje się jednocześnie pierwszym i ostatnim elementem listy.
Pełna procedura dołączania elementu z danymi na końcu listy jest zamieszczona poniżej.
Przy dołączaniu elementu do końca listy można nie używać zmiennej pomocniczej q :
IF k<>NIL
THEN
BEGIN
NEW(k^.Nast);
k:=k^.Nast;
END
ELSE
BEGIN
NEW(k);
p:=k;
END;
Jeżeli p jest pierwszym elementem listy, dołączenie elementu q przed p, czyli na początku listy:
q^.Nast:=p;
wymaga także określenia nowego początku listy:
p:=q;
Jeżeli wszystkie wygenerowane powyżej elementy będą dołączane na początku listy, utworzy się trzyelementowa lista, pokazana na rys. 13.6, w której elementy będą uporządkowane w kolejności odwrotnej do kolejności dołączania.
Rys. 13.6 Trzyelementowa lista rekordów zawierających dane Kowalskiego, Nowaka i Rosa, w postaci stosu.
Przy wykorzystaniu przydzielania i zwalniania pamięci za pomocą procedur GETMEM i FREEMEM, dane muszą być wpisywane do przydzielonego obszaru procedurą MOVE:
MOVE(Nazwa_Zmiennej_Zrodlowej,Nazwa_Zmiennej_Docelowej,Rozmiar);
Parametrem Nazwa_Zmiennej_Zrodlowej, może być zmienna wskaźnikowa.
Zmienne wskaźnikowe porównuje się, aby sprawdzić czy wskazują na ten sam adres, np.
IF p=q...
Najczęściej jednak sprawdza się czy adres jest określony:
IF p<>NIL...
Zmienne wskazywane typu napisowego i typów prostych można porównywać, wykorzystując operatory relacji:
IF p^=q^...
Podobnie można porównywać pola rekordów dynamicznych, np:
IF p^.nazwisko<=q^.nazwisko...
Mogą być one porównywane także z polami rekordów statycznych, ze zmiennymi prostymi i stałymi, typów odpowiednich do typu porównywanego pola.
Zakładanie listy jednokierunkowej, aktualizacja elementów listy oraz wyszukiwanie maksimum w liście, są pokazane w zad48.txt. W p.13.4 omówiono różne algorytmy operacji wykonywanych na liście jednokierunkowej, zaimplementowane przez odpowiednie procedury.
Operacje na liście jednokierunkowej
Struktura elementu i listy
Element listy jednokierunkowej zawiera jeden łącznik do następnego rekordu w liście oraz dowolne dane. Definicja typu elementu listy i typu wskaźnikowego może być następująca:
PElement = ^TElement;
TElement = RECORD
Dane:TDane;
Nast :PElement;
END;
W programie musi być zadeklarowana zmienna wskazująca na początek listy, np.: Pocz.
Jeżeli przewiduje się dołączanie elementów na końcu listy, można dodatkowo zadeklarować zmienną wskazującą na koniec listy, np.: Kon. Struktura takiej listy jest pokazana na rys. 13.7.
Dołączanie elementów na końcu listy, bez pamiętania wskaźnika końca listy wymaga, dla każdego dołączanego elementu, przeglądania listy w celu znalezienia jej ostatniego elementu.
VAR Pocz,Kon:PElement;
Rys. 13.7. Lista jednokierunkowa
Moduł do obsługi listy jednokierunkowej
Operacje wykonywane na listach jednokierunkowych i ich elementach, które będą omawiane w dalszych podrozdziałach, są realizowane przez odpowiednie procedury. Wykorzystują one definicje: typu elementu listy, typu wskaźnikowego, wskazującego na element listy oraz typu danych elementu listy. Zmienne reprezentujące adresy początku i końca listy oraz inne potrzebne do wykonania procedur dane, lub zwracane przez procedurę wyniki, będą przekazywane za pomocą parametrów. Wszystkie wymienione definicje typów i procedur, związane z obsługą określonej struktury danych, mogą zostać umieszczone w jednym module. Jeżeli w module obsługi struktury danych, np. listy jednokierunkowej, definiuje się typ danych, które będą stanowiły dane elementu tej struktury, (np. dane elementu listy), to ten moduł będzie mógł być wykorzystywany tylko do obsługi struktur danych, przechowujących dane tego typu. Aby opracowany zestaw procedur mógł być wykorzystany do tworzenia struktur danych tego samego rodzaju, ale przechowujących inne dane, trzeba będzie wymienić w nim definicję typu danych i obsługujące go procedury, z pozostawieniem przyjętych nazw, używanych w definicjach procedur obsługi struktury danych. Omawiane poniżej procedury, służące do obsługi listy jednokierunkowej, umieszczono w module ListyU1.pas, który znajduje się na dyskietce. Moduł ten zawiera:
definicję typu danych TDane, umieszczanych w elemencie listy. W celu uproszczenia procedur dotyczących typu TDane przyjęto, że dane wstawiane do listy będą pojedynczym napisem typu STRING,
definicję elementu listy jednokierunkowej typu TElement,
procedury i funkcje wykonujące operacje na parametrze typu TDane, stanowiącym informacyjną część elementu listy tj. :
InicjujDane(VAR Dane:TDane); - wprowadza wartość parametru Dane,
WyswietlDane(VAR Dane:TDane); - wyświetla wartość parametru Dane,
RowneDane(VAR Dane1,Dane2:TDane):BOOLEAN; - porównuje dwa parametry typu TDane i zwraca wartość TRUE, jeżeli porównywane parametry są równe,
PorownajDane(Dane1,Dane2:TDane):INTEGER; - porównuje Dane1 i Dane2 i zwraca odpowiednią wartość:
-1 - gdy Dane1<Dane2
0 - gdy Dane1=Dane2
1 - gdy Dane1>Dane2
procedury wykonujące następujące operacje na liście:
InicjujListe(VAR Pocz,Kon:PElement); - ustala wartości początkowe zmiennych wskaźnikowych, umożliwiających dostęp do listy.
NaPoczatku(VAR Pocz,Kon:PElement; NoweDane:TDane); - tworzy nowy element listy, zawierający NoweDane i dołącza go na początku listy.
NaKoncu(VAR Pocz,Kon:PElement; NoweDane:TDane); - tworzy nowy element listy, zawierający NoweDane i dołącza go na końcu listy.
WstawPoEl(VAR Pocz,Kon:PElement; Element:PElement; NoweDane:TDane); - tworzy nowy element listy, zawierający NoweDane i dołącza go po elemencie o wskazanym adresie.
WstawPrzedEl(VAR Pocz,Kon:PElement;Element:PElement;NoweDane:TDane); - tworzy nowy element listy, zawierający NoweDane i dołącza go przed elementem o wskazanym adresie.
WstawWPorzadku(VAR Pocz,Kon:PElement; NoweDane:TDane); - dołącza do listy nowy element, zawierający NoweDane tak, aby lista pozostała uporządkowana.
Przeglad(VAR Pocz:PElement); - wyświetla elementy listy.
Selekcja(VAR Pocz:PElement; Wyb:BYTE); - wybiera i wyświetla elementy listy, spełniające określony warunek.
Usun(VAR Pocz,Kon:PElement; Element:PElement); - usuwa z listy element o podanym adresie.
Usun1(VAR Pocz,Kon:PElement; Element:PElement); - usuwa z listy element o podanym adresie, wpisując na jego miejsce dane z następnego elementu. Zwalnia pamięć następnego elementu.
UsunSzukany(VAR Pocz,Kon:PElement; ZnaneDane:TDane;
VAR brak:BOOLEAN); - usuwa z listy element zawierający ZnaneDane.
UsunPoEl(VAR Kon:PElement; Element:PElement; VAR Dane:TDane;
VAR brak: BOOLEAN); - usuwa z listy element, znajdujący się po wskazanym elemencie.
Funkcje badające stan listy i zwracające następujące wyniki:
Szukaj(VAR Pocz:PElement; ZnaneDane:TDane):PElement; - wyszukuje w liście element zawierający ZnaneDane i zwraca jego adres. Do zidentyfikowania elementu wykorzystuje funkcję RowneDane.
SzukajZWart(VAR Pocz,Kon:PElement; ZnaneDane:TDane):PElement; - stosując algorytm wyszukiwania z wartownikiem, wyszukuje w liście element zawierający ZnaneDane i zwraca jego adres. Do zidentyfikowania elementu wykorzystuje funkcję RowneDane.
Zawiera(VAR Pocz:PElement; PoszukiwaneDane:TDane):BOOLEAN; - sprawdza czy w liście znajduje się element zawierający PoszukiwaneDane.
Pierwszy(VAR Pocz:PElement):TDane; - zwraca dane z pierwszego elementu listy.
Ostatni(VAR Kon:PElement):TDane; - zwraca dane z ostatniego elementu listy.
Nastepny(VAR Pocz:PElement;ZnaneDane:TDane; VAR brak:BOOLEAN):TDane; - zwraca dane z elementu listy znajdującego się po elemencie zawierającym ZnaneDane.
Poprzedni(VAR Pocz:PElement; ZnaneDane:TDane; VAR brak:BOOLEAN) :TDane; - zwraca dane z elementu listy poprzedzającego element zawierający ZnaneDane.
UsunPierwszy(VAR Pocz,Kon:PElement):TDane; - odłącza pierwszy element od listy, zwraca zawarte w nim dane i kasuje element.
DlugoscListy(VAR Pocz:PElement):INTEGER; - zwraca liczbę elementów znajdujących się w liście.
PustaLista(VAR Pocz:PElement):BOOLEAN; - zwraca wartość TRUE, jeżeli lista jest pusta.
Dołączanie elementu do listy
Dołączenie elementu na początku listy
Dla dołączanych danych jest generowany nowy rekord dynamiczny, o adresie wskazywanym przez E. Rekord wypełniany jest danymi, przekazanymi jako parametr. W pole Nast jest wpisywany adres pierwszego elementu listy. Początek listy Pocz jest przestawiany na nowy element E. Jeżeli dołączany element jest jedynym elementem listy, to jednocześnie jest też ostatnim i w tym przypadku ustalany jest także koniec listy Kon. Operację dołączenia elementu na początku listy jednokierunkowej, ilustruje rys. 13.8. Linią przerywaną pokazano połączenie przed dołączeniem nowego elementu na początku listy.
PROCEDURE NaPoczatku {dołącza element na początku listy}
(VAR Pocz,Kon:PElement;{początek, koniec listy}
NoweDane:TDane); {dane dołączanego elementu}
VAR
E:PElement;
BEGIN
NEW(E);
E^.Dane:=NoweDane;
E^.Nast:=Pocz;
IF Pocz=NIL
THEN Kon:=E;
Pocz:=E
END; {NaPoczatku}
Rys. 13. 8 Stan listy po dołączeniu nowego elementu na początku listy jednokierunkowej.
Dołączenie elementu na końcu listy
Dla dołączanych danych jest generowany nowy rekord dynamiczny, o adresie wskazywanym przez E. Rekord wypełniany jest danymi. W pole Nast, wygenerowanego rekordu jest wpisywana wartość NIL. W ostatnim elemencie listy wskazywanym przez Kon, w pole Nast jest wpisywany dołączany element E. Koniec listy Kon jest przestawiany na nowy element E. Jeżeli dołączany element jest pierwszym elementem listy, to jest ustalany także początek listy Pocz. Dołączenie elementu na końcu listy jednokierunkowej ilustruje rys. 13.9. Linią przerywaną pokazano połączenie przed dołączeniem nowego elementu na końcu listy.
Rys. 13.9 Stan listy po dołączeniu nowego elementu na końcu listy jednokierunkowej.
PROCEDURE NaKoncu {dołącza element na końcu listy}
(VAR Pocz,Kon:PElement;{początek, koniec listy}
NoweDane :TDane); {dane dołączanego elementu}
VAR
E:PElement;
BEGIN
NEW(E);
E^.Dane:=NoweDane;
E^.Nast:=NIL;
IF Pocz=NIL
THEN Pocz:=E
ELSE Kon^.Nast:=E;
Kon:=E
END; {NaKoncu}
Dołączenie elementu po elemencie o znanym adresie
Dołączenie elementu z nowymi danymi za elementem o wskazanym adresie, realizowane przez procedurę WstawPoEl jest proste, gdyż po wygenerowaniu i wypełnieniu nowego elementu trzeba tylko połączyć: - a) wskazany Element, z nowym elementem E, oraz b) nowy element z elementem następnym, po wskazanym. Operacje te trzeba wykonać w kolejności najpierw b), potem a), aby niewłaściwą kolejnością nie zniszczyć adresu elementu następnego po wskazanym. Jeżeli element wskazany był elementem ostatnim, to należy również przestawić koniec na nowy element. Ilustruje to rys. 13.10. Linią przerywaną pokazano połączenie istniejące, przed dołączeniem nowego elementu.
Rys. 13.10 Stan listy po dołączeniu elementu do listy jednokierunkowej po elemencie o znanym adresie.
PROCEDURE WstawPoEl {Wstawia nowy element zawierający nowe
dane po wskazanym elemencie}
(VAR Pocz,Kon:PElement;{nagłówek listy}
Element :PElement;{element za którym należy wstawić}
NoweDane :TDane); {wstawiane dane}
VAR
E:PElement;
BEGIN
NEW(E);
E^.Dane:=NoweDane;
E^.Nast:=Element^.Nast;
Element^.Nast:=E;
IF E^.Nast=NIL
THEN Kon:=E;
END; {WstawPoEl}
Procedura WstawPoEl jest wykorzystywana na przykład do wstawiania elementu, na końcu listy wskazywanym przez Kon, w przypadku, gdy do końca listy jest dołączony element wartownika, przyśpieszający wyszukiwanie.
Dołączenie elementu przed elementem o znanym adresie
To dołączenie jest trudniejsze, niż omówione powyżej, z tego względu, że element listy jednokierunkowej nie zawiera adresu poprzednika, z którym należałoby połączyć, dołączany element. Jeżeli Element jest pierwszym elementem listy, nowy element, zawierający nowe dane, zostanie wstawiony na początku listy. W pozostałych przypadkach, ponieważ nie jest znany adres elementu poprzedzającego Element, posłużono się sposobem omówionym przez Wirtha []. Polega on na skopiowaniu zawartości rekordu wskazywanego przez Element do nowego obszaru, przydzielonego dla zmiennej E (łącznie z adresem następnika) i wpisaniu parametru NoweDane do starego obszaru pamięci, wskazywanego przez Element. Następnie trzeba połączyć rekord, na który wskazuje Element, z rekordem na który wskazuje E. W przypadku, gdy Element był ostatnim elementem listy, trzeba także ustalić nowy koniec Kon, ponieważ zawartość ostatniego elementu listy została przesunięta do nowego obszaru, wskazywanego przez E. Dołączenie elementu z nowymi danymi do listy jednokierunkowej, przed elementem o znanym adresie, wskazywanym przez zmienną Element, ilustruje rys. 13.11. Linią przerywaną pokazano połączenie elementów listy, przed dołączeniem nowego elementu.
PROCEDURE WstawPrzedEl {wstawia nowy element przed elementem o podanym
adresie}
(VAR Pocz,Kon:PElement;{nagłówek listy}
Element :PElement;{wskazywany element}
NoweDane :TDane); {wstawiane dane}
VAR
E:PElement;
BEGIN
IF Element=Pocz
THEN NaPoczatku(Pocz,Kon,NoweDane)
ELSE
BEGIN
NEW(E);
E^:=Element^;
Element^.Dane:=NoweDane;
Element^.Nast:=E;
IF Kon=Element
THEN Kon:=E;
END;
END; {WstawPrzedEl}
Procedura WstawPrzedEl jest wykorzystywana do tworzenia jednokierunkowej listy uporządkowanej WstawWPorzadku.
Rys. 13.11. Dołączanie elementu z nowymi danymi, przed element o znanym adresie - stan listy przed i po dołączeniu nowych danych.
Dołączenie elementu do listy jednokierunkowej przy zachowaniu uporządkowania listy
W przypadku tworzenia listy uporządkowanej według wzrastającej wartości jednego z atrybutów elementów listy, wyszukuje się w liście element, zawierający Dane o wartości większej niż NoweDane i nowy element dołącza się przed nim do listy. Nie znalezienie elementu, przed którym należy wstawić nowy element oznacza, że nowy element ma być wstawiony na końcu listy. Do zidentyfikowania elementu poszukiwanego, wykorzystuje się funkcję PorownajDane, zdefiniowanej dla typu TDane. Porównuje ona NoweDane z polem Dane rekordu elementu listy i w zależności od wyniku porównania zwraca jedną z wartości (-1,0,1). Zwrot wartości -1, oznacza, że nowy element ma być dołączony przed elementem, z którym był porównywany. Zwrot wartości 0 lub 1 oznacza, że nowy element ma być dołączony w dalszej części listy, po znalezieniu właściwego miejsca. W tym przypadku, bieżący element listy E, jest przesuwany na następny element listy.
PROCEDURE WstawWPorzadku{dołącza element do listy wg. PorownajDane}
(VAR Pocz,Kon:PElement;{nagłówek listy}
NoweDane :TDane); {dane dołączanego elementu}
VAR
E :PElement;
Znal:BOOLEAN;
BEGIN
Znal:=FALSE;
E:=Pocz;
WHILE (E<>NIL) AND NOT Znal DO
BEGIN
CASE PorownajDane(NoweDane,E^.Dane) OF
-1:
BEGIN
WstawPrzedEl(Pocz,Kon,E,NoweDane);
Znal:=TRUE
END;
0,1:
E:=E^.Nast {Dla równych wstawi jako ostatni równy}
END
END;
IF NOT Znal
THEN NaKoncu(Pocz,Kon,NoweDane);
END; {WstawWPorzadku}
Nieco inaczej jest napisana procedura realizująca takie zadanie zawarta w zad44.txt. Przegląda się w niej listę, pamiętając adres elementu poprzedzającego element badany, aż do znalezienia elementu, przed który, należy wstawić nowy element. Po znalezieniu go, nowy element jest dołączany pomiędzy elementem poprzedzającym, a elementem znalezionym.
Wyświetlenie zawartości listy
Przeglądanie listy od początku do końca realizuje procedura Przegląd. Dane z przeglądanych elementów są wyświetlane za pomocą procedury WyswietlDane, zdefiniowanej dla typu TDane. Do przeglądania listy używa się dodatkowego wskaźnika E, zadeklarowanego lokalnie w procedurze. Jego wartość początkową ustala się na początek listy. Następnie w pętli, powtarzanej aż do wyczerpania listy, wyświetla się dane elementu na który wskazuje E, po czym przestawia się go na następny element listy.
PROCEDURE Przeglad {Wyświetla dane z listy od początku}
(Pocz:PElement); {początek listy}
VAR E:PElement;
BEGIN
E:=Pocz;
WHILE E<>NIL DO
BEGIN
WyswietlDane(E^.Dane);
E:=E^.Nast;
END
END; {Przeglad}
Jest możliwe wykorzystanie, do przeglądania listy parametru procedury. Zmiana jego wartości, wskazującej podczas przeglądania listy, na kolejne elementy listy, nie będzie widoczna poza procedurą, jeżeli parametr zostanie przekazany przez wartość. W tym przypadku parametr procedury przekazujący wartość początkową - adres pierwszego elementu listy, będzie w procedurze traktowany jako zmienna lokalna. Ilustruje to procedura Przeglad1
PROCEDURE Przeglad1 {Wyświetla dane z listy od początku}
(Pocz:PElement); {początek listy}
BEGIN
WHILE Pocz<>NIL DO
BEGIN
WyswietlDane(Pocz^.Dane);
Pocz:=Pocz^.Nast;
END
END; {Przeglad1}
Ten sposób jest jednak mniej czytelny dla początkujących programistów i w dalszym ciągu rozdziału, nie będzie stosowany.
Wyszukiwanie w liście
Podobnie jak dla wyszukiwania w tablicy, także w celu wyszukania unikalnego elementu w liście, można zastosować różne algorytmy np.: wyszukiwanie ze zmienną logiczną, lub wyszukiwanie z wartownikiem. Bez względu na stosowany algorytm, wyszukiwanie elementu o unikalnej wartości pola, należy zakończyć po znalezieniu elementu, spełniającego warunek poszukiwania. Funkcja Szukaj zwraca adres elementu, zawierającego poszukiwaną wartość. Do sprawdzenia zawartości elementów listy na zgodność z wartością poszukiwanych danych, przekazanych przez parametr ZnaneDane, jest wykorzystywana funkcja RowneDane, zdefiniowana dla typu TDane. W przypadku nieznalezienia elementu w liście jest zwracany pusty adres równy NIL.
FUNCTION Szukaj {Wyszukuje w liście element zawierający
ZnaneDane - alg. ze zmienna logiczna
przeglądanie od początku}
(VAR Pocz :PElement; {początek listy}
ZnaneDane:TDane) {poszukiwane dane}
:PElement;
VAR
E :PElement;
brak:BOOLEAN;
BEGIN
E:=Pocz;
brak:=TRUE;
WHILE (E<>NIL) AND brak DO
BEGIN
IF RowneDane(E^.Dane,ZnaneDane)
THEN brak:=FALSE
ELSE E:=E^.Nast
END;
Szukaj:=E
END; {Szukaj}
Algorytm wyszukiwania z wartownikiem, zakłada, że do ostatniego elementu listy, wskazywanego przez zmienną Kon jest dołączony dodatkowy element listy, do którego będą wpisywane poszukiwane dane. Lista jest przeglądana, aż do momentu, kiedy funkcja RowneDane zwróci wartość TRUE. Brak elementu z poszukiwanymi danymi ZnanaDane w liście, powoduje znalezienie elementu wartownika. W tym przypadku funkcja zwraca wartość NIL.
FUNCTION SzukajZWart {Wyszukuje w liście z wartownikiem
element zawierający ZnaneDane }
(VAR Pocz,Kon:PElement; {nagłówek listy}
ZnaneDane :TDane) {poszukiwane dane}
:PElement;
VAR
E:PElement;
BEGIN
Kon^.Nast^.Dane:=ZnaneDane;{wartownik- następny po końcu}
E:=Pocz;
WHILE NOT RowneDane(E^.Dane,ZnaneDane) DO
E:=E^.Nast;
IF E<>Kon^.Nast
THEN SzukajZWart:=E
ELSE SzukajZWart:=NIL
END; {SzukajZWart}
Wybieranie elementów spełniających określony warunek.
Przy wybieraniu elementów spełniających określony warunek wykorzystuje się schemat przeglądania całej listy taki, jak dla wyświetlania. Warunek wybrania elementu sprawdza funkcja DaneWybrane, która musi być zdefiniowana dla typu TDane. Procedura Selekcja wykorzystuje też procedurę WyświetlDane, której treść zależy od typu TDane.
PROCEDURE Selekcja {Wyświetla wybrane dane z listy od początku}
(VAR Pocz:PElement; {początek listy}
Wyb :BYTE); {parametr określający numer warunku wyboru}
VAR E:PElement;
BEGIN
E:=Pocz;
WHILE E<>NIL DO
BEGIN
IF DaneWybrane(E^.Dane,Wyb)
THEN WyswietlDane(E^.Dane);
E:=E^.Nast;
END
END; {Selekcja}
Sprawdzanie stanu listy i odczytywanie danych z różnych elementów listy
Funkcje sprawdzające: Zawiera, PustaLista i odczytujące DlugoscListy, oraz dane z różnych elementów listy - Pierwszy, Ostatni, Nastepny, Poprzedni - nie zmieniają stanu listy, ani jej elementów. Niektóre z nich wykorzystują funkcję Szukaj.
FUNCTION Zawiera {sprawdza czy element z poszukiwanymi danymi
znajduje się w liście}
(VAR Pocz :PElement; {początek listy}
PoszukiwaneDane:TDane):BOOLEAN;
VAR
Element:PElement;
BEGIN
Element:=Szukaj(Pocz,PoszukiwaneDane);
Zawiera:=Element<>NIL
END; {Zawiera}
Funkcja DlugoscListy zwraca liczbę elementów dołączonych do listy, którą liczy podczas przeglądania listy.
FUNCTION DlugoscListy {podaje liczbę elementów listy}
(VAR Pocz:PElement) {początek listy}
:INTEGER;
VAR
E:PElement;
L:INTEGER;
BEGIN
E:=Pocz;
L:=0;
WHILE E<>NIL DO
BEGIN
L:=L+1;
E:=E^.Nast;
END;
DlugoscListy:=L
END; {DlugoscListy}
W wielu programach wykorzystujących listy trzeba badać, czy lista jest pusta. Poniżej zdefiniowano funkcję, która zwraca wartość TRUE, gdy lista jest pusta.
FUNCTION PustaLista {sprawdza, czy lista jest pusta}
(VAR Pocz:PElement) {początek listy}
:BOOLEAN;
BEGIN
PustaLista:=Pocz=NIL
END; {PustaLista}
Często potrzebne jest odczytanie danych z wybranego elementu listy, bez zmiany stanu listy. Takich odczytów z różnych elementów listy dokonują poniższe funkcje. Jeżeli lista jest pusta zwracana jest wartość PusteDane typu TDane, reprezentująca brak danych.
FUNCTION Pierwszy {podaje dane z pierwszego elementu listy}
(VAR Pocz:PElement) {początek listy}
:TDane;
BEGIN
IF Pocz<>NIL
THEN Pierwszy:=Pocz^.Dane
ELSE Pierwszy:=PusteDane
END; {Pierwszy}
FUNCTION Ostatni {podaje dane z ostatniego elementu listy}
(VAR Kon:PElement) {początek listy}
:TDane;
BEGIN
IF Kon<>NIL
THEN Ostatni:=Kon^.Dane
ELSE Ostatni:=PusteDane
END; {Ostatni}
FUNCTION Nastepny {podaje dane z elementu następnego po elemencie
zawierającym podane dane}
(VAR Pocz :PElement; {początek listy}
ZnaneDane:TDane; {dane poszukiwanego elementu}
VAR brak :BOOLEAN) {sygnalizacja nieznalezienia elementu}
:TDane;
VAR
Element:PElement;
Znal :BOOLEAN;
D :TDane;
BEGIN
brak:=FALSE;
Element:=Szukaj(Pocz,ZnaneDane);
IF Element<>NIL
THEN
IF Element^.Nast<>NIL
THEN D:=Element^.Nast^.Dane
ELSE D:=PusteDane
ELSE brak:=TRUE;
Nastepny:=D
END; {Nastepny}
Funkcja Poprzedni przegląda listę, pamiętając adres elementu poprzedniego, przed badanym elementem, aż do znalezienia elementu, spełniającego określony warunek.
FUNCTION Poprzedni {podaje dane z elementu poprzedniego
przed elementem zawierającym podane dane}
(VAR Pocz :PElement; {początek listy}
ZnaneDane :TDane; {dane poszukiwanego elementu}
VAR brak :BOOLEAN) {sygnalizacja nieznalezienia elementu}
:TDane;
VAR
E,P :PElement;
Znal :BOOLEAN;
D :TDane;
BEGIN
E:=Pocz;
P:=NIL;
brak:=TRUE;
WHILE (E<>NIL) AND brak DO
BEGIN
IF RowneDane(E^.Dane,ZnaneDane)
THEN brak:=FALSE
ELSE
BEGIN
P:=E;
E:=E^.Nast
END;
END;
IF P=NIL
THEN Poprzedni:=PusteDane
ELSE
BEGIN
D:=P^.Dane;
Poprzedni:=D
END;
END; {Poprzedni}
Usuwanie elementów z listy
Procedury, usuwające element z listy, realizują odłączenie wybranego elementu z listy, po czym wykonują jedno z poniższych działań:
odczytują przechowywane w nim dane i kasują go,
kasują go,
zwracają adres odłączonego elementu do wykorzystania go w programie.
Usunięcie pierwszego elementu z listy
Zadaniem procedury UsunPierwszy jest usunięcie pierwszego elementu listy, odczytanie zapamiętanych tam danych i skasowanie elementu. Procedura odczytuje dane z pierwszego elementu listy, zapamiętuje adres pierwszego elementu jako E, przestawia początek listy na drugi element i kasuje dotychczasowy pierwszy element. W przypadku, gdy usuwany jest jedyny element listy, adres początku i końca listy jest ustawiany na NIL. Usunięcie pierwszego elementu z listy pokazuje rys. 13.12. Linia przerywana pokazuje stan listy, przed usunięciem pierwszego elementu.
Rys. 13.12 Usunięcie pierwszego elementu z listy jednokierunkowej
FUNCTION UsunPierwszy {pobiera dane z początku listy
i usuwa pierwszy element}
(VAR Pocz,Kon:PElement) {początek, koniec listy}
:TDane; {zwraca dane z pierwszego elementu}
VAR
D:TDane;
E:PElement;
BEGIN
IF Pocz=NIL
THEN UsunPierwszy:=PusteDane
ELSE
BEGIN
E:=Pocz;
D:=Pocz^.Dane;
IF Pocz=Kon
THEN
BEGIN
Pocz:=NIL;
Kon:=NIL
END
ELSE Pocz:=Pocz^.Nast;
DISPOSE(E);
UsunPierwszy:=D
END
END; {UsunPierwszy}
Usuwanie elementu z dowolnego miejsca listy
Zadaniem procedury Usun jest skasowanie elementu o znanym adresie. Procedura usuwa z listy element z miejsca, w którym się znajduje, łącząc bezpośrednio elementy poprzedzający go i następujący po nim. Ilustruje to rys.13.13. Usunięcie elementu z listy jednokierunkowej wymaga znajomości adresu elementu, poprzedzającego element usuwany. W tym celu lista jest przeglądana aż do znalezienia elementu o znanym adresie, z pamiętaniem adresu elementu, poprzedzającego element badany. W przypadku usuwania pierwszego elementu, procedura aktualizuje Pocz - początek listy. W przypadku usunięcia ostatniego elementu listy aktualizuje Kon -koniec listy.
Rys. 13.13 Usunięcie elementu o znanym adresie i dowolnym położeniu w liście jednokierunkowej
PROCEDURE Usun {usuwa z listy wskazany element - jest w liście}
(VAR Pocz,Kon:PElement;{początek, koniec listy}
Element :PElement);{element usuwany}
VAR
P,E:PElement;
BEGIN
IF Element=Pocz
THEN
BEGIN
IF Element=Kon
THEN Kon:=NIL;
Pocz:=Element^.Nast;
END
ELSE
BEGIN
E:=Pocz^.Nast;
P:=Pocz;
WHILE (E<>Element) DO
BEGIN
P:=E;
E:=E^.Nast
END;
P^.Nast:=Element^.Nast;
IF Element=Kon
THEN Kon:=P;
END;
DISPOSE(Element);
Element:=NIL;
END; {Usun}
Procedura Usun1 usuwa, dane z elementu listy, o podanym adresie Element, bez przeglądania całej listy, jak to robiła procedura Usun. W przypadku, gdy usunięty ma być pierwszy element listy, wskaźnik początku listy jest przestawiany na następny element, a pierwszy element jest zwalniany. Jednak, gdy jest usuwany element znajdujący się wewnątrz listy, na miejsce jego usuwanych danych, są wpisywane dane, z elementu następnego po elemencie usuwanym, a fizycznie jest kasowany element, znajdujący się za elementem usuwanym. Tylko w przypadku, gdy trzeba usunąć ostatni element listy, jest wymagane znalezienie, poprzedzającego go elementu, aby ustalić nowy koniec listy.
PROCEDURE Usun1 {usuwa z listy wskazany element
zakładając, że jest on w liście}
(VAR Pocz,Kon:PElement;{początek, koniec listy}
Element :PElement);{element usuwany}
VAR
P,E:PElement;
BEGIN
IF Element=Pocz
THEN
BEGIN
IF Element=Kon
THEN Kon:=NIL;
Pocz:=Element^.Nast;
DISPOSE(Element);
END
ELSE
BEGIN
IF Element<>Kon
THEN
BEGIN
P:=Element^.Nast;
Element^:=Element^.Nast^;
DISPOSE(P);
END
ELSE
BEGIN
P:=Pocz;
WHILE (E<>Element) DO
BEGIN
P:=E;
E:=E^.Nast
END;
P^.Nast:=NIL;
DISPOSE(Element);
Kon:=P;
END;
END;
Element:=NIL;
END; {Usun1}
Usunięcie elementu zawierającego znane dane
Zadaniem procedury UsunSzukany jest skasowanie elementu, zawierającego znane dane. Procedura przegląda listę i sprawdza, czy kolejny element zawiera ZnaneDane, pamiętając adres elementu, poprzedzającego element badany. Po znalezieniu elementu, łączy element poprzedzający go, z następującym po nim. W przypadku usunięcia elementu z początku lub końca listy aktualizuje odpowiednie wskaźniki.
PROCEDURE UsunSzukany {usuwa pierwszy element z podanymi danymi}
(VAR Pocz,Kon:PElement;{nagłówek listy}
ZnaneDane :TDane; {poszukiwane dane}
VAR brak :BOOLEAN);{sygnalizacja nieznalezienia elementu }
VAR
P,E:PElement;
BEGIN
brak:=TRUE;
E:=Pocz;
IF RowneDane(E^.Dane,ZnaneDane)
THEN
BEGIN
IF Element=Kon
THEN Kon:=NIL;
Pocz:=Element^.Nast;
END
ELSE
BEGIN
P:=NIL;
WHILE (E<>NIL) AND brak DO
IF RowneDane(E^.Dane,ZnaneDane)
THEN brak:=FALSE
ELSE
BEGIN
P:=E;
E:=E^.Nast;
END;
IF NOT brak
THEN
BEGIN
P^.Nast:=E^.Nast;
IF E^.Nast=NIL
THEN Kon:=P;
DISPOSE(E);
END
END;
Element:=NIL;
END; {UsunSzukany}
Usuwanie elementu znajdującego się po elemencie o wskazanym adresie
Zadaniem procedury UsunPoEl jest usunięcie elementu, który jest następnikiem elementu o znanym adresie i odczytanie zapisanych w nim danych. Procedura zwraca dane pobrane z elementu usuwanego oraz łączy wskazany Element z elementem, znajdującym się za elementem usuwanym. Najpierw w zmiennej pomocniczej E zapamiętuje się adres elementu, który ma zostać usunięty, a następnie wykonuje się połączenie poza elementem usuwanym, łącząc Element z elementem następnym, po usuwanym. Z elementu usuwanego są pobierane dane i element odłączony od listy jest kasowany instrukcją DISPOSE. W przypadku, gdy usunięty zostaje ostatni element listy, koniec listy Kon jest przestawiany na element wskazany.
PROCEDURE UsunPoEl {usuwa element po wskazanym, zwraca dane}
(VAR Kon:PElement; {koniec listy}
Element:PElement; {wskazany element}
VAR Dane:TDane; {zwracane dane}
VAR brak:BOOLEAN); {sygnalizacja nieznalezienia elementu}
VAR E:PElement;
BEGIN
IF (Element<>NIL) AND (Element^.Nast<>NIL)
THEN
BEGIN
E:=Element^.Nast;
Element^.Nast:=E^.Nast;
IF Element^.Nast=NIL
THEN Kon:=Element;
Dane:=E^.Dane;
DISPOSE(E);
END
ELSE
BEGIN
Dane:=PusteDane;
brak:=TRUE;
END;
END; {UsunPoEl}
Zwolnienie elementów listy
Listy tymczasowe, tworzone w celu wykonania jakiejś funkcji programu, powinny być kasowane po spełnieniu swojej roli, jeżeli w dalszym ciągu pracy programu nie będą już wykorzystywane. Procedura Zwolnij zwalnia pamięć, zajmowaną przez kolejne elementy listy. Po jej wykonaniu zmienna Pocz będzie wskazywać na pusty adres.
PROCEDURE Zwolnij {zwalnia listę elementów kasując elementy listy}
(VAR Pocz:PElement); {początek listy}
VAR
E:PElement;
BEGIN
WHILE Pocz<>NIL DO
BEGIN
E:=Pocz;
Pocz:=E^.Nast;
DISPOSE(E)
END
END; {Zwolnij}
Rekurencyjne procedury na liście jednokierunkowej
Ponieważ lista jest przykładem struktury rekurencyjnej, wszystkie operacje na liście można zrealizować stosując procedury rekurencyjne. Pokazuje to zadanie zad47.txt. Tylko procedura Dolacz jest mniej efektywna, w porównaniu z omówioną w p. 13.4.3.2 procedurą NaKoncu, która dołącza element na końcu listy, z wykorzystaniem wskaźnika końca listy. Rekurencyjna procedura Dolacz przechodzi przez całą listę, aż do ostatniego jej elementu, który rozpoznaje po wartości NIL, wpisanej w polu Nast. Schemat działania tej procedury, dla pięcioelementowej listy, jest pokazany na rys. 13.14. Procedura wykorzystuje definicje typu T i typu wskaźnikowego PT, z p.13.3.1, dodatkową deklarację zmiennej globalnej d, przechowującej długość listy oraz procedurę InicjujDane, która wprowadza dane do rekordu typu T.
VAR d:INTEGER;
PROCEDURE Dolacz(VAR p:PT);
BEGIN
IF p=NIL
THEN
BEGIN
NEW(p);
p^.Nast:=NIL;
InicjujDane(p^.Dane);
d:=d+1
END
ELSE Dolacz(p^.Nast)
END; {Dolacz}
Rys. 13.14. Schemat działania procedury rekurencyjnej Dolacz, przy pięciu elementach listy
Wyświetlenie zawartości listy jednokierunkowej w odwrotnym porządku, także daje się efektywnie wykonać, tylko przez procedurę rekurencyjną. Realizuje je poniższa procedura Wyswietl, która do wyświetlenia rekordu danych z elementu listy, używa procedury WyswietlDane.
PROCEDURE Wyswietl(VAR p:PT);
BEGIN
IF p<>NIL
THEN
BEGIN
Wyswietl(p^.Nast);
WyswietlDane(p^.Dane);
END;
END; {Wyswietl}
Schemat jej działania pokazano na rys 13.15.
Rys. 13.15. Schemat działania procedury rekurencyjnej Wyswietl
Operacje na liście dwukierunkowej
Struktura elementu i listy
Element listy dwukierunkowej zawiera dowolne dane oraz dwa łączniki: jeden - do następnego, drugi - do poprzedniego rekordu w liście.
PElement2=^TElement2;
TElement2=RECORD
Dane:TDane;
Nast,
Poprz:PElement2;
END;
W programie wykorzystującym listę, musi być zadeklarowana zmienna, wskazująca na początek listy, np.: Poczatek. Jeżeli przewiduje się dołączanie elementów na końcu listy, należy dodatkowo zadeklarować zmienną wskazującą na koniec listy, np.: Koniec. Struktura takiej listy jest pokazana na rys. 13.16. Rysunki są narysowane tak, jakby pole Nast było pierwszym, a pole Poprz ostatnim polem elementu.
VAR Poczatek,Koniec:PElement2;
Rys. 13.16 Lista dwukierunkowa
Zawartość modułu do obsługi listy dwukierunkowej
Operacje wykonywane na listach dwukierunkowych i ich elementach, które będą omawiane w dalszych podrozdziałach, są realizowane przez odpowiednie procedury. Wykorzystują one definicję typu elementu listy, typu danych elementu listy oraz typu nagłówka listy. Wykorzystanie nagłówka listy, skraca listy parametrów procedur. Wszystkie omawiane procedury, umieszczono w module ListyU2.pas, który znajduje się na dyskietce. Moduł ten zawiera:
definicję typu danych TDane, umieszczanych w elemencie listy. W celu uproszczenia procedur dotyczących typu TDane przyjęto, że dane wstawiane do listy będą pojedynczym napisem typu STRING,
definicję typu wskaźnikowego dla listy dwukierunkowej PElement2,
definicję typu elementu listy dwukierunkowej TElement2,
definicję nagłówka listy dwukierunkowej w postaci typu rekordowego TLista i typu wskaźnikowego PLista, związanego z tym typem.
PLista=^TLista;
TLista=RECORD {nagłówek listy}
Poczatek, {pierwszy element listy}
Koniec :PElement2; {ostatni element listy}
Dlugosc :INTEGER; {długość listy}
END;
procedury i funkcje wykonujące operacje na parametrze typu TDane, stanowiącym informacyjną część elementu listy tj. :
InicjujDane(VAR Dane:TDane); - wprowadza wartość Dane.
WyswietlDane(VAR Dane:TDane); - wyświetla wartość Dane.
RowneDane(VAR Dane1,Dane2:TDane):BOOLEAN; - porównuje dwa parametry typu TDane i zwraca wartość TRUE, jeżeli porównywane parametry są równe.
PorownajDane(Dane1,Dane2:TDane):INTEGER; - porównuje Dane1 i Dane2 i zwraca odpowiednią wartość:
-1 - gdy Dane1<Dane2
0 - gdy Dane1=Dane2
1 - gdy Dane1>Dane2.
procedury wykonujące następujące operacje na liście:
InicjujListe(VAR L:TLista); - ustala wartości początkowe pól nagłówka listy.
NaPoczatku (VAR L:TLista; NoweDane:TDane); - tworzy nowy element listy zawierający NoweDane i dołącza go na początku listy.
NaKoncu(VAR L:TLista; NoweDane:TDane); - tworzy nowy element listy zawierający NoweDane i dołącza go na końcu listy.
WstawPoEl(VAR L:TLista; Element:PElement2; NoweDane:TDane); - tworzy nowy element listy zawierający NoweDane i dołącza go po elemencie o wskazanym adresie.
WstawPrzedEl(VAR L:TLista; Element:PElement2; NoweDane:TDane); - tworzy nowy element listy zawierający NoweDane i dołącza go przed elementem o wskazanym adresie.
WstawWPorzadku(VAR L:TLista; NoweDane:TDane); - dołącza do listy nowy element zawierający NoweDane, tak aby lista pozostała uporządkowana.
PrzegladWprzod(VAR L:TLista);- wyświetla elementy listy od pierwszego do ostatniego.
PrzegladWstecz(VAR L:TLista);- wyświetla elementy listy od ostatniego do pierwszego.
Selekcja (VAR L:TLista; Wyb:BYTE); - wybiera i wyświetla elementy listy spełniające
określony warunek.
Usun(VAR L:TLista; Element:PElement2); - usuwa z listy element o podanym adresie.
UsunSzukany(VAR L:TLista; ZnaneDane:TDane; VAR brak:BOOLEAN); - usuwa z listy element zawierający ZnanaDane.
UsunPoEl(VAR L:TLista; Element:PElement2; VAR Dane:TDane;
VAR brak:BOOLEAN); - usuwa z listy element znajdujący się po wskazanym elemencie.
Funkcje badające stan listy i zwracające następujące wyniki:
SzukajP(VAR L:TLista; ZnaneDane:TDane):PElement; - wyszukuje w liście, przeglądając listę od początku, element zawierający ZnaneDane i zwraca jego adres.
SzukajK(VAR L:TLista; ZnaneDane:TDane):PElement; - wyszukuje w liście, przeglądając listę od końca, element zawierający ZnaneDane i zwraca jego adres.
SzukajzWart(VAR L:TLista; ZnaneDane:TDane):PElement; - stosując algorytm wyszukiwania z wartownikiem wyszukuje w liście, element zawierający ZnaneDane i zwraca jego adres.
Zawiera(VAR L:TLista; PoszukiwaneDane:TDane):BOOLEAN;- sprawdza czy w liście znajduje się element zawierający PoszukiwaneDane.
Pierwszy(VAR L:TLista):TDane; - zwraca dane z pierwszego elementu listy.
Ostatni(VAR L:TLista):TDane; - zwraca dane z ostatniego elementu listy.
Nastepny(VAR L:TLista; ZnaneDane:TDane; VAR brak:BOOLEAN):TDane; -
zwraca dane z elementu listy znajdującego się po elemencie zawierającym ZnaneDane.
Poprzedni(VAR L:TLista; ZnaneDane:TDane; VAR brak:BOOLEAN):TDane; - zwraca dane z elementu listy poprzedzającego element zawierający ZnaneDane.
UsunPierwszy(VAR L:TLista):TDane; - odłącza pierwszy element od listy, zwraca zawarte w nim dane i kasuje element.
DlugoscListy(VAR L:TLista):INTEGER; - zwraca liczbę elementów znajdujących się w liście.
PustaLista(VAR L:TLista):BOOLEAN; - zwraca wartość TRUE, jeżeli lista jest pusta.
Dołączanie elementu do listy dwukierunkowej
Dołączenie elementu na początku listy
Dla dołączanych danych jest generowany nowy rekord dynamiczny o adresie wskazywanym przez E. Rekord wypełniany jest danymi. W pole Nast jest wpisywany adres pierwszego elementu listy. W pole Poprz jest wpisywana wartość NIL. Jest zwiększana długość listy. Jeżeli lista nie jest pusta, do dotychczasowego pierwszego elementu listy, w pole Poprz jest wpisywany adres nowego elementu. Początek listy jest przestawiany na nowy element E. Jeżeli dołączany element jest pierwszym elementem listy, to jednocześnie jest też ostatnim, czyli jest ustalany jest nowy Koniec listy. Dołączenie elementu na początku listy dwukierunkowej, ilustruje rys. 13.17. Linią przerywaną pokazano połączenia przed dołączeniem nowego elementu na początku listy.
PROCEDURE NaPoczatku {dołącza element na początek listy}
(VAR L :TLista; {nagłówek listy}
NoweDane:TDane); {dane dołączanego elementu}
VAR
E:PElement2;
BEGIN
WITH L DO
BEGIN
NEW(E);
E^.Dane:=NoweDane;
E^.Poprz:=NIL;
INC(Dlugosc);
E^.Nast:=Poczatek;
IF Poczatek=NIL
THEN Koniec:=E
ELSE Poczatek^.Poprz:=E;
Poczatek:=E
END
END; {NaPoczatku}
Rys. 13. 17 Dołączenie nowego elementu na początku listy dwukierunkowej.
Dołączenie elementu na końcu listy
Dla dołączanych danych jest generowany nowy rekord dynamiczny o adresie, wskazywanym przez E. Rekord wypełniany jest danymi. W pole Nast nowego elementu jest wpisywana wartość NIL. Jest zwiększana długość listy. W pole Poprz jest wpisywany dotychczasowy koniec listy, wskazywany przez Koniec lub wartość NIL, jeżeli nowy element jest dołączany do pustej listy. W tym przypadku jest też ustalany Poczatek listy, ponieważ element dołączany na końcu listy jest jednocześnie pierwszym elementem listy. W ostatnim elemencie listy, wskazywanym przez Koniec, w pole Nast jest wpisywany dołączany element E. Koniec listy jest przestawiany na nowy element E. Dołączenie elementu na końcu listy dwukierunkowej ilustruje rys. 13.18. Linią przerywaną pokazano połączenie przed dołączeniem nowego elementu na końcu listy.
Rys.13.18 Dołączenie elementu listy na końcu listy dwukierunkowej
PROCEDURE NaKoncu {dołącza element na końcu listy}
(VAR L :TLista; {nagłówek listy}
NoweDane:TDane); {dane dołączanego elementu}
VAR
E:PElement2;
BEGIN
WITH L DO
BEGIN
NEW(E);
E^.Dane:=NoweDane;
E^.Nast:=NIL;
INC(Dlugosc);
IF Poczatek=NIL
THEN
BEGIN
Poczatek:=E;
E^.Poprz:=NIL
END
ELSE
BEGIN
E^.Poprz:=Koniec;
Koniec^.Nast:=E
END;
Koniec:=E
END
END; {NaKoncu}
Dołączenie elementu po elemencie o znanym adresie
Dołączenie elementu z nowymi danymi, po elemencie o wskazanym adresie, realizowane przez procedurę WstawPoEl, jest bardziej rozbudowane w stosunku do wstawienia do listy jednokierunkowej. Wymaga ono wypełniania także pól Poprz, wskazujących na poprzedni element. Po wygenerowaniu i wypełnieniu elementu, należy połączyć wskazany Element z nowym elementem E, oraz nowy element z elementem następnym, po wskazanym. W pierwszej kolejności w nowym elemencie należy wypełnić pole Nast, wpisując do niego następnik Elementu, oraz pole Poprz, wpisując do niego wartość Element. Adres nowego elementu E jest wstawiany w pole Nast Elementu, oraz w pole Poprz elementu następnego po E. Jeżeli element wskazany był ostatnim elementem listy, to należy również, przestawić koniec listy na nowy element. Ponadto jest zwiększana długość listy. Ilustruje to rys. 13.19. Linią przerywaną pokazano połączenie, przed dołączeniem nowego elementu. Pole z symbolem * powinno zostać wypełnione w pierwszej kolejności przed innymi.
Rys. 13.19 Dołączenie elementu do listy dwukierunkowej po wskazanym elemencie.
PROCEDURE WstawPoEl {Wstawia nowy element zawierający
NoweDane po wskazanym elemencie}
(VAR L :TLista; {nagłówek listy}
Element :PElement2; {element po którym należy wstawić}
NoweDane :TDane); {wstawiane dane}
VAR
E:PElement2;
BEGIN
NEW(E);
E^.Dane:=NoweDane;
E^.Nast:=Element^.Nast;
Element^.Nast:=E;
IF E^.Nast<>NIL
THEN E^.Nast^.Poprz:=E
ELSE L.Koniec:=E;
E^.Poprz:=Element;
INC(L.Dlugosc)
END; {WstawPoEl}
Procedura WstawPoEl jest wykorzystywana także do wstawiania elementu na końcu listy, wskazywanym przez L.Koniec, w przypadku, gdy do końca listy jest dołączony element wartownika, przyśpieszający wyszukiwanie.
Dołączenie elementu przed elementem o znanym adresie
Dołączenie elementu do listy dwukierunkowej wewnątrz listy jest łatwe, ponieważ każdy element zna swój poprzednik i następnik. Jeżeli Element jest pierwszym elementem listy, to nowy element, zawierający NoweDane, zostanie wstawiony na początku. W pozostałych przypadkach, po wygenerowaniu nowego elementu E i wpisaniu do niego danych NoweDane, jego adres Poprz jest wypełniany adresem elementu, poprzedzającego Element pobranym z jego pola Poprz. Do pola Nast w E, jest wpisywany Element. Adres nowego elementu E jest wpisywany do pola Nast w elemencie poprzedzającym E oraz w polu Poprz, wskazanego Elementu. W przypadku, gdy Element był pierwszym elementem listy, ustala się nowy Poczatek. Dołączenie elementu z nowymi danymi, do listy dwukierunkowej przed elementem o znanym adresie, wskazywanym przez zmienną Element, ilustruje rys. 13.20. Linią przerywaną pokazano połączenie przed dołączeniem nowego elementu.
Rys. 13.20 Dołączenie elementu z nowymi danymi, do listy dwukierunkowej przed wskazanym elementem.
PROCEDURE WstawPrzedEl {wstawia nowy element, przed element
o podanym adresie}
(VAR L :TLista; {nagłówek listy}
Element :PElement2; {wskazywany element}
NoweDane:TDane); {wstawiane dane}
VAR
E:PElement2;
BEGIN
NEW(E);
E^.Dane:=NoweDane;
E^.Poprz:=Element^.Poprz;
Element^.Poprz:=E;
IF E^.Poprz<>NIL
THEN E^.Poprz^.Nast:=E
ELSE L.Poczatek:=E;
E^.Nast:=Element;
INC(L.Dlugosc)
END; {WstawPrzedEl}
Procedura WstawPrzedEl jest wykorzystywana w procedurze WstawWPorzadku, dołączającej nowy element do listy uporządkowanej
Dołączenie elementu do listy dwukierunkowej uporządkowanej
W przypadku tworzenia listy uporządkowanej według atrybutów elementów listy, wyszukuje się w liście element zawierający Dane o wartości większej niż NoweDane i nowy element dołącza przed nim do listy. Element o takiej samej, jak w innych elementach wartości pola, według którego odbywa się porządkowanie, jest dołączany za tymi elementami, które zostały dołączone wcześniej. Nieznalezienie elementu, przed którym należy wstawić nowy element, oznacza, że nowy element ma być wstawiony na końcu listy.
Do zidentyfikowania elementu poszukiwanego wykorzystuje funkcję PorownajDane, która porównuje NoweDane z polem Dane rekordu elementu listy. Zwrot wartości -1, oznacza, że nowy element ma być dołączony przed elementem, z którym był porównywany. Zwrot wartości 0 lub 1 oznacza, że nowy element ma być dołączony w dalszej części listy, po znalezieniu właściwego miejsca. W tym przypadku, bieżący element listy jest przesuwany na następny element listy.
PROCEDURE WstawWPorzadku{dolacza element do listy wg. PorownajDane}
(VAR L :TLista; {naglowek listy}
NoweDane:TDane); {dane dolaczanego elementu}
VAR
E :PElement2;
Znal:BOOLEAN;
BEGIN
Znal:=FALSE;
INC(L.Dlugosc);
E:=L.Poczatek;
WHILE (E<>NIL) AND NOT Znal DO
BEGIN
CASE PorownajDane(NoweDane,E^.Dane) OF
-1:
BEGIN
WstawPrzedEl(L,E,NoweDane);
Znal:=TRUE
END;
0,1: E:=E^.Nast {Dla rownych wstawi jako ostatni rowny}
END
END;
IF NOT Znal
THEN NaKoncu(L,NoweDane)
END; {WstawWPorzadku}
Wyświetlenie zawartości listy
List dwukierunkowa może być przeglądana od początku do końca, lub od końca do początku. Przeglądanie listy, od początku do końca, realizuje procedura PrzegladWPrzod. Przeglądanie od końca do początku, realizuje procedura PrzegladWstecz. Dane z przeglądanych elementów są wyświetlane za pomocą procedury WyswietlDane. Do przeglądania listy używa się dodatkowego wskaźnika, zadeklarowanego lokalnie w procedurze, którego wartość początkową ustala się na początek listy, a po wykonaniu operacji na elemencie przestawia się go na następny element listy.
PROCEDURE PrzegladWPrzod{Wyświetla dane z listy od początku}
(VAR L:TLista); {nagłówek listy}
VAR E:PElement2;
BEGIN
E:=L.Poczatek;
WHILE E<>NIL DO
BEGIN
WyswietlDane(E^.Dane);
E:=E^.Nast;
END
END; {PrzegladWPrzod}
PROCEDURE PrzegladWstecz{Wyswietla dane z listy, od końca do poczatku}
(VAR L:TLista); {naglowek listy}
VAR E:PElement2;
BEGIN
E:=L.Koniec;
WHILE E<>NIL DO
BEGIN
WyswietlDane(E^.Dane);
E:=E^.Poprz;
END
END; {PrzegladWstecz}
Wyszukiwanie w liście
Przy wyszukiwaniu unikalnego elementu w liście dwukierunkowej, można zastosować takie algorytmy, jak dla listy jednokierunkowej. Ponadto można przeglądać listę od początku SzukajP, lub od końca SzukajK, w zależności od tego, w której części listy, można się spodziewać poszukiwanego elementu. Bez względu na stosowany algorytm, wyszukiwanie elementu o unikalnej wartości pola należy zakończyć, po znalezieniu elementu, spełniającego warunek poszukiwania. Funkcje wyszukujące zwracają adres elementu, zawierającego poszukiwaną wartość. Do sprawdzenia zgodności zawartości elementów listy z wartością poszukiwanych danych, przekazanych przez parametr ZnaneDane jest wykorzystywana funkcja RowneDane zdefiniowana dla typu TDane. W przypadku nieznalezienia elementu w liście jest zwracany pusty adres równy NIL.
FUNCTION SzukajP {Wyszukuje w liście element, zawierający
ZnaneDane - algorytm ze zmienna logiczna,
przeglądanie od początku}
(VAR L :TLista; {nagłówek listy}
ZnaneDane:TDane) {poszukiwane dane}
:PElement2;
VAR
E :PElement2;
brak:BOOLEAN;
BEGIN
E:=L.Poczatek;
brak:=TRUE;
WHILE (E<>NIL) AND brak DO
BEGIN
IF RowneDane(E^.Dane,ZnaneDane)
THEN brak:=FALSE
ELSE E:=E^.Nast
END;
SzukajP:=E
END; {SzukajP}
FUNCTION SzukajK {Wyszukuje w liście element, zawierający
ZnaneDane - algorytm ze zmienna logiczna,
przeglądanie od końca}
(VAR L :TLista; {nagłówek listy}
ZnaneDane:TDane) {poszukiwane dane}
:PElement2;
VAR
E :PElement2;
brak:BOOLEAN;
BEGIN
E:=L.Koniec;
brak:=TRUE;
WHILE (E<>NIL) AND brak DO
BEGIN
IF RowneDane(E^.Dane,ZnaneDane)
THEN brak:=FALSE
ELSE E:=E^.Poprz
END;
SzukajK:=E
END; {SzukajK}
Algorytm wyszukiwania z wartownikiem, zakłada, że do ostatniego elementu listy wskazywanego przez L.Koniec jest dołączony dodatkowy element listy, do którego będą wpisywane poszukiwane dane. Lista jest przeglądana aż do momentu, kiedy funkcja RowneDane zwróci wartość TRUE. Znalezienie elementu wartownika oznacza brak w liście, elementu z poszukiwanymi danymi, tj. zawierającego ZnanaDane.
FUNCTION SzukajZWart {Wyszukuje w liście z wartownikiem, element
zawierający ZnaneDane }
(VAR L :TLista; {nagłówek listy}
ZnaneDane:TDane) {poszukiwane dane}
:PElement2;
VAR
E:PElement2;
BEGIN
L.Koniec^.Nast^.Dane:=ZnaneDane;
{wartownik- nastepny element po koncu}
E:=L.Poczatek;
WHILE NOT RowneDane(E^.Dane,ZnaneDane) DO
E:=E^.Nast;
IF E<>L.Koniec^.Nast
THEN SzukajZWart:=E
ELSE SzukajZWart:=NIL
END; {SzukajZWart}
Sprawdzanie stanu listy i odczytywanie danych z różnych elementów listy
Podobnie jak dla listy jednokierunkowej, funkcje sprawdzająca i odczytujące dane z różnych elementów listy, nie zmieniają stanu listy, ani jej elementów. Niektóre z nich wykorzystują funkcję Szukaj. Przedstawione poniżej funkcje różnią się od zamieszczonych powyżej funkcji, dotyczących listy jednokierunkowej, przede wszystkim nagłówkiem i odwołaniami do pól nagłówka listy.
Znacznie prostsza jest funkcja Poprzedni, wykorzystująca pole Poprz z elementu listy. Funkcje Nastepny i Poprzedni zwracają Dane z poszukiwanego elementu i dodatkowo przez parametr wyjściowy, sygnalizują brak elementu, dla którego nie ma następnika lub poprzednika.
FUNCTION Zawiera {sprawdza. czy element z poszukiwanymi
danymi znajduje się w liście}
(VAR L:TLista; {nagłówek listy}
PoszukiwaneDane:TDane):BOOLEAN;
VAR
Element:PElement2;
BEGIN
Element:=SzukajP(L,PoszukiwaneDane);
Zawiera:=Element<>NIL
END; {Zawiera}
FUNCTION Pierwszy {podaje dane z pierwszego elementu listy}
(VAR L:TLista) {nagłówek listy}
:TDane;
BEGIN
WITH L DO
BEGIN
IF Poczatek<>NIL
THEN Pierwszy:=Poczatek^.Dane
ELSE Pierwszy:=PusteDane
END
END; {Pierwszy}
FUNCTION Ostatni {podaje dane z ostatniego elementu listy}
(VAR L:TLista) {nagłówek listy}
:TDane;
BEGIN
IF L.Koniec<>NIL
THEN Ostatni:=L.Koniec^.Dane
ELSE Ostatni:=PusteDane
END; {Ostatni}
FUNCTION Nastepny {podaje dane z elementu, następnego
po elemencie, zawierającym podane dane}
(VAR L :TLista; {nagłówek listy}
ZnaneDane:TDane; {dane poszukiwanego elementu}
VAR brak :BOOLEAN) {sygnalizacja nieznalezienia elementu}
:TDane;
VAR
Element:PElement2;
Znal :BOOLEAN;
D :TDane;
BEGIN
brak:=FALSE;
Element:=SzukajP(L,ZnaneDane);
IF Element<>NIL
THEN
IF Element^.Nast<>NIL
THEN D:=Element^.Nast^.Dane
ELSE D:=PusteDane
ELSE brak:=TRUE;
Nastepny:=D
END; {Nastepny}
FUNCTION Poprzedni {podaje dane z elementu, poprzedniego
przed elementem zawierającym podane dane}
(VAR L:TLista; {nagłówek listy}
ZnaneDane:TDane; {dane poszukiwanego elementu}
VAR brak :BOOLEAN) {sygnalizacja nieznalezienia elementu}
:TDane;
VAR
Element:PElement2;
Znal :BOOLEAN;
D :TDane;
BEGIN
brak:=FALSE;
Element:=SzukajP(L,ZnaneDane);
IF Element<>NIL
THEN
IF Element^.Poprz<>NIL
THEN D:=Element^.Poprz^.Dane
ELSE D:=PusteDane
ELSE brak:=TRUE;
Poprzedni:=D
END; {Poprzedni}
Funkcja DlugoscListy zwraca aktualną liczbę elementów listy, pobraną z nagłówka listy.
FUNCTION DlugoscListy {podaje liczbę elementów listy}
(VAR L:TLista) {nagłówek listy}
:INTEGER;
BEGIN
DlugoscListy:=L.Dlugosc
END; {DlugoscListy}
Funkcja PustaLista zwraca wartość TRUE, w przypadku, gdy długość listy jest równa 0.
FUNCTION PustaLista {sprawdza, czy lista jest pusta}
(VAR L:TLista) {nagłówek listy}
:BOOLEAN;
BEGIN
PustaLista:=L.Dlugosc=0
END; {PustLista}
Usuwanie elementów z listy dwukierunkowej
Procedury usuwające element z listy działają podobnie, jak dla listy jednokierunkowej, z tym że nie muszą znajdować elementu, poprzedzającego element usuwany, bo ten mają wpisany w polu Poprz. Jednak muszą dodatkowo wypełniać pole Poprz oraz aktualizować długość listy, przechowywaną w nagłówku listy.
Usunięcie pierwszego elementu z listy
Procedura UsunPierwszy służy do odłączenia pierwszego elementu od listy, pobrania danych w nim zapamiętanych oraz skasowania go. Procedura odczytuje dane z pierwszego elementu listy, zapamiętuje jego adres jako E, przestawia początek listy na drugi element i kasuje dotychczasowy pierwszy element. W pierwszym elemencie listy pole Poprz ustawia na NIL i zmniejsza długość listy. W przypadku, gdy usuwany jest jedyny element listy, adres początku i końca listy jest ustawiany na NIL. Usunięcie pierwszego elementu z listy dwukierunkowej, pokazuje rys. 13.21. Linia przerywana pokazuje stan listy, przed usunięciem pierwszego elementu.
Rys. 13.21 Usunięcie pierwszego elementu z listy dwukierunkowej
FUNCTION UsunPierwszy {usuwa z początku listy, zwraca dane}
(VAR L:TLista) {nagłówek listy}
:TDane;
VAR
D:TDane;
E:PElement2;
BEGIN
WITH L DO
BEGIN
IF Poczatek=NIL
THEN UsunPierwszy:=PusteDane
ELSE
BEGIN
DEC(Dlugosc);
E:=Poczatek;
D:=Poczatek^.Dane;
IF Poczatek=Koniec
THEN
BEGIN
Poczatek:=NIL;
Koniec:=NIL
END
ELSE
BEGIN
Poczatek:=Poczatek^.Nast;
Poczatek^.Poprz:=NIL
END;
DISPOSE(E);
UsunPierwszy:=D
END
END
END; {UsunPierwszy}
Usuwanie elementu z dowolnego miejsca listy dwukierunkowej
Zadaniem procedura Usun jest skasowanie elementu o znanym adresie. Procedura usuwa element listy z miejsca, w którym się znajduje, łącząc bezpośrednio elementy poprzedzający go i następujący po nim. Ilustruje to rys. 13.22. Operacje na elementach poprzednim, przed elementem usuwanym i następnym po usuwanym są wykonywane w przypadku, gdy takie elementy istnieją. W przypadku usuwania pierwszego elementu, procedura aktualizuje Początek listy. W przypadku usunięcia ostatniego elementu listy aktualizuje Koniec listy.
Rys. 13.22 Usunięcie elementu ze środka listy dwukierunkowej
PROCEDURE Usun {usuwa z listy wskazany element}
(VAR L :Tlista; {nagłówek listy}
Element:PElement2); {usuwany element}
BEGIN
IF Element^.Poprz<>NIL
THEN Element^.Poprz^.Nast:=Element^.Nast;
ELSE L.Poczatek:=Element^.Nast
IF Element^.Nast<>NIL
THEN Element^.Nast^.Poprz:=Element^.Poprz
ELSE L.Koniec:=Element^.Poprz;
DISPOSE(Element);
Element:=NIL;
DEC(L.Dlugosc)
END; {Usun}
Procedura Usun jest wykorzystywana w procedurze UsunSzukany
Usunięcie elementu zawierającego znane dane
Zadaniem procedury UsunSzukany, jest usunięcie z listy i skasowanie elementu zawierającego znane dane. Procedura przegląda listę, szukając elementu zawierającego ZnaneDane. Po znalezieniu elementu wywołuje procedurę Usun, usuwającą wskazany element. Nieznalezienie elementu jest sygnalizowane do programu głównego przez parametr brak.
PROCEDURE UsunSzukany {usuwa z listy pierwszy element
z podanymi danymi}
(VAR L :TLista; {nagłówek listy}
ZnaneDane:TDane; {poszukiwane dane}
VAR brak :BOOLEAN); {sygnalizacja nieznalezienia elementu}
VAR
E:PElement2;
BEGIN
E:=SzukajP(L,ZnaneDane,brak);
IF NOT brak
THEN Usun(L,E)
END; {UsunSzukany}
Usuwanie elementu znajdującego się po elemencie o wskazanym adresie
Zadaniem procedury UsunPoEl jest usunięcie elementu następnika, elementu o znanym adresie, odczytanie zapisanych w nim danych i skasowanie go Procedura łączy Element wskazany z elementem, znajdującym się za elementem usuwanym. Najpierw zapamiętuje w zmiennej pomocniczej E, adres elementu, który ma zostać usunięty, następnie łączy Element, z następnikiem elementu usuwanego, pobiera dane z elementu usuwanego, kasuje go instrukcją DISPOSE oraz zmniejsza długość listy. W przypadku, gdy zostanie usunięty końcowy element listy, Koniec jest przestawiany na element wskazany. Ilustruje to rys. 13.23.
Rys. 13.23 Usunięcie elementu po elemencie wskazanym.
PROCEDURE UsunPoEl {usuwa element po wskazanym, zwraca jego dane}
(VAR L :TLista; {nagłówek listy}
Element :PElement2; {wskazany element}
VAR Dane:TDane; {zwracane dane}
VAR brak:BOOLEAN); {sygnalizacja nieznalezienia elementu}
VAR E:PElement2;
BEGIN
IF (Element<>NIL) AND (Element^.Nast<>NIL)
THEN
BEGIN
E:=Element^.Nast;
Element^.Nast:=E^.Nast;
E^.Nast^.Poprz:=Element;
Dane:=E^.Dane;
DISPOSE(E);
DEC(L.Dlugosc)
END
ELSE
BEGIN
Dane:=PusteDane;
brak:=TRUE;
END;
END; {UsunPoEl}
Zwolnienie elementów listy dwukierunkowej
Zwolnienie elementów listy dwukierunkowej odbywa się na zasadach zwalniania elementów w liście jednokierunkowej. Procedura różni się tylko parametrem przekazywanym do procedury i odwołaniem do pola Poczatek w nagłówku procedury. Procedura Zwolnij zwalnia pamięć zajmowaną przez kolejne elementy listy. Po jej wykonaniu pole Poczatek w nagłówku listy, będzie wskazywać na NIL.
PROCEDURE Zwolnij {zwalnia listę elementów kasując elementy listy}
(VAR L:TLista); {nagłówek listy}
VAR
E:PElement2;
BEGIN
WITH L DO
BEGIN
WHILE Poczatek<>NIL DO
BEGIN
E:=Poczatek;
Poczatek:=E^.Nast;
DISPOSE(E)
END
END
END; {Zwolnij}
Składowanie danych z listy w pliku
Operacje na listach, których dane powinny być przechowane, do czasu ponownego uruchomienia programu, należy uzupełnić o operacje składowania i odzyskiwania rekordów listy w pliku. Elementy pliku powinny przechowywać wyłącznie dane bez łączników, których wartości mogą się zmienić, przy innych warunkach pracy programu. Poniżej e przykładzie schematu składowania będą wykorzystane deklaracje pliku, zawierającego dane z elementów listy, oraz zmiennych, reprezentujących bieżący element listy i początek listy:
VAR plik:FILE OF TDane;
b,Pocz :PElement;
Schemat składowania jest niezależny od tego czy lista jest jedno-, czy dwukierunkowa:
ASSIGN(plik,nazwapliku);
REWRITE(plik);
b:=Pocz;
WHILE b<>NIL DO
BEGIN
WRITE(plik,b^.Dane);
b:=b^.Nast
END;
CLOSE(plik);
Elementy listy wpisane do pliku mogą być zwalniane instrukcją DISPOSE.
Schemat odzyskiwania danych z pliku także nie zależy od sposobu połączenia elementów listy. Dla różnych organizacji list inna będzie treść procedury DoListy:
VAR NoweDane:TDane;
ASSIGN(plik,nazwapliku);
{$I-}
RESET(plik);
{$I+}
IF (IORESULT<>0) OR (SIZEOF(plik)=0)
THEN WRITELN('Plik nie istnieje lub jest pusty')
ELSE
BEGIN
WHILE NOT EOF(plik) DO
BEGIN
READ(plik,NoweDane);
DoListy(Pocz,NoweDane);
END;
CLOSE(plik);
END;
Procedura DoListy powinna wygenerować element listy, czyli odpowiedni rekord dynamiczny wpisać do niego w pole Dane, NoweDane odczytane z pliku oraz dołączyć rekord do listy, zgodnie z organizacją tej listy.
Składowanie danych z listy zorganizowanej jako stos, wymaga zwrócenia uwagi na następujące zasady: nowe elementy dołącza się dla stosu, na początku listy, podczas gdy zapis elementów do pliku jest zawsze na końcu. Gdyby procedura DoListy wstawiała do listy, elementy odczytane z pliku tak, jak do stosu, czyli na początku listy, kolejność elementów w stosie po zapisie ich do pliku i odczycie z pliku, byłaby odwrotna, niż przed wykonaniem tych operacji.
Listy z nagłówkiem i listy cykliczne
Aby uprościć operacje na listach i uniknąć badania warunków, czy lista jest pusta lub czy adres jest różny od NIL, a także problemów z odwoływaniem się do adresów nieprzydzielonych, stosuje się listę z nagłówkiem (ang. head). Lista taka zawiera zawsze jeden dodatkowy element "głowę", który nie zawiera danych, ani nie wskazuje na dane, natomiast wskazuje na właściwy, pierwszy element listy. Listy z nagłówkiem organizuje się jako listy proste i listy cykliczne, jedno i dwukierunkowe.
Zasady organizacji jednokierunkowej listy z nagłówkiem
Dla listy z nagłówkiem należy zadeklarować zmienną wskaźnikową, która będzie wskazywać adres nagłówka listy:
VAR Glowa:PElement;
Listę inicjalizuje się generując jej nagłówek:
NEW(Glowa)
Następnie, dla listy jednokierunkowej z nagłówkiem, ustala się w nim adres następnego elementu równy NIL:
Glowa^.Nast:=NIL;
Na rys. 13.24 pokazana jest pusta lista z nagłówkiem i czteroelementowa lista z nagłówkiem.
Rys. 13.24. Lista jednokierunkowa z nagłówkiem
Dołączanie nowego elementu na początku listy, polega na wygenerowaniu nowego rekordu, którego adres zostaje wpisany w pole Nast w rekordzie nagłówka:
NEW(Glowa^.Nast);
Dołączenie pierwszego elementu do listy polega na wpisaniu
danych do nowego elementu listy, co wymaga pobrania jego adresu z pola Nast w rekordzie Głowa:
Glowa^.Nast^.Dane:=NoweDane;
oraz pustego adresu następnika, w tym nowym, dołączanym rekordzie:
Glowa^.Nast^.Nast:=NIL;
Dołączanie kolejnego elementu do listy z nagłówkiem na jej początku jest realizowane przez sekwencję:
NEW(q);
q^.Nast:=Glowa^.Nast;
Glowa^.Nast:=q;
Należy zauważyć, że wykorzystując zmienną pomocniczą, np. q, także pierwszy element można dołączyć tak samo, jak elementy następne, ponieważ dla pustej listy Glowa^.Nast jest równy NIL.
Zasady organizacji jednokierunkowej listy cyklicznej z nagłówkiem
Lista z nagłówkiem może być zrealizowana jako lista cykliczna. W tym przypadku ostatni jej element, wskazując na następny element, wskazuje na nagłówek. Po wygenerowaniu nagłówka, gdy lista jest pusta, nagłówek ma wskazywać sam na siebie. Wykonują to instrukcje:
NEW(Glowa)
Glowa^.Nast:=Glowa;
Jednokierunkowa lista cykliczna jest pokazana na rys. 13.25.
Rys. 13.25 Lista cykliczna jednokierunkowa - a) lista pusta, b) lista zawierająca 4 elementy
Gdy w liście jest wstawiany pierwszy element, wówczas generowany jest nowy element, którego adres wpisuje się do pola Nast rekordu Glowa, zaś w nowym rekordzie w polu Nast jest wpisywany adres nagłówka Glowa:
NEW(Glowa^.Nast);
Glowa^.Nast^.Nast:=Glowa;
Dołączanie kolejnego elementu, na początku listy cyklicznej z nagłówkiem, jest realizowane przez sekwencję instrukcji:
NEW(q);
q^.Nast:=Glowa^.Nast;
Glowa^.Nast:=q;
Tu także wykorzystując zmienną pomocniczą, np. q, pierwszy element można dołączyć tak samo jak elementy następne, ponieważ dla pustej listy Glowa^.Nast wskazuje na adres taki sam jak Glowa.
Przeglądanie listy cyklicznej wykonuje się wykorzystując wskaźnik bieżący b;
VAR b:PElement2;
przesuwający się po kolejnych elementach listy. Przeglądanie zaczyna się od pierwszego elementu listy, czyli następnego po nagłówku i trwa, aż do osiągnięcia przez wskaźnik bieżący - adresu nagłówka. Schemat przeglądania listy cyklicznej jest następujący:
b:=Glowa^.Nast;
WHILE b<>Glowa DO
BEGIN
WyswietlDane(b^.Dane);
b:=b^.Nast;
END;
Zasady organizacji dwukierunkowej listy z nagłówkiem
Praktycznie stosuje się listy cykliczne z nagłówkiem, zorganizowane jako listy dwukierunkowe. Możliwe jest wówczas przeglądanie listy wprzód i wstecz oraz łatwe dodawanie elementów zarówno na początku jak i na końcu listy. Dwukierunkowa lista z nagłówkiem jest pokazana na rys. 13.26.
VAR Glowa:PElement2;
Rys. 13.26. Lista cykliczna dwukierunkowa.
Inicjowanie listy cyklicznej dwukierunkowej wymaga wygenerowania elementu i wpisania mu własnego adresu, w pola Nast i Poprz
NEW(Glowa)
Glowa^.Nast:=Glowa;
Glowa^.Poprz:=Glowa;
Gdy w liście cyklicznej wstawiany jest pierwszy element, wygenerowanie nowego elementu, wstawia jego adres do pola Glowa^.Nast. Nowemu elementowi, który jest w tym przypadku jedynym elementem listy, wypełnia się adresem nagłówka - obydwa łączniki, ponieważ nagłówek jest jednocześnie jego następnym i poprzednim elementem listy. W rekordzie nagłówka trzeba jeszcze wypełnić pole Poprz, adresem nowego elementu, który go bezpośrednio poprzedza, ponieważ w tym przypadku jest pierwszym i jedynym elementem listy:
NEW(Glowa^.Nast);
Glowa^.Nast^.Nast:=Glowa;
Głowa^.Nast^.Poprz:=Glowa;
Glowa^.Poprz:=Glowa^.Nast;
Dołączanie pierwszego i kolejnego elementu na początku listy cyklicznej, przy wykorzystaniu zmiennej pomocniczej, np. q, wykonuje się w następujący sposób:
generuje się nowy element,
w jego pole Nast wpisuje się adres elementu, dotychczas wskazywanego przez nagłówek jako pierwszy,
w jego w pole Poprz wpisuje się adres nagłówka;
dotychczasowemu pierwszemu elementowi listy wpisuje się w pole Poprz, adres nowego dołączanego rekordu,
w elemencie nagłówka, w pole Nast, wpisuje się adres nowego elementu, który staje się pierwszym elementem listy.
Realizuje to sekwencja instrukcji:
NEW(q);
q^.Nast:=Glowa^.Nast;
q^.Poprz:=Glowa;
Glowa^.Nast^.Poprz:=q; { równoważne q^.Nast^.Poprz:=q;}
Glowa^.Nast:=q;
Dołączanie kolejnego elementu na końcu listy cyklicznej wykonuje się w następujący sposób:
generuje się nowy element,
w jego pole Nast wpisuje się adres nagłówka,
w jego pole Poprz wpisuje się adres elementu poprzedniego (czyli dotychczas ostatniego elementu listy), poprzednika nagłówka
dotychczasowemu, ostatniemu elementowi listy wpisuje się w pole Nast, adres nowego, dołączanego rekordu,
w elemencie nagłówka wpisuje się jako poprzedni, adres nowego elementu, który staje się ostatnim elementem listy.
Realizuje to sekwencja instrukcji:
NEW(q);
q^.Nast:=Glowa;
q^.Poprz:=Glowa^.Poprz;
Glowa^.Poprz^.Nast:=q;
Glowa^.Poprz:=q;
Oprócz przeglądania listy cyklicznej wprzód, realizowanego tak, jak dla listy jednokierunkowej, dla listy dwukierunkowej jest możliwe także przeglądanie wstecz, realizowane jak poniżej.
b:=Glowa^.Poprz;
WHILE b<>Glowa DO
BEGIN
WyswietlDane(b^.Dane);
b:=b^.Poprz;
END;
Element nagłówka może pełnić także rolę wartownika, przy wyszukiwaniu. Znalezienie poszukiwanej danej w nagłówku, oznacza, że poszukiwanego elementu w liście nie było w liście.
W celu usunięcie pierwszego elementu z listy cyklicznej dwukierunkowej, trzeba wpisać:
adres drugiego rekordu listy, w pole Glowa^.Nast,
oraz adres nagłówka, w pole Poprz, nowego pierwszego elementu,
co realizują instrukcje:
WITH Glowa^ DO
BEGIN
Nast:=Nast^.Nast; {Pole Nast w głowie wskazuje teraz na element następny po usuwanym}
Nast^.Poprz:=Glowa;
END;
Wstawienie nowego elementu, wskazywanego przez q, do listy cyklicznej uporządkowanej wg. pola klucz, znajdującego się w rekordzie Dane typu TDane, można wykonać za pomocą przeglądania listy od końca. Podczas przeglądania należy znaleźć element b, którego wartość pola klucz jest mniejsza, lub równa wartości klucza elementu wstawianego. Nowy element powinien być wstawiony za znalezionym, wskazywanym przez b:
b:=Glowa^.Poprz;
WHILE b^.Dane.klucz>q^.Dane.klucz DO
b:=b^.Poprz;
q^.Poprz:=b;
q^.Nast:=b^.nast;
b^.Nast:=q;
q^.Nast^.Poprz:=q;
W celu znalezienia miejsca wstawienia do listy zawierającej n - elementów, wykonuje się co najwyżej - n porównań
Moduł programu, moduł uniwersalny
Moduł programu
W omówionych w tym rozdziale modułach, do obsługi struktury danych w postaci listy jednokierunkowej lub listy dwukierunkowej zdefiniowano typy:
danych TDane, stanowiący dane elementu struktury danych
elementu struktury danych TElement - dla listy jednokierunkowej, TElement2- dla listy dwukierunkowej
a także procedury i funkcje realizujące operacje wykonywane na danych oraz na strukturze danych.
Wszystkie procedury, realizujące te operacje posiadają parametr, stanowiący adres struktury (dla dynamicznych struktur danych) lub strukturę danych, na której będzie wykonywana operacja oraz dodatkowe parametry, umożliwiające wykonanie operacji, odpowiednie do celu tej operacji.
Tak zdefiniowany moduł jest modułem konkretnego programu. Zmienne, reprezentujące strukturę danych, które będą parametrami aktualnymi tych procedur, będą zadeklarowane w programie, wykorzystującym ten moduł. Jako moduły programu są napisane moduły list jedno- i dwukierunkowej omówione powyżej.
W przypadku potrzeby zastosowania takiej samej struktury danych w innym programie, użytkownik musi skopiować moduł i w jego kopii wymienić typ danych TDane i treść procedur wykonujących operacje na tych danych, tj. InicjujDane, WyswietlDane, WprowadzDane, RowneDane, PorownajDane. Jeżeli nazwy procedur dotyczących danych, wywoływanych w procedurach obsługi danych, pozostaną niezmienione, to moduł w nowej wersji będzie poprawnie współpracował z nowym typem elementu i podprogramami dotyczącymi tego typu. Jednak potrzeba ingerowania w treść modułu jest niewygodna dla programisty.
Moduł uniwersalnych procedur działających na listach
Jeżeli użytkownik zakłada, że będzie wykorzystywał taką samą strukturę danych dla innych typów danych, powinien stworzyć uniwersalny moduł obsługi tej struktury. Moduł uniwersalny obsługi list jedno- lub dwukierunkowych, zorganizowanych jako listy proste, lub cykliczne zawiera:
definicję typu elementu struktury danych TElement,
oraz procedury i funkcje, realizujące operacje wykonywane na strukturze danych.
Moduł nie zawiera definicji typu danych TDane i procedur dotyczących tego typu, ale musi znać:
nazwę modułu, w którym typ danych i jego podprogramy, realizujące związane z nim operacje będzie zdefiniowany,
nazwę typu danych,
oraz nazwy procedur
Te nazwy muszą być określone w chwili tworzenia modułu obsługi struktury danych.
Typ danych jest w tym module używany do deklaracji elementu listy oraz specyfikacji parametrów procedur, które wykorzystują dane tego typu. Moduł procedur, działających na strukturze danych, może być uniwersalny w tym sensie, że będzie wykorzystywał moduł o stałej nazwie, np. TypyUzyt, w którym zostaną zdefiniowane potrzebne mu typy danych i procedury. Należą do nich: typ danych TDane oraz procedury wykonujące operacje na tych danych, tj. InicjujDane, WyswietlDane, WprowadzDane, RowneDane, PorownajDane.
Zmienne reprezentujące strukturę danych będą deklarowane w programie, wykorzystującym standardowy moduł obsługi struktury danych oraz moduł typu danych TypyUzyt. W przypadku potrzeby zastosowania, takiej samej struktury danych w innym programie, użytkownik musi wymienić moduł TypyUzyt, pozostawiając moduł obsługi struktury danych bez zmiany. Tu także zakłada się, że nazwy procedur dotyczące danych, wywoływanych w procedurach obsługi struktury danych, pozostaną niezmienione. Struktura takiego modułu, do obsługi listy jednokierunkowej jest następująca:
UNIT ListyS1;
INTERFACE
USES TypyUzyt;
{z tego modułu importuje się typ TDane oraz procedury: InicjujDane,
WyswietlDane, WprowadzDane z parametrem(Dane:TDane),
RowneDane,PorownajDane z parametrami(Dane1,Dane2:TDane)}
TYPE
PElement2=^TElement2;
TElement2=RECORD
Dane :TDane; {dane elementu listy}
Poprz,Nast:PElement2;{poprzedni, nastepny w liscie}
END;
PTLista =^TLista;
TLista =RECORD {naglowek listy}
Poczatek, {pierwszy element listy}
Koniec :PElement2; {ostatni element listy}
Dlugosc:INTEGER; {dlugosc listy}
END;
PROCEDURE Inicjuj_Liste(VAR L:TLista);
...{Deklaracje procedur i funkcji modułu}
FUNCTION PustaLista(VAR L:TLista):BOOLEAN;
IMPLEMENTATION
...{Implementacja procedur i funkcji modułu}
BEGIN
END.
Pewne uniezależnienie się modułu obsługi struktury danych, od typu danych przechowywanych w tej strukturze można osiągnąć, zastępując pole Dane:TDane w elemencie struktury danych, polem adresu danych typu POINTER. Parametry wszystkich procedur typu TDane, zostają zastąpione typem POINTER. Jeżeli jednak procedury modułu obsługi struktury danych wykonują operacje na danych, nazwy procedur, wykonujących te operacje pozostają ustalone i muszą znajdować się w ustalonym module TypyUzyt. Procedury uniwersalne muszą mieć w tym przypadku zmienione nagłówki, w porównaniu z modułem ListyS1, natomiast nie zmieniają treści, ponieważ każdy typ wskaźnikowy może być podstawiany za typ POINTER. Struktura takiego modułu jest następująca:
UNIT ListyS2;
INTERFACE
USES TypyUzyt;
{z tego modułu importuje się procedury: InicjujDane, WyswietlDane,
WprowadzDane z parametrem(Dane:POINTER),
RowneDane,PorownajDane z parametrami(Dane1,Dane2:POINTER)}
TYPE
PElement2=^TElement2;
TElement2=RECORD
Dane :POINTER; {adres danych dowolnego typu}
Poprz,Nast:PElement2;{poprzedni, nastepny w liscie}
END;
PTLista =^TLista;
TLista =RECORD {naglowek listy}
Poczatek, {pierwszy element listy}
Koniec :PElement2; {ostatni element listy}
Dlugosc :INTEGER; {dlugosc listy}
END;
{Deklaracje procedur i funkcji modułu}
PROCEDURE InicjujListe(VAR L:TLista);
FUNCTION SzukajK(VAR L:TLista;ZnaneDane:POINTER):PElement2;
...
FUNCTION PustaLista(VAR L:TLista):BOOLEAN;
IMPLEMENTATION
...{Implementacja procedur i funkcji modułu}
BEGIN
END.
W module użytkownika, w którym będzie zdefiniowany typ TDane i jego typ wskaźnikowy PDane, parametry procedur, typu POINTER, wskazujące na dane będą mogły byś przypisywane zmiennym wskaźnikowym bezpośrednio, lub mogą podlegać konwersji typu. Jeżeli np. w procedurze
WyswietlDane(VAR Dane:POINTER);
będzie zadeklarowana zmienna wskaźnikowa WDane
VAR WDane:PDane;
to przypisanie: WDane:=Dane;
lub konwersja parametru Dane typu POINTER na zmienną wskaźnikową WDane typu PDane wymagająca przypisania:
WDane:=PDane(Dane);
pozwolą na odwoływanie się do danych typu TDane za pomocą zmiennej wskaźnikowej.
Uwagi o zasadach i stylu programowania
W starszych publikacjach zaleca się stosowanie list przy liczbie elementów listy <100. Przy znacznie większych pamięciach operacyjnych i większej szybkości współczesnych komputerów, można stosować listy o znacznie większej liczbie elementów.
Zaleca się wykorzystywanie list do reprezentowania danych, których liczba nie jest z góry określona, często się zmienia i jest duża częstotliwość odwołań do pojedynczych rekordów.
W programie, w którym generuje się dużą liczbę zmiennych dynamicznych, pochłaniających dużo pamięci sterty bez ich zwalniania, należy przewidzieć własną obsługę wyczerpania pamięci sterty, realizowaną np. przez procedurę ObsluzBlad:
NEW(p);
IF p=NIL
THEN ObsluzBlad
ELSE
BEGIN
ObsluzZmiennaDynamiczna
END;
Jeżeli w chwili wykonania instrukcji NEW, pamięć sterty będzie już wyczerpana obszar na zmienną dynamiczną nie zostanie przydzielony i adres pozostanie nieokreślony. Wykonywanie operacji zapisu w tym przypadku może doprowadzić do błędu wykonania
Dla takich programów należy wymagany rozmiar sterty określić za pomocą dyrektywy M.
Do obsługi błędów sterty można wykorzystywać własną procedurę zdalną np. HeapServer zastępującą standardową procedurę HeapError, działającą na zasadzie omówionej dla procedury MyExit (rozdz. 13).
Dla programów, które często odwołują się do liczby elementów w liście, należy przechowywać daną pamiętającą aktualną długość listy, aktualizowaną przy dołączaniu i usuwaniu elementów z listy.
Zmienna wskaźnikowa nie jest ustawiana na wartość równą NIL, po zwolnieniu elementu instrukcją DISPOSE. Jeżeli zmienna wskaźnikowa ma być w programie badana, należy po zwolnieniu pamięci, jawnie nadać zmiennej wartość równą NIL.
Dla konkretnej aplikacji należy dobrać właściwy rodzaj struktury dynamicznej, zależny od częstości wykonywania poszczególnych operacji na liście. Przy częstym dołączaniu elementów do grupy uporządkowanej i ich usuwaniu, efektywna jest lista dwukierunkowa i lista cykliczna dwukierunkowa. Przy częstym wyszukiwaniu bardziej efektywna jest kolekcja posortowana (p.14), ponieważ można dla niej stosować przyśpieszone metody wyszukiwania.
Przy nierównomiernym zwracaniu pamięci, zajmowanej przez zmienne dynamiczne można w programie zorganizować pulę zwolnionych rekordów, bez zwalniania pamięci, którą one zajmowały. Wówczas zamiast generować nowe elementy dynamiczne, pobiera się elementy zwolnione z puli, a generacja nowych elementów ma miejsce dopiero wtedy, gdy pula wolnych obszarów jest pusta. Takie rozwiązanie przyspiesza działanie programu oraz powoduje mniejszą fragmentację pamięci sterty.
Pytania kontrolne
Wymień podstawowe cechy statycznych struktur danych.
Wymień podstawowe cechy dynamicznych struktur danych.
Kiedy można stosować statyczne struktury danych, a kiedy należy stosować struktury dynamiczne ?
Co reprezentuje typ wskaźnikowy i z jakimi typami może być związany ?
Dla jakich typów danych jest praktycznie wykorzystywany typ wskaźnikowy ?
Jak deklaruje się zmienne wskaźnikowe? Jaki jest związek zmiennej wskaźnikowej z obszarem pamięci przydzielanym na zmienne dynamiczne ?
Co to jest zmienna wskaźnikowa, a co to jest zmienna wskazywana?
Jak oznacza się nieprzydzielony adres, kiedy zmienna wskaźnikowa na nic nie wskazuje?
Kiedy i jak przydziela się pamięć na zmienne dynamiczne?
Co jest parametrem instrukcji NEW i DISPOSE?
Kiedy i jak zwalnia się pamięć zajmowaną przez zmienną dynamiczną?
Na czym polega przypisanie zmiennych wskaźnikowych, do czego służy i jak się je zapisuje?
Na czym polega przypisanie zmiennych dynamicznych i jak się je zapisuje?
Jak można porównywać zmienne wskaźnikowe?
Jak można porównywać zmienne wskaźnikowe, zmienne wskazywane i ich części?
Jak zapisuje się selektor pola rekordu dynamicznego, wskazywanego przez zmienną wskaźnikową?
Jak wygląda odwołanie do zmiennej wskazywanej w instrukcji WITH?
Co to jest typ POINTER?
Jaka jest relacja pomiędzy typem wskaźnikowym i typem POINTER ?
Jaka funkcja pozwala odczytać maksymalną wielkość bloku na stercie?
Jaka funkcja pozwala odczytać wielkość wolnej pamięci sterty?
Jak wygląda deklaracja typu elementu listy jednokierunkowej ?
Jak przydzielić można pamięć na blok danych o znanej wielkości?
Jak zwalnia się pamięć zajmowaną przez blok danych o znanej wielkości?
Jak można wpisać dane do przydzielonego dynamicznie bloku pamięci?
Jak można zapobiegać fragmentacji pamięci sterty ?
Gdzie można dołączać elementy do list i jakie struktury danych uzyskuje się w zależności od tego?
Zadeklaruj:
typ rekordowy, zawierający pola nazwisko i następny,
typ wskaźnikowy, związany z tym rekordem,
trzy zmienne wskaźnikowe, zadeklarowanego typu.
Wygeneruj trzy rekordy, wpisz do nich trzy nazwiska i połącz je:
w kolejności generowania,
w kolejności odwrotnej do generowania,
w kolejności alfabetycznej nazwisk, wpisanych do rekordów.
Narysuj utworzone w ten sposób listy
Zadeklaruj:
typ rekordowy, zawierający pola nazwisko, następny i poprzedni,
typ wskaźnikowy, związany z tym rekordem,
trzy zmienne wskaźnikowe, zadeklarowanego typu.
Wygeneruj trzy rekordy, wpisz do nich trzy nazwiska i połącz je w kolejności:
generowania,
odwrotnej do generowania,
alfabetycznej nazwisk wpisanych do rekordów.
Narysuj utworzone w ten sposób listy
Dlaczego stosuje się typ wskaźnikowy, związany z typem napisowym i jak się go wykorzystuje?
Co to jest lista? Czym różni się od tablicy?
Scharakteryzuj prostą listę jednokierunkową
Scharakteryzuj prostą listę dwukierunkową
Scharakteryzuj listę z nagłówkiem i omów na czym polegają jej zalety.
Scharakteryzuj listę cykliczną.
Jak uzyskuje się dostęp do elementów listy i jaki on jest?
Wymień podstawowe operacje, które można wykonać na listach różnych typów.
Co powinno być składowane w pliku, aby można było odtworzyć listę.
Omów schemat składowania listy w pliku.
Omów schemat odtwarzania listy z pliku.
Omów schemat przeglądania listy
Jak można wyszukiwać w liście, element o unikalnej wartości pola?
Jak można wybierać z listy elementy o nieunikalnej wartości pola, lub spełniające określony warunek?
Które wstawiania do listy mają praktyczne zastosowanie ?
Jak można usunąć dowolny element z listy jednokierunkowej? Zilustruj na rysunku.
Jak można usunąć dowolny element z listy dwukierunkowej ? Zilustruj na rysunku.
Jaką listę należałoby zorganizować, jeżeli istnieje potrzeba przeglądania listy do przodu i do tyłu?
Co zawiera moduł użytkownika służący do obsługi struktury danych?
Co zawiera uniwersalny moduł do obsługi struktury danych?
Ćwiczenie - Zmienne wskaźnikowe, rekordy dynamiczne,
listy rekordów
Celem ćwiczenia jest poznanie typu wskaźnikowego, zmiennych wskaźnikowych, zmiennych dynamicznych oraz grupowania rekordów dynamicznych w listach. W tym celu należy opracować procedury realizujące algorytmy dołączania rekordu dynamicznego do listy rekordów oraz wyświetlania zawartości listy rekordów. W procedurach jest wykorzystywany typ rekordowy, opisujący elementy prostej kartoteki danych, zorganizowanej w postaci listy jedno- lub dwukierunkowej.
W ramach ćwiczeń do rozdziałów 13 i 14 należy opracować prostą kartotekę danych w postaci listy rekordów.
W ćwiczeniu można wykorzystać moduł obsługi typu rekordowego oraz moduł z procedurami pomocniczymi, opracowane w ćwiczeniach do rozdziałów 9 i 10. Program kartoteki na strukturach dynamicznych może obsługiwać tą samą kartotekę, co program zrealizowany na statycznych strukturach danych, mieć taką samą strukturę i wykonywać takie same operacje. Programy z ćwiczeń do rozdziałów 9 i 10 można odpowiednio dostosować, usuwając z nich deklaracje związane z tablicą rekordów i zastępując je deklaracjami wymaganymi dla listy rekordów, oraz modyfikując zapis algorytmów procedur, odpowiednio do typu dynamicznej struktury danych.
1. Dopisać:
definicję typu wskaźnikowego, związanego z typem elementu listy, własnej kartoteki danych,
definicję typu elementu listy, w którym pole Dane będzie typu rekordowego własnej kartoteki danych, lub uzupełnić rekord własny o pola typu wskaźnikowego, umożliwiające utworzenie jedno- lub dwukierunkowej listy rekordów.
Przykład 1.1.
Definicja typu wskaźnikowego i typu elementu listy dwukierunkowej, opisującego konta bankowe może być zrealizowana w jednej z dwóch wersji, jako typ TElement, z polem Dane typu TKonto, zdefiniowanego w ćwiczeniu do rozdziału 9, lub jako typ rekordowy, zawierający oprócz danych, także pola łączników, umożliwiające tworzenie dwukierunkowej listy rekordów:
TYPE
PElement2=^TElement2;
TElement2=RECORD
Dane:TKonto;
nast,
poprz:PElement2
END;
lub
PKonto =^TKontoD;
st20 =STRING[20];
TKontoD=RECORD
saldo,nr_konta:LONGINT;
ochrona :CHAR;
haslo,
nazwisko_imie,
kod_miasto,
ulica_numer :st20;
nast,
poprz :PKonto
END;
Przykład 1.2.
Definicja typu wskaźnikowego i typu elementu listy jednokierunkowej, opisującego abonenta w książce telefonicznej, także może być zrealizowana w jednej z dwóch wersji:
TYPE
PElement1=^TElement1;
TElement1=RECORD
Dane:TOsoba;
nast:PElement1
END;
lub
POsoba =^TOsoba;
st50 =STRING[50];
st20 =STRING[20];
TOsobaD =RECORD
nazwisko,
imie,
tytul :st20; {tytul naukowy, stopień, zawód}
adres :st50;
numer :LONGINT;
nast :POsoba;
END;
W dalszych przykładach, w celu uproszczenia odwołań do pól rekordu, będą wykorzystywane elementy listy typu TKontoD, dla kartoteki kont bankowych i TOsobaD, dla kartoteki abonentów telefonicznych oraz typy wskaźnikowe PKonto i POsoba, związane z tymi typami.
2. Uzupełnić definicje typów o typ, definiujący rekord nagłówka listy.
Przykład 2.1.
Definicja typu nagłówka listy kont bankowych, wykorzystująca rekord listy typu TKontoD, i typ wskaźnikowy PKonto:
TLista=RECORD
pierwszy, {łącznik do pierwszego elementu listy}
ostatni :PKonto; {łącznik do ostatniego elementu listy}
dlugosc :INTEGER {długość listy}
END;
Przykład 2.2.
Definicja typu nagłówka listy spisu abonentów telefonicznych, wykorzystująca rekord listy typu TOsobaD, i typ wskaźnikowy POsoba:
:
TNaglowek=RECORD
pierwszy:POsoba; {lub PElement1}
{łącznik do pierwszego elementu listy}
długosc :INTEGER; {długość list abonentów}
END;
3. Uzupełnić deklaracje zmiennych w programach o:
wskaźnik do rekordu dynamicznego,
zmienną reprezentującą nagłówek listy, lub tylko początek listy.
Przykład 3.1.
Deklaracje zmiennych w programie rejestracji kont bankowych.
VAR
Kartoteka:TLista; {identyfikator listy}
nrkonta :LONGINT; {ostatni numer konta}
Przykład 3.2.
Deklaracje zmiennych w programie kartoteki abonentów telefonicznych.
VAR
spis :TNaglowek; {identyfikator nagłówka listy}
be :POsoba; {wskaźnik do rekordów dynamicznych}
koniec:BOOLEAN; {sygnalizacja końca wykonywania operacji}
wyb :CHAR; {znak sterujący wyborem opcji}
4. Zmodyfikować odpowiednio procedurę wprowadzania danych do rekordu zastępując selektor do rekordu statycznego, selektorem rekordu dynamicznego lub pozostawić procedurę bez zmian, natomiast zmienić wywołanie procedury.
Przykład 4.1.
Do wczytanie danych konta do rekordu dynamicznego, wskazywanego przez zmienną wskaźnikową, można wykorzystać procedurę Wprowadz_Rek z modułu MKonto. W tym przypadku wykorzystuje się element listy typu TElement, typ wskaźnikowy PElement, oraz zmienną be typu PElement, która będzie wskazywać na rekord dynamiczny, wygenerowany przed wywołaniem procedury. Wywołanie procedury Wprowadz_Rek jest wówczas następujące:
Wprowadz_Rek(be^.Dane,nrkonta);
Procedura wprowadzania danych opisujących konto bankowe, do rekordu dynamicznego typu TKontoD, wskazanego przez parametr procedury be, jest następująca:
PROCEDURE Wprowadz_RekD(VAR be:PKonto; nr:LONGINT);
BEGIN
WITH be^ DO
BEGIN
nr_konta:=nr;
WRITELN(' KONTO NR ',nr);
WRITELN('Prosze podac dane:');
WRITE(' NAZWISKO I IMIE ');
READLN(nazwisko_imie);
WRITELN(' ADRES: ');
WRITE(' KOD POCZTOWY I MIASTO ');
READLN(kod_miasto);
WRITE(' ULICA - NUMER ');
READLN(ulica_numer);
WRITE('Podaj wysokosc pierwszej WPLATY ');
READLN(saldo);
WRITELN;
WRITE('Czy konto ma byc chronione HASLEM ? T/N ');
READLN(ochrona);
IF ochrona IN ['T','t']
THEN
BEGIN
WRITE(' Podaj HASLO ');
READLN(haslo)
END
END
END; {Wprowadz_RekD}
Jej wywołanie dla zmiennej wskaźnikowej adres typu PKonto, wskazującej na element listy typu TKontoD ma postać:
Wprowadz_RekD(adres,nrkonta);
Przykład 4.2.
Wczytywanie danych do elementu listy TElement jest takie samo, jak dla kartoteki kont. Poniżej zdefiniowano procedurę WprowadzD, która wprowadza dane, opisujące abonenta w książce telefonicznej, do rekordu dynamicznego typu TOsobaD, wskazywanego przez parametr be typu POsoba.
PROCEDURE WprowadzD(VAR be:POsoba);
BEGIN
WITH be^ DO
BEGIN
WRITE(' NAZWISKO ');
READLN(nazwisko);
WRITE(' IMIE ');
READLN(imie);
WRITE(' TYTUL ');
READLN(tytul);
WRITE(' ADRES ');
READLN(adres);
WRITE(' NUMER TEL. ');
READLN(numer)
END
END; {WprowadzD}
5. Zmodyfikować procedurę wyświetlania rekordu objaśniającą wyprowadzane dane.
Przykład 5.1.
Wyświetlenie danych opisujących konto bankowe, przechowywanych w elemencie listy typu TElement, wskazywanym przez zmienną be typu PElement, przy użyciu procedury Wyswietl_Rek z modułu MKonta, jest realizowane przez wywołanie:
Wyswietl_Rek(be^.Dane);
Procedura Wyswietl_RekD, która wyświetla dane, opisujące konto bankowe, z rekordu typu TKontoD, wskazanego przez parametr be typu PKonto, jest następująca:
PROCEDURE Wyswietl_RekD(be:PKonto);
BEGIN
WITH be^ DO
BEGIN
WRITELN;
WRITELN('**************************************');
WRITELN(' NUMER KONTA ',nr_konta);
WRITELN(' STAN KONTA ',saldo);
WRITELN(' NAZWISKO I IMIE ',nazwisko_imie);
WRITE(' ADRES: ');
WRITELN(' ',kod_miasto);
WRITELN(' ',ulica_numer)
END
END; {Wyswietl_RekD}
Jej wywołanie dla zmiennej adres typu PKonto jest następujące:
Wyswietl_RekD(adres);
Przykład 5.2.
Wyświetlanie treści rekordu danych, z elementu listy typu TElement, jest takie samo jak dla kartoteki kont. Poniżej zamieszczono procedurę WyświetlD, która wyprowadza na ekran, dane abonenta z książki telefonicznej, z rekordu dynamicznego TOsobaD, wskazywanego przez parametr procedury be typu POsoba.
PROCEDURE WyswietlD(be :POsoba);
BEGIN
WITH be^ DO
BEGIN
WRITELN(' NAZWISKO ',nazwisko);
WRITELN(' IMIE ',imie);
WRITELN(' TYTUL ',tytul);
WRITELN(' ADRES ',adres);
WRITELN(' NUMER ',numer)
END
END; {WyswietlD}
6. Opracować procedurę dołączającą wskazany rekord kartoteki danych do listy, jedno lub dwukierunkowej. W procedurach dotyczących kont bankowych działania są wykonywane na parametrach procedury. W procedurach dotyczących abonentów działania na liście są wykonywane na zmiennej globalnej spis, reprezentującej nagłówek listy.
Przykład 6.1.
Procedury Nowe_Konto i Dolacz_Rek są przeznaczone do wprowadzenia i dołączenia do listy kilku rekordów. Dolacz_Rek dołącza rekord, wskazany przez parametr be, do końca listy o nagłówku kol, zawierającym wskaźnik na koniec listy. Procedura Nowe_Konto wypełnia kilka rekordów nowych klientów i dołącza je do listy, wykorzystując procedury: Dolacz_Rek oraz Wprowadz_RekD (z p. 4.1.)
PROCEDURE Dolacz_Rek(be:PKonto; VAR kol:TLista);
BEGIN
IF kol.dlugosc=0
THEN
BEGIN
kol.pierwszy:=be;
be^.poprz:=NIL
END
ELSE
BEGIN
kol.ostatni^.nast:=be;
be^.poprz:=kol.ostatni
END;
kol.ostatni:=be;
be^.nast:=NIL;
kol.dlugosc:=kol.dlugosc+1
END; {Dolacz_Rek}
PROCEDURE Nowe_Konto(VAR k:TLista);
VAR
be:PKonto;
z :CHAR;
BEGIN
REPEAT
nrkonta:=nrkonta+1;
CLRSCR;
WRITELN('************************************');
WRITELN('**********ZAKLADANIE KONTA**********');
WRITELN('************************************');
WRITELN;
NEW(be);
Wprowadz_RekD(be,nrkonta);
Dolacz_Rek(be,k);
WRITE('Czy zakladasz nowe konto ?(T/N) ');
READLN(z);
UNTIL z IN ['N','n']
END; {Nowe_Konto}
Przykład 6.2.
Procedura Dolacz, dołącza rekord wskazany przez be na koniec listy o nagłówku spis, przy założeniu, że wskaźnik na ostatni element listy nie jest znany. Procedura Czyt_Abonenta wpisuje do listy dane o wielu abonentach, wykorzystując procedury: WprowadzD (z p. 4.2) i Dołacz.
PROCEDURE Dolacz(nowy:POsoba);
VAR
be:POsoba;
BEGIN
IF spis.dlugosc=0
THEN spis.pierwszy:=nowy
ELSE
BEGIN
be:=spis.pierwszy;
WHILE be^.nast<>NIL DO
be:=be^.nast;
be^.nast:=nowy;
END;
spis.dlugosc:=spis.dlugosc+1;
nowy^.nast:=NIL
END; {Dolacz}
PROCEDURE Czyt_Abonenta;
VAR
wsk :POsoba;
dalej:BOOLEAN;
z :CHAR;
BEGIN
dalej:=TRUE;
WHILE dalej DO
BEGIN
CLRSCR;
NEW(wsk);
WprowadzD(wsk);
Dolacz(wsk);
WRITELN;
WRITE('CZY KONIEC WPISYWANIA ABONENTOW ? (T/N) ');
READLN(z);
IF (z IN ['T','t'])
THEN dalej:=FALSE
END
END; {Czyt_Abonenta}
7. Opracować procedurę wyświetlania listy rekordów.
Przykład 7.1.
Procedura wyświetla listę kont bankowych.
PROCEDURE Wyswietl_Liste(VAR k:TLista);
VAR
z :CHAR;
be:PKonto;
BEGIN
WRITELN('*************************************');
WRITELN('**********LISTA KONT*****************');
WRITELN('*************************************');
be:=k.pierwszy;
WHILE be<>NIL DO
BEGIN
Wyswietl_RekD(be);
be:=be^.nast;
READKEY
END;
WRITELN('*************************************')
END; {Wyswietl_Liste}
Przykład 7.2.
Procedura Wyswietl_Spis wyprowadza spis abonentów telefonicznych, zapisanych w liście. Wyprowadzanie danych na ekran można przerwać po wyświetleniu danych z każdego rekordu.
PROCEDURE Wyswietl_Spis;
VAR
be :POsoba;
stop:BOOLEAN;
z :CHAR;
BEGIN
IF spis.dlugosc=0
THEN
BEGIN
WRITELN('Brak abonentow w spisie');
DELAY(2000)
END
ELSE
BEGIN
be:=spis.pierwszy;
stop:=FALSE;
WHILE NOT ((be=NIL) OR stop) DO
BEGIN
WRITELN('------------------------------------------') ;
WyswietlD(be);
DELAY(2000);
WRITE('dalej ? (T/N) ');
READLN(z);
IF (z IN ['N','n'])
THEN stop:=TRUE
ELSE be:=be^.nast
END
END
END; {WyswietlSpis}
8. Napisać procedurę składującą elementy listy w pliku. Dla uproszczenia można składować w pliku całe rekordy, łącznie z polami wskaźnikowymi, których wartości będą musiały być na nowo ustalane, podczas odtwarzania listy.
Przykład 8.1 Procedura Składuj11 zachowuje w pliku całe rekordy z listy kont. Procedura wykorzystuje znaną długość listy, przechowywaną w nagłówku.
PROCEDURE Skladujl(k:TLista);
VAR
nazwa:STRING;
plik :FILE OF TKontoD;
i :INTEGER;
be :PKonto;
BEGIN
WRITELN('Kopiowanie do pliku');
WRITELN('Podaj nazwe pliku');
READLN(nazwa);
ASSIGN(plik,nazwa);
REWRITE(plik);
be:=k.pierwszy;
FOR i:=1 TO k.dlugosc DO
BEGIN
WRITE(plik,be^);
be:=be^.nast
END;
CLOSE(plik)
END; {Skladujl}
Przykład 8.2.
Wykorzystywanie typu TElement2, jako typu elementu listy, pozwala na składowanie w pliku, wyłącznie danych typu TKonto, bez pól łączników. Realizuje to procedura Skladuj2. Użyty tu typ TLista, musiałby mieć zmienioną definicję, w porównaniu z zamieszczoną powyżej, związaną ze zmianą typu pól łączników na typ wskaźnikowy Element.
PROCEDURĘ Składuj(kolista);
AR
nazwa:STRING;
plik :FILE OF TKonto;
i :INTEGER;
BEGIN
WRITELN('Kopiowanie do pliku');
WRITELN('Podaj nazwe pliku');
READLN(nazwa);
ASSIGN(plik,nazwa);
REWRITE(plik);
be:=kol.pierwszy;
WHILE be<> NIL DO
BEGIN
WRITE(plik,be^.Dane);
be:=be^.nast
END;
CLOSE(plik)
END; {Skladuj2}
9. Napisać procedurę odtwarzającą listę na podstawie pliku.
Przykład 9.1
Odtwarzanie listy rekordów kont typu TKontoD, z pliku utworzonego za pomocą procedury Skladuj1, pokazano poniżej w procedurze Odtworz1, która wykorzystuje procedurę Dolacz_Rek. Procedura Odtworz1 odczytuje liczbę elementów znajdujących się w pliku, w celu odtworzenia długości listy, przechowywanej w nagłówku listy, oraz odtwarza zmienną, pamiętającą maksymalny numer konta, przechowywanego w kartotece.
PROCEDURE Odtworzl(VAR k:TLista);
VAR
plik :FILE OF TKontoD;
odtworzono:BOOLEAN;
nazwa :STRING;
i,nr_bladu:INTEGER;
be :PKonto;
z :CHAR;
BEGIN
REPEAT
odtworzono:=TRUE;
WRITELN('Podaj nazwe pliku');
READLN(nazwa);
ASSIGN(plik,nazwa);
{$I-}
RESET(PLIK);
{$I+}
nr_bladu:=IORESULT;
IF nr_bladu<>0
THEN
BEGIN
WRITELN('BLADWE/WY : ',NR_BLADU);
odtworzono:=FALSE
z:=READKEY;
END;
UNTIL odtworzono;
k.pierwszy:=NIL;
k.ostatni:=NIL;
k.dlugosc:=FILESIZE(plik);
WHILE NOT EOF(plik) DO
BEGIN
NEW(be);
READ(plik,be^);
Dolacz_Rek(be,k);
IF be^.nr_konta>nrkonta
THEN nrkonta:=be^.nr_konta
END
END; {Odtworzl}
Wadą procedury jest to, że nie da się z niej wyjść w przypadku powstania błędu, związanego z niemożnością odczytu danych z pliku. Aby tego uniknąć należy dodać drugi warunek wyjścia z pętli REPEAT, w przypadku błędu, tak jak to zrobiono w procedurze Odtworz2.
Procedurę Odtworz dla kartoteki abonentów, należy zdefiniować podobnie, pomijając elementy, których się nie wykorzystuje dla kartoteki abonentów (np. nrkonta). Ta procura powinna wywołać procedurę Dolacz, zdefiniowaną dla kartoteki kont.
.
Przykład 9.2.
Procedura Odtworz2 współpracuje z plikiem, utworzonym przez procedurę Składuj2. Odtwarza z pliku, zachowane w nim dane z elementów listy, wstawia je do na nowo wygenerowanych elementów i dołącza elementy do listy.
PROCEDURE Odtworz2(VAR k:TLista);
VAR
plik :FILE OF TKonto;
odtworzono:BOOLEAN;
nazwa :STRING;
i,nr_bladu:INTEGER;
be :PElement2;
z :CHAR;
BEGIN
REPEAT
odtworzono:=TRUE;
WRITELN('Podaj nazwe pliku');
READLN(nazwa);
ASSIGN(plik,nazwa);
{ $I-}
RESET(plik);
{$I+}
nr_bladu:=IORESULT;
IF nr_bladu<>0
THEN
BEGIN
WRITELN('BLAD WE/WY:',nr_bladu);
odtworzono:=FALSE;
z:=READKEY;
END
UNTIL odtworzono OR (z IN ['q','Q']);
k.pierwszy:=NIL;
k.ostatni:=NIL;
WHILE NOT EOF(plik) DO
BEGIN
NEW(be);
READ(plik,be^.Dane);
Dolacz_Rek(be,k);
IF be^.nr_konta>nrkonta
THEN nrkonta:=be^.nr_konta
END
END; {Odtworz2}
10. Zmodyfikować blok programu, zakładającego kartotekę danych z ćwiczenia do rozdziału 9 tak, aby zakładał, lub odtwarzał, wyświetlał i składował kartotekę danych, utworzoną z wykorzystaniem struktur listowych.
11. Uruchomić i przetestować program zakładający i wyświetlający listę rekordów kartoteki danych.
. Zadania
1. Zadeklaruj typy wskaźnikowe, związane z typami rekordowymi opisującymi kasetę pracownika, studenta oraz uzupełnij rekordy z zadania 1, w ćwiczeniu z rozdziału 9 o pola, umożliwiające łączenie rekordów w listy, jedno- lub dwukierunkowe.
2. Zadeklaruj zmienne umożliwiające dostęp do list:
a) wskaźnik początku listy kaset wypożyczonych,
wskaźnik końca listy kaset wypożyczonych,
wskaźnik początku listy kaset przechowywanych,
wskaźnik końca listy kaset przechowywanych.
b) tablicę wskaźników początków list pracowników poszczególnych instytutów, o wymiarze 1..Liczba_Instytutów,
c) tablicę nagłówków zawierających dane o początku, końcu i długości listy, dla list studentów w grupach dla wszystkich lat studiów o wymiarach [1..5] [1..Liczba_Grup].
3. Dostosuj procedury wczytywania danych do rekordu i wyświetlania ich na ekranie dla rekordów dynamicznych, wskazywanych przez zmienne wskaźnikowe
4. Napisz procedury dołączania rekordu do listy:
a.) dla kartoteki kaset:
na początek wskazanej listy kaset,
na koniec wskazanej listy kaset, gdy jest dostępny wskaźnik końca listy,
na koniec listy bez znajomości wskaźnika końca listy;
b) pracowników wskazanego instytutu, przy założeniu, że lista ma być uporządkowana alfabetycznie według pola nazwisko;
c) dla kartoteki studentów: do uporządkowanej alfabetycznie wg. nazwiska i imienia listy studentów, określonego roku i określonej grupy,
5. Napisz procedury wyświetlania rekordów ze wskazanej listy:
a) dla kartoteki kaset:
listy kaset;
b) dla kartoteki pracowników:
listy pracowników wybranego instytutu,
list pracowników wszystkich instytutów po kolei, według numeracji instytutów,
c) dla kartoteki studentów:
listy studentów ze wskazanego roku i wskazanej grupy,
listy studentów określonego roku, grupami w kolejności grup,
listy studentów określonego wydziału, latami i grupami, w kolejności wzrastających lat i numerów grup,
5. Napisz program główny, umożliwiający wybór i wykonanie operacji, realizowanych przez procedury z poprzednich zadań.