Podstawy Programowania – semestr drugi Wyk ad pi ty ł ą 1
Transkrypt
Podstawy Programowania – semestr drugi Wyk ad pi ty ł ą 1
Podstawy Programowania – semestr drugi Wykład piąty 1. Dwukierunkowa lista liniowa Dwukierunkowa lista liniowa różni się w niewielkim stopniu od jednokierunkowej listy liniowej. Zasadnicza różnica w funkcjonalności polega na tym, że elementy listy dwukierunkowej możemy przeglądać zarówno od początku do końca, jak i od końca do początku. W przypadku listy jednokierunkowej, jak sama nazwa wskazuje, przeglądanie możliwe jest tylko od początku do końca. 2. Implementacja listy dwukierunkowej jako dynamicznej struktury danych Lista dwukierunkowa, podobnie jak jej poprzedniczka zostanie opisana na przykładzie programu, który wstawia do niej kolejne elementy, tak aby wartości, które one przechowują znajdowały się w porządku niemalejącym. Definicja typu pojedynczego elementu listy dwukierunkowej (inaczej: typu bazowego listy) zawiera dwa pola wskaźnikowe (wiersze 5-9). Pole „next” przeznaczone jest na zapamiętanie adresu następnego elementu w liście, pole „prev” będzie służyło do przechowywania adresu poprzedniego elementu. Wartość tego pola dla pierwszego elementu na liście będzie wynosiła „NIL”, podobnie jak wartość pola „next” ostatniego elementu listy. Tak jak 1 w przypadku jednokierunkowej listy potrzebujemy tylko jednej globalnej zmiennej wskaźnikowej, która będzie przechowywała adres pierwszego elementu listy (wiersz 11). Pierwszy element listy jest tworzony przy pomocy procedury „create”. Może ona również służyć do wstawiania nowych elementów do listy, gdyż wywołuje procedurę „insert_node”, realizującą tę operację. Ta ostatnia różni się od swojej odpowiedniczki dla listy jednokierunkowej, tym że w jej kodzie zawarte są operacje związane z manipulacją zawartością pola „prev” elementu i że nie potrzebuje dodatkowej zmiennej lokalnej, która pamiętałaby wskaźnik na poprzedni element. Procedura ta ma dwa parametry. Przez pierwszy pobiera wskaźnik pierwszego elementu listy, natomiast poprzez drugi pobiera wartość, którą należy umieścić w elemencie. Posiada ona również dwie lokalne zmienne wskaźnikowe. W wierszach (18 – 21) tworzony jest i inicjalizowany nowy element listy, z uwzględnieniem pola „prev”, któremu należy nadać wartość początkową „NIL”. Jeśli warunek zawarty w wierszu 22, w instrukcji warunkowej jest prawdziwy, to oznacza to, że nowy element należy umieścić na początku listy. Wymaga to wykonania dwóch czynności. Najpierw w polu „next” nowego elementu zapamiętywany jest wskaźnik na element listy, który dotychczas był jej pierwszym elementem. Następnie w polu „prev” tego elementu listy należy zapamiętać adres nowego elementu (wiersz 25). Po wykonaniu tych operacji pozostaje jeszcze uczynić nowy element pierwszym elementem listy (wiersz 26) i zakończyć działanie procedury. Częściej się jednak zdarza, że musimy 1 program doubly_linked_list; nowy element wstawić nie na początku listy, lecz w środku lub na końcu. Ponieważ założyliśmy, że elementy w liście powinny być 2 uses crt; posortowane niemalejąco pod względem przechowywanych przez nie wartości, wystarczy znaleźć element przechowujący wartość większą, od 3 type tej, którą chcemy umieścić w liście i wstawić nowy element przed tym 4 wskaznik=^element; elementem. Poszukiwania tego elementu przeprowadzamy przy pomocy pętli „repeat” ... „until” (wiersze 30 - 38). Może się zdarzyć, że na liście 5 element = record nie będzie elementu o większej wartości i wtedy nowy element powinien zostać dodany do końca listy. Jest to wykonywane w wierszach 33 – 34. 6 dana:integer; Przebieg tej operacji jest podobny do tej samej operacji dla listy 7 next:wskaznik; jednokierunkowej, ale uwzględnia zapamiętanie w polu „prev” nowego elementu adresu bieżącego ostatniego elementu listy (34). Również 8 prev:wskaznik; w tym wypadku możemy zakończyć działanie procedury. Jeśli jednak w wyniku wykonania pętli znajdziemy element, przed którym należy 9 end; wstawić nowy element, to wykonywany jest kod, którego działanie 10 var można zilustrować rysunkiem: 11 first:wskaznik; 12 i:integer; 1. Przed wykonaniem wiersza 39 13 dana 14 procedure insert_node(var f:wskaznik; a:integer); nowy NIL 15 var NIL p 16 p,nowy:wskaznik; 17 begin 18 new(nowy); 19 nowy^.dana:=a; 20 nowy^.prev:=nil; 21 nowy^.next:=nil; 22 if f^.dana > a then 23 1 dana next next prev prev 2. Po wykonaniu wiersza 39 dana nowy^.next:=f; 25 f^.prev:=nowy; 26 f:=f^.prev; 27 exit; nowy NIL begin 24 28 dana prev dana p:=f; 30 repeat next prev prev Jest to tylko pewna przyjęta konwencja. W przypadku listy dwukierunkowej nie ma znaczenia, na który element tej listy wskazuje ta zmienna. 1 dana next end; 29 p Podstawy Programowania – semestr drugi 31 if p^.next=nil then 32 begin 33 p^.next:=nowy; 34 nowy^.prev:=p; 35 exit; 36 end; 37 dana until p^.dana > a; 39 nowy^.prev:=p^.prev; 40 nowy^.next:=p; 41 p^.prev^.next:=nowy; nowy next prev p dana dana p:=p^.next; 38 42 3. Po wykonaniu wiersza 40 next next prev prev 4. Po wykonaniu wiersza 41 dana nowy next p^.prev:=nowy; prev p 43 end; 44 46 var 47 new(nowy); 52 nowy^.dana:=a; 53 nowy^.next:=nil; 54 nowy^.prev:=nil; 55 f:=nowy; 56 5. Po wykonaniu wiersza 42 dana prev dana else insert_node(f,a); 60 61 procedure show_both(f:wskaznik); 62 var 63 p:wskaznik; 64 begin 2 p dana next next prev prev Po zakończeniu pętli „repeat” zmienna wskaźnikowa „p” wskazuje element listy, przed którym należy umieścić nowy element. W wierszach 39 i 40 przypisujemy odpowiednio polu „prev” i polu „next” nowego elementu adres elementu poprzedzającego element wskazywany przez „p” i adres elementu wskazywanego przez „p”. W wierszu 41 zapamiętujemy w polu „next” elementu poprzedzającego element wskazywany przez „p” adres nowego elementu. W wierszu 42, w polu „prev” elementu wskazywanego przez „p” również umieszczamy adres nowego elementu. Po tych operacjach otrzymujemy konfigurację przedstawioną na dole ilustracji. 59 end; Procedura „delete_node” realizuje operację usuwania elementu z listy. Pisząc tego rodzaju procedurę należy uwzględnić trzy przypadki. Można oczywiście wyróżnić czwarty przypadek, w którym w liście nie ma elementu, który chcemy usunąć, ale wówczas procedura po prostu nie wykonuje żadnej czynności. W przypadku pierwszym elementem usuwanym będzie element znajdujący się na początku listy. Usunięcie go jest dokonywane w wierszach 96 -104. W wierszu 98 zapamiętujemy w zmiennej „tmp” adres elementu, który jest jego następnikiem, po czym w wierszu 99 zwalniamy pamięć przeznaczoną na pierwszy element. Pierwszym elementem listy zostaje element, którego adres pamiętamy w zmiennej „tmp”. Przepisujemy więc jego adres do zmiennej „p”, ustawiamy wartość jego pola „prev” na „NIL” i modyfikujemy wskaźnik na pierwszy 2 element listy („f”), tak, aby wskazywał na niego . Po wykonaniu tych czynności możemy zakończyć działanie procedury. Obsługa drugiego przypadku jest powiązana z obsługą trzeciego. W wierszach 105 – 108 przeszukujemy iteracyjnie listę szukając elementu, który przechowywałby zadaną wartość – ten element będziemy chcieli while f<> nil do 66 begin 67 p:=f; 68 write(f^.dana:3); 69 f:=f^.next; 70 end; 71 writeln; 72 while p<>nil do 74 nowy next end 58 73 prev begin 51 65 next prev if f=nil then 50 57 next nowy:wskaznik; 48 begin 49 dana dana 45 procedure create(var f:wskaznik; a:integer); begin write(p^.dana:3); Adres pierwszego elementu listy zawiera parametr „f” procedury, natomiast zmienna lokalna „p” jest tylko zmienną pomocniczą. 2 Podstawy Programowania – semestr drugi 75 usunąć. Może się okazać, że takiego elementu nie będzie na liście, wówczas po prostu kończymy wykonanie procedury (wiersz 107). W przeciwnym przypadku po zakończeniu pętli wskaźnik „p” będzie wskazywał na element przeznaczony do usunięcia. Wykonywane w tym wypadku operacje można zilustrować rysunkiem: p:=p^.prev; 76 end; 77 writeln; 78 end; 1. Przed wykonaniem wiersza 109 79 p 80 procedure remove(var f:wskaznik); 81 var 82 tmp:wskaznik; 83 begin dana dana dana next next next prev prev prev 84 while f<>nil do 2. Po wykonaniu wiersza 109 85 begin p 86 tmp:=f^.next; 87 dispose(f); 88 f:=tmp; 89 end; dana dana dana next next next prev prev prev 90 end; 91 procedure delete_node(var f:wskaznik; a:integer); 3. Po wykonaniu wiersza 110 92 var p 93 tmp,p:wskaznik; 94 begin 95 p:=f; dana dana dana next next next prev prev prev 96 if p^.dana=a then 97 begin 98 tmp:=p^.next; 99 dispose(p); 100 p:=tmp; 101 p^.prev:=nil; 4. Po wykonaniu wiersza 111 102 f:=p; 103 exit; 104 end; 105 repeat 106 p:=p^.next; 107 if p=nil then exit; 108 until p^.dana=a; 109 if p^.next<>nil then p^.next^.prev:=p^.prev; 110 p^.prev^.next:=p^.next; 111 dispose(p); p 113 114 115 begin clrscr; 117 writeln(MemAvail); 118 for i:=1 to 5 do create(first,i); 119 show_both(first); dana next next next prev prev prev Rysunek nie ilustruje przypadku, który jest uwzględniony w wierszu 109. Może się zdarzyć, że będziemy usuwać ostatni element z listy, wówczas odwołanie „p^.next^.prev” byłoby nieprawidłowe i mogło spowodować błędne działanie programu. Przed jego wykonaniem należy więc sprawdzić, czy istnieje następny element względem tego, który wskazuje zmienna „p”. Jeśli tak, to jego polu „prev” należy przypisać adres elementu poprzedzającego element wskazywany przez „p”, jeśli nie to nic nie musimy robić. W kolejnym wierszu, polu „next” elementu poprzedzającego element wskazywany przez „p” przypisujemy adres elementu następnego po elemencie wskazywanym przez „p” (jeśli taki element nie istnieje, to pole to będzie miało nadaną wartość „NIL”). Grube kreski przekreślające usuwany element na rysunku symbolizują zwalnianie przeznaczonej dla niego pamięci. Dane które się w niej znajdują nie ulegają od razu zniszczeniu, niemniej nie należ się już do nich odwoływać. Procedura usuwania wszystkich elementów z listy dwukierunkowej jest taka sama jak w programie używającym listy jednokierunkowej i nie będzie tutaj opisywana. Procedura „show_both” wyświetla zawartość elementów listy na dwa sposoby : najpierw począwszy od pierwszego elementu, a potem począwszy od ostatniego elementu. Pierwsza pętla „while” (wiersze 65 -70) realizuje pierwszy etap działania tej procedury. Po jej zakończeniu zmienna 112 end; 116 dana dana 3 Podstawy Programowania – semestr drugi 120 for i:=10 to 15 do insert_node(first,i); 121 show_both(first); 122 insert_node(first,7); 123 show_both(first); 124 insert_node(first,16); 125 show_both(first); 126 insert_node(first,0); 127 show_both(first); 128 delete_node(first,0); 129 show_both(first); 130 delete_node(first,4); 131 show_both(first); 132 delete_node(first,15); 133 show_both(first); 134 remove(first); 135 writeln(MemAvail); 136 readln; pomocnicza „p” zawiera adres ostatniego elementu na liście. Kolejna pętla „while” (wiersze 72 – 76) realizuje wypisanie zawartości elementów listy w porządku odwrotnym (od ostatniego do pierwszego elementu). W bloku głównym programu, podobnie jak w przypadku programu korzystającego z listy jednokierunkowej tworzona jest lista zawierająca elementy o wartościach od 1 do 5, następnie dodawane są do niej elementy o wartościach od 10 do 15 przy pomocy procedury „insert_node”, a następnie za pomocą tej samej procedury dodawane są pojedyncze elementy o wartościach odpowiednio 7, 16,0. Po każdej takiej operacji wstawienia wypisywana jest na ekran zawartość listy. W dalszej części programu usuwane są z listy elementy o wartościach 0,4,15, (tu również po każdej operacji następuje wypisanie zawartości listy na ekran), po czym cała lista jest usuwana. 137 end. 3. Uwagi końcowe Podobnie jak listę jednokierunkową listę dwukierunkową można zaimplementować w oparciu tablicę, jednak ta forma realizacji listy dwukierunkowej nie będzie tutaj 3 omawiana . Zarówno stos, jak i kolejka mogą być oparte o listę dwukierunkową, lecz zazwyczaj te terminy oznaczają struktury oparte o listę jednokierunkową. Czas 4 wykonania operacji wstawiania i usuwania elementu z listy (po tym jak ten element znajdziemy) jest stały, niezależny od liczby elementów w liście , natomiast czas 5 wyszukania pojedynczego elementu jest wprost proporcjonalny do liczby elementów listy . Odwrotnie sytuacja wygląda w przypadku tablicy. Tablica charakteryzuje się stałym czasem dostępu do elementu, natomiast wstawianie (o ile jest wystarczająca ilość miejsca w tablicy) i usuwanie elementów z tablicy jest proporcjonalne do ich liczby. Tak więc tablica jest strukturą danych o dostępie swobodnym (bezpośrednim), natomiast listy są strukturami o dostępie sekwencyjnym. W procedurach „insert_node” (wiersz 41) oraz „delete_node” (wiersz 110) występują dość złożone odwołania do elementów. Okazuje się jednak, że odwołania te mogą być jeszcze bardziej skomplikowane. Rozważmy następującą listę dwukierunkową: p 5 4 9 1 3 2 next next next next next NIL NIL prev prev prev prev prev Wskaźnik „p” wskazuje na czwarty element tej listy. Wyrażenie p^.next^.next^.dana można przeczytać następująco: uzyskaj adres elementu następnego względem elementu wskazywanego przez „p”, następnie z tego elementu uzyskaj adres kolejnego po nim elementu i z tego elementu uzyskaj wartość zapisaną w polu „dana”. Tą wartością oczywiście będzie „2”. Podobne wyrażenia możemy budować wykorzystując wskaźnik „prev”, np.: p^.prev^.prev^.prev^.dana. To wyrażenie będzie miało wartość „5”. Można oczywiście w wyrażeniu użyć obu wskaźników równocześnie: p^.next^.next^.prev^.prev^.prev^.prev^.dana. Ostatnie wyrażenie ma wartość: „4”. Po uważnej analizie tego wyrażenia można dojść do wniosku, że można je uprościć, otrzymując taką samą wartość. Wystarczy zauważyć, że pola „prev” i „next” wzajemnie „znoszą się”. Wniosek końcowy: należy zrozumieć w jaki sposób się buduje i jak działają tak rozbudowane wyrażenia, po czym nigdy ich nie stosować w programach :-) 3 4 5 Nie jest ona tak wygodna jak implementacja w postaci struktury dynamicznej Używając notacji asymptotycznej możemy napisać, że jest on równy O(1) Podobnie jak wcześniej możemy wyrazić to za pomocą notacji asymptotycznej: O(n), gdzie „n” to liczba elementów na liście. 4