Seznamy dat

Všechny typy proměnných, se kterými jsme se doposud setkali, byly statické. Nemohli jsme je vytvářet a rušit za běhu programu. Nyní se podíváme, jak toto omezení obejít.

Úvod

Prvně si ovšem musíme objasnit pár slovíček:
statická proměnná = proměnná, který v paměti zabírá předem stanovené místo. Potřebné místo se zabírá (alokuje) již při překladu (=spuštění) programu. Pole a všechny proměnné, které jsme až dosud poznali (kromě typu soubor), jsou statické. Například nemůžeme do stoprvkového pole přidat sto prvý prvek.
Lze to sice obejít pomocí toho, že definujeme pole s většími rozměry, než jsou potřeba, ale není to ideální řešení - zabíráme zbytečně místo v paměti a nemůžeme vědět, že jsme ho alokovali dost i pro náročné údaje. (Dobře, mám seřadit několika prvků, tak si nadefinuji 1000 prvkové pole, bude sice z větší části nevyužito, ale co když nějaký šílenec zadá 9999 hodnot?...)

dynamická proměnná
= proměnná, která se vytvoří až v průběhu programu.

Dynamické proměnné

S dynamickými proměnnými se musí manipulovat pomocí ukazatelů (nemůžeme totiž dopředu znát jejich název...). Pořádně si zopakujte použití ukazatelů (minulá lekce).
K práci s dynamickými proměnnými slouží několik procedur:

Procedury pro práci s dynamickými proměnnými
Abecední pořadí Příkaz Význam
1 New(Ukazatel); procedura vytvoří novou dynamickou proměnnou a nastaví na ni ukazatel Ukazatel
2 Dispose(Ukazatel); procedura zruší dynamickou proměnnou, na kterou ukazuje ukazatel Ukazatel

Ukažme si to na příkladu:

program Ukazka;
type PString = ^string;
var retezec : Pstring;
begin
 New(retezec); {Vytvoří novou dynamickou proměnnou typu string}
 retezec^:='Ahoj lidi';
 Writeln(retezec^);
 Dispose(retezec); {Zruší dynamickou proměnnou, na kterou ukazuje retezec}
 New(retezec); {Nová dynamická proměnná, s původní vůbec nesouvisí}
 Writeln(retezec^);
 Dispose(retezec);
 Readln;
end.

Dynamická proměnná má ten typ, na který ukazuje daný ukazatel. Nyní již umíme s dynamickými proměnnými zacházet. Osamostatněná dynamická proměnná výhody nepřináší, ale existují tzv. seznamy proměnných. (Existence seznamu je umožněna jednou zvláštností typu ukazatel).

Seznamy

Definujeme-li ukazatel, nemusíme předem definovat typ, na který ukazuje. (Ale musíme ho dodefinovat později).
Toto jsou tedy zcela správné zápisy:

type PJmeno = ^Jmeno;
PSeznam = ^Seznam;
Jmeno = string;
Seznam = record
          Prijmeni : string;
          Dalsi    : PSeznam;
         end;

Měli-li jsme jednoduchou dynamickou proměnnou, nemohli jsme nadefinovat novou, neboť ta původní by se nám ztratila. Jak vidíte na příkladu, dovoluje nám zvláštnost ukazatele dynamické proměnné "zřetězit". Někde si uděláme ukazatel na první proměnnou, ta bude ukazovat na druhou, ta na třetí....

Takto zřetězeným proměnným se říká seznam. Rozlišujeme dva druhy seznamů.

  1. Seznam jednostranně zřetězený
    Každý prvek ukazuje na prvek následující (či předchozí, ale jaký je mezi nimi rozdíl?)
  2. Seznam oboustranně zřetězený
    Každý prvek ukazuje jak na prvek předchozí, tak i následující.

Ostatní druhy seznamů (=prapodivně zřetězené) pro nás nemají význam.

V praxi se postupuje tak, že se nejdříve udělá jakési držadlo - to neobsahuje žádné podstatné informace, pouze ukazuje na začátek skutečného seznamu dat (u oboustranně zřetězeného seznamu pak musí ukazovat i na prvek poslední).
Zpočátku neukazují držadla nikam (ukazatele mají hodnotu nil) Pak se vytvoří dynamická proměnná a ukazatele se odpovídajícím způsobem změní.

Podívejme se na to, jak vytvořit takový jednostranně zřetězený seznam.

program PrikladNaSeznam;
{Program vytvoří jednostranně zřetězený seznam, chcete-li skončit, zadejte prázdné jméno}
type PSeznam = ^TSeznam;
     TSeznam = record
                Jmeno : string;
                Vek   : Byte;
                Dalsi : PSeznam;
               end;
var Prvni,Soucasny : PSeznam;
    Seznam : TSeznam;
    ZJmeno  : string;
    ZVek    : Byte;
begin
 New(Prvni); {Vytvoříme novou dynamickou proměnnou, na kterou bude ukazovat Prvni}
 Soucasny:=Prvni; {Soucasny ukazuje tamtéž}
 Soucasny^.Dalsi:=nil; {Nic dalšího zatím není}
 Soucasny^.Jmeno:='nikdo';
 Soucasny^.Vek:=0; {Tím bychom měli hotové jakési držadlo}
 
repeat
 Writeln('Jméno : ');
 Readln(ZJmeno);
 Writeln('Věk : ');
 Readln(ZVek);
 if ZJmeno <> '' then
  begin
   New(Soucasny^.Dalsi);      {Nová dynamická proměnná}
   Soucasny:=Soucasny^.Dalsi; {Ukazatel současný přesměrujeme na ní}
   Soucasny^.Jmeno:=ZJmeno;
   Soucasny^.Vek:=ZVek;
   Soucasny^.Dalsi:=nil;
  end;
until ZJmeno='';
end.

Napsali jsme program, který vytvoří jednoduchý seznam proměnných. Abyste si práci se seznamem procvičili - dopište ho tak, aby získaný seznam i vypsal.
Podívejte se také na to, jak vytvořit a vypsat oboustranně zřetězený seznam. Jak vypíšete oboustranně zřetězený seznam od konce? A jak to uděláte s jednosměrně zřetězeným?

Např. u jednosměrně zřetězeného seznamu je výhodné definovat seznam druhý, oboustranně zřetězený a ten pak vypsat...

Občas se nám stane, že potřebujeme nějaký prvek ze seznamu vyřadit. Lze to udělat tak, že vymyslíme nějakou smyšlenou hodnotu a danému prvku tuto hodnotu přiřadíme. Při vypisování pak všechny takovéto hodnoty budeme ignorovat. Tento způsob ale není moc efektivní.

Vyřazení prvku z jednostranně zřetězeného seznamu je poměrně jednoduché. Musíme ovšem začít s prvek Predchazejicim. Nejprve nastavíme zvláštní ukazatel na mazaný prvek. (PSmaz:=Predchazejici.Dalsi). Poté změníme hodnotu ukazatele Predchazejiciho na prvek následující za Mazanym (Predchazejici.Dalsi:=Predchazejici.Dalsi^.Dalsi); A nyní zbývá odstranit příslušnou dynamickou proměnnou (Dispose(PSmaz);). (Nakreslete si obrázek, data jsou čtverečky a ukazatele jsou šipky)
Vyřazení prvku z oboustranně zřetězeného seznamu si objasníme na jednoduchém příkladu. Zde také pochopíte vkládání prvku do tohoto seznamu. (U jednostranně zřetězeného seznamu se pak sami pokuste vymyslet, jak by se dalo udělat.)

program PrikladNaSeznam;
{Program vytvoří oboustranně zřetězený seznam a umožní jeho úpravy)}
type PSeznam = ^TSeznam;
     TSeznam = record
                Jmeno : string;
                Vek   : Byte;
            Predchozi : PSeznam;
                Dalsi : PSeznam;
               end;
var Hlavicka,Soucasny : PSeznam;
    Seznam : TSeznam;
    ZJmeno  : string;
    ZVek    : Byte;
    r       : char;

procedure Inicializace; {Vytvoří si jakési držadlo}
begin
 New(Hlavicka); {Vytvoříme novou dynamickou proměnnou, na kterou bude ukazovat Hlavicka}
 Hlavicka^.Dalsi:=Hlavicka; {Nic dalšího zatím není, tak ať ukazuje na sebe - kruhový seznam}
 Hlavicka^.Predchozi:=Hlavicka;
 Hlavicka^.Jmeno:='nikdo';
 Hlavicka^.Vek:=0; {Tím bychom měli hotové jakési držadlo}
 Soucasny:=Hlavicka; {na které teď ukazuje Soucasny}
end;

procedure Zadej; {Vytvoří oboustranně zřetězený kruhový seznam}
begin
 repeat
 Write('Jméno : ');
 Readln(ZJmeno);
 if ZJmeno <> '' then
  begin
   Write('Věk : ');
   Readln(ZVek);

   New(Soucasny^.Dalsi);      {Nová dynamická proměnná}
   Soucasny^.Dalsi^.Predchozi:=Soucasny; {Ukazatel Predchozi nové proměnné je nastaven
                                   na současnou hodnotu Soucasny}
   Soucasny:=Soucasny^.Dalsi; {Ukazatel současný přesměrujeme na novou proměnnou}
   Soucasny^.Jmeno:=ZJmeno;
   Soucasny^.Vek:=ZVek;
   Soucasny^.Dalsi:=Hlavicka; {Ať se nám kruh neporuší}
   Writeln;
  end;
 until ZJmeno='';
end;

procedure Vypis; {Vypíše celý seznam}
begin
 Writeln('Celý seznam :');
 Soucasny:=Hlavicka;
 repeat
 Soucasny:=Soucasny^.Dalsi;
 Writeln(Soucasny^.Jmeno:40,Soucasny^.Vek:20);
 until Soucasny^.Dalsi=Hlavicka;
 Writeln('To je vše');
end;

procedure Zmen; {Vypíše všechny prvky a u každého se zeptá na možnost změny}
var c:char;
begin
 Writeln('Celý seznam :');
 Soucasny:=Hlavicka;
 repeat
 Soucasny:=Soucasny^.Dalsi;
 Writeln(Soucasny^.Jmeno:20,Soucasny^.Vek:20, ' Změnit(A/N):');
 Readln(c);
 c:=UpCase(c);
 if C='A' then
  begin
   Write('Nové jméno :');
   Readln(ZJmeno);
   Write('Nový věk :');
   Readln(ZVek);
   Soucasny^.Jmeno:=ZJmeno;
   Soucasny^.Vek:=ZVek;
  end;
 until Soucasny^.Dalsi=Hlavicka;
 Writeln('To je vše');
end;
procedure Odstran; {Odstraní vybrané prvky ze seznamu}
var Smaz:PSeznam;
    c   :char;
begin
 Writeln('Celý seznam :');
 Soucasny:=Hlavicka;
 repeat
 Soucasny:=Soucasny^.Dalsi;
 Writeln(Soucasny^.Jmeno:40,Soucasny^.Vek:20,'Odstranit(A/N)');
 Readln(c);
 c:=UpCase(c);
 if C='A' then
  begin
   Smaz:=Soucasny;
   Soucasny^.Predchozi^.Dalsi:=Soucasny^.Dalsi; {Predchozi prvek ukazuje na prvek za mazaným}
   Smaz^.Dalsi^.Predchozi:=Smaz^.Predchozi;{Následující prvek ukazuje před mazaný}
   Soucasny:=Smaz^.Predchozi; {Soucasny, jako by tam mazaný prvek vůbec nebyl}
   Dispose(Smaz); {Teprve nyní můžeme proměnnou vymazat}
  end;
  until Soucasny^.Dalsi=Hlavicka;
 Writeln('To je vše');
end;
procedure Pridej; {Přidá nový prvek do seznamu - před vybraný prvek}
var Novy:PSeznam;
    c   :char;
begin
 Writeln('Celý seznam :');
 Soucasny:=Hlavicka;
 repeat
 Soucasny:=Soucasny^.Dalsi;
 Writeln(Soucasny^.Jmeno:20,Soucasny^.Vek:20,'Přidat před něj nový(A/N)');
 Readln(c);
 c:=UpCase(c);
 if C='A' then
  begin
   Write('Nové jméno :');
   Readln(ZJmeno);
   Write('Nový věk :');
   Readln(ZVek);
   New(Novy);
   Novy^.Jmeno:=ZJmeno;
   Novy^.Vek:=ZVek;

   Novy^.Dalsi:=Soucasny;
   Novy^.Predchozi:=Soucasny^.Predchozi;
   Soucasny^.Predchozi:=Novy;
   Novy^.Predchozi^.Dalsi:=Novy;

  end;
  until Soucasny^.Dalsi=Hlavicka;
 Writeln('To je vše');
end;

begin
 Inicializace;
 repeat
 writeln('Co chcete dělat : ');
 writeln;
 writeln('V - Vytvořit seznam');
 writeln('Z - Zobrazit seznam');
 writeln('O - Opravit údaje');
 writeln('S - Smazat některá data');
 writeln('P - Přidat nové prvky');
 writeln('K - Končit');
 Readln(R);
 R:=UpCase(R);
 case R of
  'V': Zadej;
  'Z': Vypis;
  'O': Zmen;
  'S': Odstran;
  'P': Pridej;
 end;
 until R='K';
end.

Doufám, že jste na tomto příkladu pochopili výhodnost používání procedur a příkazu case. Nechápete-li seznamy, nic si z toho nedělejte, mně jejich pochopení trvalo rok a půl. Chcete-li seznamy pochopit (a umět s nimi pracovat), kreslete si obrázky - prvek je obdélníček, ze kterého vedou šipky, kam všude ukazuje. Ale hlavně to chce cvik, a proto žádný strach a směle do toho!

Např. jednosměrně zřetězený seznam :
-------         --------        ---------    --------
|     |-------->|      |------->|       |--->|      |----->...
-------         --------        ---------    --------
Nyní se pomocí těchto schémat pokuste přepsat příklad tak, aby pracoval s
jednostranně zřetězeným seznamem. (Nakreslete diagram a zjistěte, co je třeba
udělat, přidáváme-li nový prvek, které ukazatele se musí změnit, kolik pomocných
ukazatelů je třeba...)
Rada : Při přidávání a odebírání vypisujte pomocí Writeln až prvek následující za
Soucasnym, či si udělejte pomocný ukazatel na předchozí proměnnou - u jednostranně
zřetězeného seznamu musíte totiž znát prvek před prvkem vyřazovaným (viz diagram).
Až program přepíšete, čtěte dále.

K řazení seznamu jen krátce - obvykle se používá stejná metoda, jak u pole (porovnáváme dva prvky a ty pak prohodíme. Obvykle se prohazují jen hodnoty, ukazatele se nechávají být). Vytvoříme si pomocnou proměnnou, do které zaznamenáváme počet prohození. Po každém průchodu celého seznamu začneme od začátku (a vynulujeme pomocnou proměnnou). To opakujeme tak dlouho, dokud není seznam seřazen (= hodnota naší pomocné proměnné je nula).

Na závěr si uvědomme, že jednostranný seznam se dá velice dobře uložit ve formě souboru (položku po položce).

To by bylo pro dnešek vše. DCV: Stačí, pochopili-li jste dnešní lekci. Zkuste si napsat seznam spolužáků a jejich sourozenců. (Ukazatel na seznam sourozenců je součástí dynamické proměnné spolužák ==> rozvětvený seznam).