Documente Academic
Documente Profesional
Documente Cultură
CURS
PROIECTAREA ALGORITMILOR
2019
Capitolul 1
1
1.1. Organizarea internă a datelor. Informaţii. Date.
Informaţiile prelucrate sau reţinute în memoria calculatorului se numesc date. Toate datele care
intră, sunt prelucrate sau sunt memorate în calculator sunt reprezentate în formă binară (codificate
numeric prin 0 şi 1) astfel încât procesorul să le poată interpreta. Reprezentarea internă a datelor se face
diferenţiat, în funcţie de tipul lor.
Unitatea elementară de măsura pentru informaţie este Bitul (Binary digiT = cifră binară).
Cea mai mică unitate de memorare adresabilă de către procesor este octetul (BYTE-ul). Un
octet are 8 biţi numerotaţi de la 0 la 7 (bitul cel mai puţin semnificativ este bitul 0).
În cadrul memoriei, octeţii sunt numerotaţi. Numărul de ordine al unui octet constituie adresa lui
în memorie. Adresele de memorie sunt necesare în vederea accesului la informaţii.
Multiplii BYTE-ului
1KB 1MB 1GB 1TB
210 B 210 KB 210 MB 210 GB
Exerciții:
A. Date alfanumerice
Datele alfanumerice se reprezintă în memorie pe câte un Byte şi sunt alcătuite din litere mari şi
mici ale alfabetului englez, cifre, spaţii, caractere speciale (precum ?, @, #, $, %, ^, &, *, (, ), <, >, !
etc), caractere greceşti şi alte semne.
Codificarea acestor caractere se face folosind un cod numit cod ASCII (acronim de la American
Standard Code for Information Interchange). Conform acestui cod, setul de caractere de bază primeşte
coduri între 0-127, iar setul extins între 128-255.
Se observă ca numărul 255 reprezentat în binar este 1111 1111, deci este cel mai mare număr pe
care il putem reprezenta pe 8 biţi, de unde rezultă intervalul 0-255 folosit pentru codurile ASCII.
Este important pentru problemele ce se vor rezolva parcurgand modulele următoare să reţinem
ordinea în care sunt aşezate pe „axa” codurilor ASCII caracterele litere mici, litere mari şi cifrele. Se
observă din graficul de mai jos că cifrele încep de la codul 48, fiind plasate înaintea literelor. Urmează
literele mari (începand cu codul 65) şi abia apoi literele mici.
Coduri ASCII
48 49 57 65 66 97 98
0 255
0 1 9 A B a b
Caractere
2
Asupra datelor de tip alfanumeric se pot face de regula operaţii de concatenare (din două şiruri de
caractere se obţine un singur şir) şi operaţii de comparare (comparaţia se execută prin compararea
codurilor ASCII).
Urmarind imaginea de mai sus, se pot observă urmatoarelel inegalităţi: „A”> „0”, „a”> „A” sau
„a”> „0”.
Codurile ASCII sunt prezentate în următorul tabel:
3
Å 143 » 175 ╧ 207 239
É 144 ░ 176 ╨ 208 240
æ 145 ▒ 177 ╤ 209 241
Æ 146 ▓ 178 ╥ 210 242
ô 147 │ 179 ╙ 211 243
ö 148 ┤ 180 ╘ 212 244
ò 149 ╡ 181 ╒ 213 245
û 150 ╢ 182 ╓ 214 246
ù 151 ╖ 183 ╫ 215 247
ÿ 152 ╕ 184 ╪ 216 248
Ö 153 ╣ 185 ┘ 217 249
Ü 154 ║ 186 ┌ 218 250
¢ 155 ╗ 187 █ 219 √ 251
£ 156 ╝ 188 ▄ 220 ⁿ 252
¥ 157 ╜ 189 ▌ 221 ² 253
Pt 158 ╛ 190 ▐ 222 ▪ 254
ٱ 159 ┐ 191 ▀ 223 255
#include <iostream>
using namespace std;
int main()
{
char c;
cout << "Enter a character: ";
cin >> c;
cout << "ASCII Value of " << c << " is " << int(c);
return 0;
}
B. Date numerice
Reprezentarea în memoria unui calculator a numerelor întregi fără semn depinde de lungimea
cuvântului utilizat (numărul de biţi). Pentru a reprezenta un număr întreg pozitiv x pe k biţi se face
conversia numărului respectiv la baza 2, iar configuraţia binară obţinută se completează la stânga cu
zerouri până se obţin k cifre. Dacă numărul de cifre binare obţinute prin conversie este mai mare decât
k, atunci reprezentarea se trunchiază.
Cel mai mare număr întreg reprezentabil pe k biţi este:
2k 1
11 12 2k 2k 1 20 2k 1
2 1
k ori
Deci pe k biţi se pot reprezenta numerele întregi cuprinse între 0 şi 2k – 1, în total 2k numere.
Astfel:
pentru k = 8 biți = 1B (byte) domeniul de valori este: 0...255
pentru k = 16 biți = 2B domeniul de valori este: 0...65 535
pentru k = 32 biți = 4B domeniul de valori este: 0...2 147 483 647
4
62:2= 31 rest 0
31:2=15 rest 1
15:2=7 rest 1
7:2=3 rest 1
3:2=1 rest 1
1:2=0 rest 1
Citind resturile de jos în sus, rezultatul final al conversiei este numărul binar : 111110.
Pentru a obține configurația binară dorită (k=8 biti), vom completa la stânga numărul binar de mai sus
până se obțin 8 cifre. Astfel, obținem: 00111110.
B7 B6 B5 B4 B3 B2 B1 B0
0 0 1 1 1 1 1 0
Prin toate cele 3 metode, numerele întregi pozitive se codifică prin conversie în baza 2 pe k-1 biţi
şi completarea primului bit cu zero.
Exemplu: Astfel, dacă k =16, atunci reprezentare internă a numărului zecimal 72 în binar, este :
1001000.
72:2= 36 rest 0
36:2=18 rest 0
18:2=9 rest 0
9:2=4 rest 1
4:2=2 rest 0
2:2=1 rest 0
1:2=0 rest 1
5
Numerele întregi pozitive reprezentabile pe k biţi (prin oricare din cele trei metode) sunt cuprinse
în intervalul [0, 2k-1-1] (deoarece primul bit este rezervat, iar reprezentarea se face doar pe restul de k-1
biţi).
Prezentăm mai departe modul în care se face codificarea numerelor întregi negative prin fiecare
din cele trei metode.
Prin metoda semn şi valoare (cod direct) se codifică valoarea absolută a numărului întreg
negativ pe k-1 biţi şi se completează primul bit cu 1. În consecinţă, numerele întregi negative care se pot
reprezenta prin această metodă sunt cuprinse în intervalul [-2k-1 +1, -1].
Pentru a obţine codul invers vom parcurge următoarele etape:
1) se obţine reprezentarea valorii absolute a numărului întreg negativ pe k biţi;
2) în reprezentarea obţinută (la pasul 1) se înlocuieşte fiecare bit 0 cu 1 şi 1 cu 0.
Intervalul în care se găsesc numerele întregi care pot fi reprezentate prin această metodă este
acelaşi ca pentru metoda precedentă.
Pentru a obţine codul complementar se parcurg următorii pași:
1) se obţine codul invers pe k biţi;
2) se adună o unitate la valoarea obţinută (la pasul 1).
Exemplu: Dacă k =16, să se reprezinte în binar următorul număr zecimal -72, prin intermediul celor 3
metode prezentate mai sus.
Cod direct:
B15 B14 B13 B12 B11 B10 B9 B8 B7 B6 B5 B4 B3 B2 B1 B0
1 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0
Cod invers:
B15 B14 B13 B12 B11 B10 B9 B8 B7 B6 B5 B4 B3 B2 B1 B0
1 1 1 1 1 1 1 1 1 0 1 1 0 1 1 1
Cod complementar:
B15 B14 B13 B12 B11 B10 B9 B8 B7 B6 B5 B4 B3 B2 B1 B0
1 1 1 1 1 1 1 1 1 0 1 1 1 0 0 0
Reprezentarea în virgulă fixă. În acest caz se presupune că toate numerele au virgula plasată în aceași
poziție, chiar dacă acest lucru nu corespunde formei externe de reprezentare. Procesul de translatare din
forma externă în forma internă și invers se realizează cu ajutorul unor coeficienți de scalare aleși în mod
corespunzător de către programator. De obicei, se consideră că virgula este plasată imediat după poziția
cifrei-semn, caz în care numerele sunt fracții pure. În caz contrar, reprezentarea are un număr fix de biți
pentru partea întreagă și pentru partea fracționară. Reprezentarea numerelor reale în virulă fixă se poate
face utilizând cele 3 metode anterior prezentate (codul direct, invers și complementar).
6
Exemplu: Să se reprezinte în cod binar (pentru k = 8 biti) utilizând codul direct, următoarele numere
reale: +0.9375 și -0.9375.
Pentru a transforma partea fracţionară 0.9375 în binar va trebui să efectuăm înmulţiri succesive
cu 2 şi să preluăm partea întreagă a rezultatelor.
Algoritmul se opreşte în momentul în care partea fracţionară devine 0 sau s-a atins precizia dorită
(numărul de cifre pentru reprezentarea rezultatului).
Astfel:
0.9375 x 2 = 1.875 Partea întreagă :1
0.875 x 2 = 1.75 Partea întreagă :1
0.75 x 2 = 1.5 Partea întreagă :1
0.5 x 2 = 1.0 Partea întreagă :1
0 x 2=0.0 Partea întreagă :0
0 x 2=0.0 Partea întreagă :0
0 x 2=0.0 Partea întreagă :0
B7 B6 B5 B4 B3 B2 B1 B0
0 1 1 1 1 0 0 0
B7 B6 B5 B4 B3 B2 B1 B0
1 1 1 1 1 0 0 0
Verificare:
B6 21 B5 22 B4 23 B3 24 B2 25 B1 26 B0 27 1 21 1 22 1 23 1 24 0.938
Observaţie: În timp ce numerele întregi se pot reprezenta exact în binar, numerele subunitare se
reprezintă aproximativ, făcând excepţie numai acele numere subunitare care se pot scrie sub forma de
sumă de puteri negative ale lui 2 (0.5; 0.25 etc.).
Exemplu: Să se reprezinte în cod binar (pentru k = 16 biti) utilizând codul direct, următoarele numere
reale: +43.625 și -43.625. Din cei 16 biti, un bit este rezervat pentru semn, 7 biti pentru partea intragă și
8 biți pentru partea fracționară.
Pentru a transforma partea fracţionară 0.625 în binar va trebui să efectuăm înmulţiri succesive cu
2 şi să preluăm partea întreagă a rezultatelor.
Astfel:
7
Pentru a transorma partea întregă în binar va trebui să efectuăm împărțiri succesive cu 2 şi să
preluăm restul.
Astfel:
43:2= 21 rest 1
21:2=10 rest 1
10:2=5 rest 0
5:2=2 rest 1
2:2=1 rest 0
1:2=0 rest 1
În concluzie: 43= 1010112. Pentru a obține configurația binară dorită (k=7 biti), vom completa la
stânga numărul binar de mai sus până se obțin 7 cifre. Astfel, obținem: 43= 01010112.
În aceste condiții numărul real +43.625 în cod direct, este:
Cod direct:
B15 B14 B13 B12 B11 B10 B9 B8 B7 B6 B5 B4 B3 B2 B1 B0
0 0 1 0 1 0 1 1 1 0 1 0 0 0 0 0
Partea intragă Partea fracționară
Pe de altă parte, numărul real -43.625 în cod direct, este:
Cod direct:
B15 B14 B13 B12 B11 B10 B9 B8 B7 B6 B5 B4 B3 B2 B1 B0
1 0 1 0 1 0 1 1 1 0 1 0 0 0 0 0
Partea intragă Partea fracționară
Verificare:
D1 B7 21 B6 22 B5 23 B4 24 B3 25 B2 26 B1 27 B0 28 1 21 1 23 0.625
D2 B14 26 B13 25 B12 24 B11 23 B10 22 B9 21 B8 20 1 25 1 23 1 21 1 20 43
D D1 D2 43.625
Reprezentarea în virgulă mobilă. Numerele reale reprezentate în virgulă mobilă sunt de următoarea
formă:
x M bE
unde: b este valoarea bazei; M este un număr subunitar numit mantisă, iar E este un exponent.
Numărul reprezentat în virgulă mobilă se numește normalizat dacă prima cifră după virgulă a
mantisei este diferită de zero.
Numerele se reprezintă în formatul cu virgulă mobilă prin bitul de semn al numărului, exponent
şi mantisă:
k-1 0
BS Exponent Mantisă
e biţi m biţi
BS – bitul de semn;
b – baza de reprezentare a numărului;
bm – baza de reprezentare a mantisei;
e – numărul de biţi pe care se reprezintă exponentul;
8
m – numărul de cifre ale mantisei;
p – numărul de cifre ale părţii fracţionare a mantisei.
Mantisa se reprezintă, de regulă, în valoare absolută (prin mărime), baza de reprezentare b m fiind
2, 4, 8 sau 16. Corespunzător, mantisa este o succesiune de cifre în baza 2, 4, 8 sau 16, fiecare cifră fiind
reprezentată în memoria calculatorului pe 1, 2, 3 şi respectiv 4 biţi. Condiţia de normalizare, care asigură
unicitatea reprezentării numărului real în calculator, stabileşte numărul de cifre ale părţii fracţionare a
mantisei. Considerând mantisa subunitară, având prima cifră după virgulă semnificativă, vom avea
următoarea condiţie de normalizare: 0.1bm mantisa 1 .
Dacă bm este 2, bitul cel mai semnificativ al mantisei este întotdeauna 1. Acest bit, în general, nu
se reprezintă în memoria calculatorului (se numeşte bit ascuns), efectul fiind acela de a dubla numărul de
mantise distincte ce se pot reprezenta.
Observaţie: Tehnica bitului ascuns se referă doar la reprezentarea numerelor în memoria
calculatorului, nu şi la operaţiile efectuate de unitatea aritmetico-logică.
Pentru a creşte precizia reprezentării, trebuie mărit numărul de cifre ale mantisei. De regulă, acest
număr se dublează, de unde rezultă şi denumirea de reprezentare în dublă precizie. Numărul total de
mantise ce se pot reprezenta este: Nmantise = (b – 1) * bm-1.
Pentru reprezentarea exponentului pe e biţi, care este întotdeauna un număr întreg pozitiv sau
negativ, se utilizează, în general, un cod în exces. Avantajul este acela de a lucra numai cu numere
întregi fără semn, deci operaţiile sunt mai simple. De regulă, e = 7 sau 8 biţi.
Prin reprezentarea în cod exces, domeniul de valori al exponentului D = [-(2e-1-1), (2e-1-1)] se
transformă în D’ = [1, 2e – 1]. Această convenţie se mai numeşte şi notaţie polarizată, polarizarea fiind
numărul care trebuie scăzut din reprezentarea normală, fără semn, pentru a se obţine valoarea reală a
exponentului.
Valoarea 0 a exponentului în cod exces este rezervată pentru reprezentarea numărului 0.
Pentru a mări domeniul de reprezentare, se alege baza numărului 4, 8 sau 16. Baza numărului (b)
şi baza mantisei (bm) sunt, de regulă, egale.
Exemple de formate de reprezentare în virgulă mobilă sunt formatul DEC, formatul IBM şi
standardul IEEE.
În cazul standardului IEEE, baza de reprezentare este b = 2. În simplă precizie, un număr se
reprezintă pe 32 de biţi. Exponentul se reprezintă pe e = 8 biţi, în cod exces 127. Valoarea 255 a
exponentului în cod exces are semnificaţia de ±∞, după cum bitul de semn este 0 sau 1. Mantisa are m =
24 de biţi, din care p = 23 de biţi pentru partea fracţionară. Mantisa se reprezintă în valoare absolută,
folosind tehnica bitului ascuns. Condiţia de normalizare a mantisei este:
1.000 002 mantisa 1.111 112
Deoarece partea întreagă a mantisei este întotdeauna 1, aceasta nu se mai reprezintă în calculator.
Se vor reprezenta doar cifrele de la dreapta virgulei, în total 23 de biţi. Precizia reprezentării este de 6
cifre zecimale.
31 30 23 22 0
BS Exponent + 127 Mantisa normalizată
31 30 20 19 0
BS Exponent + 1023 Mantisa
31 0
9
Mantisă
deci : 53=(110101) 2 .
Pentru a transforma partea fracţionară 0.5 în binar va trebui să efectuăm înmulţiri succesive cu 2
şi să preluăm partea întreagă a rezultatelor. Algoritmul se opreşte în momentul în care partea fracţionară
devine 0. Astfel 0.5x2=1.0. Rezultă că partea întreagă este 1, iar partea fractionară este 0. Deci 0.5=(1) 2
În concluzie: -53.5 = (- 110101.1) 2 . În aceste condiții numărul real -53.5 se reprezinte în virgulă
mobilă, astfel:
BS Exponent Mantisa
1 00000101 10101100000000000000000
BS Exponent Mantisa
1 10000100 10101100000000000000000
10
Constantele sunt date care nu îşi modifică valoarea. Aceste valori fixe reprezintă caractere,
şiruri de caractere, numere întregi sau raţionale. Ca şi în cazul variabilelor, constantele au un
nume, o valoare (dar care nu se poate modifica), un tip şi o adresă de memorie. Este necesar,
ca şi la variabile, o declarare pentru a specifica tipul, numele şi valoarea constantei.
B. Tipuri de date
Prin tip de date se intelege o mulţime pentru care se definesc urmatoarele proprietăţi:
dimensiunea zonei de memorie asociate unui element
timpul de viaţă asociat datei
mulţimea operaţiilor prin care valorile tipului pot fi modificate
operatorii utilizaţi şi restricţiile asupra acestora
Tipurile de date pot fi predefinite (tipuri fundamentale) şi definite de utilizator.
În funcţie de limbajul folosit, tipurile fundamentale de date au alte denumiri, însă conceptual ele
vizează aceleaşi domenii de valori. În modulele urmatoare vom prezenta comparativ, tipurile
fundamentale de date pentru mai multe limbaje de programare.
O expresie este formată dintr-unul sau mai mulţi operanzi asupra cărora acţionează operatori.
De exemplu, în expresia 2 * a – b + c / 2, a, b, c sunt operanzii iar *, -, +, / sunt operatorii.
Operaţiile sunt prelucrarile în care intră datele. Ele pot fi aritmetice şi nearitmetice (logice,
relaţionale, cu şiruri de caractere, de conversie dintr-un tip de date în altul). Vom studia pe rand
operatorii care se folosesc în cadrul acestor operaţii.
Operatori aritmetici
Operatorii aritmetici sunt: +, -, *, /, %, unde semnul de împărţire „/” are sensul de cât al
împărţirii (în cazul împărţirilor cu cât şi rest) sau de împărţire reală iar semnul „%” reprezintă restul
împărţirii a două numere întregi.
* / % + -
Ordinea de efectuare a operaţiilor este dată de prioritatea operatorilor aritmetici (cea cunoscută în
matematică: înmulţiri şi împărţiri şi apoi adunări şi scăderi). Aceştia sunt operatori binari adică
acţionează asupra a doi operanzi.
În plus există şi operatorii unari plus şi minus (+, -), care acţionează asupra unui singur operand
şi au sensul de semn al numărului (pozitiv sau negativ).
Operatori relaţionali
Sunt cei folositi şi în matematică: > (mai mare), < (mai mic), ≥ (mai mare sau egal), ≤ (mai
mic sau egal), = (egal), ≠ (diferit). Ei precizează o relaţie de ordine sau de egalitate între date, care
11
poate fi îndeplinită sau nu. Expresiile construite cu operatorii relaţionali pot fi evaluate la o valoare de
adevar: „adevarat” sau „fals”, după cum este îndeplinită relaţia sau nu.
În funcţie de limbajul de programare folosit, apar convenţii de notaţie specifice pentru operatori
(de exemplu semnul „diferit” va fi implementat în C++ ca „ != ” iar în Pascal ca „ <> ”, pe când
semnele ≤ şi ≥ vor fi implementate ca <= şi >=, la fel, în ambele limbaje).
Operatorii relaţionali sunt operatori binari şi se pot aplica numai operanzilor numerici, logici şi
de tip caracter (ordinea caracterelor fiind cea data de codul ASCII, despre care am vorbit în fişa
anterioară).
Nu există o ordine specifică a operaţiilor atunci când folosim operatorii relaţionali. Operaţiile se
efectuează în ordinea apariţiei operatorilor, de la stanga la dreapta.
Operatori logici
Operatorii logici sunt folosiţi pentru determinarea valorii de adevar a propoziţiilor logice şi
anume „adevarat” sau „fals”, în unele limbaje codificate cu „1” respectiv „0”.
Operatorii logici sunt: negatia logică (not), şi logic (and), sau logic (or). Operatorul „not” este
unar, în timp ce „and” şi „or” sunt binari.
Rezultatul expresiilor ce conţin operatori logici este cel prezentat în logică matematică şi descris
în tabelul urmator:
p Q not p p or q p and q
0 0 1 0 0
0 1 1 1 0
1 0 0 1 0
1 1 0 1 1
12
6 Disjunctia logică sau (or) De la stanga la dreapta
* 1 este * ordinea în care se execută,
prioritatea dacă există mai multe
maximă operaţii cu aceeaşi prioritate
A. Structuri de date
Doar rareori programele prelucrează numai date simple (numere întregi, reale, caractere). De cele
mai multe ori programele prelucrează volume mari de date şi pentru că prelucrarea să se realizeze
eficient este necesară organizarea datelor în structuri.
Structurile de date sunt modalitati de stocare a datelor într-un calculator astfel încât ele să poată fi
folosite în mod eficient. Uneori, dacă se face o alegere optimă a structurii de date, implementarea va
conduce la un algoritm eficient, care utilizează mai puţine resurse (ca de exemplu memoria necesară şi
timpul de execuţie).
Pentru o structură de date trebuie specificate trei caracteristici:
descrierea tipului de baza al componentelor
metoda de structurare
modul de acces la componente
Deoarece aceste caracteristici, în limita unor restricţii, sunt la alegerea programatorului, tipurile
structurate nu se mai pot numi tipuri de date standard, recunoscute şi alocate automat de către compilator
la simpla apariţie a unui cuvânt rezervat tipului respectiv.
13
După tipul • omogene (componentele sunt de acelasi tip)
elementelor • neomogene (componentele sunt de tipuri diferite)
După • statice (se aloca un anumit spatiu in memorie la inceputul executiei programului,
stabilitatea care nu se va mai modifica în timpul rularii programului)
structurii • dinamice (numarul de componente se modifica în timpul executiei programului)
14
structura de tip fişier
Primele trei tipuri se referă la structurarea datelor în zone de lungime fixă ale memoriei interne.
Al patru-lea tip se referă la structurarea datelor pe suport extern, care, faţă de memoria internă, se poate
considera nelimitat.
Există mai multe situatii când sunt necesare mai multe date de prelucrat în cadrul unei probleme.
Iată două exemple: Se citesc 100 de numere.
Să se precizeze dacă sunt distincte sau nu.
Să se afişeze în ordine inversă citirii.
Pentru rezolvare este necesar să reţinem o sută de variabile de tip întreg. Denumirea acestora prin
nume diferite ar fi greu de realizat. Cea mai bună soluţie este de a da un nume unic tuturor acestor valori
şi de a ne referi la grupul lor prin acest nume, specificand numărul de elemente din grup.
Fiecare element va fi adresat printr-un număr de ordine, numit indice. Dacă adresarea unui
element din tablou se face după un singur indice, atunci tabloul se numeşte unidimensional (mai pe
scurt vector); dacă adresarea se face după doi indici (linia şi coloana), atunci tabloul se numeste
bidimensional (matrice).
Exemplu de vector unde elementul al 3-lea poate fi accesat prin: V[3]
Vectorul V
1 2 3 4 5
24 5 -9 17 88
Exemplu de matrice unde elementul al 3-lea de pe linia 2 poate fi accesat prin: A[2][3]
Matricea A(3x5)
1 2 3 4 5
1
24 5 -9 17 88
2 0 34 8 -7 -2
3 56 3 4 1 -9
Un tablou este deci o structură omogenă de date indexată, care cuprinde un număr finit de
componente, toate având acelaşi tip, pe care îl numim tip de bază.
Structura de tip tablou impune ca elementele să fie asezate în memorie în succesiune continua de
octeţi, fiecare componentă ocupând acelaşi număr de octeţi cât specifică tipul de baza.
Indicele este o valoare ordinală care identifică în mod unic o componetă (un element) a tabloului.
Prelucrarea unui tablou se bazează, în general, pe execuţia unor operaţii asupra componentelor
sale. Operaţiile sunt cele permise de tipul de bază al tabloului.
Pentru definirea unui tablou este necesar să se ştie numărul maxim de componente care pot
apărea în prelucrarile din cadrul problemei, în scopul declarării corecte a spaţiului pe care îl va ocupa
acesta.
Exemplu: Programul următor citeşte şi afişează un tablou. La început se citesc numărul de linii
şi de coloane ale tabloului (m şi n). Programul este realizat în C++. Observație: O matrice de 10 linii și
9 coloane, ce are elemente de tip întreg, se declară în C++, astfel: int a[10][9];
15
#include <iostream.h>
main()
{ int m,n,i,j,a[10][9];
cout<<"m="; cin>>m;
cout<<"n="; cin>>n;
for (i=0;i<m;i++)
for(j=0;j<n;j++)
{ cout<<"a["<<i+1<<','
<<j+1<<"]=";
cin>>a[i][j];
}
for (i=0;i<m;i++)
{ for (j=0;j<n;j++)
cout<<a[i][j]<<' ';
cout<<endl;
}
}
Se comportă ca un vector de caractere, avănd însă operaţii specifice tipului de date şir de
caractere. Aceste operaţii diferă în funcţie de limbajul de programare folosit.
Exemplu de şir de caractere: conţine atât litere, cifre cât şi caractere speciale
Articolul este o structură de date eterogenă (cu elemente de tipuri diferite), cu acces direct la
elementele sale, între care există o relaţie de ordine ierarhică.
Variabilele de tip articol se reprezintă intern ca succesiuni de câmpuri elementare, ce pot fi de
tipuri diferite, cu reprezentarea internă şi lungimea fizică specifice tipurilor lor. Lungimea zonei de
memorie rezervată pentru variabila de tip articol rezultă din însumarea lungimilor câmpurilor. Aceasta
nu poate depăşi 65520 octeţi (ca orice variabilă de tip structurat).
16
ELEV
In figura de mai sus avem un exemplu de articol cu trei campuri de tipuri diferite: nume de tip şir
de caractere, vârstă de tip întreg şi medie de tip real. Adresarea câmpurilor (prin numele lor) se face
folosind operatorul „ . ” (punct). Dacă se declară o variabilă „e” de tip ELEV, atunci un element ar
putea fi accesat pe câmpuri astfel: „e.nume”, „e.varsta”, „e.medie”
Datele de tip articol pot fi adresate în două moduri: global sau pe câmpuri (cum am văzut în
exemplul anterior). Adresarea globală este permisă numai în operaţia de atribuire, cu condiţia ca ambele
variabile (sursă şi destinaţie) să fie articole de acelaşi tip.
Exemplu: În programul C prezentat mai jos, se crează o structură de tip articol (student).
Structura are trei câmpuri diferite: nume (șir de caractere), număr de identificare (număr întreg) și notă
(float).
#include <stdio.h>
struct student {
char nume[50];
int identificare;
float nota;
} s;
int main() {
printf("Introduceti informatii:\n");
printf("Introduceti numele: ");
fgets(s.nume, sizeof(s.nume), stdin);
printf("Introduceti numarul de identificare: ");
scanf("%d", &s.identificare);
printf("Introduceti nota: ");
scanf("%f", &s.nota);
printf("Afisarea informatiilor:\n");
printf("Nume: ");
printf("%s", s.nume);
printf("Numarul de identificare: %d\n", s.identificare);
printf("Nota: %.1f\n", s.nota);
return 0;
}
#include <stdio.h>
int main()
{
char name[50];
17
int marks, i, num;
printf("Introduceti numarul de studenti: ");
scanf("%d", &num);
FILE *fptr;
fptr = (fopen("C:\\student.txt", "w"));
if(fptr == NULL)
{ printf("Error!");
exit(1); }
for(i = 0; i < num; ++i)
{ printf("Student%d\nIntroduceti numele: ", i+1);
scanf("%s", name);
printf("Introduceti numarul de identificare: ");
scanf("%d", &marks);
fprintf(fptr,"\nName: %s \nMarks=%d \n", name, marks); }
fclose(fptr);
return 0;
}
C. Structuri dinamice de date
Lista este o structură dinamică de date, înţelegând prin aceasta faptul că ea are un număr variabil
de elemente. La început lista este o mulţime vidă. În timpul execuţiei programului se pot adăuga
elemente noi sau se pot şterge elemente din listă. Elementele unei liste sunt de acelaşi tip, şi anume un
tip utilizator.
Există situaţii în care este dificil să se evalueze numărul maxim al elementelor unei liste, precum
şi situaţii când numărul lor diferă de la execuţie la execuţie. În aceste situaţii nu este potrivit să se aleagă
ca structuri de date cele alocate static (de tipul vector sau matrice). În schimb este avantajoasă structură
alocată dinamic (de tip listă).
Legatura elementelor unei liste se face cu ajutorul pointerilor (adrese către elementele următoare)
care intră în compunerea elementelor listei. Listele organizate în acest fel se numesc liste înlănţuite.
Elementele unei liste se numesc noduri. Nodul este un articol declarat de utilizator şi contine
campuri cu informaţia utilă şi un camp ce conţine adresa unde se va regăsi elementul urmator în listă.
Dacă între nodurile unei liste există o singură legatură (spre elementul următor), atunci lista se
numeşte simplu înlănţuită.
18
p
14 29 7
În mod analog, lista este dublu înlănţuită dacă între nodurile ei sunt definite două legături (şi
spre elementul următor şi spre cel precedent).
14 29 7
O stiva este o listă simplu înlănţuită gestionată conform principiului LIFO (Last în First Out).
Conform acestui principiu, ultimul nod pus în stivă este primul nod care este scos din stivă. Stiva, ca şi
lista, are două capete: baza stivei şi vârful stivei. Cu alte cuvinte stiva este un caz particular al listelor
înlănţuite.
Exemple de stive din viata reala.
19
Pentru crearea stivei se va folosi operaţia PUSH în mod repetat, iar pentru ştergerea stivei se va
folosi operatia POP în mod repetat.
Cele două operaţii se realizează în varful stivei. Astfel, dacă se scoate un element din stivă,
atunci acesta este cel din vârful stivei. Dacă se adaugă un element în stivă, atunci acesta se pune în
vârful stivei.
PUSH POP
Vârful stivei
Stiva: LIFO
Structura de tip COADĂ
O coadă este o listă simplu înlănţuită gestionată conform principiului FIFO (First în First
Out). Conform acestui principiu, primul nod pus în coadă este primul nod care este scos din coadă.
Coada, ca şi lista, are două capete: primul şi ultimul element.
Asupra unei cozi se definesc operaţiile:
1. adaugăre element la coadă;
2. extragere element din coadă;
Pentru crearea cozii se va folosi operaţia de adăugare în mod repetat, iar pentru ştergerea cozii se
va folosi operaţia de extragere în mod repetat.
Cele două operaţii se realizează în locuri bine stabilite: adaugărea se face după ultimul element al listei
iar extragerea se face din capul listei.
Coada: FIFO
Extragere Adăugare
Grafurile sunt structuri de date care se pot implemente atat ca structuri de date alocate static cât şi
alocate dinamic. Grafurile sunt utilizate în modelarea problemelor legate de activitati întâlnite în
realitatea de zi cu zi. Structura unui graf reflectă structură unei probleme reale.
Grafurile sunt formate din puncte (numite noduri sau vârfuri - engleza = nodes / vertices) şi
conexiuni între noduri (numite muchii – eng;eza edges).
De exemplu, în figura de mai jos avem două grafuri A şi B, fiecare cu câte 5 noduri şi număr
diferit de muchii.
20
Se numeşte graf neorientat, o pereche ordonată de multimi notată G = (V,E), unde V = {v1,
v2, …, vn} este o mulţime finită şi nevidă de elemente numite noduri sau vârfuri iar E = {e1,e2,…,en}
este o mulţime de perechi neordonate de elemente din E numite muchii.
Se numeşte graf orientat o pereche ordonată de mulţimi G=(V,E), unde unde V = {v1, v2, …,
vn} este o multime finită şi nevidă, numită mulţimea nodurilor sau vârfuri, iar E = {e1,e2,…,en} este o
mulţime formată din perechi ordonate de elemente ale lui E, numită mulţimea arcelor.
Un exemplu de graf orientat este: reţeaua de străzi a unui oraş. Străzile sunt muchii în graf, iar
intersecţiile reprezintă vârfurile grafului. Întrucât mergând pe jos ne putem deplasa pe orice stradă în
ambele sensuri, vom spune că din punctul de vedere al pietonilor, „graful unui oraş” este neorientat.
Cu totul altfel stau lucrurile în ceea ce priveşte conducătorii auto, pentru că în orice oraş există
străzi cu sens unic. Pentru un şofer străzile trebuie să primească în graf o anumită orientare. Desigur că
acele străzi pe care se poate circula în ambele sensuri vor primi orientare dublă. Am ajuns astfel la
noţiunea de graf orientat.
Alte exemple de grafuri din viaţa reală:
Pentru a defini notiunea de ARBORE, vom defini numai cateva notiuni legate de grafuri (restul
se vor studia la modulul respectiv)
Lanţ = este o secvenţă de noduri ale unui graf neorientat cu proprietatea că oricare două noduri
consecutive din lant au o extremitate comuna (spunem ca sunt adiacente)
21
Ciclu = Un lanţ în care primul nod coincide cu ultimul
Graf conex = graf în care între oricare 2 noduri există un lanţ.
Un arbore cu radacină este un graf neorientat conex fără cicluri în care unul din noduri este desemnat
ca rădăcină. Nodurile pot fi aşezate pe niveluri începând cu rădăcina care este plasată pe nivelul 1.
Radacina este un nod special care generează asezarea unui arbore pe niveluri; Această operaţie
se efectuează în funcţie de lungimea lanţurilor prin care celelalte noduri sunt legate de rădăcină.
Un nod y este descendentul nodului x într-un arbore cu rădăcină dacă este situat pe un nivel mai
mare decât nivelul lui x şi există un lanţ care le uneşte şi nu trece prin rădăcină.
Exemplu de graf cu 5 niveluri, radacina fiind pe nivelul 1
Într-un arbore cu rădăcină, un nod este frunză dacă nu are nici un descendent direct.
În modulele urmatoare se vor trata amănunţit toate aceste structuri de date, specificând
instrucţiunile folosite în limbajele de programare studiate.
22
Capitolul 2
Algoritmi
Noţiunea de algoritm este prezentă azi în contexte diferite. Termenul algoritm vine de la numele
matematicianului persan Abu Ja’Far Mohamed ibn Musa al Khowarizmi (circa 825 e.n.), care a scris o
carte cunoscuta sub denumirea latina de “Liber algorithmi”. Tot el a introdus denumirea de “algebra” în
matematică.
În trecut, termenul de algoritm era folosit numai în domeniul matematicii, însa datorită
dezvoltării calculatoarelor, astăzi “gândirea algoritmică” nu mai este un instrument specific matematicii
ci folosit în diverse domenii.
Prin algoritm înţelegem o succesiune finită de operaţii cunoscute care se execută într-o
succesiune logică bine stabilită astfel încât plecand de la un set de date de intrare, să obtinem într-un
interval de timp finit un set de date de ieşire.
Caracteristicile algoritmilor
Finitudine – proprietatea algoritmilor de a furniza datele de ieşire într-un timp finit (adica dupa
un număr finit de paşi).
De exemplu, dacă avem următoarea problemă: Se citeşte un număr n natural. Să se efectueze
operaţia de extragere a radicalului şi să se afişeze rezultatul. Această problemă nu este un proces finit,
deoarece nu s-a specificat precizia cu care se va furniza rezultatul.
Claritatea - algoritmul trebuie să descrie operaţiile clar şi fără ambiguiăţi.
Generalitatea – proprietatea algoritmilor de a rezolva o intreagă clasă de probleme de acelaşi
fel.
De exemplu adunarea 2+8 este o problemă care adună numai aceste două numere, însă dacă
elaboram o metodă de rezolvare care va aduna a+b, unde a şi b pot avea orice valori întregi, spunem ca
am realizat un algoritm general.
Corectitudinea – spunem că un algoritm este corect dacă el furnizează în mod corect datele de
ieşire pentru toate situaţiile regăsite în datele de intrare.
De exemplu, trebuie să evaluam expresia E=a/b+c. O succesiune de paşi pentru evaluarea
expresiei este:
se citeşte a, b, c
se calculează a/b, apoi rezultatul se adună cu c.
se atribuie lui E valoarea calculată
se afişează E
Acest algoritm NU furnizeaza rezultatul corect pentru toate valorile de intrare. În cazul în care
b=0, împărţirea nu se poate efectua dar algoritmul nu verifică acest lucru.
Există totuşi algoritmi care sunt corecţi, clari, generali şi furnizează soluţia într-un timp finit însă
mai lung sau folosesc mai multă memorie decât alţi algoritmi. Aceasta înseamnă că atunci când
elaborăm un algoritm, nu ne oprim la prima soluţie găsită. Vom încerca să gasim algoritmi care să dea
soluţia într-un timp cât mai scurt, cu cât mai puţină memorie folosită. Cu alte cuvinte vom încerca să
elaboram algoritmi eficienţi.
Numim deci eficienţă - capacitatea algoritmului de a da o soluţie la o problema într-un timp de
executie cât mai scurt, folosind cât mai puţină memorie.
23
Rezolvarea unei probleme este un proces complex, care are mai multe etape.
1. Analiza problemei, pentru a stabili datele de intrare şi de ieşire.
2. Elaborarea unui algoritm de rezolvare a problemei.
3. Implementarea algoritmului într-un limbaj de programare.
4. Verificarea corectitudinii algoritmului implementat.
5. Analiza complexitatii algoritmului.
Implementare
Analiza Elaborarea Verificare Analiza
în limbaj de
problemei algoritmului corectitudine complexităţii
programare
Observație: Toate aceste etape vor fi evidentiate pe algoritmii ce vor fi prezentaţi în fişele suport
şi modulele următoare.
Reprezentarea algoritmilor
După etapa de analiză a problemei în care s-au stabilit datele de intrare şi cele de ieşire, urmează
etapa de elaborare a algoritmului.
Acesta trebuie reprezentat într-un mod inteligibil. Întrebarea este cum putem să reprezentăm
algoitmul astfel încât să fie înteles de cei ce îl citesc?
Un posibil răspuns ar fi – printr-un limbaj de programare. Este un raspuns bun pentru cei ce
cunosc acel limbaj de programare, însa ceilalţi nu vor întelege nimic. Nu putem să impunem altora să
învete acel limbaj doar pentru a intelege algoritmii descrişi de noi. În plus, se observă că nu există niciun
limbaj de progamare care să dureze sau care să fie acceptat de toata lumea. Este deci necesară utilizarea
unui limbaj comun de repezentare a algoritmilor, dând apoi posibilitatea fiecarui programator să
“traducă” algoritmul în ce limbaj de programare doreşte. De-a lungul timpului s-au remarcat două
modalităţi de reprezentare a algoritimilor: schemele logice şi limbajul pseudocod.
Scheme logice
Schemele logice reprezintă un algoritm în mod grafic, folosind blocuri diferite pentru operaţii
diferite. Această metodă are unele dezavantaje: schemele sunt stufoase, greu de urmărit. În tabelul
urmator prezentăm tipurile de blocuri folosite în reprezentarea algoritmilor. Schemele logice sunt mai
utile celor care abia învaţă să programeze şi deci sunt în faza de formare a gândirii algoritmice.
Recomandăm ca la scrierea schemelor logice să se scrie mai întâi conţinutul blocului şi apoi să se
deseneze blocul corespunzător.
24
Blocurile specifice schemelor logice sunt:
25
Limbajul Pseudocod
a, b, c intregi -
Declaratii de variabile
x,z reale
x ← 10 x ← expresie
Instrucţiune de atribuire
a ← a+1
26
2.3. Programarea structurată. Structuri liniare, structuri alternative, structuri repetitive
(cu test final, cu test iniţial, cu număr cunoscut de paşi). Teorema de structură Bohm-
Jacopini.
A. Comentarii
Putem adaugă comentarii în cadrul algoritmului pentru a descrie operaţiile efectuate sau a da
indicaţii necesare la implementare. Adeseori, când se lucrează în echipă, comentariile sunt foarte
necesare.
Sunt mai multe variante în care putem să scriem comentarii. În general, fiecare programator va
folosi ceea ce crede că este mai uşor de înteles sau mai rapid de scris. În algoritmii prezentaţi în acest
modul, comentariile încep cu semnul „//” şi se vor scrie la începutul fiecarui rând de comentariu. Nu este
necesară scrierea semnului şi la sfârşitul rândului. Prezentam ca exemplu două tipuri de comentarii
Exemplu 1
// Acesta este un comentariu
// Fiecare rând de comentariu începe cu semnul „//”
Exemplu 2
/* Acest comentariu se poate scrie
pe mai multe linii, iar sfarsitul comentariului
se face cu */
În cadrul programului C++, comentariile pentru o singură linie încep cu //. Tot ce este urmat de
aceste slash-uri, până la sfârșitul liniei, va fi ignorat de compilator. Dacă vrem să scriem un comentariu
27
care se întinde pe mai multe linii, acesta trebuie să înceapă cu /* și să se termine cu */. Un exemplu în
acest sens, este preentat mai jos:
#include <iostream>
int main() {
cin >> a >> b; // Citim a și b.
cout << a + b << '\n'; // Afișăm a + b. '\n' înseamnă enter.
return 0; // Returnăm 0 deoarece, dacă s-a ajuns aici,
// programul s-a executat fără probleme.
} /* Acoladele grupează mai multe instrucțiuni formând
un block de cod. */
B. Declararea variabilelor
variabila tip
//Exemple
a întreg
b real
x caracter
În cadrul programului C++, variabilele locale se declară într-un anumit bloc al programului, în
corpul unei funcții. Un exemplu în acest sens este prezentat mai jos:
#include <iostream>
using namespace std;
void F(){
int x;
x = 5;
cout << x << endl;
}
28
int main(){
int y = 10;
F();
cout << y << endl;
return 0;
}
Variabilele x și y declarate în programul de mai sus sunt locale. Variabila x poate fi utilizată
numai în funcție F(), iar variabila y numai în funcția main(). Mai mult, cele două variabile ar fi putut
avea același nume și nu ar fi fost nicio confuzie.
Variabilele locale respectă următoarele reguli:
li se alocă memorie în zona de stivă;
sunt vizibile numai în blocul în care au fost declarate;
durata de viață a lor este execuția instrucțiunilor din blocul în care au fost declarate;
sunt inițializate cu valori aleatorii. Mai precis, standardul C++ nu garantează inițializarea lor cu o
anumită valoare. Asta nu înseamnă că nu este posibil ca variabilele locale să fie inițializate de
exemplu cu 0 într-o anumită implementare a compilatorului, dar nu ne putem baza pe acest lucru.
Pe de altă parte, variabilele globale se declară în afara oricărei funcții. La declarare, ele sunt
inițializate cu 0. Un exemplu în acest sens este prezentat mai jos:
#include <iostream>
using namespace std;
int x;
void F(){
cout << x << endl;
x = 10;
}
int y;
int main(){
cout << x << " " << y << endl;
x = 5; y = 15;
F();
cout << x << " " << y << endl;
return 0;
}
În programul de mai sus variabilele x și y sunt globale. Variabila x poate fi utilizată atât în
funcția main() cât și in F(), iar variabila y numai în main().
Variabilele globale respectă următoarele reguli:
li se alocă memorie în zona de date;
sunt vizibile în toate funcțiile care urmează în codul sursă declarării lor;
durata de viață a lor este execuția întregului program;
sunt inițializate cu valoarea 0.
Observație: Într-un program putem avea și variabile globale și variabile locale, ba chiar variabile
globale și locale cu același nume. Următorul program exemplifică această situație.
#include <iostream>
using namespace std;
int x;
void F(){
cout << x << endl; //5, variabila globala
29
int x = 10;
cout << x << endl; //10, variabila locala in F()
{
int x = 20;
cout << x << endl; //20, variabila locala în F(), blocul interior
}
cout << x << endl; //10, variabila locala in F()
}
int y;
int main(){
cout << x << endl; //0, variabila globală
x = 5;
cout << x << endl; //5, variabila globala
F();
cout << x << endl; //5, variabila globala
int x = 100;
cout << x << endl; //100, variabila locala in main()
return 0;
}
Observație: Dacă într-un program avem variabile cu același nume, dar cu domenii de vizibilitate
diferite, are prioritate variabila cu domeniul de vizibilitate cel mai mic. În particular, dacă ave o variabilă
globală și una locală cu același nume are prioritate variabila locală.
Observație: Dacă declarăm o variabilă în expresia de inițializare a unei instrucțiuni for, ea va fi
vizibilă numai în expresiile de control ale instrucțiunii for și în blocul subordonat acesteia. De exemplu:
C. Instrucţiunea de citire
Efectul instrucţiunii este de a da valori (de la tastatură sau dintr-un fişier) variabilelor de intrare
cu care lucrăm.
//Exemplu
Citeste a, b, x
#include <iostream>
using namespace std;
int main ()
{
int i;
cout << "Please enter an integer value: ";
cin >> i;
30
cout << "The value you entered is " << i;
cout << " and its double is " << i*2 << ".\n";
return 0;
}
Observație: Prin instrucțiunea int i se declară variabila i de tip intreg, iar prin cin>> i se citește de la
tastatură o anumită valoare. Valoarea intodusă de la tastatură se transmite programului atunci când se
apasă tasta ENTER. Programul C++ asteptă atâta timp cât este necesar citirea variabilelor de la tastatură.
Funcția cin se poate utiliza si pentru introducerea de la tastatură a unui șir de caractere. În cele ce
urmează, se prezintă un exemplu în acest sens.
#include <iostream>
using namespace std;
int main()
{
char name[20], address[20];
cout << "Name: ";
cin.getline(name, 20);
return 0;
}
Citirea dintr-un fișier, în cadrul programului C++ se face utilizând streamurile de fișier, la fel ca
în programul următor:
#include <iostream>
#include <fstream>
using namespace std;
ifstream fin("fisier.in");
int main()
{
int a;
fin >> a;
cout << "Ai citit a = " << a;
return 0;
}
Observatie: Pentru separarea valorilor dintr-un fisier se utilizeaza spatiul sau linia noua.
Valorile preluate dintr-un fisier text pot fi orice tip de date cunoscute de noi: numere, caractere, siruri de
caractere, s.a.m.d.
D. Instrucţiunea de scriere
31
Instrucţiunea afişează pe ecran sau în fişier valorile variabilelor.
//Exemplu
Scrie a, b
#include <iostream>
#include <fstream>
using namespace std;
ifstream fin("fisier.in");
ofstream fout("fisier.out");
int main()
{
int a, b;
fin >> a >> b;
fout << a << " + " << b << " = " << a + b;
return 0;
}
Observație: In fisier s-a scris pe rand valoarea din variabila a, apoi sirul de caractere ” + „ valoarea din
b, alt sir de caractere ” = ” si finalul este suma celor doua variabile.
Observație: În vederea rulării programului prezentat mai sus, trebuie ca fisierele („fisier.in” si
„fisier.out”) sa se afle in acelasi folder cu fisierul principal .cpp.
E. Instrucţiunea de atribuire
Efectul instrucţiunii este acela de a atribui valoarea din dreapta săgeţii variabilei specificată în
stanga. În cazul în care în dreapta avem o expresie, aceasta se va evalua şi apoi valoarea va fi atribuită
variabilei din stanga.
variabilă ← expresie
//Exemplu:
a ← 56
b ← a-2*a
c ← c+1
Ultima atribuire are un sens deosebit, adică variabila c va lua valoarea avută la pasul anterior al
algoritmului marită cu 1.
Atribuirea (asignarea) în C++ se face cu operatorul = (operatorul de atribuire). Atribuirea este
operaţia prin care puteţi schimba valoarea unei variabile în mod direct. Spre exeplu:
32
int a = 40;
În exemplu anterior prezentat variabila a de tip int a fost asignată cu valoarea 40.
Observație: Variabilele declarate global (în afara oricărei funcţii şi clase) sunt iniţializate automat cu
valoarea implicită. Pentru tipurile întregi aceasta este 0, pentru bool este false, iar pentru char este '\0'.
Observație: Atunci când daţi valori variabilelor încercaţi să nu amestecaţi tipurile. De exemplu unei
variabile de tip int nu-i daţi valoarea 5.678 (double). Bineînţeles că în variabila int va fi memorat doar 5
(partea întreagă a lui 5.678), deoarece compilatorul face conversia automat.
Exemplu: Rulați următorul program.
#include <iostream>
using namespace std;
int main()
{
int a, b = 10;
double x, z, pi = 3.14;
a = b = 30;
x = pi; // x este initializat
z = x * pi; // * - inmultire
b = (a + b) - 2 * b;
// cout afiseaza pe ecran
cout << a << ' ' << b << ' ' << x << ' ' << z;
system("PAUSE"); // PAUZA
return 0;
}
F. Blocul de instrucţiuni
Este folosit pentru a efectua mai multe instrucţiuni, în ordinea în care sunt scrise. Sunt mai multe
variante de marcare a începutului şi sfarşitului de bloc de instrucţiuni. Mai jos prezentăm două dintre ele,
urmând ca pe parcursul modulului să folosim varianta cu paranteze.
//Exemplu 1
| instructiune1
| instructiune2
| instructiune3
|_▄
//Exemplu 2
{ instructiune1
instructiune2
instructiune3
}
33
Interschimbarea a două valori (numită şi regula celor trei pahare)
Fie două variabile întregi a şi b. Valorile lor se citesc de la tastatură. Să se interschimbe valorile
celor două variabile apoi să se afişeze noile valori, pe acelaşi rând cu un spaţiu între ele.
Exemplu: dacă pentru variabilele a şi b se citesc valorile 5 şi 8, se va afişa: 8 5
#include <iostream>
using namespace std;
int main()
{
int a,b,aux;
cin>>a>>b;
aux=a;
a=b;
b=aux;
cout<<a<<b;
return 0;
}
Fie x un număr întreg format din exact 5 cifre. Să se afişeze cifra unităţilor şi cea a sutelor, pe
acelaşi rând, cu un spaţiu între ele.
Exemplu: dacă pentru x se citeşte valoarea 12345 se va afişa 5 3.
34
x întreg //date de intrare
c1,c2 întregi //date de manevră
citeşte x
//reţin cifra unităţilor în c1
c1 ← x % 10
x ← x/100 //elimin cifra unităţilor şi a zecilor
//reţin cifra sutelor în c2
c2 ← x % 10
scrie c1, c2
Explicarea algoritmului: Pentru a obţine cifrele unui număr trebuie să efectuăm împărţiri la 10.
Am arătat că operatorul „%” returnează restul împărţirii. În cazul în care un număr se împarte la 10,
atunci restul este chiar ultima cifră, iar câtul împărţirii este numărul fară ultima cifra. În cazul împărţirii
la 100 restul returnează ultimele 2 cifre, iar câtul este numărul fară ultimele 2 cifre. Pentru a afişa cifra
sutelor este suficient să eliminăm ultimele 2 cifre (prin împărţire la 100) şi să afişăm ultima cifră a
numărului nou obţinut.
#include <iostream>
using namespace std;
int main()
{
int x,c1,c2;
cout<<"Introduceti valoarea lui x (din 5 cifre) ? ";
cin>>x;
c1=x%10; // cifra unitatilor se retine in c1
x= x/100; // se elimina cifra unitatilor si a zecilor
c2=x%10; // cifra sutelor se retine in c2
cout<<c1<<c2;
return 0;
}
Auzim în viaţa de zi cu zi afirmaţii de genul: DACĂ obţin note de promovare la toate examenele,
ATUNCI voi lua diploma, ALTFEL trebuie să mai învăţ.
Se remarcă trei cuvinte ce au un rol deosebit: DACĂ, ATUNCI, ALTFEL. Propoziţia are trei
componente şi anume:
condiţie, transcrisă prin “obţin note de promovare la toate examenele”, condiţie pe care o notăm
cu c;
acţiune transcrisă prin “ voi lua diploma”, notată cu p, acţiune asociată cu ATUNCI, adică se
execută doar dacă “obţin note de promovare la toate examenele”;
acţiune transcrisă prin “ trebuie să mai învăţ”, notată cu q, acţiune asociată cu ALTFEL, adică se
execută dacă NU “obţin note de promovare la toate examenele”;
Folosind notaţiile făcute, afirmaţia se poate transcrie în pseudocod sau schemă logică. Secvenţa
de instrucţiuni se numeşte structură alternativă şi se poate reprezenta şi grafic. Structura alternativă
admite şi o formă particulară, caz în care avem o ramură vidă (adică nu se execută nici o operaţie):
35
┌dacă c atunci ┌dacă c atunci
| execută p | execută p
|altfel |▄
| execută q
|▄
NU DA
altfel atunci NU DA
Condiţie altfel atunci
Condiţie
Execută q Execută p
Execută p
Exista cazuri în care condiţia poate fi mai complexă, de genul: DACA obţin note de promovare la
toate examenele ŞI toate notele sunt peste 9, ATUNCI voi putea beneficia de bursă, ALTFEL nu.
Notând prima condiţie cu c1 (obţin note de promovare la toate examenele), cu c2 a două condiţie
(toate notele sunt peste 9), cu p acţiunea „voi putea găsi un job cu salariu mai mare” şi cu q acţiunea
„salariul va fi mai mic”, se va folosi operatorul logic „and” iar condiţia va fi compusă: „c1 and c2”.
Observaţii
Atât ramura „ATUNCI” cât şi „ALTFEL” permit executărea unei singure instrucţiuni. În cazul în
care este necesară efectuarea mai multor instrucţiuni, acestea se grupează într-o singură
instrucţiune compusă.
Uneori avem o instrucţiune de decizie subordonată unei alte instrucţiuni (de decizie sau de alt
fel). Este important ca instrucţiunea subordonată să fie scrisă identat faţă de instrucţiunea care o
subordonează. Acest mod de scriere nu este obligatoriu pentru funcţionarea algoritmului însă
face programele mai uşor de urmărit şi de actualizat.
Se introduce de la tastatură un număr întreg x. Să se testeze dacă numărul este par sau nu şi să se
afişeze un mesaj corespunzător.
Exemplu: dacă pentru x se citeşte valoarea 4123 se va afişa “Nu este par” iar pentru valoarea
588 se va afişa “Este par”.
36
Explicarea algoritmului: Pentru a verifică dacă un număr este par trebuie să verificăm dacă
restul împărţirii lui la 2 este „0”. în caz afirmativ rezulta ca numărul este par, altfel el este impar.
Exemplu:
#include<iostream>
using namespace std;
int main()
{
int x;
cout<<"Care este numarul? "; cin>>x;
if (x%2==0)
cout<<"Este par.";
else
cout<<"Nu este par.";
return 0;
}
Se introduc de la tastatura două numere întregi x şi y. să se afiseze numărul care este mai mare
intre cele două. în caz ca sunt egale, se va afişa un mesaj corespunzator.
Exemplu: dacă pentru x şi y se citesc valoarile 612 şi 3129 se va afişa “3129” iar pentru
valoarile 58 şi 58 se va afişa “Numerele sunt egale”.
Explicarea algoritmului: Pentru a afişa maximul intre două numere le vom compara. Dacă cele
două numere sunt egale vom afişa mesajul „Sunt egale”, altfel verificăm dacă x>y, situatie în care vom
afişa pe x, altfel vom afişa pe y.
Exemplu:
#include<iostream.h>
using namespace std;
int main ()
{
int x,y;
cout<<"x=" ; cin>>x;
cout<<"y=" ; cin>>y;
{
if (x==y)
37
cout<<"Numerele sunt egale" <<endl;
else
if (x>y)
cout<<"Maximul este " <<x <<endl;
else
if (x<y)
cout<<"Maximul este " <<y <<endl;
}
return 0;
}
Folosind notaţiile făcute, structură repetitivă cu test iniţial se poate scrie astfel:
Observaţii:
Pentru ca structură repetitivă să nu intre într+un ciclu infinit, trebuie ca secvenţa de instrucţiuni
să modifice cel puţin una din variabilele care intervin în condiţie astfel încât aceasta să poată
deveni falsă la un moment dat.
Dacă de la bun început condiţia are valoarea fals, secvenţa de instrucţiuni nu se execută nici
măcar o data.
38
Să consideram urmatoarea problemă: Se citeşte un număr n, întreg, cu cel mult 9 cifre. Se cere
să se afişeze suma cifrelor numărului n.
Explicarea algoritmului: Rezolvarea presupune că se extrag pe rând cifre din număr şi se
adaugă la sumă. Soluţia se poate exprima în cuvinte astfel: CAT TIMP numărul este diferit de zero
(deci mai sunt cifre de extras), EXECUTĂ extrage şi elimină ultima cifră din numărul n apoi adaugă
cifra la sumă.
Soluţia are două componente:
condiţia, trascrisă prin “ numărul este diferit de zero”;
acţiune transcrisă prin “ extrage şi elimină ultima cifră din numărul n apoi adaugă cifra la sumă”;
Notând numărul dat cu „n”, cifra eliminată cu „c” şi suma cifrelor cu „s”, algoritmul de
rezolvare va fi:
n ← [n / 10]
S←S +c
Scrie S
STOP
Exemplu:
#include <iostream>
using namespace std;
int main()
{
int n,c,s;
cout<<"Dati valoarea lui n (un numar format din cel mult 9 cifre) = ";cin>>n;
while(n>0)
{
c=n%10;
n=n/10;
s=s+c;
39
}
cout<<"Suma cifrelor lui "<<n<<" este "<<s<<endl;
return 0;
}
Ca şi structură repetitivă cu test iniţial, structură repetitivă cu test final are aceleaşi două
componente:
condiţia, o expresie logică ce poate fi evaluată prin valoarea TRUE sau FALSE, condiţie pe care
o notăm cu c;
actiune, o secvenţă de instrucţiuni ce se vor executa repetat, notată cu a, acţiune asociată cu
EXECUTĂ;
În structură repetitivă cu test final mai întâi se execută secvenţa de instrucţiuni “a” şi apoi se
evaluează condiţia. De aici şi numele de structură cu test final.
În pseudocod forma generală a structurii repetitive cu test final este:
┌execută
| a
└cât timp c
Denumim această structură în mod obişnuit EXECUTĂ – CÂT TIMP sau DO – WHILE,
însa există variante la fel de utilizate ale acestei structuri şi anume: REPETĂ – PÂNĂ CÂND sau
REPEAT – UNTIL.
Observaţii:
Pentru ca structură repetitivă să nu intre într+un ciclu infinit, trebuie ca secvenţa de instrucţiuni
să modifice cel puţin una din variabilele care intervin în condiţie astfel încât aceasta să poată
deveni falsă la un moment dat.
Spre deosebire de structură repetitivă cu test iniţial, structură repetitivă cu test final efectuează o
data secvenţa de instrucţiuni înainte de a testa condiţia.
40
START c caracter //date de intrare
nr întreg //date de ieşire
nr ← 0
nr ← 0
execută
| citeşte c
Citeşte c
| dacă (c=’a’sau c=’e’sau
| | c=’i’sau c=’o’sau c=’u’)
NU C= DA
vocala
| | atunci
| | nr ← nr + 1 //numar vocala
nr ← nr + 1 | |▄
└cât timp c≠”.”
scrie nr
DA
C ≠ “.”
NU
Scrie nr
STOP
Exemplu:
#include <iostream>
#include <string.h>
using namespace std;
int main()
{
char c[256],vocale[]="aeiou";
cout << "Scrieti propozitia: ";
cin.get(c,255); // citeste in c
int i,nr = 0;
do{
i=i+1;
if (strchr(vocale,c[i]))
nr=nr+1;
}
while (i < strlen(c)) ;
cout<<"Numarul de vocale din textul citit este: "<<nr<<'\n';
return 0;}
Observație: Variabila c din cadrul programului de mai sus, conține maxim 256 de caractere.
41
|▄
unde:
exp_i şi exp_f sunt expresii ale căror valori sunt evaluate în cadrul repetiţiilor;
contor este o variabilă ce va lua prima dată valoarea expresiei iniţiale exp_i, urmând apoi să se
modifice până la valoarea expresiei finale exp_f;
a este secvenţă de instrucţiuni ce se va executa repetat;
Principiul de funcţionare al structurii repetitive cu număr cunoscut de repetiţii este următorul (am
făcut presupunerea că exp_i <= exp_f):
Pasul 1: Se evaluează exp_i (expresia iniţială);
Pasul 2: Se atribuie variabilei contor valoarea expresiei exp_i;
Pasul 3: Se evaluează exp_f (expresia finală);
Pasul 4: Dacă valoarea variabilei contor este mai mare decât valoarea expresiei exp_f, atunci se
iese din structură repetitivă. Dacă valoarea varibilei contor este mai mică sau egală cu valoarea
expresiei exp_f, atunci se execută secvenţa de instrucţiuni „a” şi se incrementează (îşi măreşte
valoarea cu 1) valoarea variabilei contor, după care se reia pasul 3.
Observaţii:
Exp_i şi exp_f pot fi expresii de evaluat sau doar variabile simple ce au valori date.
De regulă folosim structuri repetitive cu număr cunoscut de repetiţii în care dorim ca variabila
contor să creasca de la exp_i la exp_f, caz în care evident valoarea exp_i trebuie să fie mai
mica decât exp_f.
Într-o astfel de structură, secvenţa de instrucţiuni se execută de (exp_f – exp_i +1) ori. Dacă însă
folosind acest tip de structură, valoarea iniţială a lui exp_i este mai mare decât exp_f, atunci secvenţa de
instrucţiuni „a” nu se execută niciodată.
Dacă forma structurii PENTRU este:
unde x este o variabilă numerică, atunci contorul va creşte din x în x. Când x lipseşte din structură
PENTRU, contorul, creşte cu 1.
În structurile repetitive cu număr cunoscut de repetiţii în care dorim ca variabila contor să
scadă de la exp_i la exp_f, trebuie ca exp_i >= exp_f, iar variabila contor va scădea din x în x sau cu
câte o unitate. Dacă însă folosind acest tip de structură, valoarea iniţiala a lui exp_i este mai mica decât
exp_f, atunci secvenţa de instrucţiuni „a” nu se execută niciodată.
S=1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 = 55.
42
START n întreg //date de intrare
S întreg //date de ieşire
Citeşte n i întreg //date de manevră
S ←0 citeşte n
S←0
i ←1
i←1
pentru i = 1, n execută
|S←S+i
DA |▄
i <= n scrie S
NU S←S +i
i←i+1
Scrie S
STOP
Se observă că folosind scheme logice nu se poate reprezenta structură PENTRU decât cu ajutorul
uneia din structurile repetitive cu testare iniţială sau finală.
Exemplu:
#include <iostream>
using namespace std;
int main()
{
int n, s=0, i;
cout<<"n?"; cin>>n;
for (i=1; i<=n; i++)
s=s+i;
cout<<s;
return 0;
}
Se observă că este necesară testarea iniţială a condiţiei deoarece, spre deosebire de structură
WHILE, structură DO-WHILE efectuează cel puţin o dată secvenţa înainte de a testa condiţia.
Simularea structurii repetitive DO-WHILE cu WHILE se face astfel:
43
┌execută a //se execută secvenţa a
| a ┌cât timp c execută
└cât timp c | a
|▄
Se observă că în acest caz este necesar să executăm o dată secvenţa de intrucţiuni în afara
ciclului.
Cele două structuri (WHILE şi DO - WHILE) sunt echivalente (nefiind necesară existenţa ambelor),
însă în funcţie de problemă, vom alege structură repetitivă adecvată, care este mai potrivită pentru
descrierea clară a algoritmului.
Simularea structurii repetitive FOR cu WHILE se face astfel:
Se observă că în acest caz este necesar să scriem explicit instrucţiunea care creşte contorul cu 1.
Simularea structurii repetitive FOR cu DO - WHILE se face astfel:
Dintre toate aceste structuri repetitive, singura indispensabilă este cea cu test iniţial (CÂT TIMP
sau WHILE), celelalte putând fi obţinute din aceasta, după cum am văzut mai sus.
44
2.4. Proiectarea algoritmilor: top-down, bottom-up, modulară, structurată
Există două metode generale de proiectare a algoritmilor, a căror denumire provine din modul de
abordare a rezolvării problemelor: metoda top-down (descendentă) şi metoda ascendentă (bottom-
up).
A. Metoda top-down
Algoritmul
Problema iniţială principal
Descompunerea
Subprogramul Subprogramul
modulului de calcul 1 2
în 2 subprograme
B. Metoda bottom-up
În cazul metodei ascendente (bottom-up) va fi scris mai intai subalgoritmul apelat şi apoi
cel care apelează. Se ajunge astfel la o mulţime de subalgoritmi care se apeleaza între ei. Este important
să se cunoască care subalgoritm apelează pe care, lucru redat printr-o structură arborescentă, ca şi în
cazul programarii descendente.
Această metodă are ca principal dezavantaj faptul că erorile vor fi detectate târziu, abia în faza
de asamblare. Se poate ajunge chiar la concluzia că anumiţi subalgoritmi implementaţi, deşi sunt corecti,
nu sunt utili.
45
De cele mai multe ori proiectarea algoritmilor combină cele două metode, ajungând astfel la o
proiectare mixtă.
46
2.5. Algoritmi elementari ce folosesc structuri fundamentale
În continuare vom prezenta câţiva algoritmi utili care prelucrează numere întregi şi naturale.
2.5.1. Divizibilitate
Se citesc două numere întregi a şi b. Să se realizeze un algoritm care să verifice dacă cele
două numere sunt divizibile (a divizibil cu b sau b divizibil cu a).
De exemplu dacă se citesc numerele a = 25 şi b = 5 atunci algoritmul va afişa „DA”, iar în cazul
în care se citesc a = 25 şi b = 10 se va afişa „NU”.
În pseudocod algoritmul de rezolvare este:
a, b întregi
citeşte a, b
dacă (a % b = 0 sau b % a = 0) atunci
| scrie „DA”
| altfel
| scrie „NU”
|▄
Exemplu:
#include<iostream>
using namespace std;
int main()
{
int a,b;
cout<<"Numarul a: "; cin>>a;
cout<<"Numarul b: "; cin>>b;
if (a%b==0|| b%a==0)
cout<<"DA";
else
cout<<"NU";
return 0;
}
2.5.2. Paritate
a întreg
citeşte a
dacă (a % 2 = 0) atunci
47
| scrie „PAR”
| altfel
| scrie „IMPAR”
|▄
#include<iostream>
using namespace std;
int main()
{
int a;
cout<<"Numarul a: "; cin>>a;
if (a%2==0)
cout<<"PAR";
else
cout<<"IMPAR";
return 0;
}
a,i întreg
citeşte a
pentru i ← 1, a execută
| dacă (a % i = 0) atunci
| | scrie i
| |▄
|▄
Exemplu:
#include <iostream>
using namespace std;
void afisareDivizori(int x)
{
48
for(int i = 1; i <= x; i++)
{
if(x % i == 0)
cout << i << " ";
}
}
int main()
{
int a;
cout << "a = ";
cin >> a;
afisareDivizori(a);
return 0;
}
Se citeşte un număr întreg a. Să se realizeze un algoritm care să afişeze toti divizorii proprii
ai numărului a.
De exemplu dacă se citeşte pentru a valoarea 12 atunci algoritmul va afişa „2 3 4 6”, iar în cazul
în care se citeşte a = 13 se va afişa mesajul „nu există divizori proprii”.
În pseudocod algoritmul de rezolvare este:
a, i, sem întreg
sem ← 0 //folosită pentru a reţine dacă am găsit divizori
citeşte a
pentru i ← 2, [a/2] execută
| dacă (a % i = 0) atunci
| | scrie i
| | sem ← 1 //marchez faptul ca am găsit divizori
| |▄
|▄
daca sem = 0 atunci
| scrie „nu există divizori proprii”
|▄
Exemplu:
#include <iostream>
using namespace std;
int main()
{
int a,i,sem;
sem=0;
49
cout << "a = ";
cin >> a;
for(int i = 2; i <= a/2; i++)
{
if(a % i == 0)
{cout << i << " ";
sem = 1;}
}
if (sem == 0)
{cout<<"Nu aexista divizori proprii";}
return 0;
}
a, i, prim întreg
citeşte a
dacă a <= 1 atunci
| scrie „numar NEPRIM”
| altfel
| | prim ← 1 //presupunem ca numărul este prim
| | pentru i ← 2, [a/2] execută
| | | dacă (a % i = 0) atunci
| | | | prim ← 0 //am găsit divizori deci nr nu e prim
| | | |▄
| | |▄
| | dacă prim = 1 atunci
| | | scrie „numar PRIM”
| | |altfel
| | |scrie „numar NEPRIM”
| | |▄
| |▄
|▄
Explicarea algoritmului: Se cunoaşte faptul că un număr este prim dacă NU are divizori proprii.
De asemenea se ştie că numărul 1 NU este prim, de aceea vom trata separat cazul a=1.
Algoritmul foloseşte o structură repetitivă cu număr cunoscut de repetiţii, în care se caută
divizori proprii. În caz că se gasesc divizori, numărul nu este prim altfel numărul este prim. Se foloseste
o variabilă semafor numită „prim” care iniţial are valoarea „1” şi se modifică în „0” doar dacă se găsesc
divizori.
Exemplu:
#include <iostream>
using namespace std;
50
int main()
{
int a,i,prim;
cout << "a = ";
cin >> a;
if (a <= 1)
{cout<<"numar NEPRIM";}
else
{prim=1;
for(int i = 2; i <= a/2; i++)
{
if(a % i == 0)
{prim = 0;}
}
if (prim == 1)
{cout<<"numar PRIM";}
else
{cout<<"numar NEPRIM";}
}
return 0;
}
51
Apoi se creşte d şi se repetă instrucţiunile pentru a se verifica dacă acest nou număr este divizor
şi a afla puterea.
Explicarea algoritmului: Algoritmul folosit este algoritmul lui EUCLID. Exista şi algoritmul
care afla cmmdc prin scadere însa nu este eficient (despre eficienţa algoritmilor vom vorbi tema
următoare).
Algoritmul lui Euclid foloseşte o structură repetitivă cu test iniţial. Mai întâi aflăm restul
împărţirii lui a la b şi cât timp acest rest este diferit de 0, vom înlocui pe a cu b şi pe b cu restul obţinut,
după care recalculăm restul împărţirii noului a la noul b.
Euclid a demonstrat că oricare ar fi numerele a şi b iniţiale, repetând operaţiile descrise mai sus,
găsim restul = 0. În acel moment putem afirma că cmmdc(a,b) este ultimul rest nenul. Deoarece
variabila b ia mereu valoarea restului, afişam pe b ca fiind cmmdc.
52
Explicarea algoritmului: Algoritmul verifică fiecare număr cuprins între 1 şi n dacă este număr
perfect. Această verificăre se face iniţializând suma cu 0 pentru fiecare număr şi căutând divizorii de la 1
la jumatatea numărului. Divizorii se adaugă la sumă iar la final aceasta este comparată cu numărul testat.
Se citesc două numere întregi a şi b. Să se realizeze un algoritm care să verifice dacă cele
doau numere sunt prietene. Spunem ca două numere sunt prietene dacă suma divizorilor proprii
ai unui număr este egală cu celalalt şi invers.
De exemplu dacă se citeşte pentru a valoarea 284 şi pentru b valoarea 220 atunci algoritmul va
afişa mesajul „numere prietene”.
În pseudocod algoritmul de rezolvare este:
a, b, sa, sb întreg
citeşte a, b
sa ← 0
sb ← 0
pentru i ← 2, [a/2] execută
| dacă (a % i = 0) atunci
| | să ← să + i //suma divizorilor proprii numărului a
| |▄
|▄
pentru i ← 2, [b/2] execută
| dacă (b % i = 0) atunci
| | sb ← sb + i // suma divizorilor proprii numărului b
| |▄
|▄
daca sa = b şi sb = a atunci
| scrie „numere prietene”
| altfel
| scrie „NU sunt numere prietene”
|▄
Explicarea algoritmului: Algoritmul calculează suma divizorilor lui a în sa şi suma divizorilor
lui b în sb.
Apoi verifică dacă sa = b şi sb = a. Dacă condiţia este adevarată se afişează „numere prietene”
altfel se afişează „Nu sunt numere prietene”.
2.5.10. Factorial
53
scrie p
Se citeşte un număr întreg n (2< n <= 20). Să se realizeze un algoritm care să afişeze al n-lea
termen din şirul lui Fibonacci.
De exemplu dacă se citeşte pentru n valoarea 8 atunci algoritmul va afişa 21, deoarece al 8-lea
termen din şirul lui Fibonacci este 21.
În pseudocod algoritmul de rezolvare este:
n, f1, f2, f3, i întreg
citeşte n
f1 ← 1
f2 ← 2 // iniţializarea primilor termeni din şir
pentru i ← 3, n execută
| f3 ← f2 + f1 //calculul termenului curent din şir
| f1 ← f2
| f2 ← f3
|▄
scrie f3
1 dacă n = 1 sau n = 2
Fibo(n) =
54
scrie inv
Se citeşte un număr întreg a. Să se realizeze un algoritm care să verifice dacă numărul citit
este sau nu palindrom. Numim palindrom un număr care este egal cu oglinditul său.
De exemplu dacă se citeşte pentru a valoarea 323 atunci algoritmul va afişa „PALINDROM”, iar
dacă va citi 123 va afişa „NU”.
În pseudocod algoritmul de rezolvare este:
De exemplu dacă se citeşte pentru n valoarea 5 şi apoi valorile 12, 220, 23, 89, 146 atunci
algoritmul va afişa mesajul 220, deoarece 220 este valoarea maximă citită.
În pseudocod algoritmul de rezolvare este:
n, i, x, max întreg
citeşte n
citeşte x//citesc primul număr din şir separat de celelalte
max ← x //iniţial max = primul număr citit din şir
pentru i ← 2, n execută
| //de la 2 pentru că am citit deja un număr din şir
| citeşte x
| dacă (x > max) atunci
55
| | max ← x //max = noua valoare a lui x
| |▄
|▄
scrie max
Capitolul 3
3.1. Corectitudinea algoritmilor. Surse de erori în elaborarea algoritmilor (erori în datele
iniţiale, erori de rotunjire, erori de metodă, erori reziduale, erori de sintaxă)
În elaborarea algoritmilor apar frecvent erori ce vor trebui descoperite şi remediate. Prezentăm
câteva dintre cele mai întâlnite tipuri de erori şi câteva metode de tratare a lor.
Erori în datele iniţiale. Acest tip de erori provin din introducerea, ca valori de intrare, a unor
date de tip diferit de cel prevazut la elaborarea algoritmului. În aceste situatii algoritmul nu se
comportă în modul aşteptat, generând erori sau eventuale date de ieşire eronate.
Variabile globale şi locale. O altă eroare frecventă este confuzia între variabilele globale şi
locale, atunci când se doreşte de exemplu a utiliza în exteriorul unei funcţii o variabilă care a fost
declarată în interiorul unei funcţii.
Erori de trunchiere, erori de rotunjire şi erori de calcul
a) Eroare de trunchiere este diferenţa dintre rezultatul exact (pentru datele de intrare curente) şi
rezultatul furnizat de un algoritm dat utilizând aritmetica exactă.
b) Eroare de rotunjire este diferenţa dintre rezultatul produs de un algoritm dat utilizând
aritmetica exactă şi rezultatul produs de acelaşi algoritm utilizând o aritmetică cu precizie
limitată (de exemplu aritmetica virgulei mobile).
c) Eroarea de calcul este suma dintre eroarea de trunchiere şi eroarea de rotunjire, dar de obicei
una dintre acestea predomină.
Erori reziduale. Erorile reziduale sunt acele erori care rezultă din diferenţa între valorile
obţinute efectiv şi cele asteptate a fi obţinute.
Erori de sintaxă. Erorile de sintaxă sunt erorile cele mai frecvente şi cel mai uşor de corectat.
De cele mai multe ori, la rularea programelor se identifică corect sursa erorilor în programele pe
care le-am realizat.
În general, erorile de sintaxă provin din:
56
greşeli de tastare
confuzia între majuscule şi minuscule;
inversarea literelor;
greşeli de punctuaţie
inchiderea incorecta a parantezelor, a blocurilor de instrucţiuni;
ghilimele şi apostrofuri plasate greşit;
greşeli de plasare a instrucţiunilor
confuzia între şirurile de caractere şi numere
numerele sunt tratate ca şiruri de caractere;
şirurile de caractere sunt tratate ca numere.
Erori de logică. Erorile de logică se generează atunci când nu se obţin rezultatele aşteptate. Deşi
codul sursă este corect din punct de vedere sintactic, deşi nu sunt generate erori în timpul
execuţiei programului (apeluri de funcţii incorecte; atribuiri de valori pentru variabile
nedeclarate; imposibilităţi aritmetice – împărţire la zero etc.), totuşi programul conţine erori de
logică (semantice).
Identificarea erorilor de logică constituie o etapă dificilă pentru începători, dar nu de nedepaşit.
Foarte multe erori de logică sunt generate de o analiză superficială a aplicaţiei, o proiectare defectuoasă
a programului, şi ca urmare de un cod sursă incorect!
Atribuire şi egalitate. În programare una dintre erorile cele mai frecvente comise de către
începători este confuzia între operatorul de atribuire şi operatorul de egalitate. Aceste erori sunt
câteodată dificil de identificat în măsura în care ele nu generează întotdeauna un mesaj de eroare.
57
Verificarea corectitudinii algoritmului implementat. Un algoritm realizat trebuie să fie corect,
clar, sigur în funcţionare, uşor de modificat, portabil, eficient, însoţit de o documentaţie corespunzătoare.
Există numeroase tehnici de verificăre şi validare a algoritmilor, adresate în general programatorilor
experimentaţi, dar şi uşor accesibile unui începător în programare.
Dintre acestea amintim tehnica testarii programelor şi depanarea programelor.
Tehnica testarii programelor. Acţiunea de testare a programelor se deosebeşte de celelalte
faze prin care trec acestea (proiectare, programare, documentaţie, etc.) prin caracterul ei în aparenţă
“demolator”. Astfel, în timp ce alte faze au o esenţă constructivă, testarea are în aparenţă un caracter
distructiv, deoarece scopul ei este de a pune în evidenţă proasta funcţionare a programului, de a găsi
defectele acestuia şi nu părţile sale bune.
Analizând problema mai atent, realizăm de fapt că scopul testării este în realitate tot constructiv,
acela de a pune în funcţiune un program care să funcţioneze la parametri prevăzuţi.
Se ştie că, într-un algoritm este oricând posibilă prezenţa unei/unor erori, oricât de precisă şi laborioasă
ar fi metodologia de elaborare. Procesul de detectare şi apoi de eliminare a erorilor unui algoritm are
două componente, numite: verificare; validare.
Aceste douà activităţi ar trebui să caracterizeze practic toate etapele prin care trece un program,
de la formularea cerinţei de rezolvare a unei probleme, la analiza acesteia, la identificarea şi apoi
descrierea algoritmului de rezolvare a problemei, a codificării datelor şi validarea rezultatelor obţinute.
În acest sens, activitatea de verificăre şi validare a unui produs program urmăreşte în principal,
urmàtoarele:
descoperirea defectelor programului
certificarea faptului că programul va funcţiona corect în condiţii de exploatare curentă.
Testarea programului pe diverse seturi de date de test. Testarea programului rămâne metoda
de bază pentru verificărea corectitudinii unui program, succesul ei fiind condiţionat în primul rând de
experienţa programatorului, de complexitatea şi completitudinea setului de date folosite în procesul
testării, de analiza riguroasă, atentă a rezultatelor obţinute în urma fiecărui test.
Prin testarea programului se înţelege deci executărea programului respectiv cu scopul de a
descoperi o anomalie sau eroare. Ea se bazează pe construirea unor eşantioane de date de intrare care să
conducă la depistarea unor erori în functionarea programului, într-un timp cât mai scurt şi cu efort cât
mai mic.
Practic, pornind de la nişte date de test construite de el, programatorul aşteaptă să obţină la final
sau pe parcurs, anumite rezultate. Putem spune că succesul testării depinde de “arta” programatorului de
a-şi construi setul de date de test.
Seturile de date de test trebuie elaborate cu atenţie, astfel încât să acopere, pe cât posibil, toate
variantele de executie a algoritmului, inclusiv situatii de exceptie, şi să verifice dacă fiecare
subproblemă a problemei date este rezolvată corect (dacă este posibil, se va testa separat fiecare modul
de program).
Metode de testare a programelor
În acest sens, au apărut, în ultimul timp, o serie de metode de elaborare a datelor de test, care
ajută programatorul, oferindu-i posibilitatea de a aborda sistematic activitatea de testare a programelor,
cu o probabilitate crescută de depistare a erorilor.
Aceste metode pot fi denumite:
testarea funcţională sau metoda cutiei negre, care presupune construirea datelor de test astfel
încât să permită testarea fiecărei funcţiuni a programului;
testarea structurală sau rnetoda cutiei transparente, care presupune construirea datelor de
test astfel încât toate părţile programului să poată fi testate.
Succesul activităţii de testare constă deci în conceperea unor date de intrare prin prelucrarea
cărora defectele algoritmului şi deci şi ale programului să fie puse în evidenţă prin observarea şi analiza
rezultatelor obţinute.
De aceea el este în mare măsura dependent de experienţa şi îndemânarea programatorului, de
abilitatea lui de a-şi construi datele de test cât mai complete, complexe, cuprinzătoare din punct de
58
vedere al situaţiilor sau valorilor de excepţie ce pot apare în execuţia corectă a programului.
Testarea unui program trebuie să se finalizeze, pentru a fi utilă, cu semnalarea erorii şi
localizarea ei. De aceea, testarea programului este urmată de depanarea lui.
Depanarea unui program. Depanarea constă în localizarea erorii, determinarea naturii sale şi
corectitudinea ei. Ea se poate face în mod:
static, după executărea programului
dinamic, în timpul execuţiei acestuia
Limbajele de programare oferă, în ultimile lor versiuni, un depanator simbolic integrat, care
permite depanarea uşoară, plăcută şi eficientă a programelor prin următoarele operaţii:
executarea pas cu pas a programului (un pas înseamnă de fapt o instrucţiune executăbilă);
observarea, în timpul execuţiei, a valorilor unor variabile sau expresii specificate de programator
(care apar într-o fereasträ specială - Watch Window);
specificarea unor puncte de suspendare a execuţiei programului;
modificarea valorilor unor variabile.
În activitatea de testare şi depanare a programelor, erorile datorate variabilelor neiniţializate
sunt greu de semnalat şi de localizat, mai ales atunci când aparent totul funcţionează corect.
În acest sens amintim variabila cu rol de contor. Aceasta trebuie iniţializată (de regulă cu 0).
De asemenea, expresia care stabileşte dacă un ciclu se execută sau nu trebuie astfel formulată sau
iniţializată încât să asigure sau nu prima execuţie, aşa cum necesită algoritmul de prelucrare descris. În
acest sens, trebuie să facem precizarea că adeseori, suntem nevoiţi să facem noi, prin program,
iniţializarea variabilei care controlează execuţia ciclului, pentru a asigura execuţia lui pentru prima dată.
Deci, ciclul cu testarea iniţială a condiţiei trebuie să fie bine analizat, verificat şi testat din punctul de
vedere al expresiei care-i controlează reluarea.
Practica a dovedit, în timp, că oricât de numeroase ar fi testele efectuate asupra unor programe
foarte complexe, ele nu pot garanta funcţionarea corectă a acestora. Ele rămân deosebit de utile pentru
semnalarea multora dintre erori şi deasemenea pentru familiarizarea programatorului cu algoritmul, cu
modul său de lucru.
#include <iostream.h>
int v[10],n;
int max(int i,int j)
{ int a,b;
if (i==j) return v[i];
else
{ a=max(i,(i+j)/2);
b=max((i+j)/2+1,j);
if (a>b) return a;
else return b;
}
59
}
main()
{ cout<<"n="; cin>>n;
for (int i=1;i<=n;i++)
{ cout<<"v["<<i<<"]=";
cin>>v[i];
}
cout<<"max="<<max(1,n); }
#include <iostream.h>
int a[10],n;
void sort(int p,int q,
int a[10])
{
int m;
if (a[p]>a[q])
{ m=a[p];
a[p]=a[q];
a[q]=m;
}
}
void interc(int p,int q,
int m,int a[10])
{ int b[10],i,j,k;
i=p; j=m+1; k=1;
while (i<=m && j<=q)
if (a[i]<=a[j])
{ b[k]=a[i];
i=i+1;
k=k+1;
}
else
{ b[k]=a[j];
j=j+1;
k=k+1;
}
if (i<=m)
for (j=i;j<=m;j++)
60
{ b[k]=a[j];
k=k+1;
}
else
for (i=j;j<=q;j++)
{ b[k]=a[i];
k=k+1;
}
k=1;
for (i=p;i<=q;i++)
{ a[i]=b[k];
k=k+1;
}
}
void divimp (int p,int q,
int a[10])
{ int m;
if ((q-p)<=1) sort(p,q,a);
else
{ m=(p+q)/2;
divimp(p,m,a);
divimp(m+1,q,a);
interc(p,q,m,a);
}
}
main()
{ int i;
cout<<"n="; cin>>n;
for (i=1;i<=n;i++)
{ cout<<"a["<<i<<"]=";
cin>>a[i];
}
divimp(1,n,a);
for (i=1;i<=n;i++)
cout<<a[i]<<" ";
}
Problema 3. Fie vectorul a cu n componente numere întregi (sau reale). Se cere ca vectorul să fie sortat
crescător. Se cere să se testeze programul în C++ aferent problemei.
Rezolvare:
Este necesară o funcţie POZ care tratează o porţiune din vector, cuprinsă între indicii daţi de li
(limita inferioară) şi ls (limita superioară). Rolul acestei funcţii este de a poziţiona prima componentă
a[li] pe o poziţie k cuprinsă între li şi ls, astfel încât toate componentele vectorului cuprinse între li şi k-1
să fie mai mici sau egale decât a[k] şi toate componentele vectorului cuprinse între k+1 şi ls să fie mai
mari sau egale decât a[k].
În această funcţie există două moduri de lucru:
a) i rămâne constant, j scade cu 1;
b) i creşte cu 1, j rămâne constant.
Funcţia este concepută astfel:
iniţial, i va lua valoarea li, iar j va lua valoarea ls (elementul care iniţial se află pe poziţia li se va
găsi mereu pe o poziţie dată de i sau de j);
61
se trece în modul de lucru a);
atât timp cât i<j, se execută:
dacă a[i] este strict mai mare decât a[j], atunci se inversează cele două numere şi se schimbă
modul de lucru;
i şi j se modifică corespunzător modului de lucru în care se află programul;
k ia valoarea comună a lui i şi j.
Alternanţa modurilor de lucru se explică prin faptul că elementul care trebuie poziţionat se
compară cu un element aflat în dreapta sau în stânga lui, ceea ce impune o modificare corespunzătoare a
indicilor i şi j.
Observație: După aplicarea funcţiei POZ, este evident că elementul care se află iniţial în poziţia
li va ajunge pe o poziţie k şi va rămâne pe acea poziţie în cadrul vectorului deja sortat, fapt care
reprezintă esenţa algoritmului.
Funcţia QUICK are parametrii li şi ls (limita inferioară şi limita superioară). În cadrul ei se
utilizează metoda DIVIDE ET IMPERA, după cum urmează:
• se apelează POZ;
• se apelează QUICK pentru li şi k-1;
• se apelează QUICK pentru k+1 şi ls.
Programul în C++ este următorul:
#include <iostream.h>
int a[100],n,k;
void poz (int li,int ls,int& k,int a[100])
{ int i=li,j=ls,c,i1=0,j1=-1;
while (i<j)
{ if (a[i]>a[j])
{ c=a[j]; a[j]=a[i];
a[i]=c; c=i1;
i1=-j1; j1=-c;
}
i=i+i1;
j=j+j1;
}
k=i;
}
void quick (int li,int ls)
{ if (li<ls)
{ poz(li,ls,k,a);
62
quick(li,k-1);
quick(k+1,ls);
}
}
main()
{ int i;
cout<<"n="; cin>>n;
for (i=1;i<=n;i++)
{ cout<<"a["<<i<<"]=";
cin>>a[i];
}
quick(1,n);
for (i=1;i<=n;i++)
cout<<a[i]<<endl;
}
Problema 4. Se dau 3 tije simbolizate prin a, b, c (a se vedea figura următoare). Pe tija a se găsesc
discuri de diametre diferite, aşezate în ordine descrescătoare a diametrelor privite de jos în sus. Se cere
să se mute discurile de pe tija a pe tija b, utilizând ca tijă intermediară tija c, respectând următoarele
reguli:
la fiecare pas se mută un singur disc;
nu este permis să se aşeze un disc cu diametrul mai mare peste un disc cu diametrul mai mic.
63
Priviţi următoarele exemple:
1) pentru n=2, avem: H(2,a,b,c)=H(1,a,c,b),ab,H(1,c,b,a)=ac,ab,cb;
2) pentru n=3, avem: H(3,a,b,c)=H(2,a,c,b),ab,H(2,c,b,a)=H(1,a,b,c),ac,H(1,b,c,a), ab,
H(1,c,a,b), cb, H(1,a,b,c)=ab,ac,bc,ab,ca,cb,ab.
Programul în C++ este următorul:
#include <iostream.h>
char a,b,c;
int n;
void han (int n,char a,
char b,char c)
{ if (n==1) cout<<a<<b<<endl;
else
{ han(n-1,a,c,b);
cout<<a<<b<<endl;
han(n-1,c,b,a);
}
}
main()
{ cout<<"N="; cin>>n;
a='a'; b='b'; c='c';
han(n,a,b,c);
}
Rezolvare:
Fie a, b ; a b . Rezulta a q b r ; unde q este câtul, iar r este restul împărțirii lui a la b. În
cele ce urmează vom arăta că orice divizor a lui a și b este divizor și pentru r.
Avem: r a q b . Dacă d este un divizor a lui a și b, fie a s d și b t d . Rezultă că
r s d q t d s q t d . Deci d este divizor a lui r. Cum orice divizor al lui a si b este divizor si
pentru r atunci si cel maim mare divizor se refera si la r . Este suficient sa continuam procesul cu
numerele b si r . Cum r este mai mic ca b în valoare absoluta, vom gasi r = 0 într -un numar finit de
pasi .
64
În situaţia în care se găsesc mai mulţi algoritmi care rezolvă corect aceeaşi problemă, ne putem
pune întrebarea pe care dintre ei îi vom alege pentru a scrie programul?
Această alegere este o cerinţă critică pe care un algoritm trebuie să o satisfacă, pentru că un
algoritm care are nevoie de un an ca să ruleze, sau necesită un GB de memorie internă nu este utilizabil.
Pentru a alege cel mai bun algoritm, trebuie să analizăm aceşti algoritmi în scopul determinării
eficienţei lor şi, pe cât posibil, a optimalităţii lor.
Analiza algoritmilor. Complexitatea unui algoritm se referă la cantitatea de resurse consumate
la execuţie - adică timp de executie şi spaţiu de memorie.
Din această cauză putem spune că eficienţa unui algoritm se evaluează din două puncte de
vedere:
din punctul de vedere al spaţiului de memorie necesar pentru memorarea valorilor variabilelor
care intervin în algoritm (complexitatea spaţiu);
din punctul de vedere al timpului de execuţie (complexitate timp).
Complexitatea spaţiu depinde mult de tipurile de date şi de structurile de date folosite.
Pentru a estima complexitatea timp vom presupune că se lucrează pe un calculator „clasic”, în
sensul că o singură instrucţiune este executată la un moment dat. Astfel, timpul necesar execuţiei
programului depinde de numărul de operaţii elementare efectuate de algoritm.
Să ne amintim faptul că un algoritm efectuează trei operaţii de bază: intrare/ieşire, atribuire şi
decizie.
În general, operaţiile de intrare / ieşire sunt o constantă pentru algoritmii care rezolvă o anumită
problemă. De exemplu, dacă citim n întregi şi afişam pe cel mai mare dintre ei, indiferent de algoritmul
ales pentru implementare, se vor execută n operaţii de intrare şi una de ieşire. Adică numărul de operaţii
de intrare / ieşire este constant, indiferent de algoritm. Din acest motiv nu vom analiza aceste operaţii.
Între celelalte două operaţii (de atribuire şi de decizie) vom considera că una dintre ele este cea
de bază şi vom estima de câte ori se execută aceasta. O vom alege pe cea a carui număr de executări este
mai usor de estimat, sau pe cea care necesită mai mult timp de execuţie.
Se poate măsura complexitatea exact (cantititativ), adică numărul de operaţii elementare sau se
poate măsura aproximativ (calitativ), rezultând clasa de complexitate din care face parte algoritmul.
Măsurarea cantitativă / exactă a complexităţii. Numărul de operaţii elementare. Vom
încerca să calculam într-o funcţie f(n) numărul de operaţii elementare executate de algoritm.
Să luam ca exemplu sortarea unui vector de n elemente
....
pentru i ← 1, n-1 execută
| pentru j ← i+1, n execută
| | dacă (v[i] > v[j]) atunci
| | |aux ← v[i]
| | |v[i] ← v[j]
| | |v[j] ← aux
| | |▄
| |▄
|▄
....
Vom avea trei cazuri: cazul cel mai favorabil, cel mai defavorabil şi cazul mediu.
Aici, cazul cel mai bun este vectorul gata sortat, deci:
f1(n) = (n-1) + (n-2) + ... + 3 + 2 + 1 = (n-1)*n/2
(se efectuează numai comparaţia de (n-1)*n/2).
Cazul cel mai defavorabil este vectorul sortat invers, deci:
f2(n) = 4* [(n-1) + (n-2) + ... + 3 + 2 + 1] = 4*(n-1)*n/2 = 2(n-1)*n
(se efectuează toate cele patru operaţii de (n-1)*n/2 ori, adica 2(n-1)*n)
65
Cazul mediu se ia ca medie aritmetică a celorlalte două cazuri, adică:
f3(n) = [ (n-1)*n/2 + 4*(n-1)*n/2 ] / 2= 5(n-1)*n/4.
Asa se poate calcula complexitatea exactă. Calculele sunt destul de laborioase şi din această
cauză metoda se foloseste în acei algoritmi unde este usor de calculat.
De obicei se foloseşte complexitatea aproximativă - aproximarea asimptotică a funcţiilor de
complexitate exactă.
Aici putem estima că f1(n) = f2(n) = f3(n) = O(n2). Această notaţie (O(n2)) o vom explica în
rândurile ce urmează.
Notaţii. În practică sunt folosite diferite notaţii care sunt utile pentru analiza performantei şi a
complexităţii unui algoritm. Aceste notaţii marginesc valorile unei funcţii f date cu ajutorul unor
constante şi al altei funcţii. Există trei notaţii cunoscute în acest sens:
Notaţia O (margine superioară – upper bound)
T(n) = O(f(n)), dacă există constantele c şi n0 astfel încât T (n) cf (n) , pentru n n0 .
Notaţia Ω (margine inferioară – lower bound)
T (n) ( g(n)) , dacă există constantele c şi n0 astfel încât T ( n) cg ( n) , pentru n n0 .
Notaţia Θ (categorie constantă – same order)
T (n) (h(n)) dacă şi numai dacă T (n) (h(n)) şi T (n) (h(n)) .
Ideea acestor definiţii este de a stabili o ordine relativă între funcţii, luând în considerare rata lor
de creştere, şi nu valori în anumite puncte.
De exemplu, deşi 10.000*n este mai mare decât n2 pentru valori ale lui n<100, rata de creştere a
funcţiei n2 este mai mare, ceea ce ne permite să afirmăm că pentru valori ale lui n mari (n>100) funcţia
n2 este mai mare.
Complexitatea algoritmilor este deci o funcţie g(n) care limitează superior numărul de operaţii
necesare pentru dimensiunea n a problemei. Există două interpretări ale limitei superioare:
complexitatea în cazul cel mai defavorabil: timpul de execuţie pentru orice dimensiune dată va fi
mai mic sau egal decât limita superioară
timpul de execuţie pentu orice dimensiune dată va fi media numărului de operaţii pentru toate
instanţele posibile ale problemei.
Analiza complexitatii determina timpul în care operatiile de baza ale unui algoritm sunt executate
petru fiecare set de date de intrare.
Sunt mai multe cazuri în care se determina complexitatea unui algoritm:
1. Cazul defavorabil, W (n): în cât timp operatiile de baza sunt executate în cazul defavorabil .
2. Cazul cel mai bun, B (n): în cât timp operatiile de baza sunt executate în cazul cel mai bun.
3. Fiecare caz, T (n) : în cât timp operatiile de baza sunt executate pentru fiecare caz.
4. Cazul mediu, A (n): în cât timp operatiile de baza sunt executate în medie.
Deoarece este dificil să se estimeze o comportare statistică ce depinde de dimensiunea intrării, de
cele mai multe ori este folosită prima interpretare, cea a cazului cel mai defavorabil.
În majoritatea cazurilor, complexitatea lui f(n) este aproximată de familia sa O(g(n)), unde g(n)
este una dintre functiile:
n (complexitate liniară)
Log(n) (complexitate logaritmică)
na , cu a>=2 (complexitate polinomială)
an (complexitate exponentială)
n! (complexitate factorială)
O( g(n) ) este deci clasa de complexitate cu care se aproximează complexitatea unui algoritm.
Pentru n suficient de mare au loc inegalitaţile:
log(n) < n < n*log(n) < n2 < n3 < 2n
66
ceea ce implică
O(log(n)) < O(n) < O(n*log(n)) < O(n2) < O(n3) < O(2n)
În situaţia în care găsim mai mulţi algoritmi pentru rezolvarea unei probleme, stabilim
complexitatea fiacarui algoritm, apoi, pentru a opta pentru un algoritm optimal, vom alege dintre toţi
algoritmii pe cel cu ordinul de complexitate mai mic.
Reguli generale de estimare a complexităţii
Pentru a putea determina funcţia care modelează timpul de execuţie al unui algoritm, trebuie să
prezentăm mai întâi câteva reguli generale de calcul a complexităţii unui algoritm.
Cicluri. Timpul de execuţie al unui ciclu este cel mult timpul de execuţie al instrucţiunilor din
interiorul ciclului înmulţit cu numărul de iteraţii. De regulă se estimează ca o structură repetitivă este de
ordinul O(n).
Cicluri imbricate. Analiza se realizează din interior spre exterior. Timpul de execuţie al
instrucţiunilor din interiorul unui grup de cicluri imbricate este dat de timpul de execuţie al
instrucţiunilor înmulţit cu produsul numărului de iteraţii ale tuturor ciclurilor. Cu alte cuvinte, dacă
avem două cicluri imbricate (for în for spre exemplu) putem aproxima complexitatea la O(n2).
Structuri secvenţiale. În acest caz timpii de execuţie se adună, ceea ce înseamnă că maximul lor
contează, adică gradul lui n va fi dat de gradul cel mai mare.
Structuri decizionale. Timpul de execuţie al instrucţiunii decizionale este cel mult timpul de
execuţie al testului plus maximul dintre timpii de rulare pe ramura ATUNCI, respectiv ALTFEL.
Dacă există apeluri de funcţii, acestea trebuie analizate primele.
Exemplu de alegere a algoritmului optim
Să presupunem că avem urmatoarea problemă: Se citeşte o matrice de la tastatură cu n linii şi n
coloane. Se cere să se realizeze un program care afişează suma elementelor de pe diagonala principală.
Avem două variante de algoritmi:
ALGORITM NEEFICIENT ALGORITM EFICIENT
citeşte n citeşte n
//citire matrice //citire matrice
pentru i ← 1, n execută pentru i ← 1, n execută
| pentru j ← 1, n execută | pentru j ← 1, n execută
| | citeşte a[i][j] | | citeşte a[i][j]
| |▄ | |▄
|▄ |▄
S←0 S←0
pentru i ← 1, n execută pentru i ← 1, n execută
| pentru j ← 1, n execută | s ← s + a[i][i]
| | dacă (i = j) atunci |▄
| | |s ← s + a[i][j] scrie s
| | |▄
| |▄
|▄
scrie s
Explicarea algoritmului: Observăm că amândouă variantele citesc matricea folosind două
structuri for (pentru) imbricate. Această secvenţă de algoritm are complexitatea O(n2), după cum am
arătat mai sus.
Algoritmul din stanga (cel descris ca neeficient) parcurge apoi toată matricea şi doar în cazul în
care i=j adună elementul curent din matrice la sumă. Instrucţiunea de decizie o execută de nxn ori,
rezultând o complexitate de O(n2). Ar însemna că pentru n=100 (să luăm exemplu o valoare mică),
instrucţiunea de decizie se va execută de 100x100 ori adică de 10000 ori.
Pe de altă parte, algoritmul din dreapta, foloseşte faptul că pe diagonala principală elementele au
indicii egali astfel:
67
Se foloseşte o structură repetitivă în care se parcurge fiecare linie, adunând la sumă elementul
a[i][i].
Rezultă că această secvenţă de algoritm are complexitatea doar de O(n). Cu alte cuvinte, în cazul
lui n=100, atribuirea se execută de 100 de ori, câte o dată pentru fiecare linie.
Se observă cum un mic artificiu conduce la un algoritm mult mai eficient.
Problema 6: Calculați complexitatea următorului algoritm, ce face referire la căutarea secvențială într-
un tablou s 1 2 n . Algoritmul în pseudocod este prezentat mai jos:
Rezolvare:
1. Atunci când elementul x nu este în tablou avem cazul cel mai defavorabil. Astfel W n n 1 .
2. Atunci când elementul x este primul element din tablou avem cazul cel mai bun. Astfel B n 1.
3. T (n) nu se calculeaza deoarece operatiile de baza nu sunt executate de acelasi numar de ori
pentru toate instanțele de dimensiune n .
4. Pentru A n avem două cazuri:
Cazul 1. x este în tablou. Toate componentele tabloului au valori diferite. Rezulta ca x poate fi gasit în
1
fiecare dintre ele cu aceeasi probabilitate, . Rezulta ca:
n
BIBLIOGRAFIE
Cărți:
1. Stan Claudia, Stănică Giovanna, “Proiectarea algoritmilor. Material de predare“, 2009.
2. Cristian Georgescu, 1999. “Analiza si proiectarea sistemelor informatice“, Editura Radial, Galaţi
3. Mihaela Georgescu, 2002, “Structuri de date si baze de date“, Editura Pax Aura Mundi, Galaţi
4. Popescu T.& colectiv, 1999, “Dictionar de informatica“, Editura stiintifica si enciclopedica,
Bucureşti
5. Maxim I., 1997, Metodica predării informaticii, Universitatea Ştefan cel Mare, Suceava, curs
litografiat
68
6. Ionescu C., 1999, Metodica predării informaticii, Universitatea Babeş- Bolyai, Cluj, curs
litografiat,
7. Wirth N., 1976, Algorithms+Data Structures=Programs, Prentice Hall, Inc
8. Sorin, T., Cerchez E., Şerban M., 1999, Informatica, Varianta C++, manual pentru clasa a IX-a,
Ed L&S Infomat, Bucureşti
9. Sorin, T., Cerchez E., Şerban M., 1999, Informatica, Varianta Pascal, manual pentru clasa a IX-
a, Ed L&S Infomat, Bucureşti
10. Sorin, T., 1997, Bazele programării în C++, Ed. L&S Infomat, Bucureşti
11. Sorin, T ., 1996, Tehnici de programare, Ed. L&S Infomat
12. Hutanu, V., Sorin, T., 2006, Informatica: manual pentru clasa a XI-a, Ed L&S Soft, Bucureşti
13. Tomescu I., 1994, Bazele informaticii (Manual pentru clasa a X), Ed. Didactică şi Pedagogică
14. Stoilescu D., 1998, Manual de C/C++ pentru licee, Ed. Radial, Galaţi,
15. Pătruţ B., Miloşescu M., 1999, Informatică - manual pentru clasa a IX-a, Ed. Teora,
16. Lica D., Onea E., 1999, Informatica, manual pentru clasa a IX-a, Ed. L&S Infomat,
17. Knuth D. E., 1973, Tratat de programarea calculatoarelor, vol. I, II, III, Ed. Tehnică, Bucureşti,
18. Ivaşc C., Prună M., Mateescu E., 1997, Bazele Informaticii (Grafuri şi elemente de
combinatorică) - Caiet de laborator, Ed. Petrion
19. Ivaşc C., Prună M., 1995, Bazele informaticii, Ed. Petrion
20. Giumare C., Negreanu L., Călinoiu S., 1997, Proiectarea şi analiza algoritmilor. Algoritmi de
sortare, Ed. All
21. Cormen T., Leiserson Ch., Rivest R., 1990, Introduction to Algorithms, MIT Press.
22. Andonie R., Gârbacea I., 1995, Algoritmi fundamentali, o perspectivă C++, Ed. Libris.
23. Buneci M.R., Metode numerice. Lucrări de laborator. Ed. Academica Brâncuși, Târgu-Jiu, 2003.
Site-uri web:
24. ***. http://www.allaboutcircuits.com/vol_4/chpt_2/3.html
25. ***. http://www.wikipedia.org/.
26. ***. http://www.ecvale.com/index.php?main_page=pub_eind_info&pubs_id=290807217 .
27. ***. http://hal.archives-ouvertes.fr/docs/00/28/14/29/PDF/floating-point-article.pdf
28. *** http://profu.info/limbajul-c/
29. ***. http://www.structuri.ase.ro/
30. ***. http://corina.doit.ro/graf/
31. ***. http://en.wikipedia.org/wiki/Big_O_notation
32. ***. http://www.stud.usv.ro
33. ***. http://www.science.upm.ro/~traian/web_curs/Asm/downloads/ascii.pdf
34. ***. http://id.inf.ucv.ro/~bazavan/courses/CB1103/Sinteze%201.pdf
69