Sunteți pe pagina 1din 30

PCL-2, Seria B,

2020-2021
Curs 1
Cuprins
1. RECURSIVITATE
• Noţiuni introductive
• Funcţii recursive. Incărcarea stivei
• Ieşirea din recursivitate
• Concluzii
• Exemple

2
1. RECURSIVITATE
1.1. Noţiuni introductive

• Noţiunea de recursivitate din programare, derivă în mod


natural din noţiunea matematică de recursivitate

• In matematică o noţiune este definită recursiv dacă în


cadrul definiţiei apare însăşi noţiunea care se defineşte
• Algoritmi recursivi:
– atunci când o problemă se rezolvă prin rezolvarea unor versiuni
mai mici (simple) ale aceleași problemei

3
• Factorialul:
• n!= 1*2*3*...*n, ca şi definiţie nerecursivă
• n! = n * (n-1)!, ca şi definiţie recursivă
• 0! = 1

• Funcţia lui Fibonacci:


• f(n) = 0, dacă n=0
• f(n) = 1, dacă n=1
• f(n) = f(n-1) + f(n-2), dacă n>= 2

• Combinările:
C(n,k) = C(n-1, k) + C(n-1, k-1), C(n,0) =1; C(n,1)=n;

• Aranjamentele:
A(n,k ) = n*A(n-1, k-1), A(n,1)=n; 4
• cmmdc(m, n) =
• m, dacă n=0
• cmmdc(n, m%n), altfel

• cmmdc(m, n) =
• m, dacă m=n
• cmmdc(m-n, n), dacă m>n
• cmmdc(m, n-m), dacă m<n

5
• Tipuri de recursivitate:
– Date definite recursiv (la nivelul limbajului C/C++ prin
intermediul structurilor ce au un câmp de tip pointer la
structura ce tocmai se definește)
– Proceduri/Funcții recursive

6
1.2. Funcţii recursive

• In programare, recursivitatea este exprimată cu ajutorul


subprogramelor, iar în limbajul C/C++ cu ajutorul funcţiilor

• O funcţie este recursivă, dacă executarea ei implică cel


puţin un autoapel
• Autoapelarea se poate realiza:
– direct, permiţând ca funcţia să se apeleze direct prin ea însăşi
(recursivitate directă)
– indirect, fie prin intermediul altor funcţii care se apelează circular
(recursivitate indirectă)

7
• Factorialul
• n! = n * (n-1)!
• 0! = 1

int factorial(int n)
{
if (n == 0)
return 1;
else
return n*factorial(n-1);
}

void main(void)
{
cout << “Factorial de 3 = ” << factorial(3) << endl;
}
8
• Orice apel recursiv al unei funcţii poate fi văzut ca o nouă
instanţă de calcul pentru acea funcţie

• La fiecare apel, pe stiva program se rezervă spațiu pentru


un nou set de variabile locale (inclusiv parametrii formali)

• Aceste variabile locale au acelaşi nume la fiecare apel dar


au locaţii şi valori diferite de la un apel la altul şi ca urmare
orice conflict de nume este evitat (domeniul de vizibilitate)

9
10
#include <iostream>
#include <conio.h>
using namespace std;

int factorial(int n);

void main(void)
{
int n = 4;
cout << "\nStart proces recursiv: Factorial(" << n << ")...\n" ;
cout << "\n\nFactorial(" << n << ") = " << factorial(n) << endl;
_getch();
}

11
int factorial(int n)
{
int val;
cout << "\n\t factorial(" << n << ")";
if (n == 0)
{
cout << "\n\n\t return 1";
return 1;
}
else
{
val = n*factorial(n-1);
cout << "\n\t return " << val;
return val;
}
}
12
• La apelul unei funcţii (inclusiv la un autoapel), pe stivă se
vor depune:
– parametrii de apel (valorile parametrilor transmişi prin valoare sau
adresele parametrilor transmişi prin referinţă) în locațiile rezervate
parametrilor formali
– variabilele locale (dacă sunt)
– adresa de revenire la instrucţiunea imediat următoare apelului

• La terminarea execuției funcției, stiva este eliberată și


ajunge în aceeași stare ca înainte de apel

13
Stiva program

adresa2

Stiva program 0

adresa2 adresa2

Stiva program 1 1

adresa2 adresa2 adresa2

Stiva program 2 2 2

adresa1 adresa1 adresa1 adresa1

3 3 3 3

14
• Şirul lui Fibonacci
• f(n) = 0, dacă n=0
• f(n) = 1, dacă n=1
• f(n) = f(n-1) + f(n-2), dacă n>=2

int fib(int n)
{
if (n < 2)
return n;
else
return (fib(n-1) + fib(n-2)); // recursivitate neliniara
}

void main(void)
{
cout << “Fibonacci de 7 = ” << fib(7) << endl;
}
15
• Metoda este însă foarte ineficientă, timpul de calcul fiind
de ordinul Φ (numit secţiunea de aur) ce reprezintă un
timp exponenţial
• Ineficienţa este dată şi de faptul că se evaluează unele
elemente ale şirului de mai multe ori:

16
17
• Primele n numere din şirul lui Fibonacci se pot calcula
iterativ folosind un şir ajutător :

int fib(int n, int *p)


{
int i=2;
p[0]=0; p[1]=1;
for(; i < n; i++)
p[i] = p[i-1] + p[i-2];
}

• Acest algoritm rezolvă aceiaşi problemă însă într-un


timp liniar şi nu exponenţial
18
• Numerele din şirul lui Fibonacci se pot calcula individial şi
fără şirul ajutător:

int fib(int n)
{
int i=1, j=0, k;
for(k=1; k < n; k++)
{
j = i+j;
i = j-i;
}
return j;
}

19
• Combinările
C(n,k) = C(n-1,k) + C(n-1,k-1); C(n,0) =1; C(n,1)=n;
int comb(int n, int k)
{
if(k==0) return 1;
if(k==1) return n;
return (comb(n-1, k) + comb(n-1, k-1)); // recursivitate neliniara
}

• Şi în acest caz se fac evaluări multiple:


– C(5,3) = C(4,3) + C(4,2)
– C(4,3) = C(3,3) + C(3,2)
– C(3,2) = C(2,2) + C(2,1)
– C(4,2) = C(3,2) + C(3,1)
– C(3,2) = C(2,2) + C(2,1)
20
• C.m.m.d.c.
cmmdc(m, n) = m, dacă n=0
cmmdc(n, m%n), altfel

int cmmdc(int m, int n)


{
if(n== 0)
return (m);
else
return (cmmdc(n, m%n));
}

21
• Precizări
– Implementările anterioare sunt simpliste
– Implementările reale ar trebui să mai aibă în vedere și:
• Verificări/validări ale parametrilor de apel coroborate cu tipul
de date asociat și cu constrângeri specifice problemei de
rezolvat (de exemplu parametrul funcției factorial() este de tip
întreg cu semn, adică funcția ar putea fi apelată și cu valori
negative ale parametrului de apel...)
• Estimarea domeniului de valori pentru tipul returnat și alegerea
unui tip asociat corespunzator (unsigned, unsigned long.
double, ...)

22
• Câteva concluzii:
– Un algoritm recursiv presupune mai multe nivele de
lucru
– Pe fiecare nivel se întâmplă acelaşi lucru dar la o altă
dimensiune a problemei (de obicei, mai mică) (avem
așa numitul caz recursiv)
– Există cel puţin un nivel ce admite o rezolvare directă
(cazul/cazurile de bază)

– D.p.d.v. al limbajului de programare apare un


autoapel:
• Autoapelul se face pentru o altă dimensiune (de obicei mai
mică) a problemei
• Există o dimensiune a problemei pentru care nu se mai face
autoapel ci se returnează un rezultat (cazul/cazurile de bază)
23
1.3. Ieşirea din recursivitate
• Orice apel de funcţie provoacă memorarea pe stivă a
parametrilor acelei funcţii, a variabilelor locale şi a
adresei de revenire la prima instrucţiune de după apel
– ca urmare, la apeluri repetate, spaţiul ocupat pe stivă creşte și
poate ajunge în afara limitelor admise

• Pentru evitarea situaţiei în care funcţia se apelează pe ea


însăşi la infinit, este obligatoriu ca autoapelul să fie legat
de îndeplinirea unei condiţii care să asigure oprirea din
acel ciclu de autoapeluri
– frecvent, cazul recursiv (autoapelul) apare pe o ramură a unei
instrucțiuni de decizie

• Posibilităţi:
– se asociază funcţiei recursive un parametru întreg n şi apelul se
face cu valorile (n-1), (n-2),… cât timp n>0
– se asociază funcţiei recursive un parametru logic iar apelul se
face atâta timp cât parametrul are valoarea TRUE
24
1.4 Concluzii
• La implemetarea unei funcții recursive, se verifică
următoarele:
– Existența cazului/cazurilor de bază și returnarea de valori
corecte
– Existența cazului/cazurilor recursive și returnarea de valori
corecte
– Evitarea recursivității infinite

• Utilizarea tehnicilor recursive nu conduce, de obicei, la


soluţii optime:
– algoritmii recursivi necesită mai multe resurse decât cei iterativi
• apelurile repetate necesită operaţii repetate pe stivă
• poate apare fenomenul de depăşire a stivei

– unii algoritmi recursivi sunt inerent ineficienţi (Fibonacci,


combinări: anumite valori sunt calculate de mai multe ori)
– varianta recursivă se preferă în cazurile în care înlocuirea
recursivităţii cu iteraţia cere un efort deosebit, algoritmul
pierzându-şi din claritate iar testarea şi întreţinerea devin dificile
– soluţia recursivă duce însă la soluţii mai elegante şi mai uşor de 25
urmărit
• Exemplul 1: suma cifrelor unui număr întreg
– Abordare:
• Recursivă:
– ultima cifră + suma cifrelor numărului din stânga
– cazul de bază: numărul are o singură cigră
• Nerecursivă (iterativă): suma cifrelor (cifră cu cifră)

int suma_cifre_recursiv(int n)
{
if (n < 10)
return n;
else
return n%10 + suma_cifre_recursiv((int)(n/10));
}

int suma_cifre_iterativ(int n)
{
int s=0;
while(n != 0) {
s += n%10;
n /= 10;
}
return s; 26
}
int suma_cifre_recursiv(int n)
{
int val1, val2;
cout << "\n\t suma_cifre_recursiv(" << n << ")";
if (n<10)
{
cout << "\n\n\t return " << n;
return n;
}
else
{
val1 = n%10;
val2 = suma_cifre_recursiv((int)(n/10));
cout << "\n\t return " << val1 << " + " << val2;
return val1 + val2;
}
}

27
• Exemplul 2: suma elementelor unui vector numeric
– Abordare:
• Recursivă:
– ultimul element + suma vectorului format din primelor (n-1) elemente
– cazul de bază: vectorul are un singur element
• Nerecursivă: suma, element cu element

int suma_elem_recursiv(int tab[ ], int n)


{
if (n==1)
return tab[0];
else
return tab[n-1] + suma_elem_recursiv(tab, n-1);
}

int suma_elem_iterativ(int tab[ ], int n)


{
int s=0;
for(int i=0; i<n; i++)
s += tab[i];
return s; 28
}
• Exemplul 3: şir palindrom
– Un şir este palindrom dacă:
• primul caracter este identic cu ultimul caracter
• restul şirului este un şir palindrom
• cazul de bază: șirul are cel mult un element
• exemple: tot, rotor, madam, racecar, 02022020

int mirror(char *s)


{
char buf[250]=“”;
if (strlen(s) <= 1)
return 1;
else
if(toupper(s[0]) == toupper(s[strlen(s) - 1]))
return mirror(strncpy(buf, s+1, strlen(s) - 2);
else
return 0;
}

29
• Există tipuri de probleme care de obicei se rezolvă cu
ajutorul funcţiilor recursive, în general în cazul
problemelor a căror metode de rezolvare se pot defini
recursiv:
– metode de divizare, divide et impera;
– metode de căutare cu revenire, backtracking;
– metode în care datele sunt definite recursiv, etc.

30

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