Dynamické datové struktury

Všechny proměnné, s kterými jsme až doposud pracovali, můžeme označit jako statické. Proměnná se vytvoří (vyhradí se pro ni místo v paměti) po spuštění aplikace při deklaraci a zaniká po skončení procedury, pokud je lokální, nebo po skončení aplikace, pokud je globální. Při deklaraci proměnných takto jednoznačně určujeme, jaké množství paměti využijeme pro uložení dat v programu.

Pascal však nabízí možnost, jak v průběhu aplikace vytvářet nová místa v paměti, kam můžeme ukládat data. Tento postup realizujeme pomocí tzv. dynamických proměnných. Proměnné vytváříme specielním příkazem, neuvádíme je v deklaraci. V takových proměnných pak můžeme uchovávat hodnoty stejně jako v proměnných statických. K dynamickým proměnným však nepřistupujeme pomocí jména, ale pomocí ukazatelů (pointer).

Ukazatel je proměnná specielního typu. Každá proměnná typu ukazatel má tzv. doménový typ. Doménový typ ukazatele určuje, jaká dynamická proměnná bude vytvořena. Vytvoříme-li novou dynamickou proměnnou, vždy máme v nějaké proměnné typu ukazatel zapsáno místo v paměti, kde se nová dynamická proměnná nachází. K hodnotě dynamické proměnné pak přistupujeme právě pomocí ukazatele.

Takto vypadá deklarace proměnné typu ukazatel:

var u:^integer;

Deklarujeme proměnnou u, která je typu ukazatel. Za dvojtečkou zapíšeme znak šipka ^ a jméno doménového typu. Proměnná u tak bude ukazovat na dynamickou proměnnou typu integer. K vytváření dynamických proměnných použijeme proceduru New. Tato procedura má jeden parametr typu ukazatel. Po zavolání procedury New se v paměti vytvoří nová proměnná takového typu, který je doménovým typem parametru. S nově vytvořenou proměnnou pak můžeme pracovat prostřednictvím ukazatele, který jsme uvedli jako parametr procedury New.

Pokud chceme pracovat s hodnotou dynamické proměnné, pracujeme s ukazatelem na tuto dynamickou proměnnou. Uvedeme jméno proměnné typu ukazatel, která byla parametrem procedury New, a znak šipka. Tím získáme přístup k místu v paměti, kde se nachází nová dynamická proměnná a můžeme pracovat s její hodnotou. Pokud máme deklarovánu proměnnou u typu ukazatel s doménovým typem integer, můžeme takto vytvořit a použít novou dynamickou proměnnou typu integer:

New(u);
u^:=5;

Vytvořili jsme novou proměnnou typu integer (dynamicky) a přiřadili jsme jí hodnotu 5. Stejným způsobem pomocí ukazatele bychom mohli hodnotu dynamické proměnné využít třeba k přiřazení nebo jako parametr podprogramu.

Pokud vytvoříme nějakou dynamickou proměnnou, až ji přestaneme používat, nezaniká sama, měli bychom ji proto zrušit. Zrušení dynamické proměnné provede procedura Dispose, která jako parametr dostane ukazatel na rušenou dynamickou proměnnou. Dynamickou proměnnou typu integer, se kterou jsme pracovali, zrušíme takto:

Dispose(u);

Ukazatel u pak můžeme použít k vytvoření nové dynamické proměnné. Pokud nějaká proměnná typu ukazatel neukazuje na žádnou dynamickou proměnnou (před použitím New nebo po použití Dispose), její hodnota je nedefinovaná. Proměnné typu ukazatel můžeme přiřadit hodnotu nil. Tím dáme najevo, že ukazatel neurčuje žádnou dynamickou proměnnou. Je možné zapsat podmínku, kdy se ptáme, jestli má ukazatel hodnotu nil.

Příklad 1:

Po stisku tlačítka vytvořte dynamickou proměnnou typu integer. Dejte jí náhodnou hodnotu a vypište ji. (dynam.dpr)

V proceduře reagující na stisk tlačítka deklarujeme proměnnou typu ukazatel, jejíž doménový typ je integer. V těle pak vytvoříme novou dynamickou proměnnou, se kterou dále pracujeme. Na konci procedury dynamickou proměnnou opět zrušíme.

procedure TForm1.Button1Click(Sender: TObject);
var u:^integer;
begin
New(u);
u^:=Random(100)+1;
Label1.Caption:=IntToStr(u^);
Dispose(u);
end;

Nesmíme zapomenout inicializovat generátor náhodných čísel v proceduře FormCreate. Ke statickým proměnným přistupujeme pomocí jména, k dynamickým používáme přístup pomocí ukazatele a znaku šipka. Pak můžeme využívat dynamickou proměnnou stejně, jako statickou.

Je pravda, že použití dynamické proměnné v předchozím příkladu je zbytečné. Mnohem lepší řešení by bylo se statickou proměnnou typu integer. Kdy má tedy použití dynamických proměnných smysl? Pokud předem víme, kolik proměnných budeme potřebovat, tak asi ne. Jestliže však množství hodnot, s kterými budeme pracovat, předem neznáme, můžeme si pro ně vytvářet proměnné dynamicky a jejich počet tedy přizpůsobovat počtu hodnot. K tomu ale potřebujeme vytvořit složitější datovou strukturu. Pokud by každá dynamická proměnná měla mít svůj ukazatel, který bychom deklarovali, nic tím nezískáme. Pro práci s  dynamickými datovými strukturami typicky využijme záznamy.

Vytvoříme záznam, který bude mít jednu z položek typu ukazatel, další položky pak mohou být různých jiných typů. Aby použití takového záznamu mělo smysl, položka záznamu typu ukazatel bude ukazovat na dynamickou proměnnou, která je také typu záznam (stejný záznam). Takové dynamické proměnné pak můžeme prostřednictvím položek typu ukazatel navzájem propojovat. V jednom záznamu máme ukazatel na další záznam atd.

Typ pro záznam definujeme obvyklým způsobem, specielní zápis však musíme použít pro definici typu ukazatele:

type
  Uk=^Zaz;
  Zaz=record
    hod:integer;
    dal:Uk;
    end;

Tento záznam má první položku s názvem hod typu integer. Tuto položku můžeme využít pro práci s hodnotou typu integer. Druhá položka dal je typu ukazatel. Typ pro tuto položku musí být definován dříve v unitě. Ještě před definicí typu záznam tedy definuji typ ukazatele na tento záznam. Takto definovaný typ je typem položky záznamu dal. V definici typu ukazatel tak vlastně jako doménový typ využívám typ dosud nedefinovaný. Toto je jediná výjimka v Pascalu, kdy mohu využít dosud nedefinovaný typ. Doménový typ pro ukazatel samozřejmě musím definovat později. V tomto konkrétním případě tedy definuji typ Uk, což je ukazatel na dynamickou proměnnou dosud neznámého typu Zaz. Tento typ definuji vzápětí. Je to záznam, jehož položka dal je typu Uk. Tato položka je tedy ukazatelem na dynamickou proměnnou typu Zaz.

Záznamy s touto strukturou se spojují do tzv. seznamů. Do seznamů můžeme přidávat prvky, rušit je, seznamy můžeme třídit apod. Typicky přitom používáme jednu proměnnou typu ukazatel na záznam, která ukazuje na první prvek seznamu. Takovou proměnnou obvykle nazýváme hlavou seznamu. Jedna z položek prvního prvku pak ukazuje na druhý prvek atd. Stačí tedy používat jednu proměnnou typu ukazatel a v seznamu může být uchováno velké množství hodnot. Pro každou je vytvořena dynamická proměnná.

obrázek 1: Záznamy spojené do seznamu

Seznam je jednou z často používaných dynamických datových struktur. Pro práci se seznamem obvykle deklarujeme několik ukazatelů a vytváříme mnoho záznamů dynamicky. Nemusíme tedy předem vědět, kolik bude mít seznam prvků. Seznamy prvků mají široké využití, ukážeme si zde několik typů seznamů a práci s nimi. Předvedeme si také použití seznamů a návrh jiné dynamické datové struktury.

V následujících příkladech využijeme tlačítka typu TBitBtn s následujícím významem:

obrázek 2: Funkce tlačítek

Příklad 2:

Naprogramujte operace s jednosměrným seznamem. (seznam1.dpr)

Pro práci s jednosměrným seznamem si definujeme typ záznam a ukazatel na něj:

type
  Uk=^Zaz;
  Zaz=record
    hod:integer;
    dal:Uk;
    end;

V jednotlivých proměnných typu záznam budeme v položce hod uchovávat celé číslo a v položce dal ukazatel na další prvek seznamu. Tato struktura tedy vystihuje označení “jednosměrný”. V seznamu se můžeme pohybovat pouze jedním směrem, od hlavy ke konci. Na formuláři budeme zobrazovat vždy hodnotu aktuálního prvku a hodnotu jeho následníka. V memu pak vypíšeme všechny prvky seznamu. K práci se seznamem využijeme 2 globální proměnné typu ukazatel. Proměnná k bude ukazovat vždy na začátek seznamu, proměnná l na aktuální prvek. Umožníme přidávat prvky, procházet seznam prvek po prvku, rušit jednotlivé prvky a zrušit celý seznam.

obrázek 3: Jednosměrný seznam

Jako prvky seznamu použijeme celá čísla z intervalu 1..100, které budeme náhodně generovat. V reakci na událost OnCreate formuláře inicializujeme generátor náhodných čísel a hodnoty ukazatelů k,l nastavíme na nil. K výpisu prvků a celého seznamu připravíme procedury PisPrvek a PisSeznam. Obě procedury mají jako parametr ukazatel. Procedura PisPrvek dostává jako parametr ukazatel na aktuální prvek, do labelů vypisuje jeho hodnotu a hodnotu následníka:

procedure TForm1.PisPrvek(z:Uk);
begin
if z=nil then
  begin
  Label1.Caption:='nil';
  Label2.Caption:='nil';
  end
else
  begin
  Label1.Caption:=IntToStr(z^.hod);
  if z^.dal=nil then
    Label2.Caption:='nil'
  else
    Label2.Caption:=IntToStr(z^.dal^.hod);
  end;
end;

Pokud má parametr hodnotu nil nebo neexistuje následník, je do příslušného labelu vypsán textový řetězec “nil”.

Výpis celého seznamu do mema zajistí procedura PisSeznam, která dostává jako parametr ukazatel na začátek seznamu. Prochází celý seznam a každou hodnotu přidává do mema:

procedure TForm1.PisSeznam(z:Uk);
begin
Memo1.Clear;
while z<>nil do
  begin
  Memo1.Lines.Add(IntToStr(z^.hod));
  z:=z^.dal;
  end;
end;

Pokud stiskneme tlačítko pro přidání prvku, prvek je přidáván vždy na začátek seznamu. Nastaví se také jako aktuální prvek a vypíše se. Současně se aktualizuje výpis celého seznamu:

procedure TForm1.Button1Click(Sender: TObject);
var n:integer;
m:Uk;
begin
New(m);
n:=Random(100)+1;
m^.hod:=n;
m^.dal:=k;
k:=m;
l:=k;
PisPrvek(l);
PisSeznam(k);
end;

Pro vytvoření nové dynamické proměnné se využívá pomocná lokální proměnná m typu ukazatel na záznam. Nově vytvořená dynamická proměnná se připojí do seznamu, pomocná proměnná m zaniká při ukončení procedury.

Při stisku tlačítka pro přechod na následující prvek se aktualizuje hodnota proměnné l, pokud následující prvek existuje. Současně se aktualizuje výpis hodnoty:

procedure TForm1.BitBtn1Click(Sender: TObject);
begin
if l<>nil then
  if l^.dal<>nil then
    begin
    l:=l^.dal;
    PisPrvek(l);
    end;
end;

Stisk tlačítka pro přechod na začátek seznamu přiřazuje do proměnné l hodnotu proměnné k, tedy ukazatel na první prvek seznamu. Opět se aktualizuje výpis prvku:

procedure TForm1.BitBtn2Click(Sender: TObject);
begin
l:=k;
PisPrvek(l);
end;

Nejnáročnější částí projektu je vypuštění aktuálního prvku ze seznamu. Pokud je v seznamu nějaký prvek, musíme rozlišit, zda vypouštíme první prvek, poslední prvek nebo prvek z vnitřku seznamu:

procedure TForm1.BitBtn3Click(Sender: TObject);
var m:Uk;
begin
if l<>nil then //je co zrušit
  if l=k then //první prvek
    begin
    k:=l^.dal;
    Dispose(
l);
    l:=k;
    end
  else
    if l^.dal=nil then //poslední prvek
      begin
      m:=k;
      while m^.dal<>l do
        m:=m^.dal;
      m^.dal:=nil;
      Dispose(l);
      l:=m;
      end
    else //prostřední prvek
      begin
      m:=l^.dal;
      l^.hod:=m^.hod;
      l^.dal:=m^.dal;
      Dispose(m);
      end;
PisPrvek(l);
PisSeznam(k);
end;

Protože vypouštíme prvek, musíme rušit příslušnou dynamickou proměnnou. V případě prvního prvku posuneme ukazatel k na následující prvek a zrušíme první prvek (stále na něj ukazuje l). Pak ještě posuneme l na nový první prvek.

V případě, že se jedná o poslední prvek, potřebujeme získat ukazatel na předposlední prvek. Použijeme pomocnou proměnnou m, která projde v cyklu prvky od prvního až k prvku, který má jako následníka poslední prvek. U předposledního prvku zapíšeme, že nemá následníka a pak můžeme zrušit poslední prvek a přesunout ukazatel l na bývalý předposlední prvek.

Ke zrušení prvku v prostředku seznamu se použije malý trik. Ve skutečnosti budeme rušit jeho následníka, ovšem zkopírujeme hodnotu a ukazatel z následníka do aktuálního prvku. Tím ze seznamu odstraníme rušenou hodnotu.

Po vypuštění prvku aktualizujeme výpis prvku v labelech a výpis celého seznamu v memu.

Zrušení celého seznamu provádí procedura Zrus. Jako parametr dostává ukazatel na hlavu seznamu. Seznam projde od začátku prvek po prvku a každý záznam zruší. Tak uvolníme paměť, kterou jsme využili pro vytváření nových dynamických proměnných.

procedure TForm1.Zrus(z:Uk);
var m:Uk;
begin
while z<>nil do
  begin
  m:=z;
  z:=z^.dal;
  Dispose(m);
  end;
end;

Tuto proceduru voláme v reakci na stisk tlačítka pro zrušení seznamu. Aktualizujeme ještě výpis aktualního prvku a vymažeme memo. Hodnoty ukazatelů k,l nastavíme na nil.

V případě, že se uživatel rozhodne ukončit aplikaci, zrušíme také celý seznam. Ve chvíli, kdy uživatel ukončí aplikaci, se vyvolává událost OnClose formuláře. My ještě můžeme na tuto událost reagovat a provést akce, které souvisejí s ukončením aplikace. Můžeme třeba i nepovolit ukončení aplikace. V proceduře FormClose tedy zrušíme vytvořený seznam zavoláním procedury Zrus. Paměť použitá pro dynamické proměnné tak bude uvolněna.

Máme připravenu aplikaci, která provádí základní operace s jednosměrným seznamem. I v dalších příkladech budeme pracovat s podobnými dynamickými strukturami.

Příklad 3:

Naprogramujte operace s jednosměrným setříděným seznamem. (seznam2.dpr)

Jedná se pouze o malé rozšíření předchozího příkladu. Práce se seznamem, datové typy i zpracování je úplně stejné jako v předchozím příkladu. Jediný rozdíl je v proceduře pro přidávání nového prvku. Protože má být seznam setříděný, musíme pro nový prvek najít správnou pozici v seznamu. Musíme rozlišit případ, kdy je seznam prázdný a kdy jsou v seznamu prvky. Pokud už v seznamu nějaké prvky jsou, musíme pro novou hodnotu rozlišit případy, kdy patří na začátek seznamu, konec seznamu nebo doprostřed seznamu:

procedure TForm1.Button1Click(Sender: TObject);
var n:integer;
m:Uk;
begin
New(m);
n:=Random(100)+1;
if k<>nil then
  begin
  l:=k;
  while ((l^.hod<=n) and (l^.dal<>nil)) do
    l:=l^.dal;
  if l^.hod>n then //zatřidit
    if k=l then //na první misto
      begin
      m^.hod:=n;
      m^.dal:=k;
      k:=m;
      l:=k;
      end
    else //doprostřed
      begin
      m^.dal:=l^.dal;
      m^.hod:=l^.hod;
      l^.dal:=m;
      l^.hod:=n;
      end
  else //na konec
    begin
    l^.dal:=m;
    m^.hod:=n;
    m^.dal:=nil;
    l:=m;
    end
  end
else //první prvek
  begin
  m^.hod:=n;
  m^.dal:=nil;

  k:=m;
  l:=k;
  end;
PisPrvek(l);
PisSeznam(k);
end;

Proměnná l ukazuje na první prvek a prochází jednotlivé prvky v cyklu tak dlouho, dokud není následující prvek větší nebo se nedostanu na konec seznamu. Po skončení while cyklu ukazuje l na prvek, před který vložíme novou hodnotu (rozlišíme případy, zda je to první prvek nebo ne) nebo na poslední prvek, pokud chceme vložit maximální hodnotu. Máme vytvořenu novou dynamickou proměnnou (ukazuje na ní ukazatel m), kterou využijeme podle situace.

Pokud patří nová hodnota na začátek, uložíme ji do nové proměnné a aktualizujeme ukazatel k na začátek seznamu.

Patří-li hodnota doprostřed seznamu, použijeme kopii obsahu proměnné, na kterou ukazuje l, do nové dynamické proměnné. Novou hodnotu pak zapíšeme do prvku, kam ukazuje l.

Největší prvek přidáme na konec seznamu, položka dal posledního záznamu bude ukazovat na novou dynamickou proměnnou, kam přiřadíme vygenerovanou hodnotu. Aktualizujeme ukazatel l na nově vytvořenou hodnotu.

Jedná-li se o úplně první prvek v seznamu, vytvoříme novou dynamickou proměnnou, která bude obsahovat vygenerovanou hodnotu a nastavíme na ni ukazatele k,l.

Kdykoliv přidáváme do seznamu nový prvek, zatřídíme ho na správné místo a tak udržujeme seznam hodnot neustále setříděný. Tento postup by se dal použít třeba při třídění čísel z textového souboru (je to výhodné, ani nemusíme znát počet čísel v souboru, seznam bude mít stejně prvků jako soubor).

Příklad 4:

Naprogramujte operace s obousměrným seznamem. (seznam3.dpr)

V obousměrném seznamu uchováváme ukazatel na následníka prvku i na předchůdce prvku v seznamu. Tato struktura je výhodná, pokud potřebujeme mít přístup k oběma souvisejícím hodnotám. Pro práci s obousměrným seznamem musíme rozšířit definici typu záznam:

type
  Uk=^Zaz;
  Zaz=record
    pred:uk;
    hod:integer;
    dal:uk;
    end;

V záznamu přidáme položku pred, která je ukazatelem na předchozí prvek. Nové prvky obousměrného seznamu přidáváme na aktuální pozici v seznamu, není třeba si pamatovat ukazatel na začátek seznamu. V tomto příkladu tedy budeme pracovat s jediným globálním ukazatelem, který určuje aktuální prvek v seznamu. Tomu budeme muset přizpůsobit procedury PisSeznam, kde musíme začít výpis od začátku seznamu (musíme dojít na začátek) a dále proceduru Zrus, kdy musíme projít úsek nalevo i napravo od aktuálního prvku. V memu budeme opět vypisovat celý seznam, v labelech vypíšeme vždy hodnotu aktuálního prvku, jeho předchůdce a následníka.

Novou hodnotu přidáváme vždy před aktuální prvek. Předchůdce aktuálního prvku bude ukazovat na novou dynamickou proměnnou jako na svého následníka a aktuální prvek získá jako předchůdce nový prvek:

if l<>nil then
begin
if l^.pred<>nil then
  l^.pred^.dal:=m;
m^.pred:=l^.pred;
m^.dal:=l;
l^.pred:=m;
end

Pak přesuneme ukazatel l na nový prvek a aktualizujeme výpis prvku a celého seznamu. Zvlášť musíme zapsat řešení případu, že je zadán úplně první prvek.

Na formuláři jsou tlačítka pro posun vlevo a vpravo. Pokud to jde, posuneme ukazatel na aktuální prvek a vypíšeme ho do labelů.

Při rušení prvku musíme přepojit ukazatele předchůdce na následníka rušeného prvku a ukazatel následníka na předchůdce:

if ((l^.dal<>nil) and (l^.pred<>nil)) then
  begin
  l^.pred^.dal:=l^.dal;
  l^.dal^.pred:=l^.pred;
  k:=l;
  l:=l^.dal;
  end

Tento postup provádíme, pokud má rušený prvek předchůdce i následníka. V jiných případech musíme podle situace upravit přiřazení ukazatelů.

Pro použití obousměrného seznamu se rozhodneme podle zadání úlohy, kterou se rozhodneme řešit s použitím dynamických proměnných. Pokud však použitím obousměrného seznamu nezjednodušíme práci s daty, je lepší volit jednosměrný seznam, kde jednotlivé prvky zabírají méně místa v paměti.

Seznamy dynamických proměnných se typicky využívají k realizaci dvou datových struktur, které mají široké využití při zpracování různých algoritmů. Jedná se o zásobník a o frontu. Zásobník i fronta jsou seznamy hodnot, které se uchovávají k pozdějšímu zpracování. Liší se však přístupem k jednotlivým hodnotám.

Zásobníku se říká LIFO z anglického Last In First Out, tedy “poslední dovnitř, první ven”. Hodnoty, které se ukládají do zásobníku, se postupně uchovávají a pokud vybírám hodnotu ze zásobníku, vezmu hodnotu, která do zásobníku přibyla naposledy. Nová hodnota se tedy přidává na tzv. vrchol zásobníku, odkud se hodnota také vybírá. Při realizaci zásobníku pomocí seznamu si tedy stačí pamatovat ukazatel na začátek seznamu, který představuje vrchol zásobníku. Nové hodnoty se do seznamu přidávají na začátek, stejně tak ze začátku seznamu se hodnoty získávají. Výběr hodnoty ze zásobníku znamená zrušení příslušného prvku v seznamu.

U fronty se používá označení FIFO z anglického First In First Out, tedy “první dovnitř, první ven”. Prvky se řadí za sebe, nový prvek se připojuje vždy na konec seznamu. Pokud chci vybrat z fronty prvek, vybírám prvek ze začátku fronty. Vybírám tedy prvek, který je ve frontě nejdéle. Při realizaci pomocí dynamických datových struktur je vhodné pamatovat si ukazatel na začátek fronty pro výběr prvků a ukazatel na konec fronty pro přidávání prvků.

Příklad 5:

Naprogramujte zásobník s použitím dynamických proměnných. (zasob.dpr)

Zásobník budeme reprezentovat jednosměrným seznamem prvků. Použijeme stejnou strukturu, jako u příkladů s jednosměrným seznamem. Jediný ukazatel, který si je třeba pamatovat, je ukazatel na vrchol zásobníku. Do zásobníku budeme vkládat náhodná čísla z intervalu 1..100. Vložení čísla do zásobníku zajistí procedura Pridej, která jako parametr dostane ukazatel na zásobník a přidávanou hodnotu. Vytvoří novou dynamickou proměnnou, kterou připojí na začátek seznamu a aktualizuje hodnotu ukazatele na vrchol zásobníku.

Výběr ze zásobníku realizujeme funkcí Vyber. Tato funkce dostane jako parametr ukazatel na vrchol zásobníku, zruší první prvek seznamu a vrátí jeho hodnotu. Využijeme toho, že pracujeme pouze s čísly z intervalu 1..100, a pokud budeme volat funkci Vyber a zásobník bude prázdný, návratová hodnota funkce bude nula.

Na formuláři jsou 2 tlačítka. První vkládá do zásobníku nové číslo a druhé číslo vybírá. Obsah zásobníku je graficky znázorněn. Vypisujeme na Canvas formuláře vždy maximálně 10 prvků ze zásobníku. Pro zjednodušení výpisu máme deklarovánu globální proměnnou Pocet, jejíž hodnota udává, kolik je v zásobníku čísel. Pokud počet čísel v zásobníku přesáhne 10, zobrazíme komponentu ScrollBar, která umožní procházet celý zásobník.

Komponenta ScrollBar (posuvná lišta), jejíž zástupce se nachází na liště Standard čtvrtý zprava, má podobně jako UpDown vlastnosti Min, Max a Position. Pozice (a současně hodnota vlastnosti Position) na ScrollBaru je indikována posuvným obdélníkem. Pokud počet čísel přesáhne 10, zobrazíme ScrollBar a v reakci na jeho událost OnScroll, která se vyvolává, pokud uživatel mění myší nebo klávesnicí pozici ScrollBaru, měníme zobrazená čísla ze zásobníku. Po každém přidání nebo ubrání čísla ze zásobníku rozhodujeme, zda bude ScrollBar zobrazen a aktualizujeme vlastnost Max ScrollBaru podle počtu čísel v zásobníku na hodnotu Pocet-10.

Výpis zásobníku realizuje procedura Zobraz, která dostává jako parametr ukazatel na vrchol zásobníku a pořadí čísla od dna zásobníku, které se má ve výstupu objevit nejvýše. Pokud je čísel 10 a méně, zobrazí se celý zásobník, je-li jich více, je začátek seznamu přeskočen. Procedura Zobraz je volána po kliknutí na tlačítka nebo po posunu ScrollBaru.

obrázek 4: Zásobník

V reakci na stisk tlačítka pro vložení prvku tedy generujeme novou hodnotu a voláním procedury Pridej ji připojíme na začátek seznamu. Aktualizujeme obsah labelů a zobrazíme zásobník od vrcholu. V reakci na stisk tlačítka pro výběr voláme funkci Vyber, která, pokud není zásobník prázdný, vrací hodnotu z vrcholu zásobníku. Je-li zásobník prázdný, zobrazíme o tom zprávu. Také aktualizujeme labely a výpis zásobníku.

K inicializaci zásobníku slouží procedura Init. Tato procedura pouze nastavuje ukazatel na vrchol zásobníku na nil. Je volána v proceduře FormCreate společně s procedurou Randomize a vynulováním proměnné Pocet.

Při ukončení aplikace reagujeme na událost OnClose formuláře a pokud není zásobník prázdný, zrušíme zbývající prvky opakovaným výběrem, dokud se zásobník nevyprázdní.

Procedury připravené pro práci se zásobníkem (Init, Pridej, Vyber) bychom mohli využívat opakovaně i pro práci s několika zásobníky najednou. Zásobník je určen vždy pouze ukazatelem, který předáváme jako parametr. Procedura zobraz je však závislá na hodnotě globální proměnné Pocet. Tuto hodnotu bychom však také mohli předávat jako parametr a zobecnit tak proceduru Zobraz pro práci s více zásobníky. Typicky však do zásobníku “není vidět” a používá se pouze vnitřně pro zpracování různých úloh.

Příklad 6:

Naprogramujte frontu s použitím dynamických proměnných. (fronta.dpr)

Pro realizaci fronty zvolíme stejnou strukturu seznamu, ale vzhledem k tomu, že je výhodné pamatovat si ukazatel na začátek i konec fronty, pro proměnnou, která bude určovat frontu, zvolíme typ záznam:

type
  Ukaz=^Prvek;
  Prvek=record
    hod:integer;
    dal:Ukaz;
    end;
  TFron=record
    zac,kon:Ukaz;
    end;

Proměnná, která bude určovat frontu, bude tedy typu záznam se dvěma složkami, ukazateli na začátek a konec fronty. Tato proměnná se využije jako parametr pro procedury Init, Pridej a Vyber, které realizují práci s frontou. Jejich zápis je přizpůsoben frontě, zápis reakcí na přidání a vybrání prvku prostřednictvím stisku tlačítek je však prakticky totožný s předchozím příkladem. Procedurám reagujícím na stisk tlačítek pro vložení a vybrání prvku je jedno, jak jsou prvky ukládány a vybírány, důležité je, že prvek může být uložen a vybrán.

obrázek 5: Fronta

Procedura Zobraz je zpracována opět velmi podobně, jako parametr dostává však pouze ukazatel na začátek fronty. Jinak je výpis prvků realizován stejně. Stejně tak je v tomto příkladu použita komponenta ScrollBar.

Naprosto analogické je také zrušení zbylých prvků ve frontě při ukončení aplikace a volání inicializace fronty při spuštění aplikace.

Pokud bych nepřidával v těchto dvou programech grafický výstup, procedury reagující na přidání nebo vybrání prvku by byly zcela shodné. Pouze bych pracoval v případě zásobníku s proměnnou typu ukazatel, která ukazuje na vrchol zásobníku, a v případě fronty s proměnnou typu záznam, která určuje její začátek a konec. Bez použití seznamů můžeme zásobník a frontu realizovat staticky pomocí pole, počet prvků je však omezen rozsahem pole.

Binární vyhledávací strom je další datová struktura, která slouží k uchování hodnot a realizuje se typicky použitím dynamických proměnných. Strom je datová struktura, kde je jeden prvek tzv. kořen (v následujícím obrázku prvek s hodnotou 32). Kořen může mít následníky, tyto následníci další následníky atd. Vzniká tak struktura, kterou můžeme rozdělit do jednotlivých vrstev podle vzdálenosti prvků od kořenu. Pro binární strom platí, že každý prvek stromu má maximálně dva následníky (levého a pravého). Binární vyhledávací strom je pak specielním případem stromu, do kterého vkládáme hodnoty podle určitých pravidel. Pro každý prvek platí, že všichni jeho leví následníci mají menší hodnotu, všichni praví následníci mají hodnotu větší. Pro každý prvek, který chceme přidat do binárního vyhledávacího stromu, tak najdeme jednoznačné místo, kam ho uložit.

obrázek 6: Uložení čísel v binárním vyhledávacím stromu

Všimněte si, že pokud bychom prošli prvky stromu zleva doprava, tak jak jsou zapsány na obrázku, jsou hodnoty setříděné od nejmenší do největší. Takový výpis je jednou z možných operací s binárním vyhledávacím stromem. Je to však náročnější operace, stejně jako vypouštění prvků ze stromu. Zpracujeme tedy pouze jednodušší operace.

Příklad 7:

Naprogramujte přidávání prvků do binárního vyhledávacího stromu. (strom1.dpr)

V tomto příkladu realizujeme pouze přidávání prvků do binárního vyhledávacího stromu. Přidáme také grafický výstup a umožníme procházet prvky stromu. Zapíšeme i možnost zrušení celého stromu.

Pokud chceme přidat nový prvek, po kliknutí na tlačítko se generuje náhodně hodnota z intervalu 1..100 a najde se pro ní správné umístění ve stromu. Tlačítka s šipkami slouží pro zobrazení levého resp. pravého následníka. Tlačítko se čtvercem zobrazí kořen stromu. V memu se vypisují hodnoty v pořadí, jak byly vygenerovány. Výpis hodnot v memu nesouvisí s jejich uložením ve stromu. Po kliknutí na tlačítko pro rušení stromu je zrušen celý strom. Stejně tak se ruší celý strom při ukončení aplikace.

obrázek 7: Binární vyhledávací strom

Pro práci se stromem definujeme typ záznam, který obsahuje hodnotu a ukazatele na levého a pravého následníka. Dále využijeme dvě proměnné typu ukazatel. Proměnná k ukazuje na kořen stromu, proměnna l na aktuální prvek. Procedura PisPrvek dostane jako parametr ukazatel na aktuální prvek a vypíše ho do labelu včetně hodnot jeho následníků.

Reakce na stisk tlačítek pro zobrazení následníků jsou velmi jednoduché. Pokud má zobrazený prvek následníka, posuneme se na něj a zobrazíme ho. Při přechodu do kořene stromu pouze nastavíme aktuální pozici na ukazatel na kořen stromu a prvek v kořeni vypíšeme.

Přidávání prvku je zajímavější. Vytvoříme novou dynamickou proměnnou m, jako její hodnotu zapíšeme vygenerované číslo n a levého i pravého následníka nastavíme na nil. Nová hodnota se stane tzv. listem stromu (nemá následníky). Použijeme pomocnou proměnnou p, ve které si pamatujeme předka pro novou dynamickou proměnnou. Strom procházíme od kořene vždy tím směrem, aby byla zachována vlastnost binárního vyhledávacího stromu. Tedy pokud je nová hodnota menší než hodnota v testovaném prvku, posuneme se doleva, jinak se posuneme doprava (tedy pokud se vyskytnou některé hodnoty vícekrát, umísťují se vpravo). Takto postupujeme tak dlouho, až najdeme prvek, ke kterému připojíme novou dynamickou proměnnou m jako následníka. Pokud zadáváme první prvek, vytvoří se kořen, jinak připojíme list.

p:=nil;
l:=k;
while l<>nil do
  begin
  p:=l;
  if l^.hod>n then
    l:=l^.levy
  else
    l:=l^.pravy;
  end;
if p=nil then
  k:=m
else
  if p.hod>n then
    p^.levy:=m
  else
    p^.pravy:=m;

Pak už jen nastavíme aktuální prvek na nový list a vypíšeme jeho hodnotu do labelu a do mema. Pokud chceme strom procházet a jsme v listu, musíme se vždy vrátit do kořene.

Největší problémy způsobí zrušení této dynamické struktury. Pokud chceme zrušit strom, znamená to projít všechny jeho prvky a žádný nevynechat. Je nutné si uvědomit, že pokud přejdeme na následníka nějakého prvku, k původnímu prvku není možné se přímo vrátit. Jedna z variant by byla rozšířit datovou strukturu o ukazatel na předchůdce. Toto řešení se však nepoužívá. Lepší je pamatovat si dosud nezrušené prvky a vracet se k nim.

Tento postup se dá realizovat několika způsoby. Prvním z nich je průchod do hloubky, ke kterému se využívá zásobník. Pokud se nacházím v prvku stromu, dám na zásobník ukazatele na jeho následníky a prvek můžu zrušit. Pak vyberu jeden ukazatel ze zásobníku a postup opakuji tak dlouho, dokud není zásobník prázdný. Strom se v tomto případě ruší postupně po jednotlivých větvích. Dříve zruším ty větve, které dám později na zásobník.

V proceduře Zrus si lokálně definuji typ pro zásobník. Je to záznam, který má 2 položky, ukazatel na prvek stromu a ukazatel na další prvek v zásobníku:

type
  Uz=^Zasob;
  Zasob=record
    u:Uk;
    d:Uz;
    end;
var x,y:Uz;

Deklarujeme lokální proměnné x,y (jinak to ani nejde, tento lokálně definovaný typ nemůžu využit v jiné části projektu) typu Zasob a zásobník můžeme používat. Tentokrát nebudeme zapisovat podprogramy pro práci se zásobníkem, inicializaci, přidávání a vybírání prvků provedeme přímo. Proměnná x ukazuje na vrchol zásobníku, y je pomocná.

Na začátku dáme do zásobníku kořen stromu, případně necháme zásobník prázdný:

if k<>nil then
  begin
  New(x);
  x^.u:=k;
  x^.d:=nil;
  end
else
  x:=nil;

Pak v cyklu vždy vybereme ze zásobníku ukazatel na jeden prvek stromu, uložíme do zásobníku ukazatele na jeho následníky (existují-li) a prvek zrušíme pomocí Dispose. Cyklus skončí, pokud je zásobník prázdný.

while x<>nil do
  begin
  l:=x^.u; //vybírám prvek ze zásobníku
  y:=x;
  x:=x^.d;
  Dispose(y); //ruším prvek zásobníku
  if l^.levy<>nil then //přidávám do zásobníku levého následníka
    begin
    New(y);
    y^.u:=l^.levy;
    y^.d:=x;
    x:=y;
    end;
  if l^.pravy<>nil then //přidávám do zásobníku pravého následníka
    begin
    New(y);
    y^.u:=l^.pravy;
    y^.d:=x;
    x:=y;
    end;
  Dispose(l); //ruším prvek stromu
  end;

Při výběru prvku ze zásobníku je také nutné zrušit prvek zásobníku. Po vyprázdnění zásobníku jsou zrušeny všechny prvky stromu a také všechny prvky, které jsem vytvořil v zásobníku. Všechna paměť, která byla použita pro dynamické proměnné, je opět uvolněna.

Příklad 8:

Naprogramujte přidávání prvků do binárního stromu. Strom rušte pomocí fronty. (strom2.dpr)

Práce se stromem je naprosto stejná jako v minulém příkladu, změníme pouze proceduru Zrus. Tentokrát použijeme průchod do šířky. Prvky budou rušeny po jednotlivých úrovních. K tomuto postupu využijeme frontu.

Na začátku dáme do fronty kořen stromu. Potom v cyklu vždy vybereme z fronty prvek stromu, všechny jeho následníky přidáme do fronty a vybraný prvek zrušíme. Postup budeme opakovat, dokud se fronta nevyprázdní. Při tomto řešení se ve frontě postupně za sebe řadí prvky tak, jak jsou zapsány v jednotlivých úrovních. Nejdříve tedy zrušíme kořen, nakonec list, který je v poslední úrovni. Směr rušení prvků v jednotlivých úrovních závisí na pořadí přidávání následníků do fronty.

Opět si definujeme lokální typ pro prvky fronty. Opět je to záznam, který obsahuje ukazatel na prvek stromu a ukazatel na další prvek fronty. Pro frontu tentokrát nedefinuji typ záznam, ale použiji více proměnných typu ukazatel na prvek fronty.

type
  Uf=^Fronta;
  Fronta=record
    u:Uk;
    d:Uf;
    end;
var w,x,y:Uf;

Proměnná x ukazuje na začátek fronty, w na konec fronty, y je pomocná. S frontou budu opět pracovat přímo.

Na začátku těla procedury dám do fronty kořen stromu a v cyklu, který probíhá, dokud není fronta prázdná, vybírám prvek z fronty, zapisuji do fronty jeho následníky a vybraný prvek ruším:

while x<>nil do
  begin
  l:=x^.u; //vybírám prvek z fronty
  y:=x;
  x:=x^.d;
  Dispose(y); //ruším prvek fronty
  if l^.levy<>nil then //přidávám do fronty levého následníka
    begin
    New(y);
    y^.u:=l^.levy;
    y^.d:=nil;
    w^.d:=y;
    w:=y;
    end;
  if l^.pravy<>nil then //přidávám do fronty pravého následníka
    begin
    New(y);
    y^.u:=l^.pravy;
    y^.d:=nil;
    w^.d:=y;
    w:=y;
    end;
  Dispose(l); //ruším prvek stromu
  end;

Po výběru prvku z fronty opět ruším prvek fronty. Jakmile je fronta prázdná, je zrušen celý strom i všechny prvky fronty.

Průchod stromu do hloubky nebo do šířky se dá využít např. i pro výpis všech prvků stromu. Záleží na konkrétní situaci, který z uvedených postupů zvolit.

Příklad 9:

Naprogramujte přidávání prvků do binárního stromu. Strom rušte pomocí rekurzivní procedury. (strom3.dpr)

Celé zpracování příkladu zůstává stejné, změníme pouze opět způsob, jak rušit celý strom. Struktura binárního stromu je v podstatě rekurzivní strukturou. Část stromu má stejnou strukturu jako celý strom. Procedura Zrus bude tentokrát rekurzivní.

Pokud chceme rušit binární strom, znamená to zrušit kořen a zrušit oba podstromy. První podstrom má kořen v levém následníkovi, druhý podstrom má kořen v následníkovi pravém. Podstromy se pak zruší stejným způsobem. Procedura Zrus dostane jako parametr ukazatel na kořen stromu. Pokud má tento prvek stromu levého následníka, volá tato procedura sama sebe s ukazatelem na levého následníka jako s parametrem. Stejná situace je u pravého podstromu. Nakonec zrušíme prvek, na který ukazuje parametr.

procedure TForm1.Zrus(z:Uk);
begin
if z^.levy<>nil then Zrus(z^.levy); //zrušení levého podstromu
if z^.pravy<>nil then Zrus(z^.pravy); //zrušení pravého podstromu
Dispose(z); //zrušení kořenu
end;

V proceduře FormClose voláme proceduru Zrus pouze tehdy, je-li ve stromu alespoň jeden prvek (kořen). Toto řešení je velice jednoduché a není třeba využívat žádné další datové struktury. Je to varianta průchodu do hloubky, pořadí rušení prvků je však jiné, než při použití zásobníku. Při tomto zápisu je nejdříve rušen list zcela vlevo a postupně se zleva ruší nejmenší podstromy. Mírnou modifikací této procedury bychom získali proceduru, která vypíše prvky binárního vyhledávacího stromu od nejmenšího k největšímu, tedy setříděné.

Využití dynamických datových struktur je velmi výhodné ve chvíli, kdy nevíme s jakým množstvím dat budeme pracovat. Typicky využíváme záznamy, které obsahují hodnotu a ukazatele na další záznamy. Pro uložení jedné hodnoty tak potřebujeme více místa v paměti a toto řešení je tedy náročnější na paměť, než práce se statickými proměnnými. Pokud vytváříme nové dynamické proměnné, měli bychom je také rušit, když je už nepotřebujeme. I když Delphi po ukončení aplikace automaticky uvolňují veškerou použitou paměť, ve všech příkladech jsme před ukončením aplikace rušili vytvořené struktury. Použití dynamické datové struktury může být pouze jednou z částí větší aplikace a v dalších částech aplikace je pak možné uvolněnou paměť znovu využít (ve starších verzích Pascalu nebyla paměť po ukončení programu automaticky uvolňována a rušení použitých dynamických proměnných tedy bylo nutné).

K dynamickým proměnným vždy přistupujeme pomocí ukazatelů. Proměnné typu ukazatel mají specielní způsob deklarace, ve které vždy uvádíme doménový typ. Pro definici typu ukazatel existuje v Pascalu výjimka, doménový typ nemusí byt v okamžiku definice typu ukazatele definován. Je však nutné tak učinit později.

Dynamické datové struktury se vždy dají nahradit statickou reprezentací (nejčastěji pomocí polí), v takovém případě je však omezen počet prvků (v novějších verzích Delphi je už možné využívat pole s proměnnou délkou a tak se dá toto omezení obejít) a práce např. se stromem uloženým v poli je náročnější. Pro úlohy, kde se pracuje se seznamy prvků, stromy nebo grafy, je využití dynamických datových struktur typické.