Lista cykliczna

Transkrypt

Lista cykliczna
Podstawy Programowania – semestr drugi
Wykład szósty
1.
Dwukierunkowa lista cykliczna
Lista cykliczna jest listą, której elementy tworzą pętlę, a więc nie można wyróżnić w niej elementu początkowego, ani końcowego. Listę tę można utworzyć z listy
jednokierunkowej lub dwukierunkowej. Na jej bazie można tworzyć stosy i kolejki. Ze względu na strukturę tej listy żaden z jej elementów nie zawiera pola
wskaźnikowego, którego wartość wynosiłaby „nil”. Schematycznie można ją przedstawić następująco:
2.
dane
dane
dane
dane
dane
dane
next
next
next
next
next
next
prev
prev
prev
prev
prev
prev
Implementacja listy cyklicznej jako dynamicznej struktury danych
Lista cykliczna, podobnie, jak dwie poprzednie listy zostanie przedstawiona na przykładzie programu, który tworzy listę i umieszcza w niej elementy, tak aby wartości
w nich przechowywane były posortowane niemalejąco. Pojedynczy element tej listy będzie miał taką samą strukturę wewnętrzną jak element listy dwukierunkowej
(wiersze 10 – 14), choć można do budowy tej listy użyć elementów znanych z listy jednokierunkowej. Wybór elementu z dwoma polami wskaźnikowymi,
przechowującymi adresy poprzedniego i następnego elementu w liście, pozwala poruszać się po niej w obu kierunkach, ale ta własność nie będzie wykorzystywana, ani
prezentowana w programie. Musimy również zadeklarować zmienną wskaźnikową, która będzie „pamiętała” adres jednego z elementów listy. Ponieważ lista jest
cykliczna nie ma znaczenia, który z elementów to będzie. W programie zdefiniowane są procedury, które wykonują podstawowe operacje na elementach listy: tworzenie
pierwszego elementu, wstawianie, usuwanie, wyświetlanie zawartości listy i usuwanie listy. Oprócz nich można również zdefiniować inne operacje, jak np.: rozdzielanie
listy na dwie odrębne, według ustalonego kryterium. Pierwszą procedurą zdefiniowaną w programie jest procedura o nazwie „create”. Jej zadaniem jest stworzenie
pierwszego elementu listy cyklicznej. Posiada ona dwa parametry. Przez pierwszy zwracany jest adres utworzonego w procedurze elementu, poprzez drugi jest
pobierana wartość, która ma być umieszczona w tym elemencie. W wierszu 23 programu przydzielana jest pamięć na element listy, a adres początku tej pamięci
umieszczany jest w parametrze „lp”, następnie (wiersz
24) w polu „dana” elementu umieszczana jest wartość,
1 program lista_cykliczna;
którą chcemy zapamiętać. W wierszach 25 i 26
inicjalizowane są pola „next” i „prev” . Początkowo będą
2
one przechowywały ten sam adres, co parametr „lp”, co
3 uses
oznacza,
że
element
będzie
zarówno
swoim
poprzednikiem, jak i następnikiem. Innymi słowy,
4 crt;
procedura „create” tworzy jednoelementową listę,
spełniającą wszystkie założenia listy cyklicznej.
5
6 const ile=2;
Operację wstawiania elementu realizuje procedura
„insert_node”. Posiada ona dwa parametry, które są takie
same jak parametry procedury „create”, z tym że obydwa
są przekazywane przez wartość. Ponieważ nie ma
konieczności modyfikacji wartości zmiennej podstawionej
pod parametr „lp”, przez który przekazywany jest adres
jednego z elementów listy, można zastosować wyżej
opisane rozwiązanie, które upraszcza kod procedury.
Zakładamy, że procedura będzie wstawiała elementy do
listy, tak aby wartości w nich przechowywane były
uporządkowane niemalejąco. Oznacza to, że nowy
element będzie wstawiany przed elementem, którego
wartość jest większa od jego wartości, lub jeśli nie będzie
takiego elementu, to zostanie wstawiony przed
elementem
o
najmniejszej
wartości.
Procedura
„inset_node” nie zajmuje się wyszukiwaniem tych
elementów. Robi to funkcja „find_next”, która również
korzysta z funkcji pomocniczej „find_minimum”. Zdaniem
„find_next” jest znalezienie elementu przed którym
7
8 type
9
wskaznik=^element;
10
element=record
11
dana:integer;
12
next:wskaznik;
13
14
prev:wskaznik;
end;
15
16 var
17 lp:wskaznik;
należy wstawić nowy element, czyli takiego, którego
wartość jest większa od wartości elementu wstawianego
lub którego wartość jest najmniejsza w liście. Celem
ustalenia elementu o minimalnej wartości wywoływana
jest w jej wnętrzu funkcja „find_minimum”. Ta ostatnia
pobiera przez parametr wskaźnik na pewien element
listy i wykonuje algorytm poszukiwania najmniejszej
wartości,
który
omawiany
był
na
wykładach
poświęconych tablicom, więc jego opis tutaj zostanie
pominięty. Rzeczą, na którą warto zwrócić uwagę jest
budowa pętli, która służy do iteracyjnego przeglądania
kolejnych elementów listy. Ponieważ lista cykliczna nie
ma wyróżnionego początku ani, końca i nigdzie w tej
liście nie występuje wartość „nil”, to nie mamy
bezpośredniej wskazówki kiedy zakończyć działanie tej
pętli. Nie możemy również posłużyć się wartościami
przechowywanymi w elementach, ponieważ mogą one być
18 mem:longint;
19 i:byte;
20
21 procedure create(var lp:wskaznik; a:integer);
22 begin
23 new(lp);
24 lp^.dana:=a;
25 lp^.next:=lp;
26 lp^.prev:=lp;
27 end;
1
Podstawy Programowania – semestr drugi
usunięte z listy lub mogą się powtarzać. Aby uniknąć
niekończącej się pętli w wierszu 34 programu, w zmiennej
„start” zapamiętano adres, który początkowo był zapisany
w lp”. W pętli „repeat” ... „until” jego wartość ulega zmianie,
aż ponownie osiągnie wartość początkową. Wtedy należy
zakończyć wykonanie pętli (wiersz 44). Po zakończeniu
pętli, która obejmuje wszystkie elementy na liście,
w zmiennej lokalnej „result” będzie znajdował się adres
elementu listy o najmniejszej wartości, który zostanie
zwrócony jako wynik działania funkcji. Funkcja „find_next”
rozpoczyna swoje działanie od ustalenia adresu
najmniejszego elementu na liście (wiersz 52), a następnie,
w pętli przeszukuje wszystkie elementy listy, celem
znalezienia tego, który ma wartość większą od tej, która
została przekazana jej przez parametr „a” i która zostanie
zapisana do nowego elementu. Po zakończeniu pętli w „lp”
będzie zapisany adres tego elementu, lub elementu
o najmniejszej wartości. Funkcja ta jest wywoływana na
początku działania procedury „insert_node”, tuż po
utworzeniu nowego elementu (wiersz 67). W wyniku
działania tej funkcji w „lp” zostanie zapisany adres
elementu, przed którym należy wstawić nowy element.
W wierszach 67 i 68 nadawane są wartości polom „next”
i „prev” elementu wskazywanego przez wskaźnik „nowy”,
natomiast w wierszach 70 i 71 nadawane są wartości
odpowiednim polom elementu, który będzie poprzedzał
w liście nowy element i elementowi, który będzie następny
po nim. Operacje te przybiegają w ten sam sposób, jak
przebiegały w przypadku listy dwukierunkowej, ale
ponieważ teraz operujemy listą cykliczną, nie trzeba
sprawdzać czy istnieją elementy listy, których pola chcemy
modyfikować.
28
29 function find_minimum(lp:wskaznik):wskaznik;
30 var
31 start,result:wskaznik;
32 minimum:integer;
33 begin
34 start:=lp;
35 minimum:=lp^.dana;
36 result:=lp;
37 repeat
38
if minimum>lp^.dana then
39
begin
40
minimum:=lp^.dana;
41
result:=lp;
42
end;
43
lp:=lp^.next;
44 until lp=start;
45 find_minimum:=result;
46 end;
47
Operacja usuwania elementu z listy cyklicznej jest
oprogramowana w procedurze „delete_node”. Podobnie jak
procedura wstawiania, posiada ona dwa parametry.
Pierwszy parametr przekazywany jest przez zmienną, aby
uniknąć nieprawidłowego „zachowania się” procedury,
kiedy będzie usuwany element, którego adres jest
pamiętany w zmiennej globalnej „lp”. Przez drugi parametr
przekazywana jest wartość, jaką powinien mieć element,
który będzie usunięty. Aby znaleźć ten element procedura
„delete_node” korzysta z funkcji „find_next” (wiersz 79).
Ponieważ funkcja ta zwraca adres albo pierwszego
elementu o wartości większej od tej, która została podana
jej przez parametr „a” lub elementu o najmniejszej wartości,
w procedurze należy wykonać dodatkowe czynności, aby
usunąć właściwy element. Interesujący nas element może
48 function find_next(lp:wskaznik; a:integer):wskaznik;
49 var
50 start:wskaznik;
51 begin
52 lp:=find_minimum(lp);
53 start:=lp;
54 repeat
55
if lp^.dana>a then break;
56
lp:=lp^.next;
znajdować się przed elementem, którego adres zwróciła
funkcja „find_next”. Musimy więc „cofnąć się” o jeden
element, czego dokonujemy w wierszu 80 procedury. To
jednak nie wszystko, jeśli elementu o zadanej wartości nie
ma na liście, to „find_next”, albo zwróci adres elementu
o najmniejszej wartości, albo adres elementu o wartości
większej. Należy więc sprawdzić, czy element wskazywany
teraz przez „lp” jest właściwym elementem do usunięcia.
Możemy to zrobić sprawdzając wartość pola „dana” w tym
elemencie. Jeśli jest ona równa wartości parametru „a”,
wtedy element usuwany, w przeciwnym przypadku
kończymy działanie procedury. Czynność usuwania
(wiersze 83 – 87) wykonywana jest w podobny sposób, jak
miało to miejsce w przypadku listy dwukierunkowej, ale nie
musimy badać dodatkowych warunków. Wiersze 83 i 87
zapewniają, że po zakończeniu procedury zmienna globalna
„lp” nie będzie zawierała adresu elementu, który został już
zwolniony.
Procedura
„delete_node”
powinna
być
wywoływana dla listy, która ma co najmniej dwa elementy.
Warunek, czy lista istnieje nie jest sprawdzany przez
procedurę, natomiast w wierszy 78 następuje sprawdzenie,
czy lista ma więcej niż jeden element.
57 until lp=start;
58 find_next:=lp;
59 end;
60
61 procedure insert_node(lp:wskaznik; a:integer);
62 var
63
nowy:wskaznik;
64 begin
65 new(nowy);
66 nowy^.dana:=a;
67 lp:=find_next(lp,a);
68 nowy^.next:=lp;
69 nowy^.prev:=lp^.prev;
70 lp^.prev^.next:=nowy;
71 lp^.prev:=nowy;
72 end;
2
Podstawy Programowania – semestr drugi
73
Procedura „print” odpowiedzialna jest za wypisanie
wartości wszystkich elementów na ekran. Dodatkowy
parametr procedury określa ile razy zawartość listy
powinna być wypisana. Procedura wypisuje wartości
elementów listy począwszy od elementu o najmniejszej
wartości. Jeśli usuniemy z jej kodu, bądź ujmiemy
w komentarz wiersz 95, to będzie wypisywać wartości
począwszy od elementu, którego adres w danej chwili
przechowuje zmienna globalna „lp”. Ilekroć w pętli
wskaźnik „lp” osiągnie wartość, którą miał na początku
procedura umieszcza kursor w nowym wierszu i zmniejsza
wartość parametru „n”, który pełni rolę licznika pętli.
W momencie, kiedy „n” osiągnie wartość zero kończone jest
wykonanie pętli. Listę utworzoną w programie można
wyświetlać również w odwrotnej kolejności, ale nie jest to
realizowane w tej procedurze.
74 procedure delete_node(var lp:wskaznik; a:integer);
75 var
76 tmp:wskaznik;
77 begin
78 if lp=lp^.next then exit;
79 lp:=find_next(lp,a);
80 lp:=lp^.prev;
81 if lp^.dana=a then
82 begin
83
tmp:=lp^.next;
84
lp^.prev^.next:=lp^.next;
85
lp^.next^.prev:=lp^.prev;
86
dispose(lp);
87
lp:=tmp;
Wszystkie elementy z listy cyklicznej można usunąć przy
pomocy procedury „remove”. Jej kod jest podobny do kodu
jej
odpowiedniczek
dla
list
jednokierunkowych
i dwukierunkowych. Różni się od nich warunkiem
zakończenia pętli, który jest właściwy dla listy cyklicznej
i który był opisywany wcześniej. Można się zastanowić, czy
ta pętla jest napisana poprawnie, skoro jako warunek jej
zakończenia przyjmujemy zdarzenie, kiedy wskaźnik „lp”
otrzyma wartość będącą adresem elementu, który już
został zwolniony. Odpowiedź jest twierdząca, wprawdzie
wykorzystujemy wskaźnik na nieistniejący element, ale
nie sięgamy do pamięci, którą on wskazuje. Dane
w zwolnionym elemencie mogą ulec zatarciu, natomiast
adres tego elementu, który dla bezpieczeństwa jest
zapisywany w zmiennej pomocniczej „start”, nie.
88 end;
89 end;
90
91 procedure print(lp:wskaznik; n:byte);
92 var
93 start:wskaznik;
W bloku głównym programu tworzymy listę i dodajemy do
niej
elementy.
Kilkukrotne
wywołanie
procedur
„insert_node” i „delete_node”, dla różnych wartości
wstawianych, ma na celu, podobnie jak to miało miejsce
w przypadku programów ilustrujących działanie list
jednokierunkowych i dwukierunkowych, empiryczne
pokazanie, że te procedury poprawnie działają.
W stosunku
do
poprzednich
programów
został
zmodyfikowany
mechanizm
kontroli
zarządzania
pamięcią. Zamiast wypisywać na początku i na końcu
działania
programu
ilość
dostępnej
pamięci,
zapamiętujemy jej wartość początkową w zmiennej „mem”,
a następnie pod koniec odejmujemy od niej wartość, którą
zwróci funkcja „memavail”. W wyniku tej operacji
powinniśmy otrzymać wartość zero, jeśli nie, to znaczy, że
w nieprawidłowy sposób przydzielamy lub zwalniamy
pamięć.
94 begin
95 lp:=find_minimum(lp);
96 start:=lp;
97 repeat
98
write(lp^.dana,#32);
99
lp:=lp^.next;
100
if lp=start then
101
begin
102
writeln;
103
dec(n);
104
end;
105 until n=0;
106 end;
107
108 procedure remove(var lp:wskaznik);
109 var
110 start,tmp:wskaznik;
111 begin
112
start:=lp;
113
repeat
114
tmp:=lp^.next;
115
dispose(lp);
116
117
lp:=tmp;
until lp=start;
3
Podstawy Programowania – semestr drugi
118 end;
119
120 begin
121 clrscr;
122 mem:=memavail;
123 create(lp,1);
124 for i:=2 to 5 do insert_node(lp,i);
125 for i:=10 to 15 do insert_node(lp,i);
126 print(lp,ile);
127 insert_node(lp,7);
128 print(lp,ile);
129 insert_node(lp,16);
130 print(lp,ile);
131 insert_node(lp,0);
132 print(lp,ile);
133 delete_node(lp,10);
134 print(lp,ile);
135 delete_node(lp,15);
136 print(lp,ile);
137 delete_node(lp,16);
138 print(lp,ile);
139 delete_node(lp,1);
140 print(lp,ile);
141 delete_node(lp,0);
142 print(lp,ile);
143 delete_node(lp,1);
144 print(lp,ile);
145 remove(lp);
146 if mem-memavail<>0 then writeln('Błąd zarządzania pamięcią!');
147 readln;
148 end.
3.
Zastosowania listy cyklicznej
Jak wspomniano wyżej lista cykliczna może posłużyć do budowy stosu lub kolejki. Wymaga to zadeklarowania dodatkowych zmiennych pamiętających element
znajdujący się na szczycie stosu, bądź pierwszy i ostatni element kolejki. Można również napisać implementację tej listy opartą na tablicy, na wzór implementacji
kolejki, która była opisywana w materiałach do drugiego wykładu. Listy cykliczne są stosowane w postaci zarówno sprzętowej, jak i programowej. Korzystają z nich np.:
niektóre algorytmy szeregowania zadań oraz buforowania i wymiany stron w systemach operacyjnych. W książce Donalda Edwina Knutha „Sztuka programowania”
tom pierwszy, znajduje się opis algorytmu wykonującego operacje na wielomianach, które są reprezentowane za pomocą listy cyklicznej. W opisywanej tam liście
cyklicznej umieszczony jest element – atrapa, którego zadaniem jest oznaczenie „punktu startowego” listy.
4.
Uwagi odnośnie programu
Korzystając z procedur „delete_node” i „insert_node” należy zadbać, aby przekazywany im wskaźnik na listę nie był wskaźnikiem pustym, innymi słowy lista do której
będą te procedury wstawiać lub z której będą usuwać elementy nie może być listą pustą. Ani procedura „insert_node”, ani procedura „create” nie sprawdzają, czy jest
wystarczająco dużo pamięci dostępnej, żeby móc ją przydzielić nowemu elementowi, optymistycznie zakładają, że będzie na tyle wolnej pamięci1. Należy również
zadbać, aby do procedury „print” jako wartość drugiego parametru nie zostało przekazane 0, inaczej zadziała ona w sposób nieprawidłowy. Algorytm działania
procedury „delete_node” można znacznie uprościć implementując w niej bezpośrednie wyszukiwanie elementu, który ma być zwolniony, jednak wtedy rezygnujemy
z przykładu ponownego użycia kodu, jakim jest zastosowanie funkcji „find_next” :-)
1
Ten warunek nie jest sprawdzany też w programach, które były prezentowane na wcześniejszych wykładach, ani nie będzie testowany w programach zaprezentowanych
w przyszłości.
4

Podobne dokumenty