Sunteți pe pagina 1din 5

Lucrarea nr.

7
Algoritmi recursivi

1. Scopul lucrării – îl reprezintă prezentarea conceptului de recursivitate precum şi a


câtorva categorii tipice de algoritmi recursivi.

2.Aspecte teoretice
2.1. Definirea recursivităţii
Recursivitatea presupune de asemenea execuţia repetată a unei porţiuni de program. În
contrast cu iteraţia însă, în cadrul recursivităţii condiţia este verificată în decursul execuţiei
programului (nu la sfârşitul ei ca la iteraţie) şi, în caz de rezultat satisfăcător, întreaga
porţiune de program este apelată din nou ca subprogram a ei însăşi, în particular ca un
subprogram a porţiunii de program originale care însă nu şi-a terminat execuţia. În momentul
satisfacerii condiţiei de revenire, se reia execuţia programului apelant exact din punctul din
care s-a apelat pe el însuşi. Acest lucru este valabil pentru toate apelurile anterioare
satisfacerii condiţiei.
Structurile de program necesare şi suficiente în exprimarea recursivităţii sunt
subrutinele care pot fi apelate prin nume. Dacă o subrutină P conţine o referinţă directă la ea
însăşi se spune că este direct recursivă; dacă P conţine o referinţă cu o altă subrutină Q, care
la rândul ei conţine o referinţă (directă sau indirectă) la P, se spune că P este indirect
recursivă.
De regulă, unei subrutine i se asociază un set de obiecte ale subrutinei (variabile,
constante, tipuri, funcţii şi proceduri), care sunt definite local în subrutină şi care nu există
sau nu au înţeles în afara acesteia. De fiecare dată când o astfel de subrutină este apelată
recursiv, se creează un nou set de astfel de obiecte locale, specifice apelului. Deşi aceste
obiecte au acelaşi nume ca şi cele corespunzătoare lor din instanţa anterioară a subrutinei (în
calitate de program apelant), ele au valori distincte şi orice conflict de nume este evitat prin
regulile care stabilesc domeniul identificatorilor: identificatorii se referă întotdeauna la setul
cel mai recent creat de obiecte. Aceleaşi reguli sunt valabile şi în cazul parametrilor
subrutinei, asociaţi prin definiţie cu setul de variabile.
Ca şi în cazul structurilor repetitive, procedurile recursive necesită evaluarea unei
condiţii de terminare, fără de care un program recursiv duce la o buclă de program infinită.
Aplicaţiile practice au demonstrat că, deşi teoretic recursivitatea poate fi infinită, practic ea
nu numai că este finită, dar adâncimea sa este relativ mică. Motivul este că, fiecare apel
recursiv al unei subrutine necesită alocarea unui volum de memorie (in stiva) destinat
obiectelor (variabilelor) sale curente. În plus, alături de acestea mai trebuie memorată şi
starea curentă a programului, cu scopul de a fi refăcută atunci când noua activitate a
subrutinei se termină şi urmează ca cea veche să fie reluată.
Algoritmii recursivi sunt potriviţi a fi utilizaţi atunci când problema care trebuie
rezolvată sau datele care trebuiesc prelucrate sunt definite în termeni recursivi (algoritmii
implementează în acest caz definiţia recursivă). Utilizarea recursivităţii trebuie însă evitată
ori de câte ori stă la dispoziţie o rezolvare bazată pe iteraţie. De fapt, implementarea
recursivităţii pe elemente nerecursive dovedeşte faptul că, practic, orice algoritm recursiv
poate fi transformat într-unul pur iterativ. La transformare putem distinge două cazuri:
a). Cazul în care apelul recursiv al procedurii apare la sfârşitul ei, drept ultima
instrucţiune a procedurii (tail recursion). În această situaţie, recursivitatea poate fi înlocuită
cu o buclă simplă de iteraţie. Acest lucru este posibil deoarece, în acest caz, revenirea dintr-
un apel încuibat presupune şi terminarea instanţei respective a subrutinei, motiv pentru care
contextul apelului nu trebuie salvat. Astfel, dacă o procedură P(x) conţine ca şi ultim pas al
său un apel recursiv la ea însăşi P(y), atunci acest apel poate fi înlocuit cu instrucţiunea de
atribuire x=y, urmată de un salt la începutul codului lui P. Dacă P are mai mulţi parametri, ei
pot fi trataţi fiecare în parte ca x şi y. Aici y poate fi o expresie dar x trebuie să fie o variabilă
transmisibilă prin adresă, astfel încât valoarea sa să fie memorată într-o locaţie specifică
apelului.
b). Cazul în care apelurile recursive se realizează în interiorul procedurii, varianta
iterativă a acestei situaţii presupune tratarea explicită de către programator a stivei apelurilor
recursive în care se salvează contextul fiecărei instanţă de apel. Eliminarea recursivităţii
poate fi făcută, teoretic, în orice situaţie. Eliminarea recursivităţii poate duce la creşterea
performanţelor însă, în cele mai multe cazuri, algoritmul devine mult mai complicat şi mai
greu de înţeles.
În general, atunci când nu avem “tail recursion”, pentru eliminarea recursivităţii se
foloseşte o structură de date de tip stivă, definită de utilizator. Un nod al acestei stive va
conţine următoarele elemente:
- valorile curente ale parametrilor procedurii
- valorile curente ale tuturor variabilelor locale ale procedurii
- o indicaţie referitoare la adresa de retur, adică referitoare la locul în care revine
controlul execuţiei în momentul în care apelul curent al instanţei procedurii se termină.

2.2. Tehnica divizării (divide and conquer)


Una dintre metodele fundamentale de proiectare a algoritmilor se bazează pe tehnica divizării
(divide and conquer). Principiul de bază este acela de a descompune o problemă în mai multe
subprobleme a căror rezolvare este mai simplă şi din soluţiile cărora se poate determina
soluţia problemei iniţiale. Acest mod de lucru se repetă recursiv până când subproblemele
devin banale iar soluţiile lor, evidente. O aplicaţie tipică a tehnicii divizării are următoarea
structură:

procedure rezoiva (x)


begin
if x este divizibil in subprobleme then
begin
divide pe x in doua sau mai multe parti: x1.....xk;
combina cele k solutii partiale intr-o solutie pentru x
end
else
rezolva pe x direct
end;

Dacă recombinarea soluţiilor parţiale este substanţial mai simplă decât rezolvarea întregii
probleme, această tehnică duce la proiectarea unor algoritmi extrem de eficienţi.

2.3. Algoritmi recursivi pentru determinarea tuturor soluţiilor unor probleme.

A. Algoritm pentru evidenţierea tuturor posibilităţilor de împărţire a unei cantităţi de


valoare întreagă dată (N) în părţi de valoare l1 sau l2 (cu posibilitate de generalizare
pentru l1,.l2,….lk).

Se bazează pe următoarea tehnică de lucru:


- pentru N >min(l1, l1) există două posibilităţi
1). Când la început se ia o parte de valoare l1 şi restul cantităţii de N- l1 se imparte în
toate modurile posibile (apel recursiv la funcţia de împărţire, cu parametru N- l1);
2). Când la început se ia o parte de valoare l2 şi restul cantităţii N- l2 se împarte în
toate modurile posibile posibile (apel recursiv la funcţia de împărţire, cu parametru N- l1);
- pentru N= min(l1, l1) avem cazul banal de o tăietură de lungime egală cu minimul dintre l 1 şi
l2 ;
- pentru N< min(l1, l1) nu există nici o posibilitate.

B. Algoritm pentru determinarea tuturor soluţiilor de ieşire


dintr-un labirint
Algoritmul următor presupune un labirint descris cu ajutorul unui tablou tridimensional de
caractere de dimensiuni [N+1] x [N+1], în care cu ajutorul caracterul ‘*’ sunt reprezentaţi
pereţii iar cu ajutorul caracterului ' ' (blanc) culoarele; punctul de start este centrul
labirintului (dar nu este obligatoriu). Căutarea se execută astfel: dacă valoarea caracterului
din poziţia curentă este ' ', se intră pe o linie posibilă de ieşire şi se machează poziţia cu
caracterul '+'. Dacă s-a ajuns la ieşire, se execută tipărirea tabloului (labirintul şi drumul
găsit). În caz contrar se apelează recursiv procedura P pentru cele patru poziţii din vecinătatea
imediată a poziţiei curente.
Este important să subliniem că marcajul de drum se şterge, de îndată ce s-a ajuns la o
fundătură. Ştergerea se execută prin generarea unui caracter ' ' (blanc) pe poziţia curentă
înainte de părăsirea procedurii.

#include "stdafx.h"
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
using namespace std;
#define n 5

char m[n + 1][n + 1];


int i, j;
div_t a, b;

void tipar()
{
for (i = 0; i <= n; i++)
{
cout<< " ";
for (j = 0; j <= n; j++)
cout<<m[i][j];
cout<<"\n";
}
cout<<"\n";
}
void p(int x, int y)
{
if (m[x][y] == ' ')
{
m[x][y] = '+';
a = div(x, n);
b = div(y, n);
if ((a.rem == 0) || (b.rem == 0))
tipar();
else
{
p(x + 1, y);
p(x, y + 1);
p(x - 1, y);
p(x, y - 1);
}
m[x][y] = ' ';
}
}
int main()
{
for (i = 0; i <= n; i++)
for (j = 0; j <= n; j++)
m[i][j] = '*';
for (j = 2; j <= 5; j++)
m[1][j] = ' ';
m[2][1] = ' ';
m[2][2] = ' ';
m[2][4] = ' ';
m[3][2] = ' ';
m[3][3] = ' ';
m[3][4] = ' ';
m[4][2] = ' ';
m[5][2] = ' ';
tipar();
a = div(n, 2);
p(a.quot, a.quot);
}

În exemplul de mai sus, structura iniţială a labirintului a fost furnizată prin setarea
elementelor tabloului m din programul principal. Acest lucru însă se poate realiza şi într-o
altă manieră, de exemplu prin citire de la tastatură sau dintr-un fişier.

2.4 Problemă rezolvată


Se considera un set de N elemente. Să se afişeze toate permutările posibile care se pot realiza
între aceste elemente.

#include "stdafx.h"
#include <iostream>
#include <conio.h>
#include <stdio.h>
using namespace std;

int a[20], n, i;
int pr[20];
void perm(int i)
{
int j, f;
if (i == n + 1)
{
cout<<"\n";
for (f = 1; f <= n; f++)
cout<<pr[f];
}
else
for (j = 1; j <= n; j++)
if (pr[j] == -1)
{
pr[j] = a[i];
perm(i + 1);
pr[j] = -1;
}
}
int main()
{
cout<<"Dati numarul de elemente = ";
cin>>n;
for (i = 1; i <= n; i++)
{
cout<< "\n\nDati valoarea elementului "<<i<<" = ";
cin>>a[i];
pr[i] = -1;
}
perm(1);
_getch();
}

3. Probleme propuse

1. Se dă un vector, v, cu n elemente. Folosind o funcție recursivă, calculați suma elementelor


vectorului (dimensiunea vectorului și elemente vectorului vor fi citite de la tastatură).
2. Pentru problema clasică de determinare a tuturor soluţiilor de ieşire dintr-un labirint,
prezentată în cadrul acestei lucrări:
a). să se scrie programul de aflare a optimului (drumul cel mai scurt de iesire);
b). să se scrie un program pentru determinarea tuturor soluţiilor şi a optimului de ieşire dintr-
un labirint tridimensional.
3. Se consideră un set de N caractere. Să se afişeze toate combinaţiile de N luate câte M ale
acestor caractere.
4. Se consideră un set de N studenţi. Să se grupeze studenţii în grupuri de câte 5, şi să se
afişeze pentru fiecare grupă toate permutările posibile.
5. Să se scrie câte o funcţie recursivă pentru:
a). transformarea unui întreg din baza 10 în altă bază data
b). tipărirea nodurilor unei liste în ordine inversă
c). analog cu b), dar pentru elementele unui tablou
d). copierea în ordine inversă a liniilor dintr-un fisier text, în altul.