14.11.2002
|
čo sa budeme dnes učiť
- ukážeme, ako sa dá aj veľmi
zložitá rekurzívna procedúra
prepísať do nerekurzívneho tvaru
- využijeme na to zásobník
- zásobník použijeme aj na spracovanie
rôznych typov aritmetických výrazov
- uvidíme, čo sú výrazy
v infixovom, prefixovom a postfixovom
tvare
- zadefinujeme ešte jednu údajovú
štruktúru: rad (front) - podobá
sa na zásobník s tým rozdielom,
že hoci hodnoty vkladáme na koniec radu,
vyberáme ich zo začiatku radu
Prepis rekurzie na nerekurziu
Máme rekurzívnu korytnačiu metódu,
ktorá kreslí špirálu:
|
type
TMojaKor = class(TKor)
procedure spir(n:integer; s:real);
end;
procedure TMojaKor.spir(n:integer; s:real);
begin
if n=0 then Vlavo(180)
else begin
Dopredu(s); Vlavo(90);
spir(n-1,s+1);
Vpravo(90); Dopredu(-s);
end;
end;
|
- pomocou zásobníka vytvoríme
nerekurzívnu metódu
- najprv zadefinujeme triedu zásobník
pre našu konkrétnu úlohu: prvok zásobníka
bude záznam, ktorý obsahuje položky v-"adresa",
n a s - zapamätávané hodnoty
- tiež upravíme metódy push a pop tak,
aby sa nám so zásobníkom pracovalo
pohodlnejšie
prispôsobená trieda TStack:
|
type
TPrvok = record v,n:integer; s:real; end;
TStack = class
st:array of TPrvok;
...
procedure push(vv,nn:integer; ss:real);
procedure pop(var vv,nn:integer; var ss:real);
...
end;
procedure TStack.push(vv,nn:integer; ss:real);
begin
SetLength(st,Length(st)+1);
with st[high(st)] do begin
v:=vv; n:=nn; s:=ss;
end;
end;
procedure TStack.pop(var vv,nn:integer; var ss:real);
begin
if empty then chyba('Prázdny zásobník');
with st[high(st)] do begin
vv:=v; nn:=n; ss:=s;
end;
SetLength(st,Length(st)-1);
end;
|
prepíšme procedúru na nerekurzívnu
pomocou nášho nového zásobníka:
|
type
TMojaKor = class(TKor)
procedure spirN(n:integer; s:real);
end;
procedure TMojaKor.spirN(n:integer; s:real);
var
z:TStack;
v:integer; // niečo ako "návratová adresa"
begin
z:=TStack.Create;
z.push(1,n,s);
while not z.empty do begin
z.pop(v,n,s);
case v of
1:
if n=0 then Vlavo(180)
else begin
Dopredu(s); Vlavo(90);
z.push(2,n,s); // návrat po "rekurzívnom volaní"
z.push(1,n-1,s+1); // "rekurzívne volanie"
end;
2:
begin
Vpravo(90); Dopredu(-s);
end;
end;
end;
z.Free; // mal by existovať deštruktor TStack.Destroy
end;
procedure TForm1.Button1Click(Sender: TObject);
var
k:TMojaKor;
begin
k:=TMojaKor.Create;
k.spirN(100,1);
k.Free;
end;
|
procedúra sa dá upraviť tak, aby sa
nerobila operácia push, ak to nie je nevyhnutné:
|
procedure TMojaKor.spirN(n:integer; s:real);
var
z:TStack;
v:integer;
begin
z:=TStack.Create; v:=1;
while v<>0 do begin
case v of
1:
if n=0 then begin
Vlavo(180); v:=0; // bude treba robiť pop
end
else begin
Dopredu(s); Vlavo(90);
z.push(2,n,s);
v:=1; n:=n-1; s:=s+1;
end;
2:
begin
Vpravo(90); Dopredu(-s); v:=0; // bude treba robiť pop
end;
end;
if (v=0) and not z.empty then z.pop(v,n,s);
end;
z.Free;
end;
|
Príklad:
prepíšeme rekurzívnu
procedúru na kreslenie stromu pomocou korytnačej
grafiky na nerekurzívnu:
|
procedure TMojaKor.strom(n:integer; s:real);
begin
// časť 1:
if n=0 then begin
Dopredu(s); Dopredu(-s);
end
else begin
Dopredu(s); Vlavo(45);
strom(n-1,s*0.6);
// časť 2:
Vpravo(90);
strom(n-1,s*0.7);
// časť 3:
Vlavo(45); Dopredu(-s);
end;
end;
|
nerekurzívna verzia:
|
procedure TMojaKor.stromN(n:integer; s:real);
var
z:TStack;
v:integer;
begin
z:=TStack.Create; z.push(1,n,s);
while not z.empty do begin
z.pop(v,n,s);
case v of
1:
if n=0 then begin
Dopredu(s); Dopredu(-s);
end
else begin
Dopredu(s); Vlavo(45);
z.push(2,n,s);
z.push(1,n-1,s*0.6);
end;
2:
begin
Vpravo(90);
z.push(3,n,s);
z.push(1,n-1,s*0.7);
end;
3:
begin
Vlavo(45); Dopredu(-s);
end;
end;
end;
z.Free;
end;
|
- Touto "zásobníkovou" metódou
nevieme robiť funkcie. V nasledujúcom príklade
si to ukážeme na Fibonacciho postupnosti. Definovaná
je rekurentne takto:
F(0)=1; F(1)=1; F(N)=F(N-1)+F(N-2) pre N>1
rekurzívna procedúra,
ktorá počíta N-té Fibonacciho číslo:
|
var
ff:integer; // globálna premenná, v ktorej je vypočítaná hodnota
procedure fib(n:integer);
var
f:integer;
begin
//časť 1:
if n<2 then ff:=1
else begin
fib(n-1);
//časť 2:
f:=ff;
fib(n-2);
//časť 3:
ff:=ff+f;
end;
end;
|
nerekurzívny tvar predchádzajúcej
procedúry (TPrvok v TStack je už teraz iného
typu):
|
procedure fibN(n:integer);
var
f,v:integer;
z:TStack;
begin
z:=TStack.Create; z.push(1,n,f);
while not z.empty do begin
z.pop(v,n,f);
case v of
1:
if n<2 then ff:=1
else begin
z.push(2,n,f); // návrat
z.push(1,n-1,f); // rekurzívne volanie
end;
2:
begin
f:=ff; z.push(3,n,f); // návrat
z.push(1,n-2,f); // fib(n-2)
end;
3: ff:=ff+f;
end;
end;
z.Free;
end;
|
Príklad:
napíšeme nerekurzívny
variant procedúry na kreslenie snehovej vločky:
|
procedure TMojaKor.vlocka(n:integer; s:real);
begin
if n=0 then Dopredu(s)
else begin
vlocka(n-1,s/3);
Vpravo(60);
vlocka(n-1,s/3);
Vlavo(120);
vlocka(n-1,s/3);
Vpravo(60);
vlocka(n-1,s/3);
end;
end;
|
nerekurzívna verzia:
|
procedure TMojaKor.vlockaN(n:integer; s:real);
var
z:TStack;
v:integer;
begin
z:=TStack.Create; z.push(1,n,s);
while not z.empty do begin
z.pop(v,n,s);
case v of
1:
if n<=0 then Dopredu(s)
else begin
z.push(2,n,s); z.push(1,n-1,s/3);
end;
2:
begin
Vpravo(60); z.push(3,n,s); z.push(1,n-1,s/3);
end;
3:
begin
Vlavo(120); z.push(4,n,s); z.push(1,n-1,s/3);
end;
4:
begin
Vpravo(60); z.push(1,n-1,s/3);
end;
end;
end;
z.Free;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
k:TMojaKor;
i:integer;
begin
k:=TMojaKor.Create;
for i:=1 to 3 do begin k.vlockaN(4,100); k.vlavo(120); end;
k.Free;
end;
|
Aritmetické výrazy
- v matematike aj v programovaní je zvykom používať
tzv. infixový zápis aritmetických
výrazov; je definovaný nasledovne:
- operand (napr. číslo alebo premenná)
je infixový výraz
- ak X a Y sú infixové výrazy,
tak infixovými výrazmi sú aj
-X (X) X+Y X-Y X*Y X/Y
- pri vyhodnocovaní sa využíva obvyklá
precendencia operátorov
- v istých (programátorských)
aplikáciách sa využíva aj Poľský
zápis, buď prefix alebo postfix (tzv. prevrátený
Poľský zápis)
Prefix:
- operátor je pred operandami
- napr. "+ 1 2" môžeme prečítať
ako "sčítaj 1 a 2"
Postfix:
- operátor je za operandami
- napr. "1 2 +" môžeme prečítať
ako "zober 1 a 2; potom ich sčítaj"
Poľské formy majú oproti infixu tú
výhodu, že nepotrebujeme zátvorky na určenie
precendencie operátorov - vždy je jednoznačne
dané poradie vyhodnocovania.
Vďaka tomu je aj vyhodnocovanie takýchto výrazov
veľmi jednoduché (pomocou zásobníka).
Napr.
- pre infix: ( 3 + 4 ) * 5
- prefix: * + 3 4 5 {
používa sa v iných jazykoch, napr. LISP
}
- postfix: 3 4 + 5 *
Problémy s unárnym operátorom
"-", treba ho rozlíšiť od binárneho
"-", zvykne sa značiť iným znamienkom
(napr. s bodkou alebo "_").
program na vyhodnotenie postfixu načítaného
zo vstupu:
|
...
uses StackUnit; // type TPrvok = integer;
...
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
var
z:TStack;
x,y,i:integer;
s:string;
begin
if Key<>#13 then exit;
z:=TStack.Create;
s:=Edit1.Text; Edit1.Text:='';
i:=1;
while i<=Length(s) do begin
case s[i] of
'0'..'9':
begin
x:=0;
while s[i] in ['0'..'9'] do begin
x:=x*10+ord(s[i])-ord('0'); inc(i);
end;
z.push(x); dec(i);
end;
'_': begin z.pop(x); z.push(-x) end; // unárne mínus
'+': begin z.pop(x); z.pop(y); z.push(y+x) end;
'-': begin z.pop(x); z.pop(y); z.push(y-x) end;
'*': begin z.pop(x); z.pop(y); z.push(y*x) end;
'/': begin z.pop(x); z.pop(y); z.push(y div x) end;
end;
inc(i)
end;
z.pop(x); z.Free;
Memo1.Lines.Add(s+' = '+IntToStr(x));
end;
|
NDÚ:
- upravte program na postfix s reálnou aritmetikou
- vyhodnotiť prefix
- prepis infixu do postfixu, resp. prefixu (nie je
to triviálne)
- úplne uzátvorkovaný výraz:
je infix, v ktorom je každá operácia uzátvorkovaná,
napr.
3*4+5*6-7*8 -> (((3*4)+(5*6))-(7*8)) kvôli precendencii operátorov
- urobte prevody z/do úplne uzátvorkovaného
do/z pre/post-fixu
- vyhodnoťte úplne uzátvorkovaný
výraz
- prepis prefixu do postfixu a naopak
- vyhodnocujte výrazy s premennými, príp.
s unárnymi funkciami (spracujú sa rovnako
ako unárne mínus)
RAD - QUEUE, FRONT, FIFO
- dátová štruktúra, postupnosť
položiek zoradených za sebou,
- ak príde nová položka, pridá
sa na koniec radu (rear, tail, chvost)
- vyberať položky zo začiatku radu (front, head, hlavička)
- metódy:
- serve = obslúž
- append = pridaj na koniec
QueueUnit.pas:
|
unit QueueUnit;
interface
type
TQPrvok = string;
TQueue = class
q:array of TQPrvok;
constructor Create;
procedure serve(var p:TQPrvok);
procedure append(p:TQPrvok);
function empty:boolean;
end;
implementation
uses
Dialogs;
procedure chyba(const s:string);
begin
ShowMessage(s); halt;
end;
constructor TQueue.Create;
begin
q:=nil;
end;
procedure TQueue.serve(var p:TQPrvok);
begin
if empty then chyba('Rad je prazdny');
p:=q[0];
q:=copy(q,1,maxint);
end;
procedure TQueue.append(p:TQPrvok);
begin
SetLength(q,Length(q)+1);
q[High(q)]:=p;
end;
function TQueue.empty:boolean;
begin
Result:=q=nil;
end;
end.
|
- Napíšeme program, ktorý číta
textový súbor iba raz a vytvára
nový súbor tak, že v tomto súbore
sú všetky riadky vstupného súboru
a za nimi ešte raz tie isté riadky:
zdvojený súbor:
|
...
uses
QueueUnit;
...
procedure TForm1.Button1Click(Sender: TObject);
var
q:TQueue;
f,g:TextFile;
t:TQPrvok;
begin
q:=TQueue.Create;
AssignFile(f,'Unit1.pas'); Reset(f);
AssignFile(g,'test.pas'); Rewrite(g);
while not eof(f) do begin
readln(f,t); q.append(t); writeln(g,t);
end;
while not q.empty do begin
q.serve(t); writeln(g,t)
end;
CloseFile(g); CloseFile(f);
q.Free;
Memo1.Lines.Add('hotovo');
end;
|
NDÚ:
- prerobte triedu TQueue tak, aby sa nie pri každej
operácii append alebo serve musela meniť veľkosť
dynamického poľa q, ale aby sa podobne ako pri
zásobníku pamätal aktuálny
počet a zmena veľkosti poľa sa teda robila menej často
- reprezentácia radu v poli:
- cyklické pole - tvárime sa, že statické
pole, čo máme k dispozícii, je cyklické,
t.j. keď by chvost prišiel na koniec poľa, presunieme
ho znovu na začiatok
- problémy s rozpoznaním prázdneho
a plného radu, riešenie - pamätáme
si počet položiek
- prerobte triedu TQueue tak, aby ste využili cyklické
pole (musíte dodefinovať aj metódu full)
- rozmýšľať o viac zásobníkoch
a radoch, napr. vzájomná simulácia,
triedenie pomocou dvoch zásobníkov, zarážka
vo fronte, ...
|