Sunteți pe pagina 1din 33

LICEUL COLEGIUL NATIONAL GEORGE COSBUC MOTRU

PROIECT ATESTAT
INFORMATICA

ELEV: SOARE AZAELA


TEMA: RECURSIVITATE
CLASA: A XII-A C
PROFESOR COORDONATOR: CIOCAN NATALIA

TEMA:
RECURSIVITATE

CUPRINS
1. Despre recursivitate
pag. 4
2. 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

Despre
Recursivitate
In limbajele de programare, recursivitatea e un concept fundamental care le extinde n

mod esential puterea de exprimare (expresivitatea ): recursivitatea permite scrierea unor


programe care nu s-ar putea exprima doar cu notiunile fundamentale de secventiere si
decizie prezentate pana acum. Pentru rezolvarea practica a problemelor, recursivitatea e
foarte importanta

deoarece permite sa

descriem solutia unei probleme complexe

folosind una sau mai multe probleme de acelasi tip, dar mai simple. Ea e astfel strans
legata de principiul descompunerii n subprobleme (divide et impera ) n proiectarea
solutiilor.
Definitie si exemple
O notiune e recursiva daca e folosita n propria sa definitie. Cunoastem un exemplu
din matematica:
sirurile recurente , unde un termen al sirului e definit printr-o relatie n raport cu
termenii anteriori. Exemple sunt:
progresia aritmetica: x0 = a, xn = xn1 + p pentru n > 0.
progresia geometrica: x0 = b, xn = q xn1 pentru n > 0.
Acestea sunt recurente de ordinul I, n care termenul definit depinde doar de
termenul imediat anterior. Alte recurente mai complexe sunt:
sirul lui Fibonacci: F0 = F1 = 1, Fn = Fn1 + Fn2 pentru n 2 (un sir recurent
de ordinul II)
coeficientii binomiali: C 0 = C n = 1 pentru n 0,

+ C k1 pentru 0 < k < n.

Ck = Ck

Acest

exemplu defineste o marime n functie de doi parametri (indici), nsa de asemenea


printr-o relatie
de recurenta n raport cu termeni de indici mai mici.
O functie recursiva n C
In toate exemplele date de siruri recurente, definitia contine doua parti: cazul de

baza pentru primul termen (sau primii cativa), si relatia recursiva propriu-zisa pentru
termenul general. Aceasta sugereaza ca putem folosi operatorul conditional pentru a
4

exprima o definitie recursiva, similar cu functiile definite anterior pe mai multe cazuri.
Consideram ca exemplu functia putere (cu baza reala si exponent natural), care
poate fi privita recursiv ca progresie geometrica, indicele fiind exponentul: puterea de
exponent n (termenul de ordin n) este baza (ratia) nmultita cu puterea de exponent n1.
Grupam n definitie cazul de baza (termenul initial, 1) si termenul general n felul
urmator:
x xn1 altfel (n > 0)

Avand o definitie pe doua variante, functia se poate exprima n C cu operatorul


conditional 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 (cuvant cheie n limbajul C), corespunzand
numerelor
naturale (nenegative); orice valoare diferita de zero e deci pozitiva si tratata corect pe
ramura altfel.
Pentru scrierea functiei pwr nu au fost necesare facilitati noi de limbaj. Esential e
doar ca limbajul sa permita ca n corpul unei functii sa fie apelata chiar aceasta
functie (stim ca e permisa apelarea unei functii care e deja declarata ). In limbajul C,
dupa ce a fost scris antetul functiei ca parte a definitiei ei complete se cunosc deja numele
functiei, tipul si parametrii ei. Antetul reprezinta deci o declaratie a functiei (chiar
nainte de a fi fost scris corpul ei), ceea ce e suficient pentru a permite apelul recursiv.

Mecanismul apelului de functie. Apelul recursiv


Desi scrierea acestui prim exemplu recursiv nu a necesitat elemente noi de limbaj, pentru
a ntelege corect recursivitatea sunt necesare mai multe detalii despre mecanismul apelului
de functie. Incepem cu

un exemplu nerecursiv: functia int sqr(int x) { return x * x; } si expresia sqr(3 *


sqr(2)) .
Expresia e un apel la functia sqr.

Inainte de apel, trebuie evaluat argumentul

functiei, pentru a cunoaste valoarea a carei patrat trebuie calculat. Argumentul e un


produs n care unul din factori e el nsusi o expresie apel de functie. Ca atare, din
ntreaga expresie sa evalueaza ntai sqr(2), apoi se
nmulteste 3 cu rezultatul (4), iar cu valoarea 12 se efectueaza al doilea apel la sqr, cu
rezultatul 144.
Desi apelul exterior la sqr contine o subexpresie cu un apel la aceeasi functie,
expresia nu are caracter recursiv, ntrucat valoarea functiei sqr e calculata direct din
valoarea parametrului transmis. Valoarea lui sqr(2) e necesara ca argument pentru apel, nu
n corpul functiei ca n definitiile recursive.
Apelul

de functie

valoarea functiei

Evaluarea unui apel de functie e declansata atunci cand

e necesara n evaluarea unei expresii (inclusiv n executia unei

instructiuni de forma expresie ; ). Ea se efectueaza n urmatorii pasi:


se evalueaza toate expresiile care constituie argumentele functiei. Deci orice apeluri de
functii care apar n argumente se efectueaza nainte de apelul functiei considerate.
valorile argumentelor se atribuie parametrilor formali din antetul functiei, cu conversiile
necesare de tip (de exemplu ntregreal). Compatibilitatea de tip a expresiilor argument
e verificata deja la compilare, daca se cunosc tipurile parametrilor formali un motiv n
plus pentru care se cere ca o functie sa fie declarata nainte de a fi folosita.
se executa corpul functiei, cu parametrii formali avand valori initiale ca mai sus.
La ntalnirea instructiunii return, executia functiei se ncheie cu valoarea obtinuta
prin evaluarea expresiei date.
executia programului revine la locul de apel, unde valoarea returnata de functie e
folosita.
Transmiterea parametrilor
In limbajul C transmiterea parametrilor la functii se face prin valoare :
In momentul apelului, parametrii formali iau valoarea argumentelor (care au fost evaluate);
ei nu sunt substituiti cu expresiile argumentelor. In consecinta, pentru apelul discutat
mai sus, n instructiunea return se va evalua expresia 12 * 12 si nu (3 * sqr(2)) * (3
* sqr(2)). Altfel spus, o functie lucreaza cu valori (numerice), nu cu expresii simbolice.
Expresia 3 * sqr(2) se evalueaza o singura data, 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 augmentand functia sqr cu o instructiune de tiparire, o


practica utila n urmarirea executiei 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 rularii programului (prezentat sub textul sursa) evidentiaza si pentru
apelul printf din main evaluarea argumentelor nainte de nceperea executiei functiei.
Evaluarea argumentului al doilea produce cele doua linii tiparite n apelurile la sqr.
Aceste linii apar nainte de a scrie chiar si portiunea de text obisnuit din primul argument
(formatul), scriere n care consta tocmai executia functiei printf.
Returnarea valorilor

La ntalnirea instructiunii return, executia functiei se

ncheie; orice alte instructiuni care urmeaza n corpul functiei nu se mai executa.
Daca executia unei functii se termina prin atingerea ultimei acolade } fara a executa o
instructiune return, iar programul utilizeaza valoarea functiei, efectul e nedefinit
(programul se comporta imprevizibil).

O functie care nu prevede n orice situatie o

valoare returnata e scrisa eronat sub aspect logic.

Apelul recursiv

Discutam mecanismul apelului recursiv luand ca exemplu calculul lui 53

cu functia putere. In apelul pwr(5,

3) (x = 5, n = 3), expresia conditionala conduce la

evaluarea lui 5 * pwr(5,


2). Aceasta necesita un nou apel la pwr, de data aceasta cu parametrii x = 5, n = 2.
Procesul

se repeta cu pwr(5,

1) pana la apelul pwr(5,

0) pentru care valoarea se

calculeaza direct: 1. Din acest apel se revine n locul unde a fost facut: la evaluarea lui 5
* pwr(5,

0) care poate fi efectuata acum. Apelul pwr(5,

1) returneaza valoarea 5

folosita n calculul 5 * pwr(5, 1) pentru valoarea lui pwr(5,


2). In final, aceasta valoare, 25, e folosita 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
Urmarind secventa de apel, rezulta ca la un moment dat pot fi n executie mai multe
apeluri diferite la aceeasi functie. Fiecare apel reprezinta o instanta (copie) distincta
a functiei, cu propriile valori de parametri, cele primite n momentul apelului. La fel se
ntampla si n calculul pe hartie, prin desfasurarea formulei de recurenta:
pentru a calcula pe 53 , nlocuim pe x cu 5 si n cu 3. Trebuie calculat 52 : aplicand din
nou formula nlocuim pe n cu 2; aceasta nu afecteaza nsa instanta initiala a
problemei (53 ), unde n este n continuare 3.
3

In exemplul dat, recursivitatea are o structura liniara:

fiecare apel recursiv

genereaza un singur nou apel, pana la oprirea pentru cazul de baza. In acel moment,
sunt active toate apelurile, n numar de 4. Revenirea se face n ordine inversa fata de
cea de apel: din fiecare apel se revine n instanta care a efectuat apelul. Aici, executia
se reia n contextul

dinainte de apel: adica din locul n care a fost facut apelul (unde e folosita valoarea
returnata), si cu acele valori ale parametrilor corespunzand instantei respective.
Ca la orice apel de functie, informatia se transmite spre functia apelata prin
parametri, si napoi spre locul de apel (functia apelanta ) prin rezultat. In exemplul dat se
creeaza un lant n care la revenire, valoarea returnata de fiecare apel e folosita n
instanta apelanta pentru calculul propriului rezultat, care e transmis mai departe napoi
spre locul de apel. Rezultatul final e astfel efectul unui calcul n care cate un pas e efectuat
de fiecare din instantele apelate. Aceasta

e esenta recursivitatii si n acelasi timp

puterea ei n rezolvarea de probleme: ea permite exprimarea indirecta a solutiei prin pasi


simpli, din aproape n aproape, fara a necesita formularea directa a unei solutii
complexe.

Elementele unei definitii recursive


Intr-o definitie recursiva corecta se pot identifica urmatoarele
componente:
Cazul de baza. Trateaza situatiile (cele mai simple) n care notiunea recursiva e
definita direct.
Exemple: pentru sirurile recurente, primul termen (sau mai multi, la recurentele de
ordin > 1)
pentru liste, cea vida, sau cea cu un element; pentru expresii, constantele si
identificatorii
Relatia de recurenta : partea propriu-zis recursiva a definitiei, n care notiunea
definita apare si n corpul definitiei.

Exemple: formulele de recurenta pentru

siruri; ramura de definitie o lista e un element urmat de o lista; pentru expresii,


variantele de definitie cu expresie ntre paranteze, apel de functii (cu parametri
expresii) si cele cu expresii compuse cu operatori
Terminarea recursivitatii. Daca definitia urmeaza o ramura care contine din nou
notiunea definita, atunci definitia trebuie aplicata din nou. O notiune e corect
definita recursiv daca acest proces se opreste ntotdeauna. Pentru a fi riguroasa, o
3

definitie recursiva trebuie nsotita de o demonstratie ca aplicarea definitiei se


opreste dupa un numar finit de pasi.
Rezulta ca o definitie recursiva nu poate fi corecta fara un caz de baza, pentru
ca nu se ajunge niciodata la un punct unde notiunea poate fi definita direct. Cazul de
baza si relatia recursiva sunt de fapt alternative ale aceleiasi definitii. Acest lucru e
explicit n regulile sintactice care definesc

constructii de limbaj cum ar fi expresiile. Pentru un sir recurent putem evidentia


aceasta folosind
.

acolade: xn

xn

n=0
+p n>0

si transcrie apoi usor n program.

Pentru terminarea recursivitatii, cel mai uzual argument foloseste o masura


(cantitate) care descreste la fiecare aplicare a definitiei, pana atinge o valoare pentru care definitia e
data direct. La
siruri recurente, aceasta cantitate e chiar indicele n al termenului
general xn .
Alte exemple de recursivitate
Insiruirea vazuta recursiv

Si notiuni din afara matematicii, uneori foarte simple,

se preteaza la definitii recursive. Un tipar de definitii des ntalnit se bazeaza pe faptul


ca iteratia (repetitia) poate fi definita prin recursivitate. Putem defini astfel:
Un sir (secventa, lista) e fie un element, fie un sir urmat de
un element.
Uneori e util sa includem n definitie sirul vid, care apare natural n diverse operatii (ca
element neutru la concatenare; la initializarea sau dupa stergerea tuturor elementelor unei
liste, etc.):
Un sir e fie un sir vid, fie un element urmat de
un sir.
Cele doua variante difera atat prin cazul de baza (un element sau zero), cat si prin
pozitia notiunii recursive n definitie:

prima varianta e recursiva

la stanga

(notiunea definita recursiv sir e prima


n rescrierea sir urmat de un element), iar a doua e recursiva la dreapta , deoarece
n expandarea
element urmat de un sir notiunea definita sir e pe ultima pozitie. Tiparele de
recursivitate la stanga
si la dreapta conduc la prelucrari diferite n program, pe care le studiem si comparam
n continuare.

Recursivitatea n sintaxa limbajelor

Recursivitatea apare natural n definirea

precisa a sintaxei limbajelor de programare. Multe elemente de limbaj au n componenta


repetitia, care poate fi exprimata recursiv. Astfel, antetul unei functii poate fi definit (n
limita celor prezentate pana acum) ca:
antet-functie ::=

tip identificator ( parametri )

parametri

::=

void | lista-param

lista-param

::=

tip identificator | tip identificator , lista-param

Am folosit conventional simbolurile ::=

pentru definitie si | pentru alternativa .

Acest mod de a descrie regulile sintactice, adica gramatica unui limbaj se numeste forma
Backus-Naur (BNF).
Putem defini recursiv si alta notiune fundamentala, expresia. Din cele prezentate
pana acum:
expresie

::= constanta | identificator | identificator ( argumente )


| - expresie | expresie operator-binar expresie
| expresie ? expresie : expresie | ( expresie )

argumente

::= s | lista-argumente

lista-argumente ::= expresie | expresie , lista-argumente


unde s denota conventional alternativa cu continut vid (fara simboluri de limbaj), aici
pentru apeluri de forma functie(), fara argumente ntre paranteze.
Pentru o definitie riguroasa, trebuie sa precizam ca orice constructie de limbaj
trebuie definita printr-un numar finit de aplicari ale regulilor. Astfel, -(2 + 3) e o
expresie: se pot aplica pe rand regulile expresie ::= - expresie, expresie ::= ( expresie ),
expresie ::= expresie + expresie, si de doua ori regula expresie ::= constanta .
Doua tipare de calcul recursiv
Sa examinam un alt exemplu tipic de calcul recursiv,
factorialul.
.

n=0
si putem transcrie direct

Avem:

n! =

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 operatie efectuata e nmultirea cu n,

restul calculelor fiind efectuate anterior n


recursive care rezulta

apelul

fact(n-1) (si

din acesta). Ordinea calculelelor

celelalte apeluri

e indicata de paranteze n

expresia n! = ((((1 1) 2) . . .) (n 1)) n.


Expandand definitia pentru n 2, obtinem n! = ((n 2)! (n 1)) n. Inainte de
evaluarea lui
fact(n-2) stim ca n produs apar atat n cat si n-1, dar asa cum e scrisa functia, nu
se efectueaza direct
nmultirea lor: se apeleaza ntai fact(n-2), rezultatul e nmultit cu n-1 n cadrul
apelului fact(n-1),
si doar n final se face nmultirea cu n, pentru rezultatul lui
fact(n) .
Pornind de la aceasta observatie, rescriem factorialul

folosind asociativitatea

nmultirii, pentru a efectua cat mai multe nmultiri ndata ce factorii devin
disponibili: n! = 1 (2 (. . . ((n 1) n)))
Transcriind n C, am dori sa efectuam nmultirea (n-1)*n n cadrul apelului
functiei pentru n-1,
nainte de a calcula recursiv factorialul pentru n-2.
nmulti

In apelul pentru n-2 s-ar putea

apoi rezultatul lui (n-1)*n cu n-2, etc. Pentru aceasta, avem nevoie sa

transmitem la fiecare apel recursiv pe langa valoarea lui n si rezultatul nmultirilor deja
efectuate. Obtinem astfel:
unsigned fact_r(unsigned n, unsigned
r)
{
return n == 0 ? r : fact_r(n-1, r * n);
}

13

Parametrul r reprezinta rezultatul partial calculat. La fiecare apel recursiv, el e


nmultit cu valoarea
curenta a lui n si rezultatul e transmis mai departe la apelul pentru n-1. Cand n==0, toate
nmultirile au fost deja efectuate; rezultatul se gaseste acumulat n r si poate fi
returnat. Pe ramura recursiva, la revenire nu se mai efectueaza nici un calcul: valoarea
provenita din apelul recursiv e deja rezultatul complet si e returnata direct mai departe
la apelant.
Din primul apel pentru n, rezultatul partial pe care dorim sa-l transmitem spre apelul
pentru n-1 e tot valoarea n. Deci, initial, r ar trebui sa fie 1, si pentru a calcula pe n!
vom apela fact_r(n, 1) . De fapt, am definit o functie mai generala: fact_r(n, r)
calculeaza pe r n! .
Pentru a nu complica utilizatorul cu parametrul suplimentar datorat modului de calcul,
definim functia fact2 cu un singur parametru. Aceasta doar mpacheteaza apelul
initial fact_r(n, 1) :
unsigned fact2(unsigned n) { return fact_r(n,
1); }
Pentru factorial, putem alege ntre cele doua variante de scriere. In prima, calculul
se face la
revenirea

din apelurile recursive, iar acestea nu au nevoie de vreun rezultat partial

calculat anterior.
In a doua, rezultatul partial e transmis n adancime ca parametru, si actualizat nainte
de fiecare apel recursiv.
Exista nsa situatii n care parte din prelucrare se efectueaza nainte de fiecare apel
recursiv, fiind
necesar sa transmitem n jos la naintarea n recursivitate valori ce vor fi folosite
ulterior n calcule.

Inversarea cifrelor unui numar

Scriem o functie care ia un ntreg fara semn si-

l transforma n numarul cu aceleasi cifre zecimale dar n ordine inversa. Scriem solutia
pornind de la un exemplu: 1472. Ultima cifra, 2, devine prima cifra a rezultatului. Punem
ultima cifra ramasa din 147 dupa 2, obtinand
27 = 2 10 + 7. Din numarul ramas, 14, plasam ultima cifra dupa 27, obtinand 27
10 + 4 = 274, etc.
14

In cuvinte, pasul recursiv de prelucrare poate fi exprimat: rezultatul inversarii, daca


a mai ramas de inversat n, iar din inversarea ultimelor cifre s-a obtinut deja v, e acelasi cu
rezultatul inversarii lui n/10, cu valoarea intermediara 10 v + n mod 10. Desi enuntul
problemei are un singur parametru, solutia recursiva obtinuta manipuleaza doua
cantitati, deci scriem o functie recursiva cu doi parametri. Prelucrarea se opreste cand
n e 0 (nu mai sunt cifre de inversat), iar initial, v e valoarea fara nici o cifra, deci tot
0; astfel, pentru prima cifra c, expresia 10 0 + c da valoarea dorita 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 solutie o functie cu un singur parametru, ca n enunt, pentru a nu complica
utilizatorul cu
un parametru suplimentar pentru valoarea intermediara. Am scris astfel functia revnum
care apeleaza functia recursiva revnum r(n, 0) cu valoarea initiala necesara 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 problema e rezolvata prin reducerea la o instanta mai simpla a aceleiasi
probleme. Exprimat informal, algoritmul e:
daca numerele sunt egale, rezultatul e chiar valoarea lor
comuna
15

altfel, se scade cel mai mic numar din cel mai mare, si se repeta procedura cu
noile numere.
Exprimarea din urma (se repeta procedura) indica abordarea recursiva: solutia
se obtine re- zolvand aceeasi problema pentru valori noi ale numerelor (mai mici, deci
dupa un numar finit de pasi se ajunge la cazul de baza).
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);
}
Desi am transpus direct din cuvinte n formula recursiva si apoi n cod, a ramas
netratat un aspect:
enuntul initial e dat pentru numere pozitive, iar tipul unsigned permite si valoarea 0.
Apelarea (chiar
si accidentala) a functiei scrise mai sus cu un parametru nul va duce la o secventa
infinita de apeluri recursive, deoarece scazand 0 celalalt numar nu se modifica si
reluam acelasi apel (pana cand, n functie de mediul de rulare, programul se va
termina probabil fortat epuizand resursele de memorie).
Este important ca functiile pe care le scriem sa fie robuste si sa nu produca erori
neprevazute si catas- trofale. Ca atare, rescriem functia tinand cont ca 0 e divizibil cu
orice numar, si deci cmmdc(a, 0) = cmmdc(0, a) = a:
unsigned cmmdc(unsigned a, unsigned
b)
{
return b == 0 ? a : a == 0 ? : b
: a > b ? cmmdc(a - b, b)
16

: cmmdc(a, b - a);
}
In aceasta scriere, cazul a = b = 0 va intra pe ultima ramura, avand ca efect apelul
cmmdc(a, 0)
care va returna a.

Calculul recursiv al seriilor


Calculul sumei partiale a unei serii se preteaza natural la o exprimare recursiva. Notand
cu tn termenul
general, si sn = k=0 tk , obtinem imediat pentru termenul general: 1 + (pentru n 1),
.n

sn = sn

tn

17

si

(n > 0)
Avand o formula de calcul direct pentru termenul tn al seriei (exprimat deci ca functie
de n), putem transforma direct formula de mai sus ntr-o functie recursiva s, care
apeleaza pentru calcul functia t. Pentru exemplul simplu tn = 1/n (pentru n > 1, iar t0 =
0), putem scrie urmatorul 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);
}
int main(void)
{

printf("Constanta lui Euler e aprox. %f\n", s(1000)-log(1000));


return 0;
}
.

Din matematica, stim ca seria

n 1/n are suma infinita, si creste aproximativ ca

logaritmul natural
al lui n. Mai
precis,

n
lim (

1/k ln n) = c 0.5772...

k=1

Aproximatia calculata de program are primele 3 zecimale exacte.


18

Tipul double folosit n program reprezinta, ca si float, numere reale, dar cu precizie
mai buna (de aici si numele), si e recomandabil n calcule pentru a micsora acumularea
erorilor de rotunjire.

Este tipul standard folosit de functiile matematice de biblioteca

declarate n math.h (cum e si functia log pentru logaritmul natural) si tipul implicit
pentru constantele reale. Scrierea 1.0/n pentru termenul matematic 1/n e necesara pentru
a obtine mpartire

reala :

operandul 1.0

fiind real, va fi convertit implicit si

ntregul n. Altfel, 1/n ar fi nsemnat mpartire ntreaga, cu rest, si valoarea 0 pentru


n > 1.
Desigur ca 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 prezentata evidentiaza nsa forma generala a functiei s pentru suma
exprimata recursiv,
si programul poate fi adaptat la alta serie prin simpla nlocuire a functiei t.
E valabila aceeasi observatie generala facuta ntai pe exemplul factorialului despre
cele doua variante de definire a calculului recursiv. Functia s asa cum a fost scrisa mai
sus efectueaza calculul la revenirea din recursivitate, ultima adunare fiind sn1 + tn ,
corespunzand unei grupari a calculelor de forma: sn = (((t0 + t1 ) + t2 ) + . . .) + tn .
Cealalta alternativa o constituie transmiterea ca parametru suplimentar a unui
rezultat partial
deja calculat (initial zero), la care se acumuleaza n fiecare pas termenul curent. Astfel,
termenii sunt adunati efectiv n ordine inversa: sn = t0 + (t1 + (. . . + (tn1 + tn ))).
Functia se scrie:
double s2(unsigned n, double res)
{
return n == 0 ? res + t(0) : s2(n-1, res + t(n));
}

19

Calculul de aproximari cu o precizie data


Majoritatea exemplelor de calcul numeric recursiv date pana acum au aceeasi structura: se
calculeaza termenul unui sir definit printr-o relatie de recurenta, iar numarul de apeluri
recursive necesar pentru calcul e determinat de ordinul (indicele) termenului, dat la primul apel.
In matematica si practica ntalnim nsa adesea cazuri se doreste un calcul efectuat cu
o anumita
precizie: se genereaza o secventa de aproximari, iar cand aproximarea curenta a atins
precizia dorita, calculul se opreste.
Dam ca exemplu o metoda de aproximatie pentru calculul radacinii patrate

x. O

cunoscuta
formula matematica (poate fi derivata din metoda lui Newton, dar n acest caz particular e
mult mai veche) ne da secventa de aproximari: an+1

= (an + x/an )/2, cu o aproximare

initiala arbitrara (de exemplu a0 = 1).


Elementul cheie al solutiei e formularea problemei pentru a-i identifica si scoate n
evidenta caracterul recursiv: mai precis, parametrii si cazul de baza (oprirea din secventa de apeluri
recursive). Conditia de oprire nu e data de aproximarea initiala, ci, dupa cum rezulta din
enunt, de precizia atinsa. Pentru a o calcula, avem nevoie de aproximarea curenta, care devine
astfel parametru al problemei (cu valoarea initiala data de a0 ).
Putem atunci enunta solutia n cuvinte n felul urmator: functia cautata returneaza o
aproximatie de precizie data a lui

x, stiind o aproximatie curenta an data de

asemenea ca parametru. Daca


aproximatia curenta e suficient de buna, poate fi returnata (cazul de baza). Daca nu,
rezultatul va fi dat de aceeasi functie de calcul, apelata tot pentru x, dar cu o aproximatie
curenta mai buna, data de an+1 .
#include <math.h>
#include <stdio.h>
double rad(double x, double a)
{
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;
}
Functia standard fabs, declarata n math.h returneaza valoarea absoluta a unui numar real
(double), spre deosebire de functia abs (din stdlib.h) aplicabila doar la numere ntregi.
Logica comparatiei
este urmatoarea: a si x/a se afla ntotdeauna de o parte si de alta a valorii exacte

x.

Deci, daca
intervalul dintre ele e mai mic decat precizia dorita (n exemplul dat, 106 ), oricare din ele
va fi o aproximatie de precizie suficienta.

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 execuiei 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 existena 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);
}

Funcia este foarte ineficient, avnd complexitate exponenial, 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 distania n n raport cu
a, este situat la distana n1 fa de termenul urmtor b.
Necesitatea cunoaterii termenului urmtor impune prezena a doi termeni n lista de parametri.

Metoda divizrii (Divide et Impera).


Un algoritm recursiv obine soluia problemei prin rezolvarea unor instane mai mici ale aceleeai
probleme.

se mparte problema n subprobleme de aceeai natur, dar de dimensiune mai sczut

se rezolv subproblemele obinnduse soluiile acestora

se combin soluiile subproblemelor obinnduse soluia problemei

Ultima etap poate lipsi (n anumite cazuri), soluia problemei rezultnd direct din soluiile
subproblemelor.

Procesul de divizare continu i pentru subprobleme, pn cnd se ajunge la o dimensiune suficient


de redus a problemei care s permit rezolvarea printro metod direct.

Metoda divizrii se exprim natural n mod recursiv: rezolvarea problemei const n rezolvarea unor
instane 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);
DivImp(dim2, p2, &s2);
Combina(s1, s2, s);
}
else

RezDirect(dim, p, s);
}
Pentru evaluarea complexitii unui algoritm dezvoltat prin metoda divide et impera se rezolv o ecuaie
recurent avnd forma general: T(n)=a.T(n/b) + f(n), n care a >= 1, reprezint numrul
subproblemelor, iar n/b este dimensiunea subproblemelor, b > 1.
f(n) reprezint costul divizrii problemei n subprobleme i a combinrii soluiilor
subproblemelor pentru a forma soluia problemei.
Soluia acestei ecuaii recurente este dat de teorema master

Probleme si exercitii
Se 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)


{
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;
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

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);

return 0;
}
sau
#include <iostream>
using namespace std;

recursive.

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
*
***
*****
***
*
REZOLVARE:
#include <iostream>
using namespace std;

n=3

se

afiseaza

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,'*');
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<<" ";
}
}
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
harta si care este aria (numarul de valori de 1) maxima dintre ariile continentelor.
Exemplu:
harta.in
6

harta.out

(numarul

de

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++)
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;

continente)

}
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
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++)
if(litere(s,n,c)==map) cout<<c<<" ";
return 0;
}

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