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

Podobne dokumenty