Sunteți pe pagina 1din 6

Recursivitate

1. Noţiunea de algoritm recursiv


Un algoritm recursiv se caracterizează prin proprietatea că se auto-apeleaza, adică din interiorul
lui se apelează pe el însuşi. Din afara algoritmului facem un prim apel al acestuia, după care algoritmul
se auto-apeleaza de un anumit număr de ori: la fiecare nouă auto-apelare a algoritmului, se execută din
nou secvenţa de instrucţiuni ce reprezintă corpul său, eventual cu alte date, creându-se un aşa-numit
„lanţ de auto-apeuri recursive”.
Intuitiv, putem spune că un algoritm recursiv are acelaşi efect ca şi un ciclu: repetă execuţia unei
anumite secvenţe de instrucţiuni. Dar, la fel ca în cazul unui ciclu, este necesar ca repetarea să nu aiba
loc la infinit. De aceea, în corpul algoritmului trebuie să existe cel puţin o testare a unei condiţii de
oprire, la îndeplinirea căreia se întrerupe lanţul de auto-apeluri.
Majoritatea algoritmilor repetitivi se pot implementa atât in variantă nerecursivă (care se mai
numeşte si iterativă), folosind cicluri, cât şi în variantă recursivă. Rămâne în sarcina programatorului să
aleagă între implementarea iterativă şi cea recursivă, cântarind avantajele şi dezavantajele fiecăreia, de la
caz la caz. Varianta recursivă este recomandată în special pentru problemele definite prin relaţii de
recurenţă, care permit o formulare a rezultatelor mult mai clară şi mai concisă. Pe de altă parte,
funcţionarea algoritmilor recursivi este în general mai greu de urmărit, şi, în plus, aceştia necesită un
timp de execuţie mai lung şi un spaţiu de memorie mai mare.
Extinzând definiţia, vom numi funcţie recursivă, o funcţie care din corpul ei se apelează pe ea
însăşi. Dar orice funcţie recursivă trebuie să îndeplinească două cerinţe:
 să se poată executa cel puţin o dată fără a se auto-apela;
 toate auto-apelurile să se producă astfel încat să se tindă spre îndeplinirea condiţiei de execuţie
fără auto-apelare.

2. Exemple de algoritmi recurisivi. Relaţii de recurenţă

Şiruri definite prin relaţii de recurenţă


Un şir a1, a2 , ... ,an , ... este o succesiune de valori numite elementele şirului, aranjate într-o
ordine bine definită. Fiecare element ocupă în cadrul şirului o poziţie fixată, care se numeşte rangul
elementului.
Unele şiruri pot fi definite cu ajutorul unor formule care exprimă orice termen al şirului, începând
cu un anumit rang, în funcţie de termenul precedent sau în funcţie de termenii precedenţi. O astfel de
formulă se numeşte relaţie de recurenţa. Pentru a putea defini recurent un şir, mai trebuie sa indicăm
primul termen sau primii termeni.
Şirul lui Fibonacci: este un şir de numere întregi ( F1 ,F2 , ..., Fn ,...), definit recurent astfel: primii
doi termeni sunt egali cu 1, apoi, fiecare termen începând cu al treilea, este egal cu suma dintre
precedentul si anteprecedentul său.
Pentru un termen oarecare Fk (termenul de rang k), precedentul sau este Fk-1 (de rang k-1), iar
antecedentul său este Fk-2 (de rang k-2). Astfel, F1=1, F2=1 şi Fk=Fk-1+Fk-2 ∀ k ≥ 3.
De exemplu: F3=F2+F1=1+1=2 (pentru k=3), F4=F3+F2=2+1=3 (pentru k=4), F5=F4+F3=3+2=5
(pentru k=5), etc. Se obţine şirul 1, 1, 2, 3, 5, 8, 13, 21, 34,...
Pentru o descriere completă scriem o relaţie de recurenţă 1, pentru k = 1 şi 2
Fk = 
care înglobează atât formula de calcul, cât şi valorile termenilor  Fk −1 + Fk − 2 , pentru k ≥ 3
definiţi separat:
Caracterul recursiv al algoritmului pentru determinarea termenilor şirului lui Fibonacci este evident.
Pentru a calcula un termen oarecare Fk , avem nevoie de termenii precedenţi Fk-1 şi Fk-2. Dar aflarea
termenilor Fk-1 şi Fk-2 se poate face cu acelaşi algoritm, doar că în loc de k avem k-1 respectiv k-2. Prin
urmare, algoritmul care calculează termenul Fk trebuie să se auto-apeleze de două ori, în scopul
determinării termenilor Fk-1 şi Fk-2.
1
3. Relaţii de recurenţă pentru expresii matematice

Nu numai pentru şiruri putem defini relaţii de recurenţă, ci si pentru expresii matematice.
Factorialul unui număr natural. Factorialul unui număr natural k este k!=1•2•3•...•(k-1)•k (produsul
numerelor naturale până la k), care se mai poate scrie k!=k•(k-1)•...•3•2•1. Dar (k-1)•...•3•2•1 este
tocmai (k-1) ! (produsul numerelor naturale până la k-1). De aici se deduce o aşa numită relaţie de
recurenţă: k!=k•(k-1) ! . Observăm însă că factorialul lui 0 nu se poate calcula cu relaţia anterioară,
acesta fiind un caz care trebuie tratat separat. Folosind faptul că 0!=1 (definit matematic), obţinem relaţia
de recurenţa completă:

1 , pentru k = 0 Caracterul recursiv constă în faptul că din corpul


k!=  algoritmului care calculează k! se auto-apelează algoritmul
k ⋅ (k-1 )!, pentru k > 0 pentru a calcula (k-1)!.

Suma cu n termeni: suma printre n numere naturale impare


De exemplu pentru n=5, şirul primelor cinci numere naturale impare este(1, 3, 5, 7, 9), iar suma
acestora este S5=1+3+5+7+9. Observăm că se poate stabili o corespondenţă între rangul unui termen şi
valoarea sa. Astfel:
• primul termen, cu rangul 1, este 1, care se mai poate scrie 2*1-1;
• al doilea termen, cu rangul 2, este 3, care se mai poate scrie 2*2-1;
......................................................................................................................
• ultimul termen, cu rangul n=5, este 9, adică 2*5-1, adică 2*n-1.

Pe caz general, şirul primelor n numere naturale impare este (1, 3, 5,.., 2n-1). Notând termenii
şirului cu a1, a2, ..., an , observăm că un termen oarecare ak (de rang k) are valoarea 2*k-1. Vom spune că
şirul de mai sus este definit prin formula termenului general ak=2*k-1.
Suma primelor n elemente naturale este Sn=a1+a2+...+an=1+3+5+...+2n-1.
Dacă al n-lea termen, cel de rang n, este 2*n-1, atunci al (n-1)-ulea termen este 2(n-1)-1, adică
2*n-3 (pur şi simplu am înlocuit pe n cu n-1). Astfel, Sn=1+3+5+...+(2*n-3)+(2*n-1). Dar 1+3+5+...
+(2*n-3) reprezintă suma primelor n-1 numere naturale impare notata Sn-1, deci Sn = (2n-1)+ Sn-1 . Pentru
n=0, avem cazul particular S0=0. Obţinem astfel relaţia de recurenţă completă :

0, pentru n = 0
Sn= (2n-1 ) + S
 n −1 , pentru n > 0

Şi această relaţie generează un algoritm recursiv: pentru a calcula suma Sn, avem nevoie de suma Sn-1.
Aplicaţii:
1.Deduceţi formula termenului general pentru şirurile de mai jos:
1 2 3 4
a) 2, 4, 8, 16, ... b) 1, 5, 9, 13, ... c) , , , , ... d)* 0, 1, 1, 3, 7, 17, 41 ...
2 3 4 5
2. deduceţi o relaţie de recurenţă care să definească următoarele expresii:
a) produsul primelor n numere naturale pare p=2*4*6*.....*(2n);
b) suma primelor n numere naturale S=1+2+3+...+n;
c) expresia E=1+4+7+...+(3n-2);
d) expresia F=3*7*11*...... *(4n-1).

3. Deduceţi termenii următoarelor şiruri definite prin relaţii de recurenţă:


a) an+1=1+a 2n ( ∀ ) n ≥ 1, a0=1
b) xn=2* xn-1+xn-2 ( ∀ ) n ≥ 2, x0=0, x1=1

2
4. Rolul stivei in executarea subprogramelor recursive

Stiva este o succesiune ordonata de elemente, delimitate prin doua capete, in care adaugarea si
eliminarea elementelor se poate face pe la un singur capat, numit varful stivei. In orice moment putem
scoate din stiva doar elemental care a fost introdus ultimul, motiv pentru care spunem ca stiva
functioneaza dupa principiul LIFO(“Last In First Out”, in traducere “ Ultimul intrat primul iesit”).
Altfel spus, extragerea valorilor din stiva se face in ordine inversa introducerii lor.
Limbajul C++ dispune de propria sa stiva, numita stiva interna, gestionata de catre compilator,
care ocupa o parte din memoria interna rezervata programului. Orice functie foloseste aceasta stiva
atunci cand se executa.
In momentul in care o functie P apeleaza o functie S, se salveaza automat pe stiva interna adresa
de revenire si contextual modulului apelant P( care cuprinde totalitatea variabilelor locale si a
parametrilor transmisi prin valoare). Odata cu inchiderea executiei modulului apelat S, se revine in P,
restaurandu-se valorile salvate pe stiva interna.
In cazul unei functii recursive (care este atat modulul apelant cat si modulul apelat), acest
mecanism al stivei este de foarte mare importanta: atunci cand se executa un lant de auto-apeluri
recursive, la fiecare auto-apel variabilele locale si parametri functiei recursive se salveaza pe stiva, iar la
revenirea in ordine inversa din lant aceste valori se restaureaza de pe stiva.

5. Functii recursive

Functii recursive care returneaza o valoare

Aplicatie rezolvată: Calculul factorialului unui numar natural.


• Fiind dat un numar natural n, sa se afiseze n!, folosind o functie recursiva.
Reamintim : n!= 1 * 2 *….*n. De exemplu, 4!= 1*2*3*4=24
Algoritmul in varianta ne-recursiva
Valoarea lui n!, se calculeaza ca un produs in variabila p. Initializam p cu 1. Intr-un ciclu, in variabila k
parcurgem pe rand toate numerele naturale de la 1 la n (valoarea initiala a contorului este k=1, ciclul se
executa cat timp k<=n, iar trecerea la pasul urmator se face prin incrementarea k++). La fiecare pas
inmultim cu p valoarea curenta a lui k (p=p*k, relatie care se poate scrie si sub forma p*=k).
cin >> n; p=1;
for(k=1; k<=n; k++)
p=p*k;
cout<< “\n”<<p; 1 , pentru k = 0
Varianta recursivă: Plecăm de la relaţia de recurenţă: k!= 
k ⋅ (k - 1)! , pentru k > 0
Vom scrie o funcţie long fact(int k) care primeşte ca parametru un
k intreg şi returnează k!. Observăm că pentru a calcula k!, avem
nevoie de valoarea lui (k-1)! Care apoi se înmulţeşte cu k. Funcţia recursivă fact(k) pentru a returna
factorialul lui k ar trebui să se auto-apeleze cu parametrul k-1, apoi valoarea returnată de fact(k-1),
respectiv (k-1)!, trebuie înmulţită cu k. Aşadar am obţinut până acum relaţia:
fact(k)=k*fact(k-1)
Dar fact(k-1) se execută la fel, generând un nou auto-apel, ş.am.d. se pune însă problema opririi lanţului
de apeluri recursive la un moment dat. În corpul funcţiei trebuie să existe o condiţie care, atunci când
devine adevărată, întrerupe auto-apelurile. Aceasta este dată tocmai de cazul particular k=0, când funcţia
va returna direct valoarea 1 (0!=1). Scriem acum relaţia de recurenţă sub o altă formă, referitoare la
funcţia fact:

1 , pentru k = 0 Exemplu : pentru k=3, sintetizând lanţul recursiv obţinem:


fact(k)=  fact(3)=3*fact(2)=3*2*fact(1)=3*2*1*fact(0)=3*2*1*1=6
k * fact ( k - 1) , pentru k > 0

3
Programul este:
#include<iostream.h>
#include<conio.h>
int n; Comparaţie între varianta ne-recursivă şi varianta recursivă:
long fact(int k) • În varianta ne-recursivă, prin contorul k al ciclului vor trece pe
{ if(k==0) rând numerele care se înmulţesc, 1,2,3,…,n-1,n.
return 1; • În varianta recursivă, aceste numere vor fi succesiv valorile
else parametrului formal k, dar în ordine inversă: n,n-1,….,2,1. astfel:
return k*fact(k-1);  Prima valoare a lui k este n, dată de apelul extern fact(n);
}  La fiecare execuţie a funcţiei fact, valoarea lui k se
void main() decrementează cu 1 prin auto-apelul fact(k-1);
{ cout<<”n=”; cin>>n;  Lanţul recursiv are loc până când valoarea lui k ajunge la 0.
cout<<fact(n);
getch();
}

Aplicatie rezolvată:Şirul lui Fibonacci


Să se afişeze al n-ulea termen al şirului lui Fibonacci, folosind o funcţie recursivă. Şirul are primii doi
termeni egali cu 1 şi fiecare din următorii termeni este egal cu suma dintre termenul precedent şi
termenul antepreced
Algoritmul in varianta recursiva
Algoritmul in varianta ne-recursiva #include<iostream.h>
#include<iostream.h> int n;
void main() long fib(int k)
{ int F[20], n, k; {
cout<<”n=”; cin>>n; if(k==1 || k==2) return 1;
F[1]=F[2]=1; else return fib(k-1)+fib(k-2);
for(k=3; k<=n; k++) }
{ F[k]=F[k-1]+F[k-2]; void main()
cout<<F[k]<<” “; {
} cout<<”n=”; cin>>n;
if(n>0)
Comparaţie între varianta ne-recursivă şi
cout<<”Termenul de rang”<<n<<”este”<<fib(n);
varianta recursivă:
}
• În varianta recursivă am afişat
un singur termen, spre deosebire de varianta nerecursivă în care se afişau toţi termenii până la Fn . mai
mult decât atât, chiar şi pentru a afişa un singur termen, funcţia fib se auto-apelează de mai multe ori cu
acelaţi parametru. Pentru exemplificare, prezentăm lanţul de apeluri recursive în cazul n=5, unde se
observă că apelurile fib(3) şi fib(1) se execută de două ori, iar fib(2) de trei ori.
fib(5)=fib(4)+fib(3)=fib(3)+fib(2)+fib(3)=
=fib(2)+fib(1)+fib(2)+fib(2)+fib(1)=
=1+1+1+1+1=5
fib(5)
fib(4) fib(3)
fib(3) fib(2) fib(2) fib(1)
fib(2) fib(1)

În problemele în care nu este necesară memorarea tuturor termenilor, este preferabilă varianta recursivă.

4
Aplicatie rezolvată: Cel mai mare divizor comun
Să se scrie o funcţie recursivă pentru calculul celui mai mare divizor comun a două numere naturale a şi
b, folosind algoritmul lui Eucluid.
Varianta ne-recursiva: Algoritmul lui Euclid cu diferenţe pentru determinarea c.m.md.c a două numere
naturale a şi b se bazează pe scăderi care se repetă într-un ciclu. La fiecare pas, se modifică prin aceste
scăderi, fie a,fie b. Dacă a>b se modifică a prin scăderea a←a-b, iar dacă a<b se modifică b prin
scăderea b←b-a. Când cele două numere a şi b au devenit egale se opreşte ciclul, obţinându-se
c.m.m.d.c. care este a (sau b). Deci ciclul se repetă cât timp a este diferit de b.
Obs: Algoritmul prezentat nu va funcţiona dacă cel puţin unul dintre numere este negativ sau zero.
Pentru a evita aceste două cazuri, poate fi pusă condiţia suplimentară „(a>0) SI (b>0)”.
#include<iostream.h>
void main() Exemplu: Fie a=15; b=6
{ int a,b; Pas 1: a!=b ? 15!=6 ? da a>b ? 15>6 ?
cout<<”a=”; cin>>a; da=> a←a-b =>a=15-6, a=9;
cout<<”b=”; cin>>b; Pas 2: a!=b ? 9!=6 ? da a>b ? 9>6 ?
if(a>0 && b>0) da=> a←a-b =>a=9-6, a=3;
{while(a!=b) Pas 3: a!=b ? 3!=6 ? da a>b ? 3>6 ?
if(a>b) a=a-b; nu=> b←b-a =>b=6-3, b=3;
else b=b-a; Pas 4: a!=b ? 3!=3 ? nu => iese din
cout<<”cmmdc=”<<a;} ciclu
else cout<<”nu pot calcula cmmdc”; c.m.m.d.c. (a,b) =a=b=3
}
Varianta recursiva:
• Dacă a<b atunci variabilei a i se atribuie valoarea expresiei a-b. În varianta recursivă, a este
parametru al funcţiei recursive cmmdc. Pentru ca a să ia valoarea lui a-b,este suficient ca din interiorul
funcţiei cmmdc(a,b), să se auto-apeleze cmmdc(a-b, b). Astfel, parametrul formal a este înlocuit cu
parametrul actual a-b, sau altfel spus, „a-b devine noul a”(al doilea parametru b rămâne nemodificat).
• Dacă a>b atunci b trebuie sa ia valoarea expresiei b-a, lucru care recursiv se produce printr-un auto-
apel: din functia cmmdc(a,b) se auto-apeleaza cmmdc(a, b-a) (primul parametru a rămânând
nemodificat).
• Mai rămâne să stabilim condiţia care încheie lanţul de auto-apeluri recursive. Lanţul recursiv se
încheie în momentul în care parametrii a şi b au devenit egali, caz în care valoarea lor comună reprezintă
cel mai mare divizor comun căutat.
a, daca a = b
• Pe baza acestor observaţii, definiţia recursivă a 
cmmdc(a,b)= cmmdc (a − b, b), daca a > b
algoritmului este evidentă: cmmdc (a, b − a ), daca a < b
• Funcţia recursivă int cmmdc(int a, int b), care 
returnează cel mai mare divizor comun al numerelor a şi b, transpune efectiv într-o instrucţiune if
această definiţie:
#include<iostream.h>
int a,b; Ex: a=15, b=6.
int cmmdc(int a, int b)  În programul principal facem apelul cmmdc(15, 6);
{ if(a==b) return a;
else  În funcţie, a→15, b→6;a>b=>functia returneaza
if(a>b) return cmmdc(a-b, b); cmmdc(a-b, b), respectiv cmmdc(9,6);
else return cmmdc(a, b-a);  Auto-apelul cmmdc(9,6): a=9,b=6, a>b=>functia
} returneaza cmmdc(a-b, b), respectiv cmmdc(3,6);
void main()
{ cout<<”a=”; cin>>a;  Auto-apelul cmmdc(3,6): a=3,b=6, a<b=>functia
cout<<”b=”; cin>>b; returneaza cmmdc(a, b-a), respectiv cmmdc(3,3);
if(a>0 && b>0)  Auto-apelul cmmdc(3,3): a=3,b=3, a=b=>functia
cout<<”cmmdc=”<<cmmdc(a, b); returneaza a, adica 3, care este cel mai mare divizor
else
cout<<”nu pot calcula cmmdc”;
comun al lui 15 si 6.
}
Aplicatie:
Scrieţi o funcţie recursivă care returnează suma cifrelor unui număr natural x dat ca parametru.
Functii recursive de tip void
5
Limbajul C++ permite implementarea unor funcţii recursive care nu returnează nici o valoare
(tipul valorii returnate este void). Acestea nu se deosebesc de cele care returnează o valoare decât prin
faptul că auto-apelul funcţiei din interiorul ei nu mai este însoţit de cuvântul “return”. Astfel de funcţii
se folosesc în general atunci când dorim să realizăm afişări recursive ale unor caractere, şiruri, cuvinte.
Aplicatie rezolvată: Se consideră programul următor:
#include<iostream.h>
void ex(int k) 1. Deduceti sirul de numere afisat de catre program, daca de la
{ tastatura se introduce valoarea n=3;
if(k>0) a) 4 3 2 1 b) 3 2 1 c) 1 1 1 d) 1 2 3 4 e) 1 2 3
{ ex(k-1);
cout<<k<<” “; 2. Formulati o fraza care sa descrie actiunea functiei recursive ex.
}
} Rezolvare:
void main( ) 1) Reamintim: Mecanismul funcţiilor se bazează pe următorul principiu: în
{ momentul apelului unei funcţii, se salvează pe stivă contextul moduluilui
cout<<”n=”; cin>>n; apelant(alcătuit din variabilele locale şi parametrii transmişi prin valoare) şi
ex(n); adresa de revenire.
}
• La revenirea în modulul apelant, se restaurează în ordine inversă valorile salvate pe stivă(atât ale
variabilelor locale cât şi ale parametrilor). În cazul unei funcţii recursive, aceasta este atât modulul
apelant cât şi modulul apelat. În cazul nostru, funcţia ex nu are variabile locale, deci contextul ei, salvat
pe stivă, este reprezentat doar de parametrul k.
• Primul apel ex(n), executat în funcţia main (unde valoarea lui n se citeşte anterior de la tastatură),
declanşează un lanţ de auto-apeluri recursive. În corpul funcţiei ex, parametrul formal k este înlocuit cu
valoarea n (parametrul actual). În linia if, se testează dacă valoarea lui k este mai mare decât zero. În caz
afirmativ, se auto-apelează ex(k-1); în acest moment însă, se salvează pe stivă contextul modulului
apelant ex(n), deci valoarea lui n.
• Aşadar, dacă de la tastatură se introduce n=3, vom avea apelul ex(3), apoi:
 k→ 3; k>0? 3>0? da => se salveaza pe stiva 3 si are loc auto-apelul ex(2)
Auto-apelurile care urmeaza se executa similar:
 Auto-apelul ex(2):
k→ 2; k>0? 2>0? da => se salveaza pe stiva 2 si are loc auto-apelul ex(1)
 Auto-apelul ex(1):
k→ 1; k>0? 1>0? da => se salveaza pe stiva 1 si are loc auto-apelul ex(0)
 Auto-apelul ex(0):
k→ 0; k>0? 0>0? nu => in acest moment s-a incheiat lantul de auto-apeluri pe care il
reamintim: functia main→ex(3) →ex(2) →ex(1) →ex(0)
• Urmeaza lantul de reveniri, in ordine inversa:
ex(0) →ex(1) →ex(2) →ex(3) →functia main
Asadar, pentru n=3 au fost
aslvate pe stiva valorile
1
3,2,1, in aceasta ordine. In
Prg.princ ex(3) ex(2) ex(1) ex(0) lantul revenirii, se
2 2
restaureaza si se tiparesc
3 3 3 valorile respective in
ordinea inversa salvarii,
respectiv 1,2,3.
restaureaza restaureaza restaureaza Raspuns corect: e)
k=3 k=2 k=1
afiseaza 3 afiseaza 2 afiseaza 1

2) Pe caz general, programul afiseaza primele n numere naturale 1,2,3,...,n in aceasta ordine.

S-ar putea să vă placă și