Utilizarea si programarea calculatoarelor - Laborator 5

Recursivitatea

Apelul recursiv al funcțiilor nu se deosebește principial cu nimic de apelul oricărei funcții într-un program. La fiecare apel de funcție se salvează pe stivă contextul de apel: se reține punctul de program de unde se face apelul funcției, și unde se va reveni după încheierea acesteia, precum și valorile variabilelor locale curente, care nu sunt accesibile în funcția nou apelată, dar care își păstrează valorile și devin din nou disponibile după încheierea acesteia. Se începe execuția funcției apelate de la prima instrucțiune din corpul acesteia, cu o nouă copie pentru fiecare variabilă locală. Deci, într-o secventa e n apeluri recursive în adâncime, există n copii distincte ale variabilelor locale din funcție, dar doar cea mai recentă e activă. La sfârșitul funcției (acolada } sau instrucțiunea return), programul revine în locul de unde a fost apelat, unde s-au pastrat valorile variabilelor locale din momentul apelului.

Pentru a evidenția cele de mai sus, rulați următorul program care apelează recursiv funcția f și tipărește pe parcurs mesaje pentru a indica unde a ajuns execuția.

#include <stdio.h>

void f(int n)
{
  if (n == 0) return;
  printf("Am intrat în funcția f cu parametrul %d\n", n);
  f(n - 1);
  printf("Ieșim din funcția f cu parametrul %d\n", n);
}

int main(void)
{
  f(5);
  return 0;
}

Recursivitatea și iterația

Există o legătură strânsă între recursivitate și iterație; de multe ori, un program recursiv poate fi transformat foarte ușor într-unul iterativ și invers. În ambele cazuri, aceeași prelucrare (corpul ciclului, respectiv al funcției recursive) se repetă de mai multe ori. În cazul unui ciclu, o iterație modifică valorile unor variabile, și iterația următoare se execută pentru valorile noi; la fel, apelul recursiv se face în general cu alte valori ale parametrilor față de cele inițiale. În ambele cazuri, pentru un algoritm corect, prelucrarea trebuie să se încheie după un număr finit de pași: acest lucru e asigurat de condiția de ieșire din ciclu, respectiv de cazul de bază al recursivității, în care funcția returnează direct o valoare, fără a se mai apela recursiv.

Un exemplu simplu prezentat la curs e cel al tipăririi unui șir:

void puts_nonrec(char *s)
{
  while (*s) {		/* condiția de oprire: *s = '\0' */
    putchar(*s);	/* prelucrarea: tiparirea unui caracter */
    s = s+1;		/* variabila modificata: s (incrementat) */
  }
  putchar('\n');	/* cazul de baza: la ieșirea din iterație */
}
void puts_rec(char *s)
{
  if (*s) {		/* condiția de oprire: *s = '\0' */
    putchar(*s);	/* prelucrarea: tiparirea unui caracter */
    puts_rec(s+1);		/* noul parametru de apel: s+1 in loc de s */
  } else putchar('\n'); /* cazul de baza: la ieșirea din recursivitate */
}
Un alt exemplu este cel al cautarii binare al unei valori v într-un tablou a, între indicii l și r inclusiv.
double *bsearch_nonrec(double v, double *a, int l, int r) {
  while (l < r) {	/* cât timp intervalul are >= 2 elemente */
    int m = (l+r)/2;	/* calculează punctul de mijloc */
    if (v>a[m]) l = m+1;/* continuă căutarea în intervalul din dreapta */
    else r = m;		/* continuă căutarea în intervalul din stânga */
  }			/* iese când intervalul e degenerat, <= 1 element */
  if (v==a[l]) return a+l;	/* returnează adresa elementului gasit */
  else return NULL;		/* sau NULL dacă elementul nu a fost găsit */
}
double *bsearch_rec(double v, double *a, int l, int r) {
  int m = (l+r)/2;
  if (l < r) {		/* continuă recursivitatea dacă >= 2 elemente */
    if (v>a[m])		/* continuă căutarea în intervalul din dreapta */
      return bsearch_rec(v, a, m+1, r);	/* cu noul parametru l egal cu m + 1 */
    else		/* continuă căutarea în intervalul din stânga */
      return bsearch_rec(v, a, l, m);	/* cu noul parametru r egal cu m */
  } else		/* iese din recursivitate pentru interval <= 1 */
    if (v==a[l]) return a+l;	/* returnează adresa elementului gasit */
    else return NULL;		/* sau NULL dacă elementul nu a fost găsit */
}
De cele mai multe ori, varianta iterativă a unui algoritm e mai performantă decât cea recursivă, pentru că evită costul suplimentar al apelurilor de funcție, împreună cu salvarea pe stivă și transmiterea parametrilor. Mai mult, în cazul unei adâncimi excesive a recursivității, se poate produce depășirea stivei. Sunt însă multe cazuri în care recursivitatea e modalitatea naturală de rezolvare, și alte soluții ar fi sensibil mai complicat și neintuitiv de exprimat.

Problemă: Funcția putere

Scrieți o versiune recursivă pentru funcția putere cu bază reală și exponent întreg, bazată pe definiția recursivă: x la puterea n = x * x la puterea (n - 1) pentru n > 0. Completați apoi funcția și pentru exponent negativ.

Problemă: Inversarea unui șir

Scrieți un program care citesțe de la intrare o linie de text și o afișează în ordine inversă (de la coadă la cap). Folosiți o funcție recursivă.

Și în acest caz, programul se poate obține direct din formularea recursivă mai întâi a structurii de date care trebuie prelucrată, și apoi a prelucrării:
- o linie de text este o linie vidă (caracterul '\n') sau un caracter urmat de o linie de text
- dacă linia e formată din caracterulc și restul r, inversa ei e formată din inversa lui r urmată de c.

Se observă că efectul unei funcții recursive depinde în mod critic de locul unde e plasat apelul recursiv în cadrul prelucrării din corpul funcției (la început, la mijloc sau la sfârșit).

Ca alternativă, scrieți o variantă recursivă pentru funcția strrev care ia ca parametru un șir de caractere și returnează, alocat dinamic, inversul lui (de la coadă la cap).

Problemă: Rezolvarea unei ecuații prin metoda înjumătățirii

În semestrul trecut, ați implementat la laborator găsirea rădăcinii unei funcții continue și monotone pe un interval, cu o precizie dată, dacă funcția are semne diferite la capetele intervalului. Se calculează valoarea funcției la mijlocul intervalului și se continuă căutarea in acea jumătate de interval pe care funcția schimbă semnul. Implementați recursiv algoritmul descris mai sus.

Problemă: Minimul/maximul unui șir

Scrieți o funcție recursivă care ia ca parametri un tablou de numere și respectiv lungimea lui și returnează valoarea numărului minim din tablou. Pentru un tablou de lungime n > 1 se află întâi minimul primelor n - 1 elemente, și apoi se ia minimul dintre această valoare și ultimul element.

Problemă: Sortarea unui tablou

Sortarea prin selecție și cea prin metoda bubblesort au în comun faptul că după prima iterație exterioară, elementul extrem al tabloului (minim sau maxim, în funcție de sensul ales) este poziționat corect la capăt, după care procedura se reia pentru al doilea element în mărime, etc. Folosind una din metode, implementați recursiv procedura de sortare, dându-i ca parametri tabloul și lungimea lui. Poziționați corect ultimul (cel mai mare) element, după care apelați recursiv pentru restul tabloului (de la începutul său, dar de lungime cu 1 mai mică).

Problemă: Șiruri binare de lungime dată

Scrieți un program care tipărește în ordine lexicografică (alfabetică) toate șirurile de lungime dată N formate din cifrele 0 și 1. Folosiți o funcție recursivă care pune pe rând 0 și respectiv 1 pe poziția următoare în șir, iar dacă a ajuns la sfârșit tipărește șirul.
În acest caz, vor exista două apeluri recursive ale funcției în corpul său, iar numărul total de apeluri e exponențial (ca și numărul de șiruri distincte, 2 la puterea n).
Marius Minea
Last modified: Mon Mar 22 08:27:50 EET 2004