Documente Academic
Documente Profesional
Documente Cultură
CUPRINS
XI. Arbori..........................................................................................................107
2
Algoritmi şi structuri de date
3
Algoritmi şi structuri de date
2. Definirea algoritmilor
Aşa cum am subliniat mai sus, etapa de proiectare a algoritmilor este cea mai
complexă dintre cele etapele enumerate ale programării. Noţiunea de algoritm,
proprietăţile pe care trebuie să le îndeplinească acesta şi modalităţile de
reprezentare standardizată a algoritmilor fac subiectul paragrafelor următoare.
Cuvântul algoritm provine din pronunţia fonetica a numelui matematicianului
arab Al-Khwarizmi Muhammed ibs Musa (780-850), care se referă la reguli
precise pentru descrierea proceselor de calcul din aritmetică. Aceiaşi descriere se
regăseşte în lucrarea Elementele lui Euclid, cca. 300 î.Hr., când pentru prima dată
se descrie o secvenţă ordonată de reguli clare pentru descrierea rezolvării unor
probleme. De altfel, algoritmul de determinare a celui mai mare divizor comun a
două numere (algoritmul lui Euclid) este considerat ca primul algoritm din istorie.
În limbajul uzual, prin algoritm se înţelege o metodă de rezolvare a unei
probleme, alcătuită dintr-o mulţime de paşi, dispuşi într-o ordine stabilită, ale căror
parcurgere ne conduce la rezultatul dorit. Algoritmii informatici, despre care vom
discuta în acest capitol, sunt definiţi mult mai riguros, surprinzând proprietăţile
obligatorii pe care aceştia trebuie să le îndeplinească.
3. Proprietăţile algoritmilor:
6
Algoritmi şi structuri de date
7
Algoritmi şi structuri de date
8
Algoritmi şi structuri de date
9
Algoritmi şi structuri de date
7. Pseudocod
10
Algoritmi şi structuri de date
Sfârşit NUME-ALGORITM
sau SfAlgoritm
Citeşte a1,a2,…,an
Tipăreşte a1,a2,…,an
Structurile
Schema logică Pseudocod Limbaj natural
fundamentale
Structura [Execută] S1 Execută secvenţial
secvenţială ... blocurile de calcul
[Execută] Sn S1, S2,…Sn
Structuri Dacă Cond atunci Dacă condiţia este
alternative Execută S1 adevărată atunci se execută
Altfel blocul S1, altfel se execută
Execută S2 blocul S2.
SfDacă
Dacă Cond atunci Dacă condiţia este
Execută S adevărată atunci se execută
Sf Dacă blocul S1.
11
Algoritmi şi structuri de date
12
Algoritmi şi structuri de date
10. Subalgoritmi
13
Algoritmi şi structuri de date
Aceştia vor primi la intrare nu datele de intrare ale problemei ci date intermediare
ale problemei globale sau rezultate parţiale din procesele de calcul. Ieşirile
subalgoritmilor sunt date pe care algoritmul general le va prelucra ulterior.
Comunicarea dintre subalgoritm şi algoritmul “părinte” se realizează prin
intermediul parametrilor. În pseudocod sintaxa generală a unui subalgoritm este
următoarea:
Subalgoritm NumeSubalgoritm ( lista parametrii formali)
…. // operaţii asupra parametrilor din lista
Sfârşit NumeSubalgoritm
14
Algoritmi şi structuri de date
15
Algoritmi şi structuri de date
1. <literă>::=A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Z|Y|a|b|
c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|z|y|_
2. <cifră> ::= 0|1|2|3|4|5|6|7|8|9
3. <simboluriSpeciale>::=<simboluriSpecialeAfişabile>|
<simboluriSpecialeNeafişabile>
<simboluriSpecialeAfişabile>::=!|,|”|%|#|&|(|)|*|/|+|-|:|
;|<|=|>|?|[|]|\|~|{|}|||.|’|spaţiu|
<simboluriSpecialeNeafişabile>::=\n |\t |\b |\r |\f |\v |\a
B. Unităţile lexicale:
1. Identificatori
2. Cuvinte cheie (cuvinte rezervate)
3. Constante
4. Operatori
C. Unităţile sintactice:
1. Expresii
2. Instrucţiuni
16
Algoritmi şi structuri de date
Unităţile lexicale sunt elementele atomice ale unui program, formate cu ajutorul
simbolurilor precizate de alfabet.
B.1. Identificatorii: sunt secvenţe de litere şi cifre ale limbajului definit care
încep obligatoriu cu o literă sau caracterul ‘_’, având rolul de a identifica
conceptele utilizate în program. Identificatorii corespund unor nume de variabile,
nume de constante simbolice, nume de funcţii.
Exemple:
int a,b,c; // declararea a trei variabile simple de tip întreg
float var; //declararea unei variabile simple de tipul real, virgulă flotantă în
simplă precizie
char ch ; // declararea unei variabile de tip caracter
17
Algoritmi şi structuri de date
B.3. Constante
Constantele sunt mărimi ale căror valoare nu se modifică pe parcursul execuţiei
unui program. Constantele se clasifică în patru categorii:
- Constante caracter
18
Algoritmi şi structuri de date
Constante caracter
Constanta caracter este un simbol care are ataşată valoarea codului ASCII al
caracterului corespondent.. Un caracter este reprezentat în memoria calculatorului
pe un octet (8 biţi).
Există două categorii de caractere: caractere afişabile (imprimabile) şi caractere
neafişabile:
<constanta caracter>::= ‘ <caracter> ‘
<caracter>::=<caracter afişabil (ASCII)> |<caracter neafişabil>
O constantă caracter grafică (afişabilă) se specifică incluzând caracterul
între simbolurile apostrof: ‘ ‘.
Exemple:
‘A’ – codul ASCII 65
‘a’ – codul ASCII 97
‘+’ – codul ASCII 77
‘2’ – codul ASCII 50
O constantă caracter non-grafică (neafişabilă) au notaţii speciale, se specifică
utilizând caracterul backslash (\) şi apostroful (‘).
Exemple:
Întoarcerea cu un spaţiu (Backspace) ‘\b’ – codul ASCII 8
Retur car ‘\r’ – codul ASCII 13
Linie nouă ‘\n’ – codul ASCII 10
Constante numerice
Constantele numerice sunt în fapt numerele. În limbajul C se face o distincţie
între numere, în funcţie de tipul (întreg sau real), baza de numeraţie şi precizia
codificării.
Numerele întregi zecimale, exemple: -123, 5, +5.
Numerele octale întregi, încep cu cifra 0 şi sunt de forma: 0c…c, unde c
este cifră octală. Exemple: 0452 , unde 452 este numărul în baza 8
Numerele hexazecimale întregi: încep cu 0x sau 0X şi sunt de forma:
0xc…c unde c sunt cifre hexazecimale. Exemplu: 0xabbd.
Numerele flotante de tip real în simplă precizie
19
Algoritmi şi structuri de date
[{+/-}]dd…d . dd…d
Pot să conţină semnul (+ sau -) specificat la începutul scrierii urmat de
partea întreagă, punctul zecimal şi partea fracţionară.
Exemple: +23.567 , -.667 , .4567
Numerele flotante de tip real în dublă precizie
[{+/-}]dd…d . dd…d {E / e} [{+/-}] dd sau:
[{+/-}] <parte întreagă>.<parte fracţionară> {E/e} [{+/-}] <exponent>
Exemple:
0.345E2 (corespunzător valorii 0.345*102)
-0. 012e-3 (-0.012*10-3)
22.0e12 (=22*1012)
Constante simbolice
Definirea constantelor simbolice se face prin directiva de preprocesare define,
prin construcţia:
#define <NumeConstantă> <ValoareAsociată>
Ex: #define PI 3.1415
#define raza 124
B.4. Operatorii
Operatorii sunt reprezentaţi de simboluri sau secvenţe de simboluri care intră în
alcătuirea expresiilor.
În funcţie de tipul operaţiilor pe care le induc, operatorii limbajului C sunt
clasificaţi:
a. Operatori aritmetici
b. Operatori de incrementare şi decrementare
c. Operatori logici
d. Operatori relaţionali
e. Operatori de deplasare
f. Operatori la nivel de biţi
g. Operatori de asignare (atribuire)
h. Operatorul condiţional
i. Alţi operatori
După numărul operanzilor pe care îi implică, operatorii pot fi:
- unari
- binari
- ternari
Operatorii aritmetici
Operatori aritmetici unari:
<OperatoriAritmeticiUnari>::= - | +
Au rolul de a specifica semnul unei expresii. Se folosesc în construcţii de
următoarea formă:
{+/-}<expresie>
Dacă o expresie nu este precedată de semn se consideră implicit semnul +
(pozitiv).
Operatorii de incrementare-decrementare
<OperatorulDeIncrementare>::= ++
Se foloseşte în construcţii de forma:
++<expresie> (preincrementare)
<expresie>++ (postincrementare)
Semnificaţia acestuia este următoarea:
++<expresie> este echivalent cu <expresie> := <expresie>+1, unde
prin simbolul “:=“ se înţelege “i se atribuie”
<OperatorulDeDecrementare>::= --
Se foloseşte în construcţii de forma:
--<expresie> (predecrementare)
<expresie>-- (postdecrementare)
Semnificaţia acestuia este următoarea:
--<expresie> este echivalent cu <expresie> := <expresie>-1
Operatorii relaţionali:
<OperatorRelaţional>::= = = | != | < | <= | > | >=
Cu semnificaţia:
= = egal
!= diferit
<= mai mic sau egal
>= mai mare sau egal, etc.
Se folosesc în construcţii de forma:
<expresie1><OperatorRelaţional><expresie2>
O structură de forma anterioară este denumită expresie relaţională şi este
evaluată la una dintre valorile logice de adevăr True (1) sau False (0)
21
Algoritmi şi structuri de date
Operatorii logici:
<OperatorLogic> ::= && | || | !
Cu semnificaţia:
&& - operatorul AND (ŞI) conjuncţia
|| - operatorul OR (SAU) disjuncţia
! - operatorul NOT (negaţia)
Exemple:
(a<b) && (b !=c) , a || (!a)
Operatori de deplasare:
<OperatorDepalsare>::= << | >>
Sunt operatori binari utilizaţi în construcţii de forma:
<expresie1><OperatorDeplasare><expresie2>
unde: expresiile sunt evaluate în prealabil la valori întregi;
Operatorii de deplasare corespund operaţiilor de deplasare pe biţi, efectul
acestora fiind acela de a provoca deplasarea biţilor din reprezentarea binară a
întregului dat de expresia 1 la stânga (<<) sau la dreapta (>>) cu un număr de
poziţii specificat de valoarea întreagă a expresiei 2.
Regulă: Prin aceste operaţii de deplasare, biţii deplasaţi în afara limitei de
reprezentare se pierd, iar biţii eliberaţi sunt completaţi cu 0 sau 1 în funcţie de
semnul întregului respectiv: pozitiv sau negativ:
A<<B are semnificaţia de deplasare a biţilor din reprezentarea binară a lui
A cu B poziţii la stânga
A>>B are semnificaţia de deplasare a biţilor din reprezentarea binară a lui
A cu B poziţii la dreapta
22
Algoritmi şi structuri de date
Exemplu:
Fie A şi B două numere întregi ale căror reprezentări pe 16 biţi sunt:
A= 0001 0010 1010 1011 şi
B= 1101 1111 0000 0101
Atunci:
A&B= 0001 0010 0000 0001
A|B = 1101 1111 1010 1111
Operatorul condiţional:
Este un operator ternar format din două simboluri “?” şi “:” şi folosit în construcţii
de forma:
<expresie1>? <expresie2>:<expresie3>
Semnificaţie: expresiile 1,2,3 sunt evaluate şi valoarea de evaluare a expresiei
globale este valoarea expresiei 2 dacă expresia 1 este adevărată altfel valoarea
expresiei 3 dacă expresia 1 este falsă.
Exemple: (A<B) ? A : B
(A= =0) ? “egal cu zero” : “diferit de zero”
Efectul operatorului cast este conversia explicită a unei expresii la un alt tip de
dată decât cel implicit. Utilizarea operatorului se face prin construcţii: ( tip)
expresie, unde tip - reprezintă tipul de date spre care se face conversia.
Expresiile de forma var=expresie semnifică evaluarea prima dată a expresiei, şi
dacă tipul rezultatului obţinut este diferit de cel al variabilei, se realizează
conversia implicita la tipul variabilei. În conversiile explicite, operatorul cast apare
în expresii de forma: var = (tip) expresie şi prin această operaţie, variabila
primeşte valoarea la care a fost evaluată expresia convertita la tipul explicit tip.
23
Algoritmi şi structuri de date
C. Unităţile sintactice
C.1. Expresii
Expresiile sunt secvenţe de operatori şi operanzi. În funcţie de tipul operatorilor
utilizaţi, expresiile pot fi: expresii aritmetice, expresii logice, relaţionale, etc.
Operanzii care intră în alcătuirea expresiilor pot fi:
- variabile
- constante
- apel de funcţii
- alte expresii
C.2. Instrucţiuni
Instrucţiunile sunt propoziţii care respectă regulile limbajului de programare în
care sunt redactate. Instrucţiunile pot fi simple sau compuse.
C. Instrucţiunile de ramificare
- corespund structurilor alternative: decizie simplă şi decizie multiplă.
Instrucţiunea if
Sintaxa instrucţiunii:
if ( <expresie> )
<instrucţiune_1>
else
<instrucţiune_2>
24
Algoritmi şi structuri de date
sau:
if ( <expresie> )
<instrucţiune_1>
Efectul instrucţiunii:
<expresia> este evaluată şi dacă valoarea obţinută în urma evaluării este
diferită de 0 se execută instrucţiunea 1, altfel se execută instrucţiunea 2.
Cele două instrucţiuni 1 şi 2 se exclud reciproc.
Instrucţiunea IF simplă nu conţine partea else <instrucţiune 2>
Instrucţiunea IF completă conţine ambele ramuri.
Exemple:
Instrucţiunea switch
- reprezintă o generalizare a structurii alternative (if).
- se poate înlocui printr-o secvenţă de structuri alternative imbricate.
În structura instrucţiunii switch este regăsită o instrucţiune simplă de a cărei
prezenţă depinde execuţia corectă a instrucţiunii alternative switch. Aceasta
instrucţiune are sintaxa:
break;
Rolul instrucţiunii break este de a întrerupe execuţia instrucţiunii curente şi de a
trece la execuţia următoarei instrucţiuni ce urmează instrucţiunii switch.
Sintaxa generală:
switch (<expresie>)
{
case <val1> : <instrucţiune_1>; break;
case <val2> : <instrucţiune_2>; break;
…
case <valn> : <instrucţiune_n>; break;
[default : <instrucţiune_default>;] //opţional
};
Efectul instrucţiunii:
25
Algoritmi şi structuri de date
switch (numar)
{
case 1: printf(\n Luni”);break;
case 2: printf(\n Marti”); break;
case 3: printf(\n Miercuri”); break;
case 4: printf(\n Joi”); break;
case 5: printf(\n Vineri”); break;
case 6: printf(\n Sambata”); break;
case 7: printf(\n Duminica”); break;
default:printf(\n nu este numar 1,2,3,4,5,6,7”);
}
D. Instrucţiunile de ciclare
În pseudocod am evidenţiat cele trei tipuri de structuri repetitive. Acestora le
corespund trei instrucţiuni de ciclare în limbajului C:
Structura repetitivă precondiţionată -> instrucţiunea while
Structura repetitivă postcondiţionată -> instrucţiunea do … while
Structura repetitivă cu număr prefixat de repetări -> instrucţiunea for
Instrucţiunea while
Sintaxa:
while (<expresie>) //antetul instrucţiunii
<instrucţiune> //corpul instrucţiunii
Efectul instrucţiunii:
În primul pas, se evaluează expresia dintre paranteze. Dacă rezultatul evaluării
este o valoare non-nulă (adevărat) se execută instrucţiunea din corpul while şi
procesul se reia prin reevaluarea expresiei. Dacă rezultatul unei evaluări este nul, se
întrerupe execuţia instrucţiunii while, şi programul se continuă cu următoarea
instrucţiune ce urmează instrucţiunii while.
26
Algoritmi şi structuri de date
Exemplu 1:Se tipăreşte valoarea unui contor până când acesta devine mai mare
decât 10. Valoarea iniţială a contorului este 0.
i=0;
while (i<=10)
{
printf (“%d”, i);
i++;
}
Exemplu 2: Se calculează suma primelor 10 numere naturale, respectiv: 1+2+3+…
+10
i=1;s=0; //iniţializarea
while (i<=10)
{
s=s+i;
i++;
}
Instrucţiunea do…while
Sintaxa:
do
<instrucţiune> //corpul
while (<expresie>);
27
Algoritmi şi structuri de date
}
while (caracter<>’0’);
suma=0;
do
{
scanf(“%d”, &nr); //citeste numărul curent
suma=suma+nr;
} while (nr>0) ;
#include <stdio.h>
void main()
{
char t; //caracterul curent
int contor=0;
printf("Introduceti textul: ");
do
{
scanf("%c",&t); //citire caracter cu caracter textul
if ((t=='a')||(t=='e')||(t=='i')||(t=='o')||(t=='u'))
contor++;
} while (t!=10); //10 – este codul ASCII pentru Enter
printf("Numarul de vocale: %d",contor);
}
Instrucţiunea for
Sintaxa:
for ( <expresie1> ; <expresie2> ; <expresie3> )
<instrucţiune>
Unde:
- cele trei expresii din antetul instrucţiunii sunt separate prin
caracterul ;
- <instrucţiune> poate fi simplă, compusă sau poate conţine o
altă instrucţiune repetitivă for.
- <expresie1> - reprezintă expresia de iniţializare. (iniţializarea
contorului)
- <expresie2> - reprezintă o expresia ce este evaluată şi de a
cărei valori de evaluare depinde repetarea corpului
instrucţiunii.
- <expresie3> - reprezintă expresia de actualizare (actualizarea
contorului)
Efect:
28
Algoritmi şi structuri de date
fact=1;
for (i=1;i<=n;i++)
{
fact=fact*i;
}
29
Algoritmi şi structuri de date
Sir de formatare poate conţine doar text şi lista de expresii poate fi vidă, caz în
care prin apelul funcţiei printf se tipăreşte pe ecran textul specificat în acest cîmp.
Exemple:
printf(“exemplu de program!”)
printf(“\n acest text este afisat pe ecran”)
Specificatorii de format
<specificator de format>::= % [-] [m.[n]] [l] <conversie>
<conversie>::= d|o|x|X|u|s|c|f|e|E|g|G
Exemple:
printf(“valoarea intreaga a= %d”,a);
printf(“realul a= %5.2f”,a);
printf(“%d + %d = %d”,a,b,a+b);
Exemple:
scanf (“%d”, &x);
Redactarea unui program în limbajul C este liberă atâta timp cât se respectă
regulile sintactice, semnele de punctuaţie şi se folosesc construcţiile specifice în
mod corect. Programul C este constituit dintr-o secvenţă de instrucţiuni, într-o
ordine determinată, corespunzător algoritmului de rezolvare a problemei
31
Algoritmi şi structuri de date
Exemplu de program C:
#include <stdio.h> //rezolvarea ec. de gradul II
#include <math.h>
32
Algoritmi şi structuri de date
void main(void)
{
double a,b,c,x1,x2,delta;
printf("\nDati coeficientii ecuatiei de gradul II: ");
scanf("%lf,%lf,%lf",&a,&b,&c);
if (a==0)
{
if (b==0)
printf("ecuatie imposibila!");
else
{
x1=-c/b;
printf("Solutia ecuatiei de grad I: %lf",x1);
}
}
else
{ delta=b*b-4*a*c;
if (delta>=0)
{
x1=(-b-sqrt(delta))/2*a;
x2=(-b+sqrt(delta))/2*a;
printf("Solutiile ec. gr. II: %lf, %lf",x1,x2);
}
else
printf("Ecuatia de gr. II nu are solutii reale!");
}
}
Probleme:
1. Să se citească un sir de n numere întregi de la tastatură şi să se calculeze suma
acestora.
2. Să se citească n numere întregi şi să se numere câte elemente sunt pozitive.
3. Să se scrie un program C pentru afişarea relaţiei dintre două numere: < , > sau =.
4. Scrieţi un program C pentru rezolvarea ecuaţiei de gradul II
Obs. Pentru scrierea unui program care rezolvă ecuaţia de gradul 2 este necesară
cunoașterea funcţiei de extragere a radicalului: sqr(<parametru>) al cărei prototip
se află în biblioteca math.h
5. Să se scrie un program C care citeşte două numere reale şi un caracter: + , - , /
sau * şi afişează rezultatul operaţiei aritmetice corespunzătoare.
6. Să se scrie un program C care afişează toţi multiplii de k mai mici decât o
valoare dată n.
7. Să se scrie un program C care calculează şi afişează puterile lui 2 până la n,
adică 2, 22,23, …, 2n .
33
Algoritmi şi structuri de date
<tip>NumeFuncţie(<tip1><arg1>,…,<tipn><argn>)
{
<instrucţiuni de declarare de tip a variabilelor locale>
……………
<instrucţiuni>
……………
}
- Corpul funcţiei cuprinde între acolade {} conţine partea de declarare a
variabilele locale funcţiei respective şi partea de prelucrare a datelor:
secvenţă de instrucţiuni prin care se prelucrează variabilele locale sau
altele cu caracter global.
- În antetul unei funcţii este precizat tipul de dată pe care îl returnează
<tip>. Acesta poate fi void, caz în care funcţia nu returnează nimic.
- Parantezele rotunde (….) ce urmează numelui funcţiei delimitează lista
argumentelor funcţiei.
- Fiecărui argument îi este precizat tipul şi numele sub care este referit în
cadrul funcţiei curente.
Prototipul funcţiei: este format de antetul funcţiei din care poate să lipsească
numele parametrilor formali:
<tip>NumeFuncţie(<tip1>,…,<tipn>) ; //declararea unei funcţii
La definirea unei funcţii, argumentele precizate în antetul acesteia se numesc
parametrii formali. La apelul funcţiei, parametrii formali sunt înlocuiţi de
parametrii actuali:
34
Algoritmi şi structuri de date
Categorii de funcţii:
void NumeFuncţie ()
{
… //corpul functiei
}
NumeFuncţie();
#include <stdio.h>
void Cifre()
{
int i; //declaraţie de variabilă locală
for(i=0;i<=9;i++) printf(“%d”,i);
}
void main(void)
{
Cifre(); //apelul functiei Cifre
}
2. Funcţii cu tip
Dacă unei funcţii îi este precizat tipul (diferit de void) acest lucru ne spune
că funcţia returnează codului apelant o valoare de acest tip. În caz contrar, funcţia
nu returnează valoare. Tipul funcţiei reprezintă în fapt tipul de dată al valorii pe
care o returnează. Instrucţiunea return, folosită în cadrul funcţiei cu tip definite,
are sintaxa:
return <expresie>;
şi are rolul de a reveni în programul apelant ş de a returna acestuia o valoare de
tipul precizat.
Funcţia poate fi apelată printr-o instrucţiune de asignare:
ValoareReturnată= NumeFuncţie(); //membrul stâng al instrucţiunii este o
variabilă de tipul returnat de funcţie
35
Algoritmi şi structuri de date
#include <stdio.h>
int CitesteValoare()
{
int numar; //declaraţie de variabilă locală
scanf(“%d”,&numar);
return numar;
}
3. Funcţii parametrizate
Funcţiile cu parametri corespund acelor subalgoritmi care prelucrează date de
intrare reprezentând rezultate intermediare ale algoritmului general. Intrările
funcţiei sunt descrise prin lista parametrilor formali ce conţine nume generice ale
datelor prelucrate în corpul funcţiilor. Parametrii efectivi sunt transmişi la apelul
funcţiilor, aceştia înlocuind corespunzător parametrii generici specificaţi. În
limbajul C transmiterea parametrilor actuali funcţiilor apelate se face prin valoare:
înţelegând prin aceasta că valorile curente ale parametrilor actuali sunt atribuite
parametrilor generici ai funcţiilor.
Exemplu:
int minim(int a, int b)
{ return ( (a>b)?a:b) }
Fie funcţia:
tip NumeFuncţie(tip1 pf1, tip2 pf2, … , tipn pfn)
{…};
La apelul:
NumeFuncţie(pa1, pa2, … , pan);
se vor transmite prin valoare parametrii actuali şi fiecare parametru formal din
antetul funcţiei este înlocuit cu valoarea parametrului actual. Dacă parametrul
36
Algoritmi şi structuri de date
actual este o expresie, aceasta este evaluată la o valoare, ulterior este copiată în
parametrul formal corespunzător.
Observaţie: modificările aduse parametrilor formali în cadrul funcţiei
apelate nu afectează valorile parametrilor actuali. Exempul următor
evidenţiază acest aspect:
#include <stdio.h>
În multe situaţii este necesar ca efectul operaţiilor din corpul unei funcţii apelate
asupra parametrilor de intrare să fie vizibil şi în corpul apelant. În exemplul
anterior efectul dorit este acela ca variabila n după apel să păstreze rezultatul
ridicării la pătrat. Transmiterea prin valoare nu permite acest lucru. În schimb,
transmiterea prin adresă realizată prin intermediul variabilelor de tip pointer
asigură modificarea valorilor parametrilor actuali.
Pentru a înţelege mecanismul transmiterii parametrilor prin adresă în limbajul
C, vom defini în continuare noţiunea de pointer.
Pointeri
37
Algoritmi şi structuri de date
Exemplu:
#include <stdio.h>
void main(void)
{
int n=5;
printf(“ valoarea lui n inainte de apel este %d”,n);
// efectul: valoarea lui n inainte de apel este 5
putere(&n);
// efectul: valoarea lui n in functie este 25
printf(“ valoarea lui n dupa de apel este %d”,n);
//efectul: valoarea lui n dupa de apel este 25 (!s-a
modificat parametrul actual)
}
38
Algoritmi şi structuri de date
Pentru construirea funcţiei cerute este utilă transmiterea prin adresă, deoarece,
efectul interschimbării trebuie să fie vizibil şi în codul apelant. Prezentăm în
continuare două variante de implementare, prima dintre acestea foloseşte
transmiterea implicită prin valoare, fapt pentru care nu corespunde rezultatului
dorit. Cea de-a doua este varianta corectă, folosindu-ne de transmiterea
parametrilor prin adresă (prin pointeri).
//Apelul: //Apelul:
int x,y; int x,y;
x=1; x=1;
x=2; x=2;
interschimbare(x,y) interschimbare(&x,&y)
//Efectul //Efectul
Inainte de apel: Inainte de apel:
x=1 x=1
y=2 y=2
După apel: După apel:
x=1 x=2
y=2 y=1
Probleme:
39
Algoritmi şi structuri de date
RECURSIVITATE
<tip>NumeFuncţie(<tip1><arg1>,…,<tipn><argn>)
{
<instrucţiuni de declarare de tip a variabilelor locale>
……………
<instrucţiuni>
……………
NumeFuncţie(pa1, pa2, … , pan); //auto apelul funcţiei
……………
}
La fiecare apel al funcţiei recursive, pe stiva programului sunt depuse noul set
de variabile locale (parametrii). Chiar dacă variabile locale au acelaşi nume cu cele
existente înainte de apelarea funcţiei, valorile lor sunt distincte, şi nu există
conflicte de nume. Practic, ultimele variabile create sunt luate în considerare în
operaţiile conţinute de funcţie.
int factorial(int k)
{
if (k>1)
return (k*factorial(k-1));
else
return 1;
}
…
//apelul functiei
int p;
p=factorial(n);
40
Algoritmi şi structuri de date
Considerăm n=3
1. La apelul iniţial factorial(n) se transmite valoarea 3 parametrului formal k şi
se predă controlul funcţiei apelate
2. Din condiţia adevărată 3>=1 rezultă amânarea revenirii din funcţie şi un
nou apel factorial(2); la ieşirea din primul apel se va return 3*factorial(2).
3. Apelul factorial(2) va produce transmiterea valorii 2 la parametrul formal
k şi predarea controlului funcţiei apelate pentru valoarea curentă a
parametrului
4. Condiţia adevărată 2>=1 produce o nouă apelare factorial(1) cu amânarea
revenirii din apelul curent; la ieşirea din acest al doilea apel se va return
2*factorial(1) .
5. Apelul factorial(1) va produce transmiterea valorii 1 la parametrul formal
k şi predarea controlului funcţiei apelate pentru valoarea curentă a
parametrului
6. Din condiţia falsă 1>1 rezultă că la ieşirea din acest apel se va return 1 şi
funcţia nu se mai apelează.
7. Revenirea din ultimul apel este urmată de revenirile în cascadă din apelurile
precedente în ordinea inversă, ceea ce conduce la rezultatul 3*2*1 = 6.
factorial(n) n* factorial(n-1)
n* (n-1)*factorial(n-2)
n* (n-1)* (n-2)*factorial(n-3)
...
n* (n-1)* (n-2)*...*factorial(1)
Problema 2: Se cere determinarea celui de-al n-lea termen din şirul lui
Fibonacci.
Analiza problemei:
Se cunoaşte că:
- primii doi termeni ai şirului sunt 0,1:
o Fibonacci(0)=0
o Fibonacci(1)=1
- oricare termen este format prin însumarea celor doi termeni
precedenţi:
41
Algoritmi şi structuri de date
o Fibonacci(k)=Fibonacci(k-1)+ Fibonacci(k-2)
Funcţia recursivă este:
int Fibonacci(int k)
{
if (k<=1)
return k;
else
return Fibonacci(k-2) + Fibonacci(k-1); //două
autoapeluri
}
42
Algoritmi şi structuri de date
Exemplu:
int T[100]; //tablou de 100 intregi
Numele tabloului, T - reprezintă adresa primului element al sau, respectiv,
adresa lui T[0].
43
Algoritmi şi structuri de date
Între tablouri şi variabilele pointer există o strânsă legătură: numele unui tablou
este un pointer, fiind de fapt adresa primului element al tabloului respectiv:
tab
Exemplu:
int tab[100];
//tab – este adresa elementului t[0]
Observaţie: Deoarece memoria alocată unui tablou este o zonă contiguă, respectiv,
elementele tabloului sunt grupate, cunoscând adresa primului element, printr-o
operaţie elementară se poate accesa oricare alt element al tabloului.
Operaţii cu pointeri:
44
Algoritmi şi structuri de date
Exemplu:
int tab[100]; //tablou de 100 întregi
int i;
(tab) – adresa primului element tab[0]
(tab+1) – adresa celui de-al doilea element tab[1]
(tab+i) – reprezintă adresa celui de-al (i-1) -lea element al tabloului, respectiv este
adresa elementului tab[i]
*(tab+i) – reprezintă valoarea elementului tab[i]
Şiruri de caractere
45
Algoritmi şi structuri de date
46
Algoritmi şi structuri de date
strcpy(a,b);
strcpy(b,aux);
}
Concatenarea a două şiruri de caractere
Se realizează prin apelul funcţiei strcat:
char * strcat(char *destinatie,const char *sursa)
Apelul funcţiei are ca efect copierea şirului de la adresa sursa în zona de
memorie imediat următoare şirului de la adresa destinatie. La revenire, funcţia
returnează adresa destinaţie.
#include <stdio.h>
#include <string.h>
void main()
{
char pass[20],cuv[20];
int i,n;
pass[0]=NULL;
printf("dati cuv ");scanf("%s",&cuv);
i=(strlen(cuv)-1);
if (i%2) i--;
n=0;
while (i>=0)
{
pass[n]=cuv[i];
i-=2;
n++;
}
pass[n]=NULL;
printf("parola este %s",pass);
}
47
Algoritmi şi structuri de date
1. Parcurgerea tablourilor
48
Algoritmi şi structuri de date
2. Căutarea secvenţială
- operaţia de căutare presupune parcurgerea element cu element a vectorului, în
ordinea dată de indecşi
//Căutarea : determinarea poziţiei pe care se află valoarea căutată
int cauta_cheie(int t[], int n, int cheie)
{
int i;
for(i=0;i<n;i++)
if (t[i]==cheie)
return i; //poziţia valorii căutate
return -1;//dacă nu s-a găsit valoarea se întoarce -1
}
49
Algoritmi şi structuri de date
50
Algoritmi şi structuri de date
z[k]=y[j]
k=k+1 //trecere la următoarea poziţie în vectorul Z
j=j+1//trecere la următoarea poziţie în vectorul Y
SfDacă
SfCâttimp
Dacă i<n atunci //au mai rămas elemente în vectorul X
Pentru w de la i la n //se copiază elementele rămase în X
z[k]=x[w]
k=k+1
SfPentru
SfDacă
Dacă j<m atunci //au mai rămas elemente în vectorul Y
Pentru w de la j la m //se copiază elementele rămase în Y
z[k]=y[w]
k=k+1
SfPentru
SfDacă
p=k-1 // sau p=n+m
Tipăreşte z[1],z[2],...,z[p]
SfAlgoritm //Interclasare
TABLOURI n-DIMENSIONALE
52
Algoritmi şi structuri de date
int mat[10][10];
int i,j;
printf(“\nIntroduceti nr. de linii ”);scanf(“%d”,&m);
printf(“\nIntroduceti nr. de coloane ”);scanf(“%d”,&n);
#include <stdio.h>
#include <conio.h>
#define DimMax 10 //numărul maxim de elemente
//pe linii si coloane
54
Algoritmi şi structuri de date
c[i][j]=s;
}
}
void main()
{
int n,m,p;
int a[DimMax][DimMax];
int b[DimMax][DimMax];
int c[DimMax][DimMax];
citire_matrice(&n,&m,a);
citire_matrice(&m,&p,b);
produs(n,m,p,a,b,c);
afisare_matrice(n,p,c);
suma(n,m,a,b,c);
afisare_matrice(n,m,c);
}
ALGORITMI DE SORTARE
55
Algoritmi şi structuri de date
Exemplu:
/*program care citeste un vector de numere intregi,
ordoneaza elementele vectorului prin metoda sortarii prin selectie
si tipareste vectorul ordonat */
#include <stdio.h>
void main()
{
int a[100],n;
citireSir(a,&n);
SSort(a,n);
tiparireSir(a,n);
}
56
Algoritmi şi structuri de date
57
Algoritmi şi structuri de date
k--;
}
// k+1 reprezinta pozitia pe care se insereaza aux
x[k+1]=aux;
}
}
58
Algoritmi şi structuri de date
x[i]=x[i+1];
x[i+1]=aux;
cod=1;
//am gasit vecini care nu respecta
//relatia de ordine
}
}
}while (cod!=0);
}
59
Algoritmi şi structuri de date
unde:
<instructiune de declarare de tip>::= <tip> <lista identificatori>
Exemple:
60
Algoritmi şi structuri de date
struct DataCalendaristică
{
int zi;
int luna;
int an;
} DataNasterii, DataAngajarii;
struct {
int zi;
int luna;
int an;
} DataNasterii, DataAngajarii;
struct DataCalendaristică{
int zi;
int luna;
int an;
}
…
struct DataCalendaristică DataNasterii, DataAngajarii;
struct NumeStructură
{
<instrucţiuni de declarare de tip>
}
struct NumeStructură TablouDeStructuri[N];
61
Algoritmi şi structuri de date
struct DatăCalendaristică SirDate[100];
Pointeri la structuri
62
Algoritmi şi structuri de date
În foarte multe situaţii este nevoie ca utilizatorul să-şi definească noi tipuri de
date pe care le prelucrează în program. Introducerea unui nou tip de date utilizator
se face prin construcţiile struct. Noul tip de date este referit prin: struct
NumeStructură iar datele se vor declara prin instrucţiunile de declarare:
struct NumeStructură data1, data2, …datan;
Pentru a utiliza în mod mai flexibil numele noului tip de date introdus,
respectiv, de a renunţa la cuvântul cheie struct în declaraţii de date se va utiliza
declaraţia de tip.
Limbajul C permite atribuirea unui nume pentru un tip predefinit sau pentru nou
tip definit de utilizator prin construcţia:
typedef TIP NumeTip;
În continuare cuvântul NumeTip poate fi utilizat în declaraţii de date ca şi
cuvintele cheie (rezervate limbajului C) care denumesc tipurile (int, float, etc.)
Exemplu: Redenumirea tipului int al limbajului C:
typedef int INTREG;
Declararea unei variabile X de tipul int se poate face astfel:
INTREG X; //identică cu declaraţia: int X;
63
Algoritmi şi structuri de date
#include <stdio.h>
#include <string.h>
typedef struct{
int zi;
int luna;
int an;
}TData;
typedef struct {
char cnp[13];
char nume[20];
char prenume[40];
TData datanasterii;
char adresa[40];
}TPersoana;
void tiparirePers(TPersoana p)
{
printf("\n%s %s %s %d.%d.%d ", p.cnp,
p.nume,p.prenume,p.datanasterii.zi,p.datanasterii.luna,
p.datanasterii.an);
64
Algoritmi şi structuri de date
}
void main()
{
TPersoana per[20]; //tablou de structuri
int nr;
printf("\n dati numarul de persoana:");
scanf("%d",&nr);
for(int i=0;i<nr;i++)
citirePers(&per[i]);
sortNUME(per,nr);
for(i=0;i<nr;i++)
tiparirePers(per[i]);
}
65
Algoritmi şi structuri de date
66
Algoritmi şi structuri de date
Fiecare operaţie din cele enumerate va fi definită prin intermediul unei funcţii C
corespunzătoare în modulul prezentat mai jos.
TAD RATIONAL
#include <stdio.h>
typedef struct{
int m;
int n;
}Rational;
67
Algoritmi şi structuri de date
a->m = m_nou;
}
int getm(Rational a)
{ return a.m;
}
int getn(Rational a)
{ return a.n;
}
void print(Rational a)
{ printf("\n %d/%d ", a.m, a.n);
}
68
Algoritmi şi structuri de date
reduce(c);
}
Rational invers(Rational a)
{ Rational aux;
aux.m=a.n;
aux.n=a.m;
return aux;
}
69
Algoritmi şi structuri de date
1. Creare fişier
2. Deschidere fişier
3. Citire din fişier
4. Adaugare – scriere în fişier
5. Actualizare fişier
6. Poziţionare în fişier
7. Ştergere fişier
Pentru a putea prelucra un fişier la nivel inferior acesta trebuie să fie creat în
prealabil.
1. Crearea unui fişier nou se realizează prin apelul funcţiei creat care are
prototipul:
int creat (const char* cale, int mod );
Unde:
- cale – este un pointer spre un şir de caractere prin care este specificat
numele complet al fişierului (calea)
- mod – un număr întreg prin care se specifică modul de acces al
proprietarului la fişier. Acest mod poate fi definit folosind constante
simbolice:
S_IREAD – proprietarul poate citi fisierul
S_IWRITE – proprietarul poate scrie fisierul
S_IEXE – proprietarul poate executa programul conţinut
de fişierul respectiv
Utilizarea acestei funcţii implică includerea fişierelor: io.h şi stat.h.
70
Algoritmi şi structuri de date
Observaţii:
Dacă nu este specificată complet calea fişierului, se consideră implicit calea
curentă.
Calea completă a unui fişier este specificată printr-un şir de caractere de forma:
Litera:\Dir1 \ Dir2 …. \Dir
Unde:
- Litera poate fi A,B,C … reprezintă litera de identificare a discului (ex:
C – harddisk, A – floppy disk)
- Dir1, DIr2, … sunt nume de subdirectoare
Dacă specificăm calea fişierului în apelul funcţiei open, toate caracterele \
trebuie dublate
Exemplu:
int df;
df = open (“C:\\Borlandc\\bin\\text.cpp”, O_RDWR);
71
Algoritmi şi structuri de date
Funcţia read returnează un întreg reprezentând numărul de octeţi citiţi din fişier
sau: –1 în caz de eroare, 0 la terminarea fişierului
Efectul: Prin apelul funcţiei read se citeşte din fişier înregistrarea curentă; la un
apel următor se va citi următoarea înregistrare, ş.a.m.d până la sfârşitul fişierului.
Exemplu: citirea din fişier caracter cu caracter până la terminarea fişierului şi
afişarea pe monitor a conţinutul acestuia:
int df, i;
char c;
df=open(“proba.txt”,O_RDONLY);
do{
i=read(df, c, sizeof(char) );
//sizeof(char) returnează numărul de octeţi
//necesari tipului specificat între paranteze.
printf(“%c”, c);
}while (i>0);
4. Scrierea în fişier
Funcţia write , prototipul acesteia se află în fişierul io.h:
int write(int df, void *mem, unsigned lung);
unde:
- df – descriptorul ataşat fişierului (returnat de funcţia open)
- mem – pointer spre zona de memorie din care se preia informaţia ce se va
scrie în fişier
- lung – lungimea înregistrării exprimată în număr de octeţi
facă într-o ordine diferită decât cea implicită se poate utiliza funcţia lseek pentru
poziţionarea în fişier:
long lseek(int df, long depl, int orig);
unde:
- df – descriptorul de fişier
- depl – deplasamentul exprimat în număr de octeţi
- orig – este un număr întreg ale cărui valori semnifică:
0 – deplasamentul se consideră de la inceputul fişierului
1 – deplasamentul se consideră din poziţia curentă
6. Închiderea fişierului
Funcţia close având prototipul:
int close (int df); //df- descriptorul de fişier
Utilizarea funcţiei close implică includerea fişierului bibliotecă io.h
Returnează:
0 – la închiderea normală a fişierului
-1 – în caz de eroare
73
Algoritmi şi structuri de date
74
Algoritmi şi structuri de date
ptr = fopen("c:\\test.txt","r");
75
Algoritmi şi structuri de date
Operaţiile de inserare sau ştergere a unei înregistrări din fişier sunt operaţii
complexe realizate în mai multe etape. Se folosesc funcţiile de bibliotecă standard
enumerate (deschidere fişier, creare fişier, scriere, citire, etc.) în implementarea
operaţiilor de inserare-ştergere, însă nu există funcţii standard corespunzătoare
celor două operaţii ca atare.
O variantă de inserare sau ştergere a unei înregistrări este aceea prin care se
renunţă la folosirea unui fişier auxiliar şi se construieşte o structură liniară de tip
tablou în care sunt salvate temporar înregistrările fişierului iniţial.
Copierea conţinutului unui fişier (fis1.dat) într-un alt fişier (fis2.dat). Etape:
Creează fișierul fis2.dat
76
Algoritmi şi structuri de date
Probleme:
1. Să se scrie un program C care citeşte de la tastatură un vector de
structuri Punct3D şi le salvează într-un fişier puncte.dat.
2. Să se scrie un program C care citeşte din fişierul puncte.dat un
vector de structuri Punct3D şi afişează coordonatele punctelor pe
ecran.
3. Scrieţi un program care gestionează într-un fişier structuri de tipul
Persoana (CNP, nume, prenume, data nasterii, etc.). Operaţiile
evidenţiate sunt cele de adăugare înregistrare, ştergere înregistrare,
căutare persoană după valoarea câmpului CNP, ordonarea
alfabetică a persoanelor memorate în fişier.
77
Algoritmi şi structuri de date
Una dintre cele mai costisitoare resurse ale unui program este este memoria
utilizată. Programarea este o activitate în care, în afara corectitudinii programelor
elaborate, se urmăreşte şi eficienţa acestora, măsurată prin timpul de execuţie şi
memoria utilizată. În scopul economiei resurselor, limbajele de programare oferă
programatorului instrumentele necesare unei bune gestionări a memoriei.
Alocarea memoriei pentru variabilele locale declarate într-o funcţie se face în
etapa de execuţie a programului, pe stivă, şi nu este permanentă. La ieşirea din
funcţie, stiva se curăţă, fapt pentru care datele alocate sunt distruse. Acest
mecanism de alocare este eficient şi se denumeşte alocare dinamică. În schimb,
datele declarate global au un comportament diferit. Pentru variabilele globale,
memoria este alocată în faza premergătoare execuţiei şi zona de memorie
respectivă rămâne alocată acestora până la terminarea programului. Spre exemplu,
declararea globală a unui tablou unidimensional de lungime maximă 2000
(<TipElement> tablou[2000]; ), face ca o zonă de memorie considerabilă să fie
blocată pentru această structură, indiferent de numărul elementelor folosite efectiv,
număr care în multe cazuri poate fi cu mult mai mic decât dimensiunea maximă
declarată. În astfel de situaţii este preferabilă o manieră de alocare dinamică în
vederea unei gestionări economice a memoriei. Alocarea dinamică a memoriei
poate fi simplificat definită prin totalitatea mecanismelor prin care datelor le sunt
asignate zone specifice de memorie în faza de execuţie a programelor.
Dezavantajele alocării dinamice a memoriei constau în:
1. sarcina suplimentară a programatorului de a asigura şi eliberarea
memoriei când nu mai are nevoie de datele respective
2. efectul fragmentării memoriei care poate produce imposibilitatea
alocării ulterioare a unei zone de memorie de dimensiune dorită.
Limbajul C/C++ oferă programatorului funcţii standard prin care se realizează
alocarea, respectiv, dealocarea (eliberarea) memoriei. Funcţiile malloc şi free au
prototipurile în fişierul antet: alloc.h şi gestionează o zonă de memorie specială,
denumită memoria heap .
Funcţia malloc:
void *malloc(size_t dimensiune)
Argumentul funcţiei este dimensiunea exprimată în octeţi, semnificând
dimensiunea zonei alocate. Funcţia returnează în caz de succes adresa de memorie
a zonei alocate. Este posibil ca cererea de alocare să nu poată fi satisfăcută dacă nu
mai există suficientă memorie în zona de heap sau nu există o zonă compactă de
dimensiune cel puţin egală cu dimensiune. În caz de insucces, funcţia malloc
returnează NULL.
Funcţia calloc:
void *calloc(size_t nrElemente, size_t dimensiune)
Efectul funcţiei: se alocă un bloc de memorie de mărime nrElemente *
dimensiune (această zonă nu trebuie să depăşească 64 Ko octeţi din heap) şi
conţinutul blocului alocat este şters. În caz de succes, funcţia returnează adresa
78
Algoritmi şi structuri de date
Exemplu:
tip *p;
p= (tip*)malloc(sizeof(tip)) ;
free(p);
Utilizarea celor două funcţii standard malloc şi free devine utilă şi în programe
în care se vor manipula tablouri de elemente: şiruri de caractere, vectori de date
simple, vectori de structuri, etc.
Programele următoare sunt exemple simple de gestionare eficientă a memoriei
prin alocarea dinamică a structurilor de date:
#include <stdio.h>
#include <stdlib.h>
79
Algoritmi şi structuri de date
{
printf("vector[%d]=", i+1);
scanf("%d", &vector[i]);// sau scanf("%d", vector+i);
}
return vector; //se returneaza adresa vectorului
}
void main(void)
{
int n;
int *vector;
vector=citire_vector(&n);
afisare_vector(vector, n);
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main(void)
{
char sir1[10];
char *sir2;
printf("introduceti un sir de caractere: \n");
scanf("%9s", sir1);
// se retin primele 10 caractere introduse
int dimCh=sizeof(char);
if ((sir2=(char *)malloc( (strlen(sir1)+1)* dimCh))==NULL)
{
printf("Insucces la alocarea dinamica");
exit(1);
}
strcpy(sir2, sir1);
printf("Sirul sursa: %s \n", sir1);
printf("Sirul destinatie: %s \n", sir2);
}
80
Algoritmi şi structuri de date
81
Algoritmi şi structuri de date
O structură care descrie compunerea unui element de acest gen este următoarea:
struct nod
{
//declaraţii de date – câmpuri ale informaţiei
struct nod *adr; //adresa următorului nod
}
Convenim că informaţia din noduri conţine un câmp special (cheie) ale cărui
valori sunt distincte pentru elementele aceleiaşi liste (cheie nu este un câmp
obligatoriu). Pentru a simplifica exemplele următoare, vom introduce un nou tip de
dată, tipul nodurilor, denumit TNod.
typedef struct nod
{
int cheie; //câmp cu valori unice pentru nodurile listei
//alte câmpuri
struct nod *urm; //adresa următorului nod
}Tnod;
Gestionarea unei liste de astfel de noduri necesită cunaşterea adresei primului şi
eventual al ultimului nod din listă. Reţinându-se doar adresa primului nod, celelalte
noduri pot fi parcurse, accesate, prin urmărirea legăturilor urm conţinute în
nodurile curente.
Adresa fiecărui nod este conţinută de nodul precedent, cu excepţia primului nod
al listei, pentru care nu există un nod precedent. Ultimul nod nu va referi niciun alt
nod următor, fapt care se marchează prin pointerul urm care devine Null. Figura
următoare sugerează organizarea unei liste simplu înlănţuite:
info adr info adr info adr info adr info adr
……. …….
NULL
prim ultim
Parcurgerea listei
82
Algoritmi şi structuri de date
void tiparire_lista()
{
tnod *p; //p semnifică nodul curent
p=prim; //se porneşte traversarea listei de la primul nod
while(p!=0) //câttimp nu s-a ajuns la sfîrşitul listei
{
printf("\n%d ",p->cheie);
// afişarea celorlalte câmpuri din nodul curent
p=p->urm; //trecere la următorul element
}
}
83
Algoritmi şi structuri de date
…………….
1
prim
nou
void adaugare_prim()
{
tnod *p;
p=incarca_nod();
if (prim= =0) //lista vida
{prim=p;
ultim=p;
ultim->urm=0;
return;
}
p->urm=prim;
prim=p;
}
84
Algoritmi şi structuri de date
1
ultim 2
nou
void adaugare_ultim()
{ tnod *nou; nou=incarca_nod();
if (prim==0) //lista vida
{prim=nou;
ultim=nou;
ultim->urm=0;
return;
}
ultim->urm=nou;
ultim=nou; ultim->urm=0;
}
85
Algoritmi şi structuri de date
…………….
prev 2 curent 1
nou
Funcţia următoare descrie operaţia de inserare înaintea unui nod căutat după
valoarea cheii:
void adaugare_inainte()
{
int valoare;
printf("\ndati cheia de cautat:");scanf("%d",&valoare);
tnod *p,*prec, *nou;
p=prim; //iniţializare nodul curent cu prim
while (p!=0)
{
if (p->numar!=valoare)
{//NU s-a gasit încă nodul
prec=p; //salvează adresa precedentului în prev
p=p->urm; //trece la următorul element
}
else
{ //s-a găsit nodul de cheie dată
if (p==prim)
{//caz particular, nodul căutat este primul
adaugare_prim();return;
}
else
{
nou=incarca_nod();
nou->urm=p; //reface legăturile
prec->urm=nou;
return;
}
}
}
}//sfârşit funcţie
86
Algoritmi şi structuri de date
…………….
p 2 p->urm 1
nou
Considerând nodul de cheie dată găsit: p, etapele inserării noului nod sunt:
1. stabileşte adresa urm a nodului nou ca fiind adresa nodul următor al lui p:
nou->urm=p->urm (1)
2. stabile;te adresa urm a lui p ca fiind adresa lui nou: p->urm=nou. Prin
această asignare s-a pierdut automat înlănţuirea veche între p şi p->urm
void adaugare_dupa()
{
int valoare; printf("\ndati cheia de
cautat:");scanf("%d",&valoare);
tnod *p,*nou;
p=prim;
while (p!=0)
{
if (p->numar!=valoare)
{//NU am gasit
p=p->urm;
}
else
{ //caz particular
if (p==ultim)
{
adaugare_ultim();return;
}
else
{
//alocare memorie şi încărcare nod cu inf.
nou=incarca_nod();
nou->urm=p->urm; //stabilirea legăturilor
p->urm=nou;
return;
}
}
}
}
87
Algoritmi şi structuri de date
void stergere_prim()
{
tnod *primvechi;
primvechi=prim; //salvare adresa primului element
if (primvechi==0)
{//lista este vidă, nu se va şterge nimic
printf("\nlista este vida");
return;
}
prim=prim->urm; //actualizare prim
free(primvechi);//eliberarea memoriei adresata de prim
}
88
Algoritmi şi structuri de date
p=p->urm; //trecere la următorul nod
}
//în acest punct prec este adresa penultimului nod
ultimvechi=p; //salvare vechiul nod ultim
ultim=prec; ultim->urm=0; //actualizare şi marcare ultim
free (ultimvechi); //eliberare memorie
}
prev 1
void stergere_oarecare()
{
int valoare; printf("\ndati cheia de
cautat:");scanf("%d",&valoare);
tnod *p,*prev,*pvechi;
p=prim;
while(p!=0)
{
if (p->cheie==valoare)
{//s-a găsit nodul şi acesta va fi şters
if (p==prim)
{
stergere_prim();
return;
} //caz particular
if (p==ultim)
{
stergere_ultim();
return;
} //caz particular
//cazul general
pvechi=p; //salvare adresă nod curent
prev->urm=p->urm; //refacere legături
free(pvechi); //eliberare memorie
return;
89
Algoritmi şi structuri de date
}
else
{//nu s-a găsit încă
prev=p; //salvarea adresei precedentului
p=p->urm; //trecere la următorul nod
}
}
}
4. Ştergerea listei
Ştergerea completă a listei se poate realiza prin apelul repetat al funcţiilor deja
construit de ştergere a primului, respectiv, a ultimului nod până când lista devine
vidă (pointer-ul prim devine Null). Din punct de vedere al eficienţei, variante
ştergerii listei prin apelul funcţiei ştergere_prim este preferată deoarece nu necesită
traversarea listei pentru fiecare operaţie de ştergere a unui nod.
O variantă simplă dar greşită de ştergere a listei constă în distrugerea capetelor
listei prim şi ultim, fapt pentru care gestionarea ulterioară a listei ( accesarea şi
prelucrarea nodurilor sale ) devine imposibilă. Cu toate acestea, memoria rămâne
alocată nodurilor intermediare. Prin aceasta s-a distrus doar mecanismul de
accesare a nodurilor, nu s-au distrus efectiv nodurile listei.
Funcţia următoare este o variantă de ştergere a listei, prin ştergerea repetată a
nodurilor din capătul listei.
void stergere_lista()
{
tnod*p,*primvechi;
p=prim;
while(p!=0) //cât timp mai sunt noduri în listă
{
if (prim==0)
{
printf("\nlista e complet stearsa");
return;
}
else
{ //sterg primul nod şi actualizez prim
primvechi=prim;
prim=prim->urm;
free(primvechi);
}
}
}
90
Algoritmi şi structuri de date
……. …….
prim ultim
91
Algoritmi şi structuri de date
92
Algoritmi şi structuri de date
} }
p=prim; //iniţializare adresă nod curent p=ultim; //iniţializare adresă nod curent
while(p!=0) while(p!=0)
{ {
printf("\n %d",p->cheie); printf("\n %d",p->cheie);
p=p->urm; //trece la următorul nod p=p->prec; //trece la precedentul nod
} }
} }
…………….
prim
3
4
nou 2
Observaţie: În cazul în care lista este înaintea adăugării unui nod nou, efectul
operaţiei constă în obţinerea unei liste cu un singur element, fapt pentru care
capetelor listei prim şi ultim sunt confundate şi este necesară tratarea acestui caz
particular.
void adaugare_prim()
{
tnod *nou; p=incarca_nod();
if (prim==0)
{
prim=nou;ultim=nou;
prim->prec=0;ultim->urm=0;
}
else
{
nou->urm=prim; //pasul 1
93
Algoritmi şi structuri de date
prim->prec=nou; //pasul 2
prim=nou; //pasul 3
prim->prec=0; //pasul 4
}
}
…………….
4
ultim
2
3 nou
void adaugare_ultim()
{
tnod *nou;nou=incarca_nod();
if (prim==0)
{
prim=nou;ultim=nou;
prim->prec=0;ultim->urm=0;
}
else
{
nou->prec=ultim; //(1)
ultim->urm=nou; //(2)
ultim=nou; //(3)
ultim->urm=0; //(4)
}
}
94
Algoritmi şi structuri de date
3 4
p->prec p
2 1
nou
95
Algoritmi şi structuri de date
adaugare_prim();
return;
}
}
}
}//sfarsit functie
96
Algoritmi şi structuri de date
nou->urm=p->urm;
p->urm=nou;
nou->pre=p;
(p->urm)->pre=nou;
return;
}
}
}
}
97
Algoritmi şi structuri de date
- căutarea nodului
- ştergerea propriu-zisă a nodului
Căutarea nodului se face prin parcurgerea într-un sens a listei şi compararea
valorii cheii nodurilor curente cu valoarea dată. Dacă se găseşte un nod care
verifică condiţie, se va opera etapa de ştergere propriu-zisă a nodului prin:
o salvarea adresei nodului de şters
o refacerea legăturilor pentru a asigura consistenţa listei şi
posibilitatea parcurgerii ulterioare în ambele sensuri
o eliberarea memoriei alocate nodului de şters
1
p->prec p->urm
Cazurile particulare se tratează separat: lista este deja vidă sau lista devine vidă
după ştergerea nodului.
98
Algoritmi şi structuri de date
free(pvechi); //eliberare memoriei- adresa pvechi
return;
}
else //nu s-a gasit încă
p=p->urm; //trecere la următorul nod
}
}
Ştergerea listei
Ştergerea completă listei dublu înlănţuită se poate face cu acelaşi efort de calcul
prin apelarea repetată a funcţiei stergere_prim sau stergere_ultim. Ştergerea
capetelor listei nu asigură eliberarea memoriei ocupate de nodurile intermediare.
99
Algoritmi şi structuri de date
Lista circulară este o listă (simplu sau dublu) înlănţuită cu proprietatea că toate
nodurile sunt echivalente, respectiv, nu există noduri speciale care nu conţin adresa
nodurilor succesoare sau predecesoare. Aceste noduri speciale - denumite capetele
listei au fost utilizate în gestionarea listelor simplu şi dublu înlănţuite. O listă
circulară va fi gestionată prin alte mecanisme decât cele bazate pe menţinerea
adreselor speciale prim şi ultim.
Într-o listă circulară simplu înlănţuită toate nodurile conţin adresa următorului
nod. Structura nodului este similară celei prezentate la capitolul dedicat listelor
simplu înlănţuite:
typedef struct nod
{
int cheie; //câmp cu valori unice pentru nodurile listei
//alte câmpuri
struct nod *urm; //adresa următorului nod
}Tnod;
Organizarea unei liste circulare cu noduri de acest tip este sugerată de figura
alăturată.
Orice listă simplu înlănţuită gestionată prin pointer-ii prim şi ultim se poate
transforma în listă circulară printr-o operaţie elementară de asignare:
ultim->urm=prim
Prin operaţia anterioară s-a stabilit faptul că ultimul nod al listei iniţiale va
conţine adresa primului nod al listei, ceea ce conduce la o structură de listă
circulară a cărei gestionare poate fi efectuată prin adresa pointer-ului prim, însă
fără ca acesta să semnifice adresa unui capăt al listei, ci doar adresa unui nod
oarecare.
Spre deosebire de listele simplu înlănţuite la care este suficientă cunoaşterea
adresei primului nod şi, eventual, pentru simplificarea prelucrărilor, şi a adresei
ultimului nod, într-o listă circulară, cunoaşterea adresei oricărui nod din
compunerea listei este suficientă pentru a putea gestiona această structură. Astfel,
gestionarea unei liste circulare se face prin unui pointer care referă oricare nod al
listei:
Tnod *pLC; //pointer la lista circulară
100
Algoritmi şi structuri de date
pLC
.............
Funcţia următoare adaugă noi noduri la lista circulară gestionată prin pLC în
varianta 2.
void creare_LCSI()
{
Tnod *nou; int rasp;
pLC=0; //lista este initial vida
printf("\nIntroduceti? (1/0)");scanf("%d",&rasp);
while (rasp==1)
{
nou=incarca_nod();//alocare memorie şi încărcare nod
if (pLC==0)
101
Algoritmi şi structuri de date
{//creare primul nod
// nodul pLC contine adresa sa;
pLC=nou;
//lista devine circulara
pLC->urm=pLC;
}
else
{ //adaugare nou dupa pLC
nou->urm=pLC->urm;
pLC->urm=nou;
pLC=nou; //pLC va contine adresa noului nod
}
printf("\nIntroduceti? (1/0)");scanf("%d",&rasp);
}
}
p=pLC;
do
{
//prelucrare nod referit de p
p=p->urm; //trecere la urmatorul nod
}while (p!=pLC);
void Tiparire()
{
tnod *p; //pointer-ul auxiliar
p=pLC; //iniţializare p
if (p==0) //lista este vida
return; // nu are sens continuarea parcurgerii
do
{
printf(“\n %d”,p->cheie);
p=p->urm; //trecere la urmatorul nod
}while (p!=pLC);
}
102
Algoritmi şi structuri de date
1. Inserarea unui nou nod înaintea unui nod specificat prin valoarea cheii
presupune parcurgerea următoarelor etape:
- căutarea nodului de cheie dată
- inserarea propriu-zisă dacă etapa anterioară s-a încheiat cu succes
Observaţie: În etapa de căutare a nodului de cheie dată se va reţine adresa
nodului precedent a nodului curent, pentru a face posibilă refacerea legăturilor în
faza de inserare. În caz contrar ar fi necesară o reparcurgere a listei circulare pentru
a determina precedentul nodului înaintea căruia va fi inserat noul nod.
Cunoscând adresa nodului de cheie dată şi adresa precedentului său, inserarea
propriu-zisă a unui nou nod se reduce la: alocarea memoriei, încărcarea nodului
nou cu informaţie şi refacerea legăturilor într-o manieră similară celei prezentate în
operaţia omonimă pentru liste simplu înlănţuite:
103
Algoritmi şi structuri de date
if (p->cheie==valoare) //s-a gasit nodul
break;
//iesire din instructuinea repetitiva
//p este adresa nodului gasit
}while(p!=pLC);
if (p->cheie!=valoare) //cautarea s-a incheiat cu Insucces
return;
if (p->cheie==valoare) //cautarea s-a incheiat cu Succes
{ //etapa de inserare
nou=incarca_nod(); //alocare memorie si incarcare nou
nou->urm=p;
prev->urm=nou;
}
}
Observaţii:
- nodul referit de pLC este ultimul nod verificat în etapa de căutare
- instrucţiunea decizională if (p->cheie==valoare) … este redundantă, dat
fiind faptul că o condiţie precedentă verificat situaţia opusă şi provoacă
revenirea din funcţie. Din motive de lizibilitate şi nu de optimizare a
codului am convenit să furnizăm o variantă explicită a funcţiei pentru o
urmărire uşoară a etapelor descrise.
104
Algoritmi şi structuri de date
//etapa de inserare
nou=incarca_nod(); //alocare memorie si incarcare nou
nou->urm=p->urm;
p->urm=nou;
}
Observaţie: nodul referit de pLC este primul nod verificat în etapa de căutare.
105
Algoritmi şi structuri de date
{
if (p==pLC) //nodul de sters este referit de pLC, cazul (c.)
{
pLC=prev; //actualizare pLC
free(p); //eliberare memorie
}
else //cazul general
{
prev->urm=p->urm; //refacere legaturi
free(p); //eliberare memorie
}
}
}//sfarsit functie steregereNod
106
Algoritmi şi structuri de date
p=pLC;
do
{
prev=p;
p=p->urm;
eliberare_nod(prev);
}while(p!=pLC)
pLC=0; //marcare listă vidă
}
Observaţie: primul nod eliberat este cel referit de pLC, fapt pentru care când
condiţia p==pLC devine adevărată se indică revenirea în punctul de plecare a
pointer-ului p ceea ce semnifică faptul că toate nodurile au fost şterse (inclusiv
nodul referit de pointer-ul special pLC) şi lista este vidă.
ultim->urm=prim;
prim->prec=ultim;
STIVE. COZI.
Stiva reprezintă un caz special de lista liniara în care intrările si ieşirile se fac la
un singur capăt al ei. Organizarea structurilor de date de tip stivă se poate face în
două maniere:
- secvenţial - elementele stivei sunt memorate la adrese
consecutive
- înlănţuit – elementele stivei nu ocupă adrese consecutive,
fiecare element conţine o legătură spre următorul element.
Prin organizarea secvenţială nu se poate face economie de memorie, fapt pentru
care în general se practică organizarea înlănţuită cu alocare dinamică a stivelor.
Structura de stivă se remarcă prin operaţiile specifice: push şi pop,
corespunzătoare adăugării unui element, respectiv, ştergerii unui element în/din
vârful stivei. Principiul de funcţionare al stivei este cel cunoscut sub denumirea de
LIFO (Last In First Out – ultimul intrat, primul ieşit).
107
Algoritmi şi structuri de date
info adr
Practic, stiva este o listă simplu înlănţuită pentru care operaţiile specifice se
limitează la următoarele:
- creare stivă vidă
- adăugare element (push)
- ştergere element (pop)
- şterge lista (clear)
- accesare – fără eliminare - a elementului din vârful stivei
În plus faţă de operaţiile enumerate anterior sunt posibile implementate operaţii
de verificare:
- verifică dacă stiva este plină
- verifică dacă stiva este goală
Gestionarea stivei se face în mod similar listei înlănţuite prin capetele prim şi
ultim. La nivel abstract, o stivă are o bază a sa şi un vârf, ceea ce convine unei
asocieri a nodurilor referite de prim şi ultimi cu cele două elemente specifice:
- baza stivei corespunde nodului prim şi vârful stivei corespunde
nodului ultim
În această abordare, operaţiile push şi pop se traduc prin operaţiile de:
- adăugare a unui nou nod după ultim (adăugare în vârful stivei)
- ştergere ultim (ştergere din vârful stivei)
Privitor la eficienţa operaţiilor descrise într-un capitol anterior, ne reamintim că
operaţia de adăugare a unui nou element după cel referit de pointer-ul ultim
necesita o parcurgere prealabilă a listei. În schimb, adăugarea unui nou nod
înaintea celui referit de prim este mai puţin costisitoare. Din aceste considerente, se
practică o inversare a rolurilor celor două capete ale stivei, pentru a obţine operaţii
mai eficiente:
- baza stivei corespunde nodului ultim şi vârful stivei
corespunde nodului prim
Astfel, operaţiile push şi pop se vor traduce prin:
- adăugare a unui nou nod înainte de prim (adăugare în vârful
stivei)
- ştergere prim (ştergere din vârful stivei)
108
Algoritmi şi structuri de date
Coada este un alt caz special de listă înlănţuită bazat pe principiul FIFO (First
In First Out – primul intrat, primul ieşit). Acest principiu arată că primul element
introdus în listă este şi primul care va fi şters. O structură de acest gen are două
capete, denumite sugestiv: cap şi coadă.
Operaţiile primare cu cozi sunt:
- creare stivă vidă
- adăugare element în coadă
- ştergere element din cap
- şterge lista (clear)
Spre deosebire de stivă, adăugarea şi ştergerea unui element se execută în
capetele diferite ale cozii.
Ca şi în cazul stivelor, organizarea unei cozi poate fi făcută în mod secvenţial
(static) – prin intermediul tablourilor unidimensionale sau dinamic – prin liste
simplu înlănţuite. Cea de-a doua variantă este de preferat din raţiuni economice.
prim ultim
Coada este astfel o listă înlănţuită ale cărei capete referite prin prim şi ultim
semnifică capul şi coada structurii, ceea ce permite organizarea în două maniere:
- prim referă capul listei şi ultim referă coada listei
- ultim referă capul listei şi prim referă coada listei
Conform celor două abordări enumerate anterior, operaţiile de adăugare şi
scoatere elemente în/din lista FIFO se traduc prin:
- adăugare după nodul ultim şi ştergere nod prim
- adăugare înainte de prim şi ştergere nod ultim
109
Algoritmi şi structuri de date
XI. ARBORI
Radacina
Terminal
Terminal Terminal
110
Algoritmi şi structuri de date
Nivelul 2
2 3
4 5 6 Nivelul 3
9 Nivelul 4
7 8
Fiecare subarbore al arborelui este caracterizat prin înălţimea sa, mărime care
reprezintă numărul de nivele pe care le conţine subarborele respectiv.
Dacă între fiii oricărui nod există o relaţie de ordine, spunem că arborele
respectiv este arbore ordonat.
111
Algoritmi şi structuri de date
Dacă numărul de fii ai fiecărui nod din compunerea unui arbore este 0,1 sau 2,
atunci arborele respectiv este numit arbore binar. Structura unui nod al arborelui
binare conţine:
- zona de informaţii
- legătură spre fiul stâng
- legătură spre fiul drept
Într-un arbore binar, există posibilitatea ca una sau ambele legături ale unui
părinte spre fiii săi să fie nule. Nodurile terminale au ambele legăturile nule.
Anumite noduri interne pot să aibă doar un fiu, astfel încât legătura spre celălalt fiu
este nulă.
Importanţa studierii arborilor binari este dată de multitudinea de aplicaţii
practice în care se face uz de această structură de date. În plus, un arbore poate fi
transformat şi reprezentat prin arbore binar. Transformarea presupune etapele
următoare:
1. se stabileşte legătură între fraţii de acelaşi părinte
2. se suprimă legăturile dinspre părinte, cu excepţia legăturii cu
primului fiu
Exemplu:
Fie arborele oarecare din figura următoare:
1
2 3 4
5 6
7
Figura 1 Arbore oarecare
112
Algoritmi şi structuri de date
5 3
7 6 4
Figura 2 Arbore binar
Parcurgerea arborilor
ARBORI BINARI
113
Algoritmi şi structuri de date
Absenţa unui fiu (stâng sau drept) se marchează prin valoarea Null (0) a
pointer-ului corespunzător.
Gestionarea unui arbore binare este posibilă prin adresa nodului rădăcină:
Tnod *rad;
114
Algoritmi şi structuri de date
Exemplu:
J
H I
E F G
A B C D
115
Algoritmi şi structuri de date
//autoapelare funcţie preordine
preordine(p->st);
//parcurgere subarbore stâng
//autoapelare funcţie preordine
preordine(p->dr);
}
}
116
Algoritmi şi structuri de date
{ // caută nodul din arbore care este echivalent cu pnod2
tnod *p;
if rad==0 return 0; //arborele este vid
while (p!=0)
{
if (verificare(p,pnod2)==0)
return p; //s-a găsit nodul
else
if (verificare(p,pnod2)==-1)
p= p->st; //continuare cautare la stanga
else
p= p->dr; //continuare cautare la stanga
}
return 0; //nu s-a gasit nodul cautat
}
Un caz particular de arbori binari sunt arborii binari de căutare. În plus faţă de
aspectele menţionate la prezentarea arborilor binari, un arbore de căutare prezintă o
caracteristică suplimentară: fiecare nod al său conţine o cheie (câmp cu valori unice
pentru nodurile arborelui) şi:
- toate nodurile din stânga nodului respectiv au valorile cheilor
mai mici decât cheia nodului curent
- toate nodurile din dreapta nodului respectiv au valorile cheilor
mai mari decât cheia nodului curent
117
Algoritmi şi structuri de date
4 8
2 6 9
1 3 5
Fiind dată o mulţime de informaţii ce trebuie organizate sub forma unui arbore
binar de căutare, crearea arborelui se realizează în următoarele etape:
-creare arbore vid
-creare arbore cu un singur nod (rădăcină)
-inserare noduri cât timp mai există informaţie de organizat
La fiecare pas de inserare unui nod în arborele deja format se va ţine cont de
criteriul de ordonare şi arborele rezultat va fi tot un arbore binar de căutare.
Dacă rădăcina este vidă înainte de inserare, nodul de inserat va deveni rădăcina
arborelui:
rad=nou; rad->st=rad->dr=0;
118
Algoritmi şi structuri de date
119
Algoritmi şi structuri de date
120
Algoritmi şi structuri de date
c. nodul căutat nu este nod terminal, dar are doar un subarbore stâng
- leagă părintele de subarborele stâng al nodului de şters:
o dacă nodul este legat în stânga părintelui, atunci
parinte->st=p->st – cazul c.1
o dacă nodul este legat în dreapta parintelui, atunci
parinte->dr = p->st – cazul c.2
- eliberează nodul
d. nodul căutat p nu este nod terminal, dar are doar un subarbore drept
- leagă părintele de subarborele drept al nodului p:
o dacă nodul este legat în stânga părintelui, atunci
parinte->st=p->dr– cazul d.1
o dacă nodul este legat în dreapta parintelui, atunci
parinte->dr = p->dr – cazul d.2
- eliberează nodul
e. nodul căutat p nu este terminal şi are ambii subarbori (legăturile stânga şi
dreapta sunt nenule).
Cazul e este cel mai complex caz de ştergere a unui nod. Numim predecesor al
nodului p, cel mai din dreapta nod din subarborelui stâng al lui p. Numim succesor
al nodului p, cel mai din stânga nod din subarborelui drept al lui p. Atât
predecesorul cât şi succesorul unui nod oarecare, sunt noduri terminale în arborele
dat.
Exemplu:
7 7
4 8 4 8
Nodul Nodul
de sters de sters
2 6 9 2 6 9
1 3 5 1 3 5
Predecesor Succesor
121
Algoritmi şi structuri de date
8 3 8
4
Nodul
de sters
2 6 9 2 6 9
1 3 5 1 5
Predecesor
3 7
Nodul
de sters
2 5 8
Predecesor
1 4
În acest caz, pentru nodul de şters se va actualiza legătura stângă. După operaţia
de ştergere, arborele devine:
122
Algoritmi şi structuri de date
2 7
1 5 8
123
Algoritmi şi structuri de date
if (p==rad) {rad=p->st;eliberare_nod(p);return;}
if (tatap->st==p) // cazul c.1
{
tatap->st=p->st;
eliberare_nod(p);
return;}
if (tatap->dr==p)// cazul c.2
{
tatap->dr=p->st;
eliberare_nod(p);
return;}
}//sfarsit caz c
//cazul d, p are doar subarbore drept
if (p->st==0)
{
if (p==rad) {rad=p->dr;eliberare_nod(p);return;}
if (tatap->st==p) // cazul d.1
{
tatap->st=p->dr;
eliberare_nod(p);
return;}
if (tatap->dr==p) // cazul d.1
{
tatap->dr=p->dr;
eliberare_nod(p);
return;}
}//sfarsit caz d
124
Algoritmi şi structuri de date
}//sfîrşit functie stergere
Un caz special de arbori binari ordonaţi (arbori de căutare) sunt arborii AVL
(descrişi de Adelson, Velski, Landis). Aceştia au în plus proprietatea de echilibru,
formulată prin:
Pentru orice nod al arborelui diferenţa dintre înălţimea subarborelui stâng
al nodului şi înălţimea subarborelui drept al nodului este maxim 1.
Fiecărui nod într-un arbore AVL îi este asociată o mărime denumită factor de
echilibru. Factorul de echilibru se defineşte prin diferenţa dintre înălţimile
subarborelui drept şi înălţimea subarborelui stâng, şi pentru arborii echilibraţi poate
fi una dintre valorile -1, 0, +1. Dacă factorul de echilibru pentru un nod are altă
valoare decât cele enumerate, arborele nu este arbore AVL.
Operaţiile posibile asupra arborilor AVL sunt aceleaşi operaţii ca în cazul
arborilor binari ordonaţi simpli: creare, inserare, ştergere, parcurgere, căutare. În
schimb, în operaţiile asupra arborilor AVL trebuie ţinut cont de proprietatea de
echilibru din moment ce o operaţie de inserare a unui nod nou sau de ştergere a
unui nod poate conduce la arbori binari dezechilibraţi.
Practica este următoarea: operaţiile definite pentru arborii de căutare se aplică şi
asupra arborilor AVL, însă, ulterior, este verificată îndeplinirea proprietăţii de
echilibrare. În cazul în care arborele a devenit dezechilibrat, prin operaţii
suplimentare se va reorganiza arborele pentru obţine un arbore echilibrat.
125
Algoritmi şi structuri de date
126
Algoritmi şi structuri de date
Definiţie: Graf este o pereche ordonată de mulţimi G =(X, ), unde X este o
mulţime de vârfuri, iar = X×X este o mulţime de muchii sau arce (pentru grafuri
orientate).
Exemplu:
1
2 6
4
5
3
7
127
Algoritmi şi structuri de date
Reprezentarea grafurilor
Parcurgerea grafurilor
128
Algoritmi şi structuri de date
Inserare(C,z)
SfDacă
SfPentru
SfCâttimp
SfSubalgoritm
Observaţie: marcarea unui vârf ca fiind vizitat sau nevizitat se poate realiza prin
folosirea unui tablou tab[1……n] ale cărui elemente sunt asociate vârfurilor
grafului şi au valori binare cu semnificaţia: tab[i]=1 – vârful i este vizitat, respectiv,
tab[i]=0 – vârful i este nevizitat
129
Algoritmi şi structuri de date
130
Algoritmi şi structuri de date
131
Algoritmi şi structuri de date
# P1 de dimensiunea n1,
# P2 de dimensiune n2,
...
# Pk de dimensiune nk
Cheamă DivideEtImpera(P1,n1,S1)
Cheamă DivideEtImpera(P2,n2,S2)
...
Cheamă DivideEtImpera(Pk,nk,Sk)
#combină soluţiile parţiale S1,S2,...,Sk şi obţine S_formal
SfDacă
SfSubalgoritm
Cheamă DivideEtImpera(P,n;S)
Analiza problemei:
Determinarea celui mai mare element dintr-un vector poate fi privită ca o
problemă de determinare a maximului dintre două valori intermediare,
reprezentând maximele celor două subşiruri obţinute prin împărţirea şirului iniţial
în două părţi egale:
Maxim
Maxim1 Maxim2
Maxim= maxim(Maxim1,Maxim2)
Fiecare subşir din împărţirea anterioară se va putea împărţi din nou în două părţi
de dimensiuni apropiate. Acest proces de împărţire se va încheia când un subşir de
elemente nu mai poate fi împărţit, respectiv când dimensiunea acestuia s-a redus la
1. În acest moment, subproblema devine elementară, deoarece maximul
elementelor unui vector format dintr-un singur element este însăşi elementul
respectiv.
132
Algoritmi şi structuri de date
133
Algoritmi şi structuri de date
else
return max2;
}
void main()
{
int a[NMAX],dim;
citire(a,&dim);
int max;
max=maxim(a,0,dim-1);
printf("Maximul este: %d",max);
}
Problema 2: Problema turnurilor din Hanoi. Se dau 3 tije simbolizate prin A,B,C.
Pe tija A se găsesc n discuri de diametre diferite, aşezate de jos în sus în ordine
crescătoare a diametrelor. Se cere sa se mute toate discurile de pe tija A pe tija B,
utilizând ca tija intermediara tija C, respectând următoarele reguli:
- la fiecare pas se muta un singur disc ;
- nu este permis sa se aşeze un disc cu diametrul mai mare peste un disc cu
diametrul mai mic.
A B C
Analiza problemei:
Cazul elementar al problemei constă în existenţa unui singur disc pe tija A,
ceea ce reduce rezolvarea la o mutare a discului de pe tija A pe tija B.
Pentru n=2 (două discuri), rezolvarea problemei constă în 3 mutări,
folosind tija intermediară C:
- mută discul mai mic de pe tija A pe tija C
- mută discul mai mare de pe tija A pe tija B
- mută discul mic de pe tija C pe tija B
134
Algoritmi şi structuri de date
Program:
#include <stdio.h>
void Hanoi (int n, int A, int B, int C)
{if (n>0)
{
Hanoi (n - 1, A, C, B);
printf("\nMuta discul de pe tija %c pe tija %c",A,C);
Hanoi( n-1, B, A, C);
}
}//sfarsit Hanoi
void main ()
{
int n;
printf("\nDati numarul de discuri:"); scanf("%d",&n);
Hanoi (n, ‘A’, ‘B’, ‘C’); //apel hanoi
}
135
Algoritmi şi structuri de date
136
Algoritmi şi structuri de date
După parcurgerea paşilor 1-6 se consideră subşirul x inf, …,xsup împărţit în două
subşiruri: xi, …,xsup şi xinf, …,xj. Dacă subşirurile obţinute conţin mai mult de 1
element, acestea se vor supune aceluiaşi procedeu descris.
Program C:
#include <stdio.h>
void citireSir(int x[10],int *n)
{
int i;
printf("\ndati n=");
scanf("%d",n);
for(i=0;i<*n;i++)
{printf("\nX[%d]=",i+1);
scanf("%d",&x[i]);
}
}
void tiparireSir(int x[10],int n)
{
int i;
for(i=0;i<n;i++)
printf("%d ",x[i]);
}
void SQuick(int x[10],int st, int dr)
{
int i,j,k,M;
i=st; j=dr;M=x[(st+dr)/2];
do{
//avans la stanga
while ((i<dr) && (x[i]<M)) i++;
//avans spre dreapta
while ((j>st) && (x[j]>M)) j--;
if (i<j)
{//interschimb x[i] cu x[j]
138
Algoritmi şi structuri de date
k=x[i];
x[i]=x[j];
x[j]=k;
}
if (i<=j) {i++; j--;}
}while(i<=j);
if (i<dr) SQuick(x,i,dr); //daca subsirul 1 mai are cel putin
1 elem ..
if (j>st) SQuick(x,st,j); //daca subsirul 2 mai are cel putin
1 elem ..
}
void main()
{
int a[10],n;
citireSir(a,&n);
SQuick(a,0,n-1);
tiparireSir(a,n);
}
139
Algoritmi şi structuri de date
140
Algoritmi şi structuri de date
#include <stdio.h>
void citireSir(int x[10],int *n)
{int i;
printf("\ndati n=");scanf("%d",n);
for(i=1;i<=*n;i++)
{printf("\nX[%d]=",i+1);scanf("%d",&x[i]);}
}
void tiparireSir(int x[10],int n)
{int i;
for(i=1;i<=n;i++) printf("%d ",x[i]);
}
void MergeSort(int x[], int inf, int sup)
{int mij,k,i,j;
int y[20];
if(inf<sup)
{
mij=(inf+sup)/2;
mergesort(x,inf,mij);
mergesort(x,mij+1,sup);
//merge(x,inf,sup,mij);
i=inf;
j=mij+1;
k=inf;
while((i<=mij)&&(j<=sup))
{
if(x[i]<x[j])
{ y[k]=x[i];
k++;
i++;}
else
{ y[k]=x[j];
k++;
j++;}
}
while(i<=mij)
{ y[k]=x[i]; k++; i++; }
while(j<=sup)
{ y[k]=x[j]; k++; j++; }
for(i=inf;i<k;i++)
x[i]=y[i];
}}
void main(void)
{int a[10],n;
citireSir( a,&n);
MergeSort(a,1,n);
tiparireSir(a,n);
}
141
Algoritmi şi structuri de date
142
Algoritmi şi structuri de date
Analiza problemei:
Din problema formulată mai sus putem deduce mulţimea de intrare A ca fiind
mulţimea monedelor de valori 1,k 1,k2,...,kn-1, considerând că numărul de monede de
aceiaşi valoare este nelimitat. Soluţia problemei este reprezentată dintr-o
submulţime B de monede, nu neapărat de valori diferite, astfel încât dimensiunea
submulţimii este minimă şi suma valorilor monedelor conţinute de B nu depăşeşte
suma dată S.
Rezolvarea logică a problemei constă în parcurgerea paşilor:
- iniţializează mulţimea B cu mulţimea vidă
- se alege cea mai mare unitate monetară k j care este mai mică valoric decât
suma S
- Cât timp suma S rămâne pozitivă adaugă moneda k j la mulţimea B şi scade
valoarea monedei din suma S
- alege monede de valoare imediat mai mică decât k j, respectiv, kj-1
- Cât timp suma S rămâne pozitivă adaugă moneda k j-1 la mulţimea B şi
scade valoarea monedei kj-1din suma S
- alege monede de valoare imediat mai mică decât k j-1, respectiv, kj-2
- ...
- Procesul se continuă până când nu mai există valori monetare mai mici
care ar putea fi adăugate mulţimii B
143
Algoritmi şi structuri de date
144
Algoritmi şi structuri de date
s3 t3
s2 t2
s1 t1
145
Algoritmi şi structuri de date
Contraexemplu 2.
s4 t4
s3 t3
s2 t2
s1 t1
146
Algoritmi şi structuri de date
147
Algoritmi şi structuri de date
Asupra vectorului soluţie se acţionează prin doar două operaţii: adăugare nou
element după ultimul adăugat, respectiv, eliminare ultim element adăugat. Aceste
operaţii corespund unor operaţii cunoscute: push şi pop; astfel, vectorul soluţie
funcţionează pe principiul unei stive.
O problemă se identifică ca o problemă rezolvabilă prin metoda Backtracking
dacă putem identifica următoarele aspecte din specificaţiile sale:
1. spaţiul soluţiilor este un produs cartezian S A1 A2 .. An
2. soluţia probleme poate fi reprezentată ca un vector
x {x1 , x 2 ,..., x n } S
3. există un set de condiţii prin care putem decide dacă o soluţie parţială
dată de vectorul {x1 , x 2 ,..., x k } , k n este validă → condiţiile de
validitate
4. există un set de condiţii prin care putem decide dacă o soluţie parţială
este finală → condiţiile de finalitate
5. soluţia (vectorul) se poate construi pas cu pas, astfel încât dacă
{x1 , x 2 ,..., x k } este valid, are sens completarea vectorului cu un
element pe poziţia k+1.
Considerăm soluţia parţială {x1 , x 2 ,..., x k } reprezentată ca stivă în imaginile
alăturate:
Cazul 1
… … … …
Nivelul k+1 xk+1 Nivelul k+1 xk+1
Nivelul k xk Nivelul k xk
Nivelul k-1 xk-1 Nivelul k-1 xk-1
… … … …
Nivelul 2 x2 Nivelul 2 x2
Nivelul 1 x1 Nivelul 1 x1
Cazl 2.1
… … … …
Nivelul k+1 xk+1 Nivelul k+1 xk+1
Nivelul k xk Nivelul k x k*
Nivelul k-1 xk-1 Nivelul k-1 xk-1
… … … …
Nivelul 2 x2 Nivelul 2 x2
Nivelul 1 x1 Nivelul 1 x1
148
Algoritmi şi structuri de date
Cazul 2.2
…
… … …
Procesul este unul repetitiv, pentru fiecare nivel curent al stivei se va proceda
similar. Descrierea algoritmului se poate face în două variante: iterativ şi recursiv,
şi în ambele situaţii vom considera subalgoritmii de tip funcţie prin care se verifică
condiţiile de validitate respectiv de finalitate pentru o soluţie parţială oarecare
{x1 , x 2 ,..., x k } .
Varianta Iterativă:
Varianta recursivă:
150
Algoritmi şi structuri de date
151
Algoritmi şi structuri de date
SfFuncţie
#include <stdio.h>
#include <math.h>
int final(int k)
{
if (k==n)
return 1;
else
return 0;
}
void tipareste()
{
int i;
printf("\n");
for(i=1;i<=n;i++)
printf("%d ",stiva[i]);
}
void main()
{
nrsolutii=0;
int i;
int gasit;
printf("dati n:"); scanf("%d",&n);
int k=1; //incepem de la primul nivel;
while(k>=1)
{
gasit=0;
for(i=stiva[k]+1;(i<=n) ;i++)
{
stiva[k]=i;
if (valid(k)==1) {gasit=1;break;}
152
Algoritmi şi structuri de date
}
if (gasit==0) //nu s-au mai gasit
{stiva[k]=0;
k=k-1;
}
else
if (final(k)==1)
{tipareste();
nrsolutii=nrsolutii+1;}
else
k=k+1;
}
printf("\n Numarul de solutii=%d",nrsolutii);
}
#include <stdio.h>
#include <math.h>
int stiva[100]; //solutia se construieste in stiva
int nrsolutii;
int n;
int valid(int k)
{
int i;
int cod=1;
for(i=1;i<=k-1;i++)
if ((stiva[k]==stiva[i])
|| (abs(k-i)==abs(stiva[k]-stiva[i]) ) )
cod=0;
return cod;
}
int final(int k)
{
if (k==n)
return 1;
else
return 0;
}
void tipareste()
{
int i;
printf("\n");
for(i=1;i<=n;i++)
printf("%d ",stiva[i]);
}
153
Algoritmi şi structuri de date
void dame(int k)
{
for(int i=1;i<=n;i++)
{
stiva[k]=i;
if (valid(k)==1)
if (final(k)==1)
tipareste();
else
dame(k+1);
}
}
void main()
{
printf("\n dati n:");scanf("%d",&n);
dame(1);
}
Analiza problemei:
Fie L – matricea de reprezentare a labirintului:
a 1,1 a 2,1 a n,1
a 2,1 a 2,2
L , a i, j 0,1
a m,1 a m,2 a m, n
O soluţie finală a problemei poate fi reprezentată printr-un şir al mişcărilor
efectuate în labirint, însă configuraţia stivei în care sunt salvate camerele traversate
este uşor modificată faţă de problema damelor. Astfel, fiecare poziţie nouă pe care
o încearcă persoana în labirint este identificată prin două elemente: linia şi coloana
matricei prin care este reprezentat labirintul. Această observaţie ne este utilă în
modul de construire a stivei: fiecare nivel al stivei necesită memorarea a două
elemente (spre deosebire de rezolvarea problemei 1): linia şi coloana poziţiei în
matrice.
Acest gen de probleme pentru care fiecare nivel al stivei reţine mai multe
elemente se rezolvă cu un algoritm Backtracking generalizat. Principul aplicat este
acelaşi ca şi la Backtracking-ul simplu: soluţia/soluţiile se construiesc treptat.
Soluţiile finale ale problemei labirintului sunt vectori de perechi (i,j) cu
semnificaţia poziţiei camerelor prin care a trecut persoana pentru a ieşi din labirint.
Lungimile acestor vectori soluţie sunt diferite. Anumite drumuri parcurse pot fi mai
scurte decât altele. Condiţia ca un vector de perechi (i,j) să fie soluţie finală a
154
Algoritmi şi structuri de date
problemei este adevărată dacă ultima cameră (poziţie) parcursă se află la marginea
labirintului: pe prima sau ultima coloană, sau, pe prima sau ultima linie a matricei
de reprezentare.
Condiţiile de validitate se deduc din specificaţiile problemei. Drumul persoanei
poate continua într-o nouă poziţie (i,j) dacă valoarea elementului de pe linia i şi
coloana j din matricea L este 0 (drum liber). Mai mult, pentru a evita parcurgerea
aceloraşi camere de mai multe ori, se impune restricţia ca poziţiile prin care trece
persoana să fie distincte. Astfel, persoana poate trece într-o cameră vecină dacă: nu
este zid (cameră liberă) şi nu a mai fost traversată.
De asemenea, tot specificaţiile problemei ne indică faptul că mutarea într-o
nouă cameră se va face doar prin 4 mişcări posibile în poziţiile vecine: stânga,
dreapta, sus şi jos
În continuare este descris algoritmul backtracking de rezolvare a problemei
labirintului (varianta recursivă), punând în evidenţă condiţiile de validitate şi
finalitate prin subalgoritmi corespunzători:
Funcţie Valid(k)
// k-nivelul stivei
Fie (i,j) –poziţia memorată în Stiva[k]
Dacă (a(i,j)=0) atunci
//cameră liberă
Valid=Adevărat
Pentru l de la 1 la k-1
Dacă (i, j ) Stiva[l ] atunci
//camera a mai fost traversată în drumul memorat în stivă
Valid=Fals
SfDacă
SfPentru
Altfel
//camera ocupată - zid
Valid=Fals
SfDacă
SfFuncţie
155
Algoritmi şi structuri de date
#include <stdio.h>
int n, stiva[200];
void TiparesteSolutia() {
printf("\n");
for (int i = 1; i <= n; i++)
printf("%d",stiva[i]);
}
int valid(int k) {
for (int i = 1; i < k; i++)
if(stiva[i] == stiva[k])
return 0;
return 1;
}
void permutari(int k) {
for (int i = 1; i <= n; i++)
{
stiva[k] = i;
if (valid(k))
if (k == n)
TiparesteSolutia();
else
permutari(k+1);
}
}
void main() {
printf("dati n");scanf("%d",&n);
permutari(1);
}
Varianta 2: Dacă şirul de decizii D1, D2 , …, Dn-1, Dn duce sistemul din starea
iniţială S0 în starea finală Sn în mod optim, atunci:
k 1,2,..., n , secvenţa de decizii D1 , …, Dk-1, Dk duce sistemul din
starea iniţială S0 în starea Sk în mod optim.
În funcţie de varianta principiului optimalităţii identificată din analiza
problemei, metoda de abordare a poate fi:
Metoda înainte – aplicată în cazul verificării principiului optimalităţii în
forma 1.
157
Algoritmi şi structuri de date
Esenţa celor două variante (înainte sau înapoi) este aceiaşi şi constă în
identificarea şi rezolvarea relaţiilor de recurenţă referitoare la criteriul de optimizat
sau la valoarea ce se calculează.
Rezolvarea naivă a acestei probleme ar aceea prin care se vor determina toate
subşirurile crescătoare urmând ca ulterior să se extragă acela care are lungime
maximă. Rezolvarea aceasta este corectă, însă neeficientă prin necesitatea de a
construi şi reţine toate subşirurile lui şirului dat.
O soluţie ingenioasă are fi să se calculeze şi să se reţină într-o tabelă doar
lungimile tuturor subşirurilor crescătoare, fără a le genera şi pe acestea.
În exemplul anterior, subşirurile crescătoare şi lungimile acestora sunt:
158
Algoritmi şi structuri de date
Subşirul Lungimea
7 1
123 3
45 2
23 2
5 1
3 1
Se poate observa că dacă subşirul 3 are lungime 1, subşirul care îl conţine are
lungimea mai mare cu 1, respectiv 2 (1+1) şi subşirul maximal 1 2 3 are lungime 3
(2+1).
Astfel: Dacă subşirul 1 2 3 este subşirul optimal crescător de lungime maximă
(3), care începe cu 1, atunci:
2 3 este subşirul optimal crescător de lungime maximă (2) care începe cu 2 şi
3 este subşirul optimal crescător de lungime maximă (1) care începe cu 3
Notăm L(k) lungimea celui mai mare subşir crescător care se poate forma
începând elementul de pe poziţia k din şirul dat. Relaţia de recurenţă este
următoarea:
159
Algoritmi şi structuri de date
SfDacă
SfPentru
SfAlgoritm
Exemplu:
Pentru următorul caz:
2 0 7 1
4 0 3 0
0 5 1 0
2 4 1 2
160
Algoritmi şi structuri de date
Din observaţiile anterioare, se poate deduce o relaţie de recurenţă prin care pot
fi determinate sumele maxime cu care robotul poate ajunge în oricare celulă a
tablei A:
S ij max S i 1, j , S i , j 1 aij , i, i 1,..., m şi j , j 1,..., n
161
Algoritmi şi structuri de date
Probleme:
162
Algoritmi şi structuri de date
163
Algoritmi şi structuri de date
BIBLIOGRAFIE:
164