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