Implementace seznamů do prostředí DELPHI pomocí lineárního seznamu Ukazatel a dynamické datové struktury v prostředí DELPHI Důležitým termínem a konstrukčním programovým prvkem je typ UKAZATEL. Je to vlastně datový typ proměnné, která ukazuje na jinou proměnnou, která může být několika typů – integer, char, string, nebo i rekord. Vlastní obsah proměnné typu ukazatel je místo v paměti, kde se nalézá konkrétní informace, na kterou ukazatel ukazuje. Při tvorbě programu nemáme přímý přístup k adrese v paměti (k čemu taky??), můžeme pouze vytvářet (příkazem NEW), přiřazovat, nebo mazat (příkazem DISPOSE). Můžeme však dosti rozmanitě pracovat s proměnnou na kterou tento datový typ UKAZATEL ukazuje. Každému ukazateli je nejdříve nutno před jeho použitím vytvořit místo v paměti. Tuto operaci provedeme pomocí příkazu NEW. Ten vybere náhodné nevyužité místo v paměti a dosadí do ukazatele jeho adresu. Při ukončení práce s programem je vhodné tuto náhodně vybranou paměť opět uvolnit pro jiné účely. To se provede pomocí příkazu DISPOSE. Pokud se toto neprovede, lze očekávat, že některé složitější programy zvláště na méně výkonných počítačích budou kolabovat a blokovat se, jelikož paměť vyhrazena pro běh programu bude zanesena množstvím nezrušených ukazatelů a hodnot v paměti. Ukazatel se definuje pomocí znaku ^ + typ proměnné na který takto zadeklamovaný ukazatel ukazuje. Hlavní a v podstatě téměř jediné využití datového typu ukazatel je při práci s dynamickými datovými strukturami. To znamená při práci s proměnlivým množstvím normálních proměnných. Výhody tohoto datového uspořádání jsou ty, že množství hodnot se může při běhu programu měnit. Většinou se jedná o nějaký záznam (RECORD), který by měl obsahovat alespoň jeden UKAZATEL na tentýž záznam. Při definování takovýchto typů zjišťujeme, že jeden typ proměnné je definován druhým typem – využíváme tedy princip rekurze. Tento typ dynamické proměnné se tak skládá ze dvou složek, z nichž jedna je hodnotová část, nesoucí údaj o hodnotě a druhá složka je adresní část, nesoucí údaje o adrese, většinou na svého následovníka a předchůdce. Tento princip se neustále opakuje. Jedno z nejjednodušších provedení tohoto příkazu je při tvorbě jednosměrného seznamu. Pod pojmem seznam rozumíme takovou dynamickou datovou strukturu, která umožňuje odebírat nebo přidávat libovolný prvek na kterékoliv pozici. Seznam si můžeme představit jako posloupnost prvků, kde vazba určuje následníka, případně předchůdce, vždy však nejvýše jednoho. Ve většině programů se zavádí ukazatel na začátek seznamu (ČELO, HLAVA apod.…) a na konec seznamu, teda na poslední obsazený prvek. (KONEC, POSLEDNÍ…). Při programové tvorbě seznamu využíváme většinou tyto operace pro manipulaci s prvky: • vytvoření prázdného seznamu • přidání nového prvku na konec seznamu • odebrání prvku z čela seznamu • test prázdnosti seznamu • odstranění libovolného prvku ze seznamu • vložení nového prvku na libovolné místo v seznamu
1
V programovém prostředí DELPHI by toto provedení mohlo vypadat následovně:
Operace vytvoření prázdného seznamu a vložení prvku na první pozici seznamu - zde operace VYTVOR a PRIDEJ: Procedure Vytvor (var celo:spoj); Begin celo:=nil; end; Procedure Pridej (var celo,konec:spoj;H:Thodnota); Var P:spoj; Begin NEW(P); P^.hodnota:=H; P^.dalsi:=NIL; if celo=nil then celo:=p else konec^.dalsi:=p; konec:=P; end;
//první vložená hodnota //ostatní vložené hodnoty //nový konec struktury}
Tato konstrukce je obecná a níže je kód použitý v programu implementace ADT: procedure MujCREATE2; begin P:=nil end;
Tento krok je klasické vytvoření prázdného seznamu, následuje fragment kódu pro vložení prvku s vlastnostmi načtenými z komponenty EDIT: procedure MujINSERT2(zb: Zbozi); var r: Pvag; begin new(r); // nový prvek r^.z:=zb; // naplníme zbozim r^.dalsi:=p; // připojíme k p p:=r; // p nastavíme na začátek end; var novy: Zbozi; //deklarace proměnné begin if (Edit1.Text<>'') and (Edit2.Text<>'') and (Edit3.Text<>'') and (Edit4.Text<>'') and (Edit5.Text<>'') then // musí být vyplněny všechny položky jinak vrátí chybu begin novy.Druh:=Edit1.Text; //načítání údajů z komponent EDIT (1-5) novy.Mnozstvi:=StrToInt(Edit2.Text); //převedení řetězce na číslo novy.Obchod:=Edit3.Text; novy.MaxCena:=StrToInt(Edit4.Text); novy.DatumNakupu:=Edit5.Text; MujINSERT2(novy); Edit1.Text:=''; Edit2.Text:=''; Edit3.Text:=''; Edit4.Text:='';
2
Edit5.Text:=''; end else begin Memo1.Lines.Add(''); Memo1.Lines.Add('Prosím, vyplňte všechny údaje !!'); nevyplnění všech polí end;
//hlášení při
Postup pro vložení nového prvku do již existujícího seznamu prvků Pokud potřebujeme vložit nový prvek na konec seznamu můžeme použít buď jednoduchou konstrukci tohoto typu: New(P); //prosté zařazení prvku na konec seznamu, jednoduché P^.hodn := H; řešení, které ale ne vždy vyhovuje P^.dalsi := NIL; Konec:=konec^.dalsi;
Nebo můžeme znovu zavolat výše uvedenou proceduru PRIDEJ. Pro zjednodušení grafická podoba nového seznamu:
X1,x2,x3,…. Hodnotová část Adr.na x2… Adresní část Tato procedury slouží pro vložení prvku na konec již existujícího seznamu. Pokud chceme vložit nový prvek na začátek seznamu, tzn. před prvek na který ukazuje předchozí jeho prvek v seznamu, v našem případě tedy hned za CELO a před prvek na který CELO ukazuje, mohla by být konstrukce takováto: New(P); P^.hodn := H; P^.dalsi := Celo; Celo:= P; P:= NIL;
3
Pokud chceme vkládat prvek na přímo určené místo v seznamu, musíme zavést jakousi značku, která označí ono místo podle předem daných kriterii, které si stanovíme. Nechť je pomocná značka označena jako POM, pak: New(P); P^.hodn := H; P^.dalsi := pom^.dalsi; Pom^.dalsi := P;
Zde jsme prvek vložili za hodnotu x2, tzn. ukazatel POM byl nastaven na prvek X1. Při vkládání prvku před ukazatel POM je situace poněkud komplikovanější neboť potřebujeme znát adresu prvku předcházejícího, proto bychom měli zadeklamovat a umístit pomocný ukazatel - zde PŘED.Konstrukce pro vložení prvku před ukazatele POM by mohla nyní vypadat následovně: Pred := Celo; While Pred^.dalsi <> Pom DO Pred := Pred^.dalsi; New(P); P^.hodnota := H; P^.dalsi := Pom; Pred^.dalsi := P; Pred := NIL; P:= NIL;
Prvek, jehož hodnota je rovna 5, byl nalezen příslušnou vyhledávací funkcí. Písmenem u je vyjádřen počátek seznamu, písmeno w označuje vkládaný prvek. Hledání prvku obsahuje porovnání čteného čísla s hodnotou aktuálního prvku. Pokud je hodnota prvku menší než přečtené číslo, postupujeme v lineárním seznamu k dalšímu prvku. Nalezená hodnota je tedy ta, která je poslední menší než přečtené číslo a za tuto hodnotu vložíme nový prvek.
Odstranění prvku ze seznamu Odstranění prvního prvku ze seznamu Odstranění prvního prvku je nejjednodušší typ odstranění a použijeme proceduru VYBER. Procedure Vyber(var celo:spoj; var H:hodnota); Var p:spoj; Begin if celo = NIL then writeln('chyba') else begin p:=celo; H:=celo^.hodnota; celo:=celo^.dalsi; dispose(p); end; end;
4
Takto provedená konstrukce obsahuje i test prázdnosti seznamu, který ale můžeme vynechat pokud víme, že seznam je určitě naplněn. Potom: P := Celo; Celo:= Celo^.dalsi; Dispose (P);
Odstranění posledního prvku v seznamu Opět poněkud komplikovanější řešení, jelikož při odstraňování posledního prvku potřebujeme znát adresu předposledního prvku, je nutné deklarovat a umístit ukazatel PŘED a teprve potom provést vlastní odstranění prvku. Čili: Pred := Celo; While Pred^.dalsi <> Konec DO Pred := Pred^.dalsi; P := Konec; Konec := Pred; Konec^.dalsi := Nil; Dispose(P);
Odstranění libovolného prvku ze seznamu Výše uvedené konstrukce programu platily pro odebrání dvou krajních prvků ze seznamu – prvního a posledního. Při operaci odebrání prvků, které nejsou ani na jednom umístění musíme opět použít adresu prvku který předchází odebíranému prvku. V níže uvedeném provedení se tato adresa zjišťuje ze značky PRED. Zde je nutno použít tyto příkazy, které odkazují na prvek označeny jako POM a zároveň je ukazatel umístíme PŘED. Pred := Celo; While Pred^.dalsi <> Pom DO Pred := Pred^.dalsi; Pred^.dalsi := Pom^.dalsi; Dispose(Pom); Pom := Nil; Pred := Nil;
Pokud potřebujeme vypsat hodnotu, nebo údaj na značce, například při zařazování položek do seznamu, lze použít tento zdrojový kód: procedure vypis_udaj_na_znacce; begin writeln(znacka^.hodnota); end;
Pokud chceme zjistit adresu prvku jejíž datová část je shodná s níže uvedenou podmínkou RESULT:=… použijeme právě tuto konstrukci: function Najdi (zac: UkTypPrvku; cis: integer): UkTypPrvku; begin if zac^.hodnota>cis then result:= nil else begin while (zac^.dalsiPrvek<>nil) and (zac^.dalsiPrvek^.hodnota
5
end;
Tento přiřazovací příkaz (result:=zac) má smysl i při nenalezení žádaného prvku, jelikož je funkce NAJDI deklarována v každém případě. Procedury pro výpis nebo tisk seznamu Nejprve obecná procedura: procedure vypis(zac: UkTypPrvku); begin while zac <> nil do //průchod lineárním seznamem// begin memo1.lines.add(intToStr(zac^.hodnota)); zac:= zac^.dalsiPrvek; //přechod na další prvek// end; end; begin cislo:= strToInt(edit1.text); vlozNovyPrvek(zacatek, cislo); vypis; end;
V programu ADT je použita tato konstrukce výpisu prvků: function MujFIRST2(P: Pvag): Pvag; begin result:=p end; procedure TForm2.Button6Click(Sender: TObject); var pom: zbozi; begin Memo1.Lines.Clear; MEmo1.Lines.Add(''); Memo1.Lines.Add(' Výpis seznamu :'); q:=MujFIRST2(P); if q <> nil then //průchod seznamem begin while q<> nil do begin pom:=q^.z; //přechod na další prvek Memo1.Lines.Add(pom.Druh); //načítání vlastností Memo1.Lines.Add(IntToStr(pom.Mnozstvi)); Memo1.Lines.Add(pom.Obchod); Memo1.Lines.Add(IntToStr(pom.MaxCena)); Memo1.Lines.Add(pom.DatumNakupu); Memo1.Lines.Add(''); q:=q^.dalsi; end; end else
Algoritmus tisku prvků je konkrétním případem obecného algoritmu průchodu lineárním seznamem s provedením nějaké akce. Podmínkou je, aby byl poslední prvek seznamu odlišitelný od ostatních prvků – poslední prvek má vždy hodnotu ukazatele na další prvek nil.
6
Další použité procedury pro výpis prvků v seznamu: funkce použity v ADT function MujEND2(P: Pvag): Pvag; //tato funkce vypíše poslední prvek seznamu var r: Pvag; begin r:=p; if r<>nil then while r^.dalsi<>nil do r:=r^.dalsi; //dokud r není poslední, pokračuj result:=r end;
Pro funkci zjištění následující pozice za pozicí Q použitá v programu ADT
function MujNEXT2(P: Pvag): Pvag; var r: Pvag; begin r:=q; if r<> nil then //pokud r není nil, čili pokud není r poslední prvek begin if r^.dalsi<>nil then r:=r^.dalsi; // pokud další prvek není nil end; result:=r end;
Tato funkce zajistí nalezení předcházejícího prvku, v našem případě prvku R, ležícího před Q. function MujPREVIOUS2(P: Pvag): Pvag; var r: Pvag; begin r:=p; if q<>r then // pokud už neukazuje na začátek begin if r^.dalsi<>nil then begin while r^.dalsi<>q do r:=r^.dalsi; // tak dlouho dokud r není před q end; end; result:=r end;
Když seznam už nebudeme používat, měly bychom uvolnit místo v paměti. Opět použijeme průchod lineárním seznamem. procedure TForm2.FormCreate(Sender: TObject); var pom: UkTypPrvku; begin while zacatek<>nil then begin pom:= zacatek; zacatek:= začátek^.dalsiPrvek; dispose(pom); end; end;
Internetové odkazy související s tématem: http://www.sweb.cz/david.padrta/pascal/6dynprom.html http://home.pf.jcu.cz/~edpo/program/kap11.html http://www.352.vsb.cz/Predmety/ZaklInf/dds.htm
7
Pro zájemce uvádím další možnosti použití seznamu Takhle nějak by mohl vypadat jednoduchý seznam, jehož princip je přibližně takovýto:v paměti na náhodných pozicích je uloženo pomocí příkazu RECORD, několik hodnot, nebo prvků. Na začátek seznamu ukazuje ukazatel ZACATEK a na poslední prvek ukazuje ukazatel KONEC. Každý prvek v seznamu (zde použití typu CHAR) obsahuje HODNOTU, která nese uložené informace a ukazatel na další návazný prvek v seznamu – DALSI. První prvek tak ukazuje na druhy prvek, ten ukazuje na třetí prvek a tak stále dokola až na poslední prvek, který neukazuje nikam. Mezi těmito prvky se pohybuje značka, která se využívá ke čtení dat. Nevýhodou jednosměrného seznamu, jak už vyplývá z nazvu, je průchod značky jen jedním směrem od počátku na konec, nebo skokem na začátek nebo konec seznamu type ukprvek=^prvek; prvek=record hodnota:char; {nebo něco úplně jiného} dalsi:ukprvek; end; var zacatek,znacka,konec:ukprvek; procedure vloz_na_zacatek(s:char); var p:ukprvek; begin new(p); p^.hodnota:=s; p^.dalsi:=zacatek; zacatek:=p; end; procedure vloz_na_konec(s:char); var p:ukprvek; begin new(p); konec^.hodnota:=s; konec^.dalsi:=p; konec:=p; end; function odeber_ze_zacatku:char; var p:ukprvek; begin if zacatek=konec then writeln('CHYBA') else
8
begin odeber_ze_zacatku:=zacatek^.hodnota; p:=zacatek; zacatek:=zacatek^.dalsi; dispose(p); end; end; procedure znacka_na_zacatek; begin znacka:=zacatek; end; procedure posun_znacku; begin if znacka<>konec then znacka:=znacka^.dalsi; end; procedure vypis_udaj_na_znacce; begin writeln(znacka^.hondota); end; begin {vytvořit seznam:} new(zacatek); konec:=zacatek; .
9