Wskaźniki. Listy jedno i dwukierunkowe

Sheitar

<font style="font-family: Verdana; font-size: 10pt; font-weight: bold;">Wskaźniki
Listy jedno i dwukierunkowe
</span> <font style="text-align: center; padding: 2px; width: 100%; background-color: #EAEAEA; font-family: Verdana; font-size: 8pt; font-weight: bold;">Spis treści</span>

<font style="text-align: center; padding: 2px; width: 100%; background-color: #EAEAEA; font-family: Verdana; font-size: 8pt; font-weight: bold;">Wstęp do wskaźników</span>
Otóż co to są wskaźniki? To jest poprostu liczba będąca adresem komórki pamięci. Tak więc, jeśli jakaś zmienna jest wskaźnikiem, to (o ile nie zawiera wartości nil) możemy być pewni, że na coś wskazuje (stąd nazwa, proste co nie?). Typ danych, na który wskazuje nam ów pointer może być dowolny, a na dodatek, wskaźnik, podaje nam tylko adres pierwszej komórki pamięci. Jeśli za wskaźnikiem kryje się np. typ Cardinal, to za tym co podaje nam wskaźnik kryją się jeszcze 3 bajty informacji (jak wiemy Cardinal zajmuje 4 bajty). Dlatego też niezbędne jest posiadanie wiedzy o rozmiarze, a najlepiej typie struktury, do której mamy adres. Ponadto za pomocą pointerów możemy wykorzystać wyższą pamięć komputera, a nie tylko tą, która jest przydzielona dla programu. Dla przypomnienia dodam, iż granica ta w Turbo Pascalu wynosiła 64kB. Tak, tylko tyle było miejsca na wszystkie zmienne programu, niezbyt wiele.

rys1.1.gif
Rys 1.1 Organizacja pamięci

Dereferencją nazywamy odwołanie się do elementu wskazywanego. Dla przykładu podam prosty kod, może będzie to łatwiej zrozumieć.

</p>
type
 PCardinal = ^Cardinal; // deklarujemy typ wskaźnikowy
var
 Wskaznik: PCardinal; // używamy typu wskaźnikowego
 Liczba: Cardinal;
begin
 Liczba := 1000; // przypisujemy jakąś wartość początkową
 Wskaznik := @Liczba; // Wskaznik wskazuje teraz na zmienna Liczba
 Wskaznik^ := 2000; // używamy dereferencji
 ShowMessage(IntToStr(Liczba)); // wyskakuje 2000, a nie 1000.
end;

Listing 1.1 Przykładowa dereferencja

Podsumuwując, co dają nam wskaźniki? Możliwość wykorzystania większych obszarów pamięci, a także ciekawy sposób organizacji danych. Jednem z nich są właśnie listy, których rozwinięciem są drzewka.
Jest to taki łopatologiczny opis wskaźników i może nie do końca konkretny, dający jednak z grubsza pojęcie o temacie. W przyszłości rozwinę ten temat.
<font style="text-align: center; padding: 2px; width: 100%; background-color: #EAEAEA; font-family: Verdana; font-size: 8pt; font-weight: bold;">Wstęp do list</span>
Do głowy przychodzi zapewne niektórym pytanie co to są listy? Nie, nie, to nie jest nic związanego z TListBox, ani innymi takimi wynalazkami. Otóż wyobraźcie sobie sznurek z supełkami, który trzymacie za pierwszy supełek. Żeby nie było za łatwo, to do tej scenerii dodajcie kompletne ciemności. Mając poczatek takiego sznurka, możemy bez problemu przejść na kolejny supełek, z tego na następny, z którego z koleji możemy przejść na kolejny itp, aż znajdziemy koniec sznurka. Lista jest własnie takim sznurkiem, a supełki - elementami listy.

rys2.1.gif
Rys 2.1 Uproszczony wygląd listy

Po liście można się poruszać tak długo, dopóki nie trafi się na jej koniec, w przypadku programowym koniec listy to nic innego jak wartość nil w polu Next elementu.
<font style="text-align: center; padding: 2px; width: 100%; background-color: #EAEAEA; font-family: Verdana; font-size: 8pt; font-weight: bold;">Lista jednokierunkowa</span>
Skoro wiadomo już jak wygląda lista, czas coś takiego zaprogramować. Na początek kilka deklaracji.

```delphi type PElement = ^TElement; TElement = record Next: PElement; // wskaźnik na element następny listy Dane: Integer; // tu możemy użyć dowolnego typu danych end;

public
Root: PElement; // potrzebujemy początku listy
Last: PElement; // koniec listy także się przyda
procedure GenerateList(Count: Integer);
procedure DestroyList;

 </td></tr> <tr><td style="border-width: 0px; background-color: #EAEAEA;">  Listing 3.1 <i>Deklaracje</i> </td></tr></table><br>Zmienna <i>Root</i> będzie nam zawsze wskazywała na pierwszy element w liście, natomiast <i>Last</i> na ostatni. Można by się obejść bez tej drugiej zmiennej, jednak za każdym dodaniem do listy, trzeba by przejść przez nią całą, aby odnaleźć ostatni jej element i nowy dopisać na koniec. Lepiej poświęcić te 4 bajty na wskaźnik niż tracić czas procesora, tym bardziej, że gdy lista robi się dłuższa... no sami wiecie. Teraz czas na procedurę tworzącą nam daną ilość losowych elementów i dopisującą te elementy do listy.<br><br><table align="center" border="1" style="border-color: #000000; border-width: 1px; font-family: Verdana; font-size: 8pt;" cellspacing="0"> <tr><td style="border-width: 0px;">  
```delphi
 procedure TForm1.GenerateList(Count: Integer);
 var
  n: Integer;
  NewOne: PElement;
 begin
  Randomize;
  Root := nil;
  Last := nil;
  for n := 1 to Count do
   begin
    New(NewOne);
    NewOne^.Next := nil;
    NewOne^.Dane := Random(10000);
    if Root = nil then
     begin
      Root := NewOne;
      Last := Root;
     end else begin
      Last^.Next := NewOne;
      Last := NewOne;
     end;
   end;
 end; 

Listing 3.2 Procedura generująca listę

Analizując powyższy kod, można zauważyć, że najpierw zmienne Root oraz Last są ustawiane na nil, następnie Count ilość razy przypisywana jest pamięć dla obiektu TElement, jego wartość Next jest ustawiana na nil (z tej samej przyczyny dla której poprzednie dwie zmienne też zostały ustawione na nil, otóż póki nie przypisze się żadanej przez nas wartości to wskaźnik wskazuje na coś, ale te coś jest wybrane poprostu losowo, np. w pamięci siedziała bitmapa, a następnie funkcja New pzydzieliła pamięć należący niegdyś do tej bitmapy, struktura TElement wypełni się wtedy fragmentem tej bitmapy, my tego nie chcemy więc musimy ustawić wartości po swojemu). Następnie jeśli nie mamy jeszcze pierwszego elementu to należy go stworzyć, w przeciwnym wypadku dopisujemy nowy element do końca listy.
Skoro wiemy jak tworzyć listę, przydałoby się wiedzieć jak ją usunąć z pamięci. Przecież zażądaliśmy nowych obszarów pamięci używając New i w dobrym guście byłoby po sobie posprzątać, samo się to nie zrobi.

```delphi procedure TForm1.DestroyList; var ToDelete: PElement; begin while Root < nil do begin ToDelete := Root; Root := Root^.Next; Dispose(ToDelete); end; end;
 </td></tr> <tr><td style="border-width: 0px; background-color: #EAEAEA;">  Listing 3.3 <i>Procedura usuwająca listę</i> </td></tr></table><br>W powżyszym kodzie, przesuwamy się po liście od początku powodując, że każdy kolejny element staje się początkiem, a pierwotnie pierwszy element jest zwalniany. Pętla trwa póki nie dojdziemy do końca listy. Nie jest to chyba specjalnie skomplikowane?Teraz umiemy stworzyć listę, a takżę ją usunąć. Jednak przydałoby się móc obejrzeć jak ta lista wygląda, jakoś ją wyświetlić. Otóż nic prostszego. Z pomocą przyjdzie nam mocno oklepane TMemo. <br><br><table align="center" border="1" style="border-color: #000000; border-width: 1px; font-family: Verdana; font-size: 8pt;" cellspacing="0"> <tr><td style="border-width: 0px;">  
```delphi
procedure TForm1.Button1Click(Sender: TObject);
var
 AtList: PElement;
begin
 Memo1.Lines.BeginUpdate;
 Memo1.Lines.Text := 'START';
 AtList := Root;
 while AtList <> nil do
  begin
   Memo1.Lines.Text := Memo1.Lines.Text + ' -> ' + IntToStr(AtList^.Dane);
   AtList := AtList^.Next;
  end;
 Memo1.Lines.EndUpdate;  
end;

Listing 3.4 Procedura wyświetlająca listę

Pamiętajmy jednak, że zanim ją wyświetlimy, to należałoby ową listę stworzyć, a po jej wyświetleniu, lub gdy nei będzie już potrzebna - usunąć ją.
<font style="text-align: center; padding: 2px; width: 100%; background-color: #EAEAEA; font-family: Verdana; font-size: 8pt; font-weight: bold;">Lista jednokierunkowa samoorganizująca się</span>
Wyrażenie samoorganizująca się może brzmieć jak koszmar, ale nic bardziej mylnego. Taka lista to poprostu lista, która sama się sortuje przy jej tworzeniu. Łatwo jest zauważyć, iż aby posortować listę z poprzedniego rodziału trzeba by ją całą przebudować. Zamiast przebudowywać listę, przebudujemy procedurę ją tworzącą.

```delphi procedure TForm1.GenerateList(Count: Integer); var n: Integer; AtList, NewOne: PElement; begin Randomize; Root := nil; for n := 1 to Count do begin New(NewOne); NewOne^.Next := nil; NewOne^.Dane := Random(10000); if Root = nil then begin Root := NewOne; end else begin if Root^.Dane > NewOne^.Dane then begin NewOne^.Next := Root; Root := NewOne; end else begin AtList := Root; // zaczynamy od początku while (AtList^.Next < nil) and (AtList^.Next^.Dane < NewOne^.Dane) do begin AtList := AtList^.Next; // przechodzimy element wprzód end; if AtList^.Next = nil then begin AtList^.Next := NewOne; end else begin NewOne^.Next := AtList^.Next; AtList^.Next := NewOne; end; end; end; end; end;
 </td></tr> <tr><td style="border-width: 0px; background-color: #EAEAEA;">  Listing 4.1 <i>Procedura tworząca samoorganizującą się listę</i> </td></tr></table><br>Procedura trochę już urosła. Zmienna <i>Last</i> nie jest już tutaj potrzebnam gdyż za każdym dodaniem nowego elementu, lista jest przeszukiwana w poszukiwaniu odpowiedniego miejsca do wstawienia elementu, a nie jest on już dodawany na koniec listy. Zanim zacznę tłumaczyć sposób działania to przedstawię kilka rysunków.<br><br><table align="center" border="1" style="border-color: #000000; border-width: 1px; font-family: Verdana; font-size: 8pt;" cellspacing="0"> <tr><td style="border-width: 0px;">  <img src="{{SITE URL}}bin/rys4.1.gif"> </td></tr> <tr><td style="border-width: 0px; background-color: #EAEAEA;">  Rys 4.1 <i>Lista i nowy element</i> </td></tr></table><br>Otóż mamy już jakąś listę, z posortowanymi elementami. Dodajemy do niej nowy element. Teraz trzeba znaleźć mu miejsce. Warunek <i>Root<sup>.Dane > NewOne</sup>.Dane</i> sprawdza czy wartość pierwszego elementu jest większa od wartości nowego. Jeśli tak to należy wstawić nowy element przed pierwszy. <br><br><table align="center" border="1" style="border-color: #000000; border-width: 1px; font-family: Verdana; font-size: 8pt;" cellspacing="0"> <tr><td style="border-width: 0px;">  <img src="{{SITE URL}}bin/rys4.2.gif"> </td></tr> <tr><td style="border-width: 0px; background-color: #EAEAEA;">  Rys 4.2 <i>Lista i jej nowy pierwszy element (stare połączenie pomalowane na szaro)</i> </td></tr></table><br>Ważna jest tutaj kolejność operacji. Jesli odrazu przypieszemy zmiennej <i>Root</i> adres do nowego elementu utracimy mozliwość wpisania czegokolwiek sensownego do wartości <i>Next</i> nowego elementu. Jeśli powyższy warunek okaże się fałszem wtedy nie ma innego wyjścia jak przejść przez całą listę i znaleźć element, którego następca ma wartość mniejszą od wartości nowego elementu. Stąd warunek <i>(AtList<sup>.Next <> nil) and (AtList</sup>.Next<sup>.Dane < NewOne</sup>.Dane)</i>. Pierwsza część pozwala nam bezpiecznie przemieszczać się po liście aż do końca, druga część przerywa pętlę jeśli zostanie znaleziony odpowiedni element. Po zakończeniu pętli są dwa wyjścia. Pierwsze z nich to trafienie na koniec pętli (warunek <i>AtList^.Next = nil</i>). Wtedy poprostu wstawiamy nowy element na koniec listy.<br><br><table align="center" border="1" style="border-color: #000000; border-width: 1px; font-family: Verdana; font-size: 8pt;" cellspacing="0"> <tr><td style="border-width: 0px;">  <img src="{{SITE URL}}bin/rys4.3.gif"> </td></tr> <tr><td style="border-width: 0px; background-color: #EAEAEA;">  Rys 4.3 <i>Lista i jej nowy, ostatni element (stare połączenie pomalowane na szaro)</i> </td></tr></table><br>Jeśli jednak pętla została przerwana warunkiem mniejszości, oznacza to, że "stoimy" na liście, na elemencie, którego następca ma wartość większą od wartości nowego elementu. Prościej mówiąc, zmienna <i>AtList</i> wskazuje na element, po którym należy wstawić nowy klocek. Tutaj także kolejność operacji przepisywania połączeń jest bardzo ważna i nie wolno się pomylić, ponieważ zniszczy to delikatną ciagłość listy.<br><br><table align="center" border="1" style="border-color: #000000; border-width: 1px; font-family: Verdana; font-size: 8pt;" cellspacing="0"> <tr><td style="border-width: 0px;">  <img src="{{SITE URL}}bin/rys4.4.gif"> </td></tr> <tr><td style="border-width: 0px; background-color: #EAEAEA;">  Rys 4.4 <i>Lista i jej nowy, nie pierwszy i nie ostatni, element (stare połączenie pomalowane na szaro)</i> </td></tr></table><br></div><span style="text-align: center;"> <a name="5"></a> <font style="text-align: center; padding: 2px; width: 100%; background-color: #EAEAEA; font-family: Verdana; font-size: 8pt; font-weight: bold;">Lista dwukierunkowa</span></span><div style="margin: 10px; font-family: Verdana; font-size: 8pt; text-align: justify;">Lista dwukierunkowa to nic innego jak modyfikacja listy jednokierunkowej. Zaletą takiej listy jest możliwość przejścia nie tylko do następnego elementu, ale i także do poprzedniego elementu. Schematycznie taką listę można przedstawić tak jak na poniższym szkicu.<br><br><table align="center" border="1" style="border-color: #000000; border-width: 1px; font-family: Verdana; font-size: 8pt;" cellspacing="0"> <tr><td style="border-width: 0px;">  <img src="{{SITE URL}}bin/rys5.1.gif" /> </td></tr> <tr><td style="border-width: 0px; background-color: #EAEAEA;">  Rys 5.1 <i>Lista dwukierunkowa</i> </td></tr></table><br>Natomiast deklaracja typu zmienia się tylko poprzez dodanie wartości <i>Prev</i><br><br><table align="center" border="1" style="border-color: #000000; border-width: 1px; font-family: Verdana; font-size: 8pt;" cellspacing="0"> <tr><td style="border-width: 0px;">  
```delphi
type
 PElement = ^TElement;
 TElement = record
   Next: PElement; // wskaźnik na element następny listy
   Prev: PElement; // wskaźnik na element poprzedni listy
   Dane: Integer; // tu możemy użyć dowolnego typu danych
 end;

Listing 5.1 Deklaracje

Napiszmy teraz procedurę tworzącą listę dwukierunkową. Wykorzystamy do tego procedurę z poprzedniego rozdziału, dodać nalezy tylko instrukcje tworzące połączenia wsteczne. Ponieważ w takiej liscie mamy możliwość przejścia przez nią od tyłu, warto dodać globalną zmieną Last

```delphi public Last: PElement;

[...]

procedure TForm1.GenerateList(Count: Integer);
var
n: Integer;
AtList, NewOne: PElement;
begin
Randomize;
Root := nil;
for n := 1 to Count do
begin
New(NewOne);
NewOne^.Next := nil;
NewOne^.Prev := nil;
NewOne^.Dane := Random(10000);
if Root = nil then
begin
Root := NewOne;
Last := Root;
end else begin
if Root^.Dane > NewOne^.Dane then
begin
NewOne^.Next := Root;
Root^.Prev := NewOne;
Root := NewOne;
end else begin
AtList := Root;
while (AtList^.Next <> nil) and (AtList^.Next^.Dane < NewOne^.Dane) do
begin
AtList := AtList^.Next;
end;
if AtList^.Next = nil then
begin
AtList^.Next := NewOne;
NewOne^.Prev := AtList;
Last := NewOne;
end else begin
NewOne^.Next := AtList^.Next;
NewOne^.Prev := AtList;
AtList^.Next^.Prev := NewOne;
AtList^.Next := NewOne;
end;
end;
end;
end;
end;

 </td></tr> <tr><td style="border-width: 0px; background-color: #EAEAEA;">  Listing 5.2 <i>Procedura generująca listę dwukierunkową</i> </td></tr></table><br>Teraz możecie spytać jak przejść tą listę od końca? Wystarczy zmodyfikować poprzednią procedurę służącą do wyświetlenia listy.<br><br><table align="center" border="1" style="border-color: #000000; border-width: 1px; font-family: Verdana; font-size: 8pt;" cellspacing="0"> <tr><td style="border-width: 0px;">  
```delphi
procedure TForm1.Button2Click(Sender: TObject);
var
 AtList: PElement;
begin
 Memo1.Lines.BeginUpdate;
 Memo1.Lines.Text := 'END';
 AtList := Last;
 while AtList <> nil do
  begin
   Memo1.Lines.Text := Memo1.Lines.Text + ' -> ' + IntToStr(AtList^.Dane);
   AtList := AtList^.Prev;
  end;
 Memo1.Lines.EndUpdate;
end;

Listing 5.3 Wyświetlanie listy dwukierunkowej od końca

<font style="text-align: right; padding: 2px; width: 100%; background-color: #EAEAEA; font-family: Verdana; font-size: 8pt; font-weight: bold;">19 lipiec 2004</span>

15 komentarzy

@Adam Boduch zjadło obrazki. W kodzie mamy coś takiego <img src="{{SITE URL}}bin/rys2.1.gif" /> i teraz czy tag {{SITE URL}} działa i po prostu zjadło rysunek czy jest to wymysł autora?

I mam problem. Co robię źle bo wyskakuje mi ptzy jednokierunkowej:
[Error] Unit1.pas(80): Illegal character in input file: '&' ($26)
O co chodzi, co zepsułem??

No rysunków to się nie doczekamy, co??

Rysunki to hmm pewnie jakiś błąd w Coyote.

A co z rysuneczkami? Wcięło?

Świetny art!

Ano dlatego, że najpierw (jak dodajesz nowy element) to w ostatnim zapisujesz do niego adres (żeby było wiadomo, że po tym, który jest teraz ostatni, coś będzie) a potem zapisujesz wskaźnik do ostatniego elementu listy. Jeśli zrobiłyś odwrotnie to w ostatnim elemencie listy (przynajmniej w teorii, chodzi o element wskazywany przez Last) jako następny element wskazany byłby... ten sam element. W przypadku przetwarzania element po elemencie i dostania się do tego "ostatniego" mamy martwy punkt (no, może nie taki martwy, to bodajże nieskończona pętla będzie :) ).

A ja mam kilka pytan:
Dlaczego te dwie linijki sa potrzebne:
Last^.Next := NewOne;
Last := NewOne;
I akurat w takiej kolejnosci? Jak kombinowalem wywalalem jedna albo zmienialem kolejnosc to nie dzialalo (dodawal sie i wyswietlal tylko 1 element listy).
Drugie pytanie jak zrobic zgrabne usuwanie elementow z listy, napisalem cos u siebie ale wyszedl mi niezly kolos :(

Fajny artykuł, ale i tak nic z tego ni jarzę :)

Fajny art. Dopracowane formatowanie.

Nie ma nawet co skomentować :). Wreszcie ktoś mi porządnie wytłumaczył listy :)

Świetny artykuł...
Nic dodać nic ująć.

no cycu. art pierwyj sort, i jakie ladne formatowanie...

Rysunki to zasługa Worda 2003 :)
Zastosowanie list... są to swego rodzaju "tablice" a jak wiemy to dość pożyteczny obiekt. Elementów listy może być tyle ile daje nam wynik działania "Wolna_Pamięć div SizeOf(TElement)". Co do zastosowania... głównie proste bazy danych (w bardziej złożonych to drzewka binarne). O ile dostanie się do n-tego elementu listy wymaga trochę szperania, o tyle operacje typu usunięcie 1 czy 100 pozycji ze środka to będzie jedno i to samo, poprostu przepisuje się wiązanie między elementem x, a x+1, na x i x+100. Używając tablic trzeba by przenieść wszystkie dane powyżej x+100 o 100 pozycji w dół, a to już trochę zajmuje.

Art z całą pewnością dopracowany w 100% [ rysuneczki mniam ;) ] .. - połowe z tego znałem wcześniej - jednak nauczyłem się czegoś nowego :) .. Jednak brakuje mi tu kilku przykładów praktycznego zastosowania list... - jeśli masz takowe fajnie by było je tu zamieścic...