Programarea Calculatoarelor: Laborator 3

In acest laborator se vor scrie functii care prelucreaza un text citit caracter cu caracter de la intrare, cu functia getchar().

Exemplul 1 Citirea de numere reale
Adaptăm funcția readnat scrisă la curs pentru a citi un număr real nenegativ, în format cu punct zecimal. Pentru aceasta trebuie să
-- scriem cod pentru a citi secvența de cifre de după punctul zecimal, și a calcula partea zecimală
-- combinăm acest cod cu cel care dă valoarea părții întregi
Pentru partea fracțională scriem o relație de recurență similară cu cea pentru partea întreagă. Remarcăm însă că fiecare cifră cn nou citită se înmulțește cu o altă putere (negativă) a lui 10:

  f0 = 0  (partea fracționară), p1 = 0.1 (puterea lui 10)
  fn = fn-1 + cn * pn (adaugă ultima cifră cn)
  pn+1 = 0.1*pn (puterea lui 10: 10-n)
Pornind de la aceste relații, putem scrie o funcție recursivă similară ca structură lui readnat, dar care primește pe lângă caracterul curent citit doi alți parametri: numărul acumulat până acum, și puterea negativă a lui 10 corespunzătoare poziției curente după punctul zecimal (pentru a evita recalcularea ei la fiecare pas):
double readfrac(double r, double p10, int c)
{
  return isdigit(c) ? readfrac(r+(c-'0')*p10, .1*p10, getchar()) : c
}
Această funcție trebuie apelată după întâlnirea punctului zecimal, deci o folosim în momentul în care la citirea părții întregi nu mai întâlnim o cifră, în cazul în care caracterul curent citit este punctul zecimal. Ea trebuie apelată atunci cu un caracter nou citit (primul după punct), și 0.1 ca și putere curentă a lui 10 pentru prima poziție. Ca valoare pentru rezultatul parțial transmis ca parametru putem folosi partea întreagă deja calculată, la care se va acumula atunci partea fracționară, cifră cu cifră. Obținem:
double readreal_rc(double r, int c)
{
  return isdigit(c) ? readreal_rc(10*r + (c-'0'), getchar())
    : c == '.' ? readfrac(r, .1, getchar()) // adauga partea fractionala
    : r;	// alt caracter decat punct, ramane doar partea intreaga
}
// scriem si o functie fara parametri care o foloseste pe cea ajutatoare
double readreal(void) { return readreal_rc(0, getchar()); }
Observăm că în scrierea de mai sus, secvențierea celor două prelucrări (citirea părții întregi, apoi a celei fracționare) s-a realizat prin apelul funcției pentru partea a doua în momentul în care s-a încheiat prelucrarea primei părți (aici, pe cazul de bază al recursivității). Ar fi natural să încercăm să apelăm întâi funcția readnat (nemodificată) pentru partea întreagă, apoi să apelăm readfrac și să facem suma, dar după apelul lui readnat ar trebui să știm nu doar rezultatul, ci și dacă ultimul caracter citit e punct zecimal sau nu (urmează sau nu partea fracționară).

La laborator se vor rezolva alte probleme similare, cum ar fi: citirea unui număr scris in baza 8 sau 16; adăugarea semnului și/sau părții cu exponent la citirea unui real, numărări și transformări de cuvinte cu litere mari sau mici (cu funcțiile islower(), isupper(), tolower(), toupper() din <ctype.h>, etc.

Exemplul 2 Prelucrarea de texte
Cunoscând funcțiile de citire și tipărire de caractere, putem efectua deja prelucrări simple asupra textelor citite, de exemplu: numărarea cuvintelor, determinarea celui mai lung cuvânt, rescrierea cuvintelor din text cu prima literă majusculă, toate majuscule sau toate literele mici, eliminarea spațiilor multiple dintre cuvinte, filtrarea (eliminarea) anumitor părți din text, etc.

Deși simple, aceste programe trebuie să combine adesea mai multe prelucrări. Pentru a le scrie cât mai ușor, e util să ne stabilim câteva principii. În primul rând, vom structura programele în funcții simple cu un scop bine definit. Aceste funcții trebuie să ia decizii în funcție de caracterele întâlnite. Ca și la funcțiile de citire de numere vom face așa încât ele să aibe ca parametru primul caracter ce trebuie prelucrat (el poate fi dat în momentul apelului cu funcția getchar(). Tot din același motiv, e util să structurăm funcțiile astfel încât ele să returneze următorul caracter rămas neprelucrat (asemenea lui getchar). Aceastea va permite să exprimăm prelucrări succesive înlănțuind apelurile de funcție: rezultatul fiecărui pas de prelucrare (primul caracter) e transmis ca argument următoarei prelucrări. De asemenea, nu cunoaștem când va apărea sfârșitul secvenței de caractere prelucrate; fiecare caracter citit trebuie comparat cu EOF, iar la întâlnirea acestei valori, prelucrarea se încheie.

Scriem ca exemplu un program care numără cuvintele dintr-un text. Acestea pot fi separate prin oricâte spații, care pot apărea nu doar între cuvinte, ci și la începutul sau sfârșitul textului. Scriem o funcție care sare peste spațiile (albe) întâlnite și returnează primul caracter care nu e spațiu:

int skipspace(int c)
{
  return isspace(c) ? skipspace(getchar()) : c;
}
Similar, scriem și o funcție care consumă un cuvânt de la intrare (vom număra apoi cuvintele consumate). Funcția trebuie să facă două teste: cuvântul se poate termina fie întâlnind un spațiu, fie ajungând la EOF. Funcția returnează primul caracter de după cuvânt.
int skipword(int c)
{
  return c == EOF ? c : isspace(c) ? c : skipword(getchar());
}
Putem acum combina cele două funcții. Facem numărarea prim acumularea rezultatului, incrementând la începutul unui cuvânt: dacă s-a ajuns la EOF, se returnează numărul curent, altfel se continuă numărătoarea după ce s-a consumat cuvântul și spațiile ulterioare:
unsigned wordcount(unsigned cnt, int c)
{
  return c == EOF ? cnt : wordcount(cnt + 1, skipspace(skipword(c)));
}
Apelăm funcția din programul principal, pornind numărătoarea de la 0, și eliminând eventualele spații inițiale:
int main(void)
{
  printf("%u\n", wordcount(0, skipspace(getchar())));
  return 0;
}

Exemplul 3 Fracții continue
Orice număr real pozitiv poate fi scris sub următoarea formă de fracție:
x = a_0 + 1 / (a_1 + (1 / (a_2 + .... + 1 / (a_n + ...)))).
unde numerele a0, a1, a2, ... sunt naturale.
Coeficienții se pot calcula recursiv cu relația: a_n = [x_n], x_n+1 = 1 / (x_n - a_n) (cand x_n = a_n intreg, dezvoltarea se opreste). Fracțiile continue reprezintă o bună aproximare rațională a unui număr real.
Scrieți o funcție care tipărește reprezentarea unui număr real în fracție continuă, ca mai sus (până la un termen de indice dat sau exact, în cazul numerelor raționale). Observați că paranteza închisă trebuie tipărită după revenirea din apelul recursiv.

Exemplul 4 Fractali
Fractalii sunt figuri geometrice definite printr-o structură recursivă, în care un fragment e similar cu întreaga figură.
Un mare număr de curbe geometrice cu structură de fractali pot fi definite cu ajutorul unor reguli de substituție simple.
Ideea este următoarea: cea mai simplă curbă (la nivel recursiv 0) e un segment de dreaptă. Curbele mai complicate se obștin înlocuind fiecare segment printr-o secvență de segmente mai mici și rotații de direcție, după anumite reguli.
Ca exemplu, aveți dat un program care desenează triunghiul lui Sierpinski de un ordin dat. Fișierul gl_fractal.c (nu trebuie studiat) conține programul principal și definește un set de primitive grafice simple, inspirate din facilitățile TurtleGraphics introduse de limbajul LOGO. Sistemul menține implicit poziția cursorului și orientarea acestuia. Funcțiile line și skip avansează cursorul cu distanța dată, în direcția curentă, desenând o linie și respectiv, "sărind" înainte. Funcțiile left și right schimbă orientarea curentă prin rotire la stânga și la dreapta, cu unghiul stabilit anterior de funcția angle (și care de regulă rămăne constant pentru toată figura).

Fișierul sierp3.c implementează recursiv desenarea triunghiului: Inițial (la nivelul 0), figura cea mai simplă e un segment, corespunzător cu apelul A(0). Apoi, la nivele de complexitate succesive, fiecare segment e înlocuit cu segmente și rotații, reprezentate în definițiile recursive ale funcțiilor A și B, după regulile: A->B-A-B și B->A+B+A unde + și - înseamnă rotații cu un unghi fix (60 de grade) în sens trigonometric, și respectiv antitrigonometric.

Remarcăm că pentru fiecare prelucrare (A, B) avem definite doua variante: funcțiile An și Bn care conțin prelucrări compuse, apelând recursiv prelucrările de ordin inferior (n-1), și funcțiile A și B, care efectuează decizia, realizând în funcție de n fie prelucrările compuse, fie cazul de bază.

Puteți încerca după acest model să

Pentru prima variantă puteți încerca să definiti alte figuri recursive, în care fiecare conține un număr de copii mai mici, până la un anumit nivel. De exemplu, un triunghi echilateral în care se leagă printr-un alt triunghi mijloacele laturilor, iar pentru cele 3 triunghiuri din colț se repetă recursiv procedura.

în scrierea unui astfel de program e util să păstrăm o convenție privind poziția și orientarea cursorului. Astfel, concepem funcția de desenare a unei figuri elementare în așa fel încât la sfârșit, cursorul să revină la aceeași poziție și orientare. Apoi desenăm cele 3 sub-figuri recursive, păstrând aceeași convenție privind revenirea cursorului.

Puteți desena și alte astfel de figuri, pornind de la pătrat, hexagon, etc.


Marius Minea
Last modified: Sun Mar 54 17:38:25 EET 2006