|
Posledná zmena: 25.10.2002
|
24.10.2002
|
Nekonečná rekurzia
- na testovanie rekurzívnych procedúr
môžete využiť pripravený Projekt
- do tohoto projektu môžete pridávať
ďalšie procedúry na ladenie
- v tomto testovacom projekte si všimnite, ako
sa pomocou dvoch pomocných procedúr
push a pop simuluje činnosť zásobníka
- teraz si už pozrite prvú rekurzívnu
procedúru
procedúra xy volá samu seba:
|
procedure TForm1.Button1Click(Sender: TObject);
var
k:TKor;
procedure xy(s:real);
begin
k.Dopredu(s);
k.Vlavo(60);
cakaj(1);
xy(s+1);
end;
begin
k:=TKor.Create;
xy(10);
k.Free;
end;
|
- trasujeme tento program
- z hlavného programu je volanie procedúry
xy => vytvorí sa lokálna premenná
s, ktorej hodnota je 10
- nakreslí sa čiara dĺžky s, korytnačka sa otočí
doľava o 60 stupňov
- znovu sa volá procedúra xy s parametrom
s zmeneným s+1, t.j. vytvorí sa nová
lokálna premenná s=11 (táto premenná
sa vytvára na zásobníku)
- funguje tu známy mechanizmus
volania podprogramov...
- toto sa robí donekonečna, ale časom sa zaplní
pamäť počítača, ktorá je určená
na mechanizmus volania podprogramov (je to niekoľko
100 Kb) hovoríme, že pretečie zásobník - Stack
overflow
- Zásobník (stack) = údajová
štruktúra, ktorá má tieto vlastnosti:
- pridávanie na vrch
- odoberanie z vrchu
- ešte si všimnite k.Free na konci programu
- tento príkaz podobne ako pri bitmapách
uvoľňuje korytnačku (hoci v tomto konkrétnom
príklade sa na tento riadok výpočet
nikdy nedostane)
Chvostová rekurzia (nepravá)
- niekde v tele rekurzívnej procedúry
sa musí nachádzať test, ktorý
zabezpečí, že v niektorých prípadoch
rekurzia skončí
- najčastejšie to budeme riešiť tzv. triviálnym
prípadom - na začiatok podprogramu umiestnime
podmienený príkaz if, ktorý
otestuje triviálny prípad - prípad,
keď už nebude rekurzívne volanie (riešenie
takejto úlohy je už "triviálne")
- v prípade, že if nemá splnenú
podmienku (nie je to triviálny prípad),
vykonajú sa nasledujúce príkazy,
ktoré už budú obsahovať rekurzívne
volanie (riešenie takejto úlohy je netriviálne
- vyžaduje si rekurziu)
špirála:
|
procedure TForm1.Button1Click(Sender: TObject);
var
k:TKor;
procedure spir(s:real);
begin
if s>100 then // nič nerob len skonči
else begin
k.Dopredu(s);
k.Vlavo(60);
cakaj(1);
spir(s+3);
end;
end;
begin
k:=TKor.Create;
spir(10);
k.Free;
end;
|
- trasujme volanie so simuláciou na zásobníku
(pre trasovanie je dobré zvoliť väčšiu počiatočnú
hodnotu s, napr. 95)
- všimnime si, že rekurzívne volanie procedúry
je iba na jednom mieste, za ktorým už nenasledujú
ďalšie
príkazy procedúry => toto volanie
sa dá ľahko prepísať cyklom while
=> hovoríme tomu chvostová rekurzia
(rekurzívne volanie je len na konci procedúry)
prepis pomocou while-cyklu:
|
while s<=100 do begin
k.Dopredu(s);
k.Vlavo(60);
cakaj(1);
s:=s+3;
end;
|
- rekurzia funguje aj bez grafiky
napíšeme procedúru, ktorá vypíše
čísla N, N-1, N-2, ..., 2, 1
|
procedure TForm1.Button1Click(Sender: TObject);
procedure vypis(n:integer);
begin
if n<1 then // nič nerob len skonči
else begin
Memo1.Lines.Add(IntToStr(n));
vypis(n-1);
end;
end;
begin
vypis(30);
end;
|
zistite, ako sa zmení výstup, ak sa
vymení volanie Memo1.Lines.Add a vypis?
|
procedure vypis(n:integer);
begin
if n<1 then // skonči
else begin
vypis(n-1);
Memo1.Lines.Add(IntToStr(n));
end; // už to nie je chvostová rekurzia
end;
|
Pravá rekurzia
špirála:
|
procedure spir(s:real);
begin
if s>100 then k.FP:=clRed // a skonči
else begin
k.Dopredu(s); k.Vlavo(60);
cakaj(1);
spir(s+3);
k.Vpravo(60); k.Dopredu(-s);
cakaj(1);
end;
end;
|
trasujte jej volanie pre
|
spir(189);
|
takéto rekurzívne volanie sa dá
prepísať pomocou dvoch cyklov:
|
pocet:=0;
while s<=100 do begin // čo sa deje pred rekurzívnym volaním
k.Dopredu(s); k.Vlavo(60); s:=s+3; inc(pocet);
cakaj(1);
end;
k.FP:=clRed; // triviálny prípad
while pocet>0 do begin // čo sa deje po vynáraní z rekurzie
s:=s-3; k.Vpravo(60); k.Dopredu(-s); dec(pocet);
cakaj(1);
end;
|
s procedúrou vypis sme sa už
hrali:
|
procedure vypis(n:integer);
begin
if n<1 then // skonči
else begin
Memo1.Lines.Add(IntToStr(n));
vypis(n-1);
Memo1.Lines.Add(IntToStr(n));
end;
end;
|
kde sa vypíšu hviezdičky?
|
procedure vypis(n:integer);
begin
if n<1 then Memo1.Lines.Add('***')
else begin
Memo1.Lines.Add(IntToStr(n));
vypis(n-1);
Memo1.Lines.Add(IntToStr(n));
end;
end;
|
pripomíname známu
procedúru poly:
|
procedure poly(n:integer; s,u:real);
begin
while n>0 do begin
k.Dopredu(s);
k.Vlavo(u);
dec(n);
end;
cakaj(1);
end;
|
- zistite,
čo kreslia procedúry stvorec a stvorec1
procedúra stvorec:
|
procedure stvorec(a:integer);
begin
if a>100 then // nič nerob len skonči
else begin
poly(4,a,90);
stvorec(a+5);
end;
end;
|
procedúra stvorec1:
|
procedure stvorec1(a:integer);
begin
if a>100 then { skonči } k.Vlavo(180)
else begin
poly(4,a,90);
stvorec1(a+5);
poly(4,a,90);
end;
end;
|
- Chceme vypočítať faktoriál prirodzeného
čísla N, pričom vieme, že 0!=1, n!=(n-1)!*n
výpočet faktoriálu
rekurzívnou funkciou:
|
function fakt(n:integer):integer;
begin
if n=0 then Result:=1
else Result:=fakt(n-1)*n;
end;
|
- triviálnym prípadom je tu úloha
vyriešiť napr. 0! - vieme to aj bez rekurzie
- rekurzívne prípady sú také
(neplatí triviálny prípad),
ktoré sú už tak "zložité",
že na ne potrebujeme rekurziu
- hoci toto riešenie nie je chvostová rekurzia
(po rekurzívnom volaní fakt
sa musí ešte násobiť), vieme ho jednoducho
prepísať pomocou cyklu
NDÚ
- zapíšte poly pomocou rekurzie
- bez zápisu cyklu
- zapíšte fibonacciho postupnosť rekurzívnou
funkciou - zistite, koľkokrát sa rekurzívne
zavolá pre dané n
- riešte rekurziou iné známe matematické
funkcie, napr. najväčší spoločný
deliteľ pomocou euklidovho algoritmu, umocňovanie
čísla na celočíselný exponent
pomocou násobenia, výpočet kombinačného
čísla a pod.
Binárne stromy
- nakreslime pomocou korytnačej grafiky
binárny strom úrovne n
- pre n=0 nakreslí sa čiara
- pre n>=1 čiara + binárny strom (n-1) úrovne
+ binárny strom (n-1) úrovne
- vo všetkých úrovniach sú konáre
rovnako dlhé:
binárny strom - podstromy
sa neskracujú:
|
procedure strom(n:integer);
begin
if n=0 then begin
k.Dopredu(30);
k.Dopredu(-30);
end
else
with k do begin
Dopredu(30);
Vlavo(45); // natoč sa na kreslenie ľavého podstromu
strom(n-1); // nakresli ľavý podstrom (n-1). úrovne
Vpravo(90); // natoč sa na kreslenie pravého podstromu
strom(n-1); // nakresli pravý podstrom (n-1). úrovne
Vlavo(45); // natoč sa do pôvodného smeru
Dopredu(-30); // vráť sa na pôvodné miesto
end;
cakaj(1);
end;
|
- pre n=2 sa nakreslí takýto
strom:

binárny strom tak, aby sa vetvy stromu skracovali
na polovicu vo vyšších úrovniach:
|
procedure strom1(n:integer; v:real);
begin
with k do
if n=0 then begin // s hrubšími vetvami
Dopredu(v);
Vpravo(90); Dopredu(1); Vlavo(90);
Dopredu(-v);
end
else begin
Dopredu(v);
Vlavo(45);
strom1(n-1,v/2);
Vpravo(90);
strom1(n-1,v/2);
Vlavo(45);
Dopredu(-v);
end;
cakaj(1);
end;
|
- nakoľko sa korytnačka nevracia presne po tej
istej čiare, ale s malou odchýlkou a táto
odchýlka sa stále zväčšuje, vzniká
tento zaujímavý efekt:

NDÚ:
- riešenie bez rekurzie (budeme to spoločne riešiť
neskôr)
- dokresliť stromu lístočky na posledných
vetvách
- náhodne nastaviť veľkosť podstromu, náhodne
nastaviť uhol vetiev
- podľa úrovne vnorenia meniť hrúbku
pera
- strom, ktorý sa rozvetvuje nie na dve
ale na tri časti sa nazýva ternárny
ternárny strom:
|
procedure strom3(n:integer; a:real);
var
i:integer;
begin
if n=1 then
poly(2,a,180)
else
with k do begin
Dopredu(a); Vlavo(45);
for i:=1 to 3 do begin
strom3(n-1,a/2);
Vpravo(45);
end;
Vlavo(90); Dopredu(-a);
end;
cakaj(1);
end;
|
- Napíšeme procedúru, ktorá nakreslí
obrázok sss úrovne N, veľkosti a
- n=0 (nerobí nič)
- n=1 štvorec so stranou dĺžky a
- n>1 štvorec, v ktorého každom rohu (dnu)
je obrázok sss úrovne N-1 veľkosti a/3
štvorce v každom rohu štvorca:
|
procedure sss(n:integer; a:real);
var
i:integer;
begin
if n=0 then // nerob nič len skonči
else
for i:=1 to 4 do begin
k.Dopredu(a);
k.Vlavo(90);
cakaj(1);
sss(n-1,a/3); // skúste: sss(n-1,a*0.45);
end;
end;
|
- všimnite si, že to nie je chvostová rekurzia
- obrázok vyzerá takto (druhý
je pre rekurzívne volanie sss(n-1,a*0.45)):

- klasická rekurzívna krivka
snehová
vločka:
|
var
k:TKor;
procedure vlocka(n:integer; s:real);
begin
if n=0 then k.Dopredu(s)
else begin
vlocka(n-1,s/3);
k.Vpravo(60);
vlocka(n-1,s/3);
k.Vlavo(120);
vlocka(n-1,s/3);
k.Vpravo(60);
vlocka(n-1,s/3);
end;
cakaj(1);
end;
var
i:integer;
begin
k:=TKor.Create(300,350);
for i:=1 to 3 do begin
vlocka(3,300);
k.Vlavo(120);
end;
k.Free;
end;
|

- dorobte do cyklu zmenu farby pera pred každým
volaním vlocka(...) - budete lepšie
vidieť, čo kreslí rekurzívna procedúra
- zistite, ako sa zmení obrázok
snehovej vločky, keď v cykle nahradíme k.Vlavo(120)
príkazom k.Vpravo(120)
Fraktály
C-krivka:
|
var
k:TKor;
procedure ckrivka(n:integer; s:real);
begin
if n=0 then k.dopredu(s)
else begin
ckrivka(n-1,s); k.vlavo(90);
ckrivka(n-1,s); k.vpravo(90);
end;
cakaj(1);
end;
begin
k:=TKor.Create(200,100);
ckrivka(10,2); // skúste: ckrivka(13,2);
k.Free;
end;
|

- Dračia krivka sa skladá z dvoch "zrkadlových"
procedúr: ldrak a pdrak
- problém je pri ich vzájomnom volaní
- môžeme to vyriešiť buď
pomocou direktívy forward:
|
var
k:TKor;
procedure pdrak(n:integer; s:real); forward;
procedure ldrak(n:integer; s:real);
begin
if n=0 then k.dopredu(s)
else begin
ldrak(n-1,s);
k.vlavo(90);
pdrak(n-1,s);
end;
cakaj(1);
end;
procedure pdrak(n:integer; s:real);
begin
if n=0 then k.dopredu(s)
else begin
ldrak(n-1,s);
k.vpravo(90);
pdrak(n-1,s);
end;
cakaj(1);
end;
begin
k:=TKor.Create(300,200);
ldrak(11,5);
k.Free;
end;
|
vzájomné vnorenie procedúr:
|
var
k:TKor;
procedure ldrak(n:integer; s:real);
procedure pdrak(n:integer; s:real);
begin
if n=0 then k.dopredu(s)
else begin
ldrak(n-1,s);
k.vpravo(90);
pdrak(n-1,s);
end;
cakaj(1);
end;
begin
if n=0 then k.dopredu(s)
else begin
ldrak(n-1,s);
k.vlavo(90);
pdrak(n-1,s);
end;
cakaj(1);
end;
begin
k:=TKor.Create(300,200);
ldrak(11,5);
k.Free;
end;
|
- všimnite si, že korytnačka pri kreslení krivky,
neprejde po žiadnej čiare viackrát
- krivku môžeme kresliť len jednou procedúrou,
ktorá má o jeden parameter viac a to,
či je ľavá alebo pravá:
dračia krivka jednou procedúrou
|
var
k:TKor;
procedure drak(n:integer; s:real; u:integer);
begin
if n=0 then k.dopredu(s)
else begin
drak(n-1,s,90);
k.vlavo(u);
drak(n-1,s,-90);
end;
cakaj(1);
end;
begin
k:=TKor.Create(200,100);
drak(13,3,90);
k.Free;
end;
|

Hilbertova krivka sa tiež skladá z dvoch zrkadlových
častí – definujeme ich jednou procedúrou:
|
var
k:TKor;
procedure hilbert(n:integer; s:real; u:integer);
begin
if n>0 then begin
k.vlavo(u);
hilbert(n-1,s,-u); k.dopredu(s); k.vpravo(u);
hilbert(n-1,s,u); k.dopredu(s);
hilbert(n-1,s,u); k.vpravo(u); k.dopredu(s);
hilbert(n-1,s,-u); k.vlavo(u);
cakaj(1);
end;
end;
begin
k:=TKor.Create(400,400);
hilbert(5,10,90); // skúste: hilbert(7,2,90);
k.Free;
end;
|

|

© 2002 AB, KVI blaho@fmph.uniba.sk
|