Upload
deekydz
View
10
Download
0
Embed Size (px)
Citation preview
LICEUL COLEGIUL NATIONAL “GEORGE COSBUC” MOTRU
PROIECT ATESTAT INFORMATICA
ELEV: SOARE AZAELATEMA: RECURSIVITATE CLASA: A XII-A C
PROFESOR COORDONATOR: CIOCAN NATALIA
TEMA:RECURSIVITATE
2
CUPRINS
1. Despre recursivitate …………………………………………………………………………………………… pag. 42. Apelul recursiv ………………..………………………………………………………………………………… pag. 5
3. Clasificarea recursivitatii …………………………………………………….…………………………..... pag. 20 4. Divide et impera ……………..……………………………………………………………………………..... pag. 21 5. Exerciti si probleme ………………………………………………………………………………………….. pag. 22 6. Bibliografie ………………………………………………………………………………………………………. pag. 33
3
Despre RecursivitateˆIn limbajele de programare, recursivitatea e un concept fundamental care le extinde ˆın
mod esen¸tial puterea de exprimare (expresivitatea ): recursivitatea permite scrierea unor
programe care nu s-ar putea exprima doar cu no¸tiunile fundamentale de secven¸tiere ¸si
decizie prezentate pˆan˘a acum. Pentru rezolvarea practic˘a a problemelor, recursivitatea e
foarte important˘a deoarece permite s˘a descriem solu¸tia unei probleme complexe
folosind una sau mai multe probleme de acela¸si tip, dar mai simple. Ea e astfel strˆans
legat˘a de principiul descompunerii ˆın subprobleme (divide et impera ) ˆın proiectarea
solu¸tiilor.
Defini¸tie ¸si exemple
O no¸tiune e recursiv˘a dac˘a e folosit˘a ˆın propria sa defini¸tie. Cunoa¸stem un exemplu
din matematic˘a:
¸sirurile recurente , unde un termen al ¸sirului e definit printr-o rela¸tie ˆın raport cu
termenii anteriori. Exemple sunt:
• progresia aritmetic˘a: x0 = a, xn = xn−1 + p pentru n > 0.
• progresia geometric˘a: x0 = b, xn = q · xn−1 pentru n > 0.
Acestea sunt recuren¸te de ordinul I, ˆın care termenul definit depinde doar de
termenul imediat anterior. Alte recuren¸te mai complexe sunt:
• ¸sirul lui Fibonacci: F0 = F1 = 1, Fn = Fn−1 + Fn−2 pentru n ≥ 2 (un ¸sir recurent
de ordinul II)
• coeficien¸tii binomiali: C 0 = C n = 1 pentru n ≥ 0,
C k = C k
+ C k−1 pentru 0 < k < n.
Acest
exemplu define¸ste o m˘arime ˆın func¸tie de doi parametri (indici), ˆıns˘a de asemenea
printr-o rela¸tie
de recuren¸t˘a ˆın raport cu termeni de indici mai mici.
O functie recursiv˘a ˆın CˆIn toate exemplele date de ¸siruri recurente, defini¸tia con¸tine dou˘a p˘ar¸ti: cazul de
baz˘a pentru primul termen (sau primii cˆa¸tiva), ¸si rela¸tia recursiv˘a propriu-zis˘a pentru
termenul general. Aceasta sugereaz˘a c˘a putem folosi operatorul condi¸tional pentru a
exprima o defini¸tie recursiv˘a, similar cu func¸tiile definite anterior pe mai multe cazuri.
Consider˘am ca exemplu func¸tia putere (cu baz˘a real˘a ¸si exponent natural), care
poate fi privit˘a recursiv ca progresie geometric˘a, indicele fiind exponentul: puterea de
exponent n (termenul de ordin n) este baza (ra¸tia) ˆınmul¸tit˘a cu puterea de exponent n−1.
4
Grup˘am ˆın defini¸tie cazul de baz˘a (termenul ini¸tial, 1) ¸si termenul general ˆın felul
urm˘ator:
x · xn−1 altfel (n > 0)
Av˘and o defini¸tie pe dou˘a variante, func¸tia se poate exprima ˆın C cu operatorul
condi¸tional ca ¸si exemplele nerecursive dinainte:
float pwr(float x, unsigned n)
{
return n==0 ? 1
: x * pwr(x, n-1);
}
Pentru exponent am folosit tipul unsigned (cuvˆant cheie ˆın limbajul C), corespunzˆand
numerelor
naturale (nenegative); orice valoare diferit˘a de zero e deci pozitiv˘a ¸si tratat˘a corect pe
ramura ”altfel”.
Pentru scrierea func¸tiei pwr nu au fost necesare facilit˘a¸ti noi de limbaj. Esen¸tial e
doar ca limbajul s˘a permit˘a ca ˆın corpul unei func¸tii s˘a fie apelat˘a chiar aceast˘a
func¸tie (¸stim c˘a e permis˘a apelarea unei func¸tii care e deja declarat˘a ). ˆIn limbajul C,
dup˘a ce a fost scris antetul func¸tiei ca parte a defini¸tiei ei complete se cunosc deja numele
functiei, tipul ¸si parametrii ei. Antetul reprezint˘a deci o declara¸tie a func¸tiei (chiar
ˆınainte de a fi fost scris corpul ei), ceea ce e suficient pentru a permite apelul recursiv.
Mecanismul apelului de func¸tie. Apelul recursiv
De¸si scrierea acestui prim exemplu recursiv nu a necesitat elemente noi de limbaj, pentru
a ˆın¸telege corect recursivitatea sunt necesare mai multe detalii despre mecanismul apelului
de func¸tie. ˆIncepem cu
5
2
un exemplu nerecursiv: func¸tia int sqr(int x) { return x * x; } ¸si expresia sqr(3 *
sqr(2)) .
Expresia e un apel la func¸tia sqr. ˆInainte de apel, trebuie evaluat argumentul
func¸tiei, pentru a cunoa¸ste valoarea a c˘arei p˘atrat trebuie calculat. Argumentul e un
produs ˆın care unul din factori e el ˆınsu¸si o expresie apel de func¸tie. Ca atare, din
ˆıntreaga expresie sa evalueaz˘a ˆıntˆai sqr(2), apoi se
ˆınmul¸te¸ste 3 cu rezultatul (4), iar cu valoarea 12 se efectueaz˘a al doilea apel la sqr, cu
rezultatul 144.
De¸si apelul exterior la sqr con¸tine o subexpresie cu un apel la aceea¸si func¸tie,
expresia nu are caracter recursiv, ˆıntrucˆat valoarea func¸tiei sqr e calculat˘a direct din
valoarea parametrului transmis. Valoarea lui sqr(2) e necesar˘a ca argument pentru apel, nu
ˆın corpul func¸tiei ca ˆın defini¸tiile recursive.
Apelul de func¸tie Evaluarea unui apel de func¸tie e declan¸sat˘a atunci cˆand
valoarea func¸tiei e necesar˘a ˆın evaluarea unei expresii (inclusiv ˆın execu¸tia unei
instruc¸tiuni de forma expresie ; ). Ea se efectueaz˘a ˆın urm˘atorii pa¸si:
• se evalueaz˘a toate expresiile care constituie argumentele func¸tiei. Deci orice apeluri de
func¸tii care apar ˆın argumente se efectueaz˘a ˆınainte de apelul func¸tiei considerate.
• valorile argumentelor se atribuie parametrilor formali din antetul func¸tiei, cu conversiile
necesare de tip (de exemplu ˆıntreg–real). Compatibilitatea de tip a expresiilor argument
e verificat˘a deja la compilare, dac˘a se cunosc tipurile parametrilor formali – un motiv ˆın
plus pentru care se cere ca o func¸tie s˘a fie declarat˘a ˆınainte de a fi folosit˘a.
• se execut˘a corpul func¸tiei, cu parametrii formali avˆand valori ini¸tiale ca mai sus.
La ˆıntˆalnirea instruc¸tiunii return, execu¸tia func¸tiei se ˆıncheie cu valoarea ob¸tinut˘a
prin evaluarea expresiei date.
• execu¸tia programului revine la locul de apel, unde valoarea returnat˘a de func¸tie e
folosit˘a.
Transmiterea parametrilor ˆ
In limbajul C transmiterea parametrilor la func¸tii se face prin valoare :
ˆIn momentul apelului, parametrii formali iau valoarea argumentelor (care au fost evaluate);
ei nu sunt substitui¸ti cu expresiile argumentelor. ˆIn consecin¸t˘a, pentru apelul discutat
mai sus, ˆın instruc¸tiunea return se va evalua expresia 12 * 12 ¸si nu (3 * sqr(2)) * (3
* sqr(2)). Altfel spus, o func¸tie lucreaz˘a cu valori (numerice), nu cu expresii simbolice.
Expresia 3 * sqr(2) se evalueaz˘a o singur˘a dat˘a, ˆınainte de apelul sintactic exterior la
sqr, ¸si deci apelul la sqr(2) e deja ˆıncheiat ˆın momentul celui de-al doilea apel, sqr(12).
2
Acest fapt poate fi vizualizat augmentˆand func¸tia sqr cu o instruc¸tiune de tip˘arire, o
practic˘a util˘a ˆın urm˘arirea execu¸tiei programelor.
#include
<stdio.h>
int sqr(int
x)
{
printf("calculam patratul lui %d\n", x);
return x * x;
}
int main(void)
{
printf("sqr(3 * sqr(2)) = %d\n", sqr(3*sqr(2)));
return 0;
}
calculam patratul lui
2 calculam patratul lui
12 sqr(3 * sqr(2)) =
144
Rezultatul rul˘arii programului (prezentat sub textul surs˘a) eviden¸tiaz˘a ¸si pentru
apelul printf din main evaluarea argumentelor ˆınainte de ˆınceperea execu¸tiei functiei.
Evaluarea argumentului al doilea produce cele dou˘a linii tip˘arite ˆın apelurile la sqr.
Aceste linii apar ˆınainte de a scrie chiar ¸si por¸tiunea de text obi¸snuit din primul argument
(formatul), scriere ˆın care const˘a tocmai execu¸tia func¸tiei printf.
Returnarea valorilor La ˆıntˆalnirea instruc¸tiunii return, execu¸tia func¸tiei se
ˆıncheie; orice alte instruc¸tiuni care urmeaz˘a ˆın corpul func¸tiei nu se mai execut˘a.
Dac˘a execu¸tia unei func¸tii se termin˘a prin atingerea ultimei acolade } f˘ar˘a a executa o
instruc¸tiune return, iar programul utilizeaz˘a valoarea func¸tiei, efectul e nedefinit
(programul se comport˘a imprevizibil). O func¸tie care nu prevede ˆın orice situa¸tie o
valoare returnat˘a e scris˘a eronat sub aspect logic.
3
Apelul recursiv Discut˘am mecanismul apelului recursiv luˆand ca exemplu calculul lui 53
cu func¸tia putere. ˆIn apelul pwr(5, 3) (x = 5, n = 3), expresia conditional˘a conduce la
evaluarea lui 5 * pwr(5,
2). Aceasta necesit˘a un nou apel la pwr, de data aceasta cu parametrii x = 5, n = 2.
Procesul se repet˘a cu pwr(5, 1) pˆan˘a la apelul pwr(5, 0) pentru care valoarea se
calculeaz˘a direct: 1. Din acest apel se revine ˆın locul unde a fost f˘acut: la evaluarea lui 5
* pwr(5, 0) care poate fi efectuat˘a acum. Apelul pwr(5, 1) returneaz˘a valoarea 5
folosit˘a ˆın calculul 5 * pwr(5, 1) pentru valoarea lui pwr(5,
2). ˆIn final, aceast˘a valoare, 25, e folosit˘a ˆın expresia 5 * pwr(5, 2) pentru a calcula
valoarea 125 a
lui pwr(5,
3).
pwr(5, 3)
apel ↓
↑125
5 * pwr(5,
2)
apel ↓
↑25
5 * pwr(5,
1)
apel ↓
↑5
5 * pwr(5, 0)
apel ↓
↑1
1
Urm˘arind secven¸ta de apel, rezult˘a c˘a la un moment dat pot fi ˆın execu¸tie mai multe
apeluri diferite la aceea¸si func¸tie. Fiecare apel reprezint˘a o instan¸t˘a (copie) distinct˘a
a func¸tiei, cu propriile valori de parametri, cele primite ˆın momentul apelului. La fel se
ˆıntˆampl˘a ¸si ˆın calculul pe hˆartie, prin “desf˘a¸surarea” formulei de recuren¸t˘a:
pentru a calcula pe 53, ˆınlocuim pe x cu 5 ¸si n cu 3. Trebuie calculat 52: aplicˆand din
nou formula ˆınlocuim pe n cu 2; aceasta nu afecteaz˘a ˆıns˘a instan¸ta ini¸tial˘a a
problemei (53), unde n este ˆın continuare 3.
3
ˆIn exemplul dat, recursivitatea are o structur˘a liniar˘a: fiecare apel recursiv
genereaz˘a un singur nou apel, pˆan˘a la oprirea pentru cazul de baz˘a. ˆIn acel moment,
sunt active toate apelurile, ˆın num˘ar de 4. Revenirea se face ˆın ordine invers˘a fa¸t˘a de
cea de apel: din fiecare apel se revine ˆın instan¸ta care a efectuat apelul. Aici, execu¸tia
se reia ˆın contextul
dinainte de apel: adic˘a din locul ˆın care a fost f˘acut apelul (unde e folosit˘a valoarea
returnat˘a), ¸si cu acele valori ale parametrilor corespunzˆand instan¸tei respective.
Ca la orice apel de func¸tie, informa¸tia se transmite spre func¸tia apelat˘a prin
parametri, ¸si ˆınapoi spre locul de apel (func¸tia apelant˘a ) prin rezultat. ˆIn exemplul dat se
creeaz˘a un lan¸t ˆın care la revenire, valoarea returnat˘a de fiecare apel e folosit˘a ˆın
instan¸ta apelant˘a pentru calculul propriului rezultat, care e transmis mai departe ˆınapoi
spre locul de apel. Rezultatul final e astfel efectul unui calcul ˆın care cˆate un pas e efectuat
de fiecare din instan¸tele apelate. Aceasta e esenta recursivit˘a¸tii ¸si ˆın acela¸si timp
puterea ei ˆın rezolvarea de probleme: ea permite exprimarea indirect˘a a solu¸tiei prin pa¸si
simpli, din aproape ˆın aproape, f˘ar˘a a necesita formularea direct˘a a unei solu¸tii
complexe.
Elementele unei defini¸tii recursive
ˆIntr-o defini¸tie recursiv˘a corect˘a se pot identifica urm˘atoarele
componente:
• Cazul de baz˘a. Trateaz˘a situa¸tiile (cele mai simple) ˆın care no¸tiunea recursiv˘a e
definit˘a direct.
Exemple: pentru ¸sirurile recurente, primul termen (sau mai mul¸ti, la recuren¸tele de
ordin > 1)
pentru liste, cea vid˘a, sau cea cu un element; pentru expresii, constantele ¸si
identificatorii
• Rela¸tia de recuren¸t˘a : partea propriu-zis recursiv˘a a defini¸tiei, ˆın care no¸tiunea
definit˘a apare ¸si ˆın corpul defini¸tiei. Exemple: formulele de recuren¸t˘a pentru
¸siruri; ramura de defini¸tie ”o list˘a e un element urmat de o list˘a”; pentru expresii,
variantele de defini¸tie cu expresie ˆıntre paranteze, apel de func¸tii (cu parametri
expresii) ¸si cele cu expresii compuse cu operatori
• Terminarea recursivit˘a¸tii. Dac˘a defini¸tia urmeaz˘a o ramur˘a care con¸tine din nou
no¸tiunea definit˘a, atunci defini¸tia trebuie aplicat˘a din nou. O no¸tiune e corect
definit˘a recursiv dac˘a acest proces se opre¸ste ˆıntotdeauna. Pentru a fi riguroas˘a, o
3
defini¸tie recursiv˘a trebuie ˆınso¸tit˘a de o demonstratie c˘a aplicarea defini¸tiei se
opre¸ste dup˘a un num˘ar finit de pa¸si.
Rezult˘a c˘a o defini¸tie recursiv˘a nu poate fi corect˘a f˘ar˘a un caz de baz˘a, pentru
c˘a nu se ajunge niciodat˘a la un punct unde no¸tiunea poate fi definit˘a direct. Cazul de
baz˘a ¸si rela¸tia recursiv˘a sunt de fapt alternative ale aceleia¸si defini¸tii. Acest lucru e
explicit ˆın regulile sintactice care definesc
4
construc¸tii de limbaj cum ar fi expresiile. Pentru un ¸sir recurent putem eviden¸tia
aceasta folosind
. a n = 0
acolade: xn
= x
n−1
+ p n > 0 ¸si transcrie apoi u¸sor ˆın program.
Pentru terminarea recursivit˘a¸tii, cel mai uzual argument folose¸ste o m˘asur˘a
(cantitate) care de-
scre¸ste la fiecare aplicare a defini¸tiei, pˆan˘a atinge o valoare pentru care defini¸tia e
dat˘a direct. La
¸siruri recurente, aceast˘a cantitate e chiar indicele n al termenului
general xn .
Alte exemple de recursivitate
ˆIn¸siruirea v˘azut˘a recursiv S¸i notiuni din afara matematicii, uneori foarte simple,
se preteaz˘a la defini¸tii recursive. Un tipar de defini¸tii des ˆıntˆalnit se bazeaz˘a pe faptul
c˘a itera¸tia (repeti¸tia) poate fi definit˘a prin recursivitate. Putem defini astfel:
Un ¸sir (secven¸t˘a, list˘a) e fie un element, fie un ¸sir urmat de
un element.
Uneori e util s˘a includem ˆın defini¸tie ¸sirul vid, care apare natural ˆın diverse opera¸tii (ca
element neutru la concatenare; la ini¸tializarea sau dup˘a ¸stergerea tuturor elementelor unei
liste, etc.):
Un ¸sir e fie un ¸sir vid, fie un element urmat de
un ¸sir.
Cele dou˘a variante difer˘a atˆat prin cazul de baz˘a (un element sau zero), cˆat ¸si prin
pozi¸tia no¸tiunii recursive ˆın defini¸tie: prima variant˘a e recursiv˘a la stˆanga
(no¸tiunea definit˘a recursiv ”¸sir” e prima
ˆın rescrierea ”¸sir urmat de un element”), iar a doua e recursiv˘a la dreapta , deoarece
ˆın expandarea
”element urmat de un ¸sir” no¸tiunea definit˘a ”¸sir” e pe ultima pozi¸tie. Tiparele de
recursivitate la stˆanga
¸si la dreapta conduc la prelucr˘ari diferite ˆın program, pe care le studiem ¸si compar˘am
ˆın continuare.
4
Recursivitatea ˆın sintaxa limbajelor Recursivitatea apare natural ˆın definirea
precis˘a a sintaxei limbajelor de programare. Multe elemente de limbaj au ˆın componen¸t˘a
repeti¸tia, care poate fi exprimat˘a recursiv. Astfel, antetul unei func¸tii poate fi definit (ˆın
limita celor prezentate pˆan˘a acum) ca:
antet-func¸tie ::= tip identificator ( parametri )
parametri ::= void | lista-param
lista-param ::= tip identificator | tip identificator , lista-param
Am folosit conven¸tional simbolurile ::= pentru defini¸tie ¸si | pentru alternativ˘a .
Acest mod de a descrie regulile sintactice, adic˘a gramatica unui limbaj se nume¸ste form˘a
Backus-Naur (BNF).
Putem defini recursiv ¸si alt˘a no¸tiune fundamental˘a, expresia. Din cele prezentate
pˆan˘a acum:
expresie ::= constant˘a | identificator | identificator ( argumente )
| - expresie | expresie operator-binar expresie
| expresie ? expresie : expresie | ( expresie )
argumente ::= s | lista-argumente
lista-argumente ::= expresie | expresie , lista-argumente
unde s denot˘a conven¸tional alternativa cu con¸tinut vid (f˘ar˘a simboluri de limbaj), aici
pentru apeluri de forma functie(), f˘ar˘a argumente ˆıntre paranteze.
Pentru o defini¸tie riguroas˘a, trebuie s˘a preciz˘am c˘a orice construc¸tie de limbaj
trebuie definit˘a printr-un num˘ar finit de aplic˘ari ale regulilor. Astfel, -(2 + 3) e o
expresie: se pot aplica pe rˆand regulile expresie ::= - expresie, expresie ::= ( expresie ),
expresie ::= expresie + expresie, ¸si de dou˘a ori regula expresie ::= constant˘a .
Dou˘a tipare de calcul recursiv
S˘a examin˘am un alt exemplu tipic de calcul recursiv,
factorialul.
. 1 n = 0
Avem: n! =
¸si putem transcrie direct ˆın C: (n − 1)! · n altfel
(n > 0)
unsigned fact(unsigned n)
{
return n == 0 ? 1 : fact(n-1) * n;
}
ˆIn evaluarea lui fact pentru n > 0, ultima opera¸tie efectuat˘a e ˆınmul¸tirea cu n,
restul calculelor fiind efectuate anterior ˆın apelul fact(n-1) (¸si celelalte apeluri
recursive care rezult˘a din acesta). Ordinea calculelelor e indicat˘a de paranteze ˆın
expresia n! = ((((1 · 1) · 2) · . . .) · (n − 1)) · n.
Expandˆand defini¸tia pentru n ≥ 2, ob¸tinem n! = ((n − 2)! · (n − 1)) · n. ˆInainte de
evaluarea lui
fact(n-2) ¸stim c˘a ˆın produs apar atˆat n cˆat ¸si n-1, dar a¸sa cum e scris˘a func¸tia, nu
se efectueaz˘a direct
ˆınmul¸tirea lor: se apeleaz˘a ˆıntˆai fact(n-2), rezultatul e ˆınmul¸tit cu n-1 ˆın cadrul
apelului fact(n-1),
¸si doar ˆın final se face ˆınmul¸tirea cu n, pentru rezultatul lui
fact(n) .
Pornind de la aceast˘a observa¸tie, rescriem factorialul folosind asociativitatea
ˆınmul¸tirii, pentru a efectua cˆat mai multe ˆınmul¸tiri ˆındat˘a ce factorii devin
disponibili: n! = 1 · (2 · (. . . · ((n − 1) · n)))
Transcriind ˆın C, am dori s˘a efectu˘am ˆınmul¸tirea (n-1)*n ˆın cadrul apelului
func¸tiei pentru n-1,
ˆınainte de a calcula recursiv factorialul pentru n-2. ˆIn apelul pentru n-2 s-ar putea
ˆınmul¸ti apoi rezultatul lui (n-1)*n cu n-2, etc. Pentru aceasta, avem nevoie s˘a
transmitem la fiecare apel recursiv pe lˆang˘a valoarea lui n ¸si rezultatul ˆınmul¸tirilor deja
efectuate. Ob¸tinem astfel:
unsigned fact_r(unsigned n, unsigned
r)
{
return n == 0 ? r : fact_r(n-1, r * n);
}
13
Parametrul r reprezint˘a rezultatul par¸tial calculat. La fiecare apel recursiv, el e
ˆınmul¸tit cu valoarea
curent˘a a lui n ¸si rezultatul e transmis mai departe la apelul pentru n-1. Cˆand n==0, toate
ˆınmul¸tirile au fost deja efectuate; rezultatul se g˘ase¸ste acumulat ˆın r ¸si poate fi
returnat. Pe ramura recursiv˘a, la revenire nu se mai efectueaz˘a nici un calcul: valoarea
provenit˘a din apelul recursiv e deja rezultatul complet ¸si e returnat˘a direct mai departe
la apelant.
Din primul apel pentru n, rezultatul par¸tial pe care dorim s˘a-l transmitem spre apelul
pentru n-1 e tot valoarea n. Deci, ini¸tial, r ar trebui s˘a fie 1, ¸si pentru a calcula pe n!
vom apela fact_r(n, 1) . De fapt, am definit o func¸tie mai general˘a: fact_r(n, r)
calculeaz˘a pe r · n! .
Pentru a nu complica utilizatorul cu parametrul suplimentar datorat modului de calcul,
definim func¸tia fact2 cu un singur parametru. Aceasta doar “ˆımpacheteaz˘a” apelul
ini¸tial fact_r(n, 1) :
unsigned fact2(unsigned n) { return fact_r(n,
1); }
Pentru factorial, putem alege ˆıntre cele dou˘a variante de scriere. ˆIn prima, calculul
se face la
revenirea din apelurile recursive, iar acestea nu au nevoie de vreun rezultat par¸tial
calculat anterior.
ˆIn a doua, rezultatul par¸tial e transmis ˆın adˆancime ca parametru, ¸si actualizat ˆınainte
de fiecare apel recursiv.
Exist˘a ˆıns˘a situa¸tii ˆın care parte din prelucrare se efectueaz˘a ˆınainte de fiecare apel
recursiv, fiind
necesar s˘a transmitem “ˆın jos” la ˆınaintarea ˆın recursivitate valori ce vor fi folosite
ulterior ˆın calcule.
Inversarea cifrelor unui num˘ar Scriem o func¸tie care ia un ˆıntreg f˘ar˘a semn ¸si-
l transform˘a ˆın num˘arul cu acelea¸si cifre zecimale dar ˆın ordine invers˘a. Scriem solu¸tia
pornind de la un exemplu: 1472. Ultima cifr˘a, 2, devine prima cifr˘a a rezultatului. Punem
ultima cifr˘a r˘amas˘a din 147 dup˘a 2, obtinˆand
27 = 2 · 10 + 7. Din num˘arul r˘amas, 14, plas˘am ultima cifr˘a dup˘a 27, ob¸tinˆand 27 ·
14
10 + 4 = 274, etc.
ˆIn cuvinte, pasul recursiv de prelucrare poate fi exprimat: rezultatul invers˘arii, dac˘a
a mai r˘amas de inversat n, iar din inversarea ultimelor cifre s-a obtinut deja v, e acela¸si cu
rezultatul invers˘arii lui n/10, cu valoarea intermediar˘a 10 · v + n mod 10. De¸si enun¸tul
problemei are un singur parametru, solu¸tia recursiv˘a ob¸tinut˘a manipuleaz˘a dou˘a
cantit˘a¸ti, deci scriem o functie recursiv˘a cu doi parametri. Prelucrarea se opre¸ste cˆand n
e 0 (nu mai sunt cifre de inversat), iar ini¸tial, v e valoarea f˘ar˘a nici o cifr˘a, deci tot 0;
astfel, pentru prima cifr˘a c, expresia 10 · 0 + c d˘a valoarea dorit˘a c.
#include
<stdio.h>
unsigned revnum_r(unsigned n, unsigned
r)
{
return n == 0 ? r : revnum_r(n / 10, 10 * r + n % 10);
}
unsigned revnum(unsigned n) { return revnum_r(n, 0); }
int main(void)
{
printf("%u\n", revnum(1472)); // %u pt. tiparire
unsigned return 0;
}
Dorim ca solu¸tie o func¸tie cu un singur parametru, ca ˆın enun¸t, pentru a nu complica
utilizatorul cu
un parametru suplimentar pentru valoarea intermediar˘a. Am scris astfel functia revnum
care apeleaz˘a func¸tia recursiv˘a revnum r(n, 0) cu valoarea ini¸tial˘a necesar˘a pentru al
doilea parametru.
Cel mai mare divizor comun Algoritmul lui Euclid pentru calculul celui mai mare
divizor comun a doi ˆıntregi pozitivi e un exemplu clasic de algoritm exprimat recursiv, ˆın
care o problem˘a e rezolvat˘a prin reducerea la o instan¸t˘a mai simpl˘a a aceleia¸si
probleme. Exprimat informal, algoritmul e:
– dac˘a numerele sunt egale, rezultatul e chiar valoarea lor
15
comun˘a
– altfel, se scade cel mai mic num˘ar din cel mai mare, ¸si se repet˘a procedura cu
noile numere.
Exprimarea din urm˘a (“se repet˘a procedura”) indic˘a abordarea recursiv˘a: solu¸tia
se ob¸tine re- zolvˆand aceea¸si problem˘a pentru valori noi ale numerelor (mai mici, deci
dup˘a un num˘ar finit de pa¸si se ajunge la cazul de baz˘a).
Putem scrie deci pe cazuri:
cmmdc(a, b) =
a a = b
cmmdc(a − b, b) a > b
cmmdc(a, b − a) altfel (a
< b)
¸si transcrie direct ˆın C:
unsigned cmmdc(unsigned a, unsigned b)
{
return a == b ? a
: a > b ? cmmdc(a - b, b)
: cmmdc(a, b - a);
}
De¸si am transpus direct din cuvinte ˆın formula recursiv˘a ¸si apoi ˆın cod, a r˘amas
netratat un aspect:
enun¸tul ini¸tial e dat pentru numere pozitive, iar tipul unsigned permite ¸si valoarea 0.
Apelarea (chiar
¸si accidental˘a) a func¸tiei scrise mai sus cu un parametru nul va duce la o secven¸t˘a
infinit˘a de apeluri recursive, deoarece sc˘azˆand 0 cel˘alalt num˘ar nu se modific˘a ¸si
relu˘am acela¸si apel (pˆan˘a cˆand, ˆın func¸tie de mediul de rulare, programul se va
termina probabil for¸tat epuizˆand resursele de memorie).
Este important ca func¸tiile pe care le scriem s˘a fie robuste ¸si s˘a nu produc˘a erori
neprev˘azute ¸si catas- trofale. Ca atare, rescriem func¸tia ¸tinˆand cont c˘a 0 e divizibil cu
orice num˘ar, ¸si deci cmmdc(a, 0) = cmmdc(0, a) = a:
unsigned cmmdc(unsigned a, unsigned
b)
{
16
k=0
return b == 0 ? a : a == 0 ? : b
: a > b ? cmmdc(a - b, b)
: cmmdc(a, b - a);
}
ˆIn aceast˘a scriere, cazul a = b ƒ= 0 va intra pe ultima ramur˘a, avˆand ca efect apelul
cmmdc(a, 0)
care va returna a.
Calculul recursiv al seriilor
Calculul sumei par¸tiale a unei serii se preteaz˘a natural la o exprimare recursiv˘a. Not˘and
cu tn termenul
general, ¸si sn =
.n
tk , ob¸tinem imediat pentru termenul general:
sn = sn
−1 +
tn
(pentru n ≥ 1),
¸si
(n > 0)
Avˆand o formul˘a de calcul direct pentru termenul tn al seriei (exprimat deci ca func¸tie
de n), putem transforma direct formula de mai sus ˆıntr-o func¸tie recursiv˘a s, care
apeleaz˘a pentru calcul func¸tia t. Pentru exemplul simplu tn = 1/n (pentru n > 1, iar t0 =
0), putem scrie urm˘atorul program:
#include <math.h>
#include <stdio.h>
double t(unsigned n)
{
return n == 0 ? 0 : 1.0/n;
}
double s(unsigned n)
{
return n == 0 ? t(0) : s(n-1) + t(n);
}
17
int main(void)
{
printf("Constanta lui Euler e aprox. %f\n", s(1000)-log(1000));
return 0;
}
Din matematic˘a, ¸stim c˘a seria .
n 1/n are sum˘a infinit˘a, ¸si cre¸ste aproximativ ca
logaritmul natural
al lui n. Mai
precis, n
lim (.
1/k − ln n) = γ c 0.5772...
n→∞ k=1
Aproxima¸tia calculat˘a de program are primele 3 zecimale exacte.
Tipul double folosit ˆın program reprezint˘a, ca ¸si float, numere reale, dar cu precizie
mai bun˘a (de aici ¸si numele), ¸si e recomandabil ˆın calcule pentru a mic¸sora acumularea
erorilor de rotunjire. Este tipul standard folosit de func¸tiile matematice de bibliotec˘a
declarate ˆın math.h (cum e ¸si func¸tia log pentru logaritmul natural) ¸si tipul implicit
pentru constantele reale. Scrierea 1.0/n pentru termenul matematic 1/n e necesar˘a pentru
a ob¸tine ˆımp˘ar¸tire real˘a : operandul 1.0 fiind real, va fi convertit implicit ¸si
ˆıntregul n. Altfel, 1/n ar fi ˆınsemnat ˆımp˘ar¸tire ˆıntreag˘a, cu rest, ¸si valoarea 0 pentru
n > 1.
Desigur c˘a ˆın acest program s-ar fi putut scrie mai simplu, direct
double s(unsigned n)
{
return n == 0 ? 0 : s(n-1) + 1.0/n;
}
Varianta prezentat˘a eviden¸tiaz˘a ˆıns˘a forma general˘a a func¸tiei s pentru suma
exprimat˘a recursiv,
¸si programul poate fi adaptat la alt˘a serie prin simpla ˆınlocuire a func¸tiei t.
E valabil˘a aceea¸si observa¸tie general˘a f˘acut˘a ˆıntˆai pe exemplul factorialului despre
18
cele dou˘a variante de definire a calculului recursiv. Func¸tia s a¸sa cum a fost scris˘a mai
sus efectueaz˘a calculul la revenirea din recursivitate, ultima adunare fiind sn−1 + tn,
corespunzˆand unei grup˘ari a calculelor de forma: sn = (((t0 + t1) + t2) + . . .) + tn.
Cealalt˘a alternativ˘a o constituie transmiterea ca parametru suplimentar a unui
rezultat par¸tial
deja calculat (ini¸tial zero), la care se acumuleaz˘a ˆın fiecare pas termenul curent. Astfel,
termenii sunt aduna¸ti efectiv ˆın ordine invers˘a: sn = t0 + (t1 + (. . . + (tn−1 + tn))).
Func¸tia se scrie:
double s2(unsigned n, double res)
{
return n == 0 ? res + t(0) : s2(n-1, res + t(n));
}
19
Calculul de aproxim˘ari cu o precizie dat˘a
Majoritatea exemplelor de calcul numeric recursiv date pˆan˘a acum au aceea¸si structur˘a: se
calculeaz˘a termenul unui ¸sir definit printr-o rela¸tie de recuren¸t˘a, iar num˘arul de apeluri
recursive necesar pentru calcul e determinat de ordinul (indicele) termenului, dat la primul apel.
ˆIn matematic˘a ¸si practic˘a ˆıntˆalnim ˆıns˘a adesea cazuri se dore¸ste un calcul efectuat cu
o anumit˘a
precizie: se genereaz˘a o secven¸t˘a de aproxim˘ari, iar cˆand aproximarea curent˘a a atins
precizia dorit˘a, calculul se opre¸ste.
D˘am ca exemplu o metod˘a de aproxima¸tie pentru calculul r˘ad˘acinii p˘atrate √
x. O
cunoscut˘a
formul˘a matematic˘a (poate fi derivat˘a din metoda lui Newton, dar ˆın acest caz particular e
mult mai veche) ne d˘a secven¸ta de aproxim˘ari: an+1 = (an + x/an)/2, cu o aproximare
ini¸tial˘a arbitrar˘a (de exemplu a0 = 1).
Elementul cheie al solu¸tiei e formularea problemei pentru a-i identifica ¸si scoate ˆın
eviden¸t˘a car-
acterul recursiv: mai precis, parametrii ¸si cazul de baz˘a (oprirea din secven¸ta de apeluri
recursive). Condi¸tia de oprire nu e dat˘a de aproximarea ini¸tial˘a, ci, dup˘a cum rezult˘a din
enun¸t, de precizia atins˘a. Pentru a o calcula, avem nevoie de aproximarea curent˘a, care devine
astfel parametru al problemei (cu valoarea ini¸tial˘a dat˘a de a0).
Putem atunci enun¸ta solu¸tia ˆın cuvinte ˆın felul urm˘ator: func¸tia c˘autat˘a returneaz˘a o
aproxima¸tie de precizie dat˘a a lui √
x, ¸stiind o aproxima¸tie curent˘a an dat˘a de
asemenea ca parametru. Dac˘a
aproxima¸tia curent˘a e suficient de bun˘a, poate fi returnat˘a (cazul de baz˘a). Dac˘a nu,
rezultatul va fi dat de aceea¸si func¸tie de calcul, apelat˘a tot pentru x, dar cu o aproxima¸tie
curent˘a mai bun˘a, dat˘a de an+1.
#include <math.h>
#include <stdio.h>
double rad(double x, double a)
{
20
return fabs(a - x/a) < 1e-6 ? a : rad(x, .5*(a + x/a));
}
int main(void)
{
printf("%f\n", rad(2, 1));
return 0;
}
Func¸tia standard fabs, declarat˘a ˆın math.h returneaz˘a valoarea absolut˘a a unui num˘ar real
(double), spre deosebire de func¸tia abs (din stdlib.h) aplicabil˘a doar la numere ˆıntregi.
Logica compara¸tiei
este urm˘atoarea: a ¸si x/a se afl˘a ˆıntotdeauna de o parte ¸si de alta a valorii exacte √
x.
Deci, dac˘a
intervalul dintre ele e mai mic decˆat precizia dorit˘a (ˆın exemplul dat, 10−6), oricare din ele
va fi o aproxima¸tie de precizie suficient˘a.
Clasificarea recursivitatii Recursivitate liniară.
Este forma cea mai simplă de recursivitate şi constă dintrun singur apel recursiv. De
exemplu pentru calculul factorialului avem:
f(0)=1 cazul de bază
f(n)=n*f(n-1) cazul general
Se vede că dacă timpul necesar execuţiei cazului de bază este a şi a cazului general este b, atunci timpul
total de calcul este: a+b*(n1) = O(n), adică avem complexitate liniară.
Recursivitate binară.Recursivitatea binară presupune existenţa a două apeluri recursive. Exemplul tipic îl constituie
şirul lui Fibonacci:
long fibo(int n){
if(n<=2) return 1;return fibo(n1) +fibo(n2);
21
}
Funcţia este foarte ineficientă, având complexitate exponenţială, cu multe apeluri recursive repetate.
Se poate înlocui dublul apel recursiv printrun singur apel recursiv. Pentru aceasta ţinem seama că dacă a,
b şi c sunt primii 3 termeni din şirul Fibonacci, atunci calculul termenul situat la distanţia n în raport cu
a, este situat la distanţa n1 faţă de termenul următor b.
Necesitatea cunoaşterii termenului următor impune prezenţa a doi termeni în lista de parametri.
Metoda divizării (Divide et Impera).
Un algoritm recursiv obţine soluţia problemei prin rezolvarea unor instanţe mai mici ale aceleeaşi
probleme.
se împarte problema în subprobleme de aceeaşi natură, dar de dimensiune mai scăzută
se rezolvă subproblemele obţinânduse soluţiile acestora
se combină soluţiile subproblemelor obţinânduse soluţia problemei
Ultima etapă poate lipsi (în anumite cazuri), soluţia problemei rezultând direct din soluţiile
subproblemelor.
Procesul de divizare continuă şi pentru subprobleme, până când se ajunge la o dimensiune suficient
de redusă a problemei care să permită rezolvarea printro metodă directă.
Metoda divizării se exprimă natural în mod recursiv: rezolvarea problemei constă în rezolvarea unor
instanţe ale problemei de dimensiuni mai reduse
void DivImp(int dim, problema p, solutie *s){int dim1, dim2; problema p1, p2; solutie s1, s2; if(dim > prag){Separa(dim,p,&dim1,&p1,&dim2,&p2); DivImp(dim1, p1, &s1);
22
DivImp(dim2, p2, &s2); Combina(s1, s2, s);
}elseRezDirect(dim, p, s);
}
Pentru evaluarea complexităţii unui algoritm dezvoltat prin metoda divide et impera se rezolvă o ecuaţie
recurentă având forma generală: T(n)=a.T(n/b) + f(n), în care a >= 1, reprezintă numărul
subproblemelor, iar n/b este dimensiunea subproblemelor, b > 1.
f(n) reprezintă costul divizării problemei în subprobleme şi a combinării soluţiilor
subproblemelor pentru a forma soluţia problemei.
Soluţia acestei ecuaţii recurente este dată de teorema master
Probleme si exercitiiSe citeste un vector cu n elemente numere naturale (n<=100). Sa se inlocuiasca fiecare element al vectorului cu suma cifrelor cu aceeasi paritate ca si indicele elementului. Indexarea elementelor incepe de la 1. Se vor scrie si folosi functii recursive pentru: - citirea vectorului - afisarea vectorului - calculul sumei cifrelor de o anumita paritate - inlocuirea ceruta Exemplu: Pentru datele de mai jos 7 223 435 6667 24 55 662 122 Sirul rezultat este 3 4 7 6 10 14 1 REZOLVARE:#include <iostream>
using namespace std;
void citire(int A[], int n)23
{
if(n>0)
{
citire(A,n-1);
cin>>A[n];
}
}
int sumcifp(int n, int p)
{
if(n==0) return 0;
else if(n%2==p) return sumcifp(n/10,p)+n%10;
else return sumcifp(n/10,p);
}
void inlocuire(int A[], int n)
{
if(n>0)
{
inlocuire(A,n-1);
A[n]=sumcifp(A[n],n%2);
}
}
void afisare(int A[], int n)
{
if(n>0)
{
afisare(A,n-1);
cout<<A[n]<<" ";
}
}
int main()
{
int A[101],n;
24
cin>>n;
citire(A,n);
inlocuire(A,n);
afisare(A,n);
return 0;
}
Se citeste un numar natural n cu cel mult 9 cifre. Afisati numarul de cifre distincte ale lui n. Se vor folosi exclusiv subprograme recursive. Exemplu: Pentru n=38837 se afiseaza 3 (cifrele distinte sunt 3,7 si 8). REZOLVARE:#include <iostream>
using namespace std;
int apcif(int n, int c)
{//numarul de aparitii ale lui c in n
if(n<=9) return n==c;
else if(n%10==c) return apcif(n/10,c)+1;
else return apcif(n/10,c);
}
int dist(int n, int c)
{//numara cifrele distincte ale lui n
if(c==-1) return 0;
else if(apcif(n,c)) return dist(n,c-1)+1;
else return dist(n,c-1);
}
int main()
{
int n;
cin>>n;
cout<<dist(n,9);
25
return 0;
}
sau
#include <iostream>
using namespace std;
int apcif(int n, int c)
{//verific daca c apare in n
if(n<=9) return n==c;
else if(n%10==c) return 1;
else return apcif(n/10,c);
}
int nrdist(int n)
{//numara cifrele distincte ale lui n
if(n==0) return 0;
else if(apcif(n/10,n%10)==0) return nrdist(n/10)+1;
else return nrdist(n/10);
}
int main()
{
int n;
cin>>n;
cout<<nrdist(n);
return 0;
}
Se citeste un numar natural n (n<=20). Afisati un desen format din caracterul * ca in exemplul de mai jos. Se vor folosi exclusiv subprograme recursive. Exemplu:
Pentru n=3 se afiseaza 26
* *** ***** *** * REZOLVARE:#include <iostream>
using namespace std;
void linie(int n, char c)
{
if(n>0)
{
linie(n-1,c);
cout<<c;
}
}
void sus(int n, int r)
{
if(r<=n)
{
linie(n-r,' ');
linie(2*r-1,'*');
cout<<endl;
sus(n,r+1);
}
}
void jos(int n, int r)
{
if(r<=n)
{
jos(n,r+1);
linie(n-r+1,' ');
linie(2*r-1,'*');
27
cout<<endl;
}
}
int main()
{
int n;
cin>>n;
sus(n,1);
jos(n-1,1);
return 0;
}
Se citeste un numar natural n. Sa se descompuna ca suma de puteri crescatoare ale lui 2. Se vor folosi doar prelucrari/calcule realizate cu ajutorul functiilor implementate recursiv. Exemplu: Pentru n=84 va afisa 4 16 64 (84 se descompune ca 4+16+64) REZOLVARE:#include <iostream>
using namespace std;
unsigned int pm2(int n, unsigned int p)
{
if(p*2>n) return p;
else return pm2(n,p*2);
}
void puteri2(int n)
{
if(n>0)
{
unsigned int p=pm2(n,1);
puteri2(n-p);
cout<<p<<" ";
}
}
28
int main()
{
int n;
cin>>n;
puteri2(n);
return 0;
}
sau
#include <iostream>
using namespace std;
void puteri2(int n, int p)
{
if(n>0)
{
if(n%2==1)
cout<<p<<" ";
puteri2(n/2,p*2);
}
}
int main()
{
int n;
cin>>n;
puteri2(n,1);
return 0;
}
O harta este data intr-o matrice n*m in care valorile 1 reprezinta uscatul, iar valorile 0 reprezinta apa. Doua zone de uscat se considera ca fac parte din acelasi continent daca sunt vecine pe linie sau pe coloana. Determinati numarul de continente de pe
29
harta si care este aria (numarul de valori de 1) maxima dintre ariile continentelor. Exemplu: harta.in 6 6 1 1 1 0 1 0 0 0 1 0 1 1 1 1 1 0 0 0 0 1 0 1 1 1 0 0 0 1 1 1 1 1 0 0 0 0 harta.out 4 (numarul de continente) 8 (aria continentului din stanga-sus)REZOLVARE:#include <fstream>
using namespace std;
ifstream fin("harta.in");
ofstream fout("harta.out");
int a[102][102];
void fill(int a[102][102], int n, int m, int i, int j, int c)
{
a[i][j]=c;
if(a[i-1][j]==1) fill(a,n,m,i-1,j,c);//sus
if(a[i][j+1]==1) fill(a,n,m,i,j+1,c);//dreapta
if(a[i+1][j]==1) fill(a,n,m,i+1,j,c);//jos
if(a[i][j-1]==1) fill(a,n,m,i,j-1,c);//stanga
}
int main()
{
int n,m,c=0;
fin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)30
fin>>a[i][j];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
if(a[i][j]==1)
{
c++;
fill(a,n,m,i,j,c+1);
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++) fout<<a[i][j]<<" ";
fout<<endl;
}
fout<<c<<endl;
int max=0;
for(int cul=2;cul<=c+1;cul++)
{
int s=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
if(a[i][j]==cul) s++;
if(s>max) max=s;
}
fout<<max;
fin.close();
fout.close();
return 0;
}
Scrieti o functie recursiva litera cu doi parametri s si c unde s e un cuvant, iar c este o litera si care returneaza de cate ori apare litera c in cuvantul s. b) Scrieti o functie recursiva litere cu trei parametri s, n si c unde s e un vector ce
31
memoreaza cel mult 20 de cuvinte, n e numar natural reprezentand numarul de cuvinte din vectorul s, iar c este o litera si care returneaza de cate ori apare litera c in total in cele n cuvinte din vectorul s (va folosi functia litera). c) Se citeste un numar n si un vector s de n cuvinte. Folosind functia litere, determinati si afisati literele care apar de un numar maxim de ori in cuvintele din vectorul s. Exemplu: n=3, s={"ana", "are", "mere"} => a e
REZOLVARE:
#include <iostream>
#include <cstring>
using namespace std;
int litera(char s[21], char c)
{
if(strlen(s)==0) return 0;
else if(s[0]==c) return 1+litera(s+1,c);
else return litera(s+1,c);
}
int litere(char s[21][21], int n, char c)
{
if(n==0) return 0;
else return litera(s[n],c)+litere(s,n-1,c);
}
int main()
{
char s[21][21],map=0;
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>s[i];
for(char c='a';c<='z';c++)
if(litere(s,n,c)>map) map=litere(s,n,c);
for(char c='a';c<='z';c++)32
if(litere(s,n,c)==map) cout<<c<<" ";
return 0;
}
33