Documente Academic
Documente Profesional
Documente Cultură
Tehnici de programare
Înainte de a preciza zonele de memorare a variabilelor declarate într –un program C++, vom
examina posibilitatea de redirecţionare a alocării variabilelor.
Pe parcursul studierii modulului ”Programarea procedurală” am precizat secţiunile
(c1asele) de memorare a variabilelor globale (segmentul de date) şi a variabilelor locale (stiva).
Acestea sunt clasele de mernorare implicite. Dacă dorim, putem specifica în mod explicit clasa de
memorare a unei variabile (fie ea locală sau globală), poziţionând înaintea declaraţiei variabilei
cuvântul-cheie static care reprezintă un specificator de clasă de memorare.
În cazul variabiIelor locale, specificatorul static indică faptul că variabila nu este memorată
pe stivă, ci în segmentul de date. Prin urmare, ea este initializată automat cu 0 şi are zona de
memorie alocată de la prima execuţie a blocului în care este declarată pănă la finisarea execuţiei
programului. Domeniul de vizibilitate nu se modifică (variabila rămâne vizibilă numai în interiorul
blocului în care este declarată).
Avantajele utilizării variabilelor locale statice sunt multiple: se alocă o singură dată, nu la
fiecare apel al funcţiei; îşi păstrează valorile între două apeluri consecutive ale funcţiei; domeniul
de vizibilitate fiind local, nu există riscuri ca valoarea variabilei să fie alterată de alte funcţii ale
programului, ca în cazul variabilelor globale; dacă spaţiul disponibil pe stivă devine o problemă,
reprezintă o soluţie de alocare a unor variabile locale.
În cazul variabilelor globale, indică faptul că variabila poate fi utilizată numai în cadrul
fişierului sursă în care este declarată (deci nu mai are legatură externă, nu mai poate fi utilizată în
alte fişiere sursă ale programului). Cu alte cuvinte,variabila devine locală fişierului sursă în care
este declarată.
Exemplu
void f ()
{ static int x=2;
x++i; cout << x << endl;}
Funcţia f() conţine variabila locală statică x. La primul apel al funcţiei, variabila x este
iniţializată cu valoarea 2, apoi valoarea variabilei este incrementată şi se va afişa pe ecran
valoarea 3. La al doilea apel al funcţiei f() , variabila x, fiind statică, nu mai este nici alocată, nici
iniţializată, ci îşi păstrează valoarea de la apelul precedent (3). Această valoare va fi
incrementată, apoi se va afişa pe ecran valoarea 4.
Deci la fiecare apel al funcţiei f() se va afisa o altă valoare. Dacă nu am fi declarat variabila
x utilizând specificatorul static, funcţia ar fi afişat întotdeauna valoarea 3.
În general, necesarul de memorie al unui program, scris într-un limbaj de nivel înalt, depinde
atât de tipurile variabilelor utilizate cât şi de mediul de implementare a limbajului, de modul de
gestionare a memoriei interne a calculatorului.
În procesul derulării unui program C++, spaţiul de memorie internă este divizat în căteva
secţiuni, dar le menţionăm pe trei dintre cele mai esenţiale:
─ segmentul date, destinat alocării variabilelor globale ( aceste variabile sunt declarate în
exteriorul oricărei funcţii C++), precum şi alocării variabilelor locale statice;
─ stiva, destinată alocării parametrilor actuali, variabilelor locale dinamice (acele care nu sunt
specificate prin static), şi a adreselor de revenire pe parcursul execuţiei subprogramelor
C++. Apelul unui subprogram implică depunerea datelor respective în stivă, iar ieşirea din
subprogam –eliminarea lor din stivă. Accentuăm că în cazul parametrilor variabilă în stivă se
depune numai adresa variabilei din programul (/subprogramul) apelant, iar în cazul
parametrului valoare în stivă va fi depusă o copie a datelor din lista parametrilor actuali.
─ heap-ul, utilizat pentru alocarea variabilelor dinamice. Aceste variabile sunt create şi
eventual distruse cu ajutorul subprogramelor standard new şi, respectiv delete.
Prin urmare, estimarea necesarului de memorie presupune evaluarea următoarelor
caracteristici ale unui program:
Vd(n) –volumul de memorie ocupat de variabilele globale şi a celor locale statice în
segmentul date;
Vs(n) –volumul de memorie ocupat de parametrii actuali şi de variabilele locale dinamice în
stivă; Vh(n) –volumul de memorie ocupat de variabilele dinamice în heap.
S-a constatat că prin raţionamente teoretice deseori este foarte dificil de determinat o formulă
(expresie analitică) exactă pentru Q(n). Din acest motiv se caută o limită superioară a numărului
de operaţii cerut de algoritm, analizându-se doar cazurile cele mai defavorabile. Cazul cel mai
defavorabil corespunde instanţelor pentru care numărul de operaţii efectuate este maxim. Se
procedează astfel întrucât:
1) cunoscând timpul de execuţie în cazul cel mai defavorabil, suntem siguri că pentru orice
date de intrare durata execuţiei nu va fi mai mare;
2) timpul de execuţie în mediu deseori este destul de aproape de cel în cazul cel mai
defavorabil.
Exemple:
1) Dacă Q(n) = an + b (a > 0), atunci termenul dominant din această formulă este d(n) = an.
În acest caz ordinul de complexitate al algoritmului este O(an) = O(n), adică algoritmul
respectiv este liniar.
2) Dacă Q(n) = an2 + bn + c (a > 0), atunci d(n) = an2, iar O(an2) = O(n2), motiv pentru care
spunem că este un ordin pătratic de complexitate.
Un algoritm este mai eficient decât altul dacă ordinul de complexitate al primului algoritm este
mai mic decât al celui de-al doilea. Relaţia între ordinele de complexitate are semnificaţie doar
pentru dimensiuni mari ale problemei. De exemplu, dacă considerăm Q1(n)=10n+10 şi Q2(n) =
n2, atunci se observă că Q1(n) > Q2(n) pentru n ≤ 10, deşi ordinul de creştere al lui Q1 este
evident mai mic decât al lui Q2. Prin urmare, un algoritm mai eficient decât altul reprezintă
varianta cea mai bună doar în cazul problemelor de dimensiuni mari. Majoritatea
algoritmilor întâlniţi în practică se încadrează în una dintre clasele menţionate în tabelul 2.1.
Ordin de complexitate Tipul algoritmului
O(n) Algoritm liniar.
O(nk), k-natural Algoritm polinomial. Dacă k=2, algoritmul este pătratic, iar dacă k=3,
algoritmul este cubic.
O(kn), k-natural Algoritm exponenţial. De exemplu, O(2n), O(3n), etc. Algoritmul de
tip O(n!) este tot de tip exponenţial deoarece:1×2×3×...×n>2n-1.
O(logn) Algoritm logaritmic.
O(n·logn) Algoritm liniar logaritmic.
Tabelul 2.1: Clase de algoritmi
Conform tabelului 2.2 avem: 1< logn < n < n·logn < n2 < n3 < 2n < n!
Pentru estimarea complexităţii algoritmilor deobicei se folosesc următoarele două reguli:
Regula sumei. Fie un algoritm este divizat în două părţi, ce nu au operaţii comune. Dacă
ordinul de complexitate a unei părţi este O(f(n)) iar a celei de a doua O(g(n)), atunci
complexitatea lor sumară se redă prin O(max( f(n), g(n))).
Regula produsului. Dacă o parte a algoritmului este de ordinul O(f(n)) şi se execută de g(n)
ori, atunci complexitatea lor totală se redă prin O( f(n) ∙ g(n)).
Observaţie. Nu întotdeauna putem şti de câte ori se execută o instrucţiune, chiar dacă
cunoaştem valoarea lui n. De exemplu, pentru calculul maximului elementelor unui vector cu
componente numere intregi, nu putem şti de câte ori se execută atribuirea max=A[i]. Cu toate
acestea, putem considera că există o proporţionalitate între valoarea n şi numărul de execuţii.
Ţinând cont de această observaţie vom proceda astfel:
Se alege o operaţie numită operaţie de bază, şi se determină de câte ori se execută aceasta.
Cerinţa pentru operaţia de bază este ca aceasta să se afle în cel mai interior corp de ciclu şi a
cărei contorizare permite estimarea în avans, înainte de lansarea în execuţie, a timpului de
execuţie a programului corespunzător. De exemplu, pentru problema aflării maximului,
formulată mai sus, operaţia de bază va fi comparaţia (fiind dat n se fac n-1 comparaţii pentru
calculul maximului). Deci algoritmul respectiv este liniar, adică ordinul de complexitate se redă
prin O(n).
Astfel ordinul de complexitate este determinat de structurile repetitive care se execută cu
mulţimea de
valori pentru datelele de intrare. În cazul structurilor repetitive imbricate, ordinul de
complexitate este dat de produsul dintre numerele de repetiţii ale fiecărei structuri repetitive.
Operaţia de bază este incrementarea ( i=i+1;) , cel mai defavorabil caz este acel în care
numărul x nu se află în tablou (ciclul se va executa de n ori), deci ordinul de complexitate este
O(n).
Notăm cu Q(n) numărul de paşi executaţi de algoritm. Operaţiile de bază sunt: comparaţia
A[i] cu A[j] şi interschimbarea A[i] cu A[j], situate în ciclul interior. Ciclul exterior se execută
de n−1 ori. La primul pas al ciclului exterior se vor executa n−1 paşi în ciclul interior. La al
doilea pas al ciclului exterior vor fi n−2 paşi în ciclul interior, apoi n − 3 , n −4 paşi , etc. Deci,
Exemplul_3 ( Algoritmul Euclid). Cel mai mare număr natural, care divide fără rest
numerele naturale a şi b, se numeşte cel mai mare divizor comun al numerelor a şi b.
Exemplu: CMMDC(70,105) =35.
Versiunea modernă a algoritmului Euclid se bazează pe proprietatea
CMMDC(a,b) = CMMDC(a % b, b), dacă a >b şi constă în înlocuirea consecutivă a celui mai
mare dintre numerele a şi b prin restul de la împărţirea celui mai mare la cel mai mic dintre
aceste numere; împărţirea întreagă se va continua până când restul respectiv nu se va egala cu
zero.
Exemplu: CMMDC(147, 462) = CMMDC(147, 462 % 147) = CMMDC(147, 21)=
=CMMDC(147 % 21, 21)=CMMDC(0,21)=21.
4. Se consideră mulţimea A formată din n numere întregi. Determinaţi dacă există cel puţin o
submulţime B, B ⊆ A, suma elementelor căreia este egală cu m. De exemplu, pentru A={-3, 1, 5,
9 } şi m=7, o astfel de mulţime există şi anume B={-3, 1, 9 }.
Estimaţi complexitatea temporală a algoritmului elaborat.