Sunteți pe pagina 1din 166

Algoritmi şi structuri de date

MINISTERUL EDUCAŢIEI ŞI CERCETĂRII


UNIVERSITATEA “1 DECEMBRIE 1918”

Lect. dr. Corina Rotar

ALGORITMI ŞI STRUCTURI DE DATE

Alba Iulia
2008

1
CUPRINS

I. Algoritmi şi programare. Descrierea algoritmilor.......................................2


II. Elementele limbajului de programare C..................................................15
III. Functii. Transmiterea parametrilor. Recursivitate..................................34
IV. Tablouri. Tehnici de sortare...................................................................43
V. Structuri. Tipuri de date definite de utilizator.........................................60
VI. Lucrul cu fişiere......................................................................................70
VII. Alocarea dinamica a memoriei.............................................................78
VIII. Listă simplu înlănţuită.........................................................................81
IX. Lista dublu înlănţuită.............................................................................91
X. Liste circulare. Stive. Cozi.....................................................................100
XI. Arbori...................................................................................................110
XII. Elemente de Grafuri. Algoritmi..........................................................127
XIII. Metode de elaborare a algoritmilor. Divide et Impera......................132
XIV. Metode de elaborare a algoritmilor.Greedy.......................................143
XV. Metode de elaborare a algoritmilor. Backtracking. ...........................148
XVI. Metode de elaborare a algoritmilor.Programare dinamica................158
Algoritmi şi structuri de date

I. ALGORITMI ŞI PROGRAMARE. DESCRIEREA


ALGORITMILOR

1. Programare. Etapele programării.

Programarea reprezintă activitatea complexă de elaborare a programelor.


Programarea nu se referă strict la scrierea codului sursă (descrierea în limbaj de
programare a rezolvării problemei); această activitate implică parcurgerea mai
multor etape:
a. Analiza problemei. Problemele reale cu care ne confruntăm nu sunt
formulate întotdeauna într-un stil clar, precis. De cele mai multe ori, problemele
sunt formulate incomplet, ambiguu, lăsând programatorului sarcina de a determina
corect: ce se cunoaşte din problemă şi rezultatele cerute. Etapa de analiză a
problemei se finalizează prin identificarea celor două elemente esenţiale ale
formulării unei probleme: datele de intrare şi datele de ieşire.
b. Proiectarea algoritmului poate fi considerată etapa de creativitate a
programării, în care folosindu-se de cunoştinţele şi experienţa dobândite,
programatorul va identifica metoda de rezolvare a problemei date şi va dezvolta
algoritmul corespunzător. Finalitatea acestei etape o constituie un algoritm descris
clar într-una dintre variantele de reprezentare algoritmică (scheme logice,
pseudocod, sau chiar limbaj de programare – în cazul în care problema este de
dificultate mică).
c. Traducerea în limbaj de programare (implementarea): este etapa în care se
va efectua o “traducere” în limbaj de programare a descrierii algoritmului rezultat
în etapa de proiectare. Cunoaşterea în detaliu a regulilor sintactice ale limbajului
ales, experienţa şi stilul programatorului sunt ingredientele necesare şi suficiente
ale acestei etape.
d. Traducerea în cod maşină, execuţia şi testarea. Traducerea în cod maşină
se realizează automat cu ajutorul a două componente ale mediului de programare:
compilatorului şi a editorul de legături. Testarea programului constă în execuţia sa
repetată pentru o mulţime de date de intrare (date de test) şi verificarea
corectitudinii rezultatelor oferite. Rezultatele incorecte ne semnalează o eroare
logică de proiectare şi necesită o revizuire a algoritmului şi re-parcurgerea etapelor
programării.
e. Întreţinerea este ultima etapă a programării şi constă din acţiuni de
actualizare a produsului final şi de asistenţă oferită beneficiarului.
Dintre toate aceste etape, cea care este mai solicitantă este etapa de proiectare.
Practic, parcurgerea corectă a acestei etape va face diferenţa între programatori şi
amatori. Esenţa programării constă în capacitatea programatorului de a elabora şi
descrie clar metoda de rezolvare a problemei. Orice eroare apărută la nivelul etapei

2
Algoritmi şi structuri de date

de proiectare a algoritmului va avea repercusiuni asupra produsului final, generând


erori logice.

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ă.

Definiţie: Un Algoritm (informatic) reprezintă o secvenţă


finită şi ordonată de reguli clare, a căror parcurgere ne permite ca,
pornind de la o mulţime de date de intrare, să obţinem în mod eficient
rezultatele corecte ale problemei.

3. Proprietăţile algoritmilor:

Orice algoritm informatic verifică următoarele proprietăţi:

1. Generalitate - un algoritm nu rezolvă o singură problemă, ci o clasă de


probleme de acelaşi tip.
2. Finitudine - acţiunile algoritmului trebuie să se termine după un număr
finit de operaţii, aceasta pentru orice set de date valide
3. Claritate - acţiunile algoritmului trebuie să fie clare, simple şi riguros
specificate
4. Corectitudine - algoritmul trebuie să producă un rezultat corect (date de
ieşire) pentru orice set de date de intrare valide
5. Eficienţă - algoritmul trebuie să fie eficient privind resursele utilizate, şi
anume să utilizeze memorie minimă şi să se execute într-un timp minim.

Regulile prin care algoritmul exprimă maniera de rezolvare a problemei sunt de


4 tipuri:
1. reguli de intrare prin care se realizează introducerea datelor de intrare în
memoria unui sistem de calcul virtual sau real

3
Algoritmi şi structuri de date

2. reguli de calcul care permit efectuarea operaţiilor elementare aritmetice


3. reguli condiţionale prin care se va decide continuarea sau nu a
algoritmului printr-o secvenţă de reguli
4. reguli de ieşire prin care se permite furnizarea rezultatelor finale ale
algoritmului

În funcţie de tipurile de reguli pe care le conţine, un algoritm poate fi:


- Algoritm liniar – conţine doar reguli de tipul 1,2,4
- Algoritm ramificat – conţine cel puţin o regulă de tipul 3
- Algoritmi repetitivi (ciclici) – o secvenţă de reguli se repetă de
un număr finit de ori.

4. Obiecte si operaţii ale algoritmilor


Algoritmii informatici operează cu următoarele obiecte fundamentale:
- constante
- variabile
Constanta – reprezintă o mărime a cărei valoare nu se modifică în timpul
execuţiei (parcurgerii) algoritmului.
Variabila – reprezintă o mărime a cărei valoare este modificabilă în timpul
execuţiei algoritmului.
Cuvântul variabilă este unul fundamental în programare. Prin această noţiune se
denumesc date de tipuri diferite. Tipurile de date sunt clasificate ca tipuri
elementare (întreg, real, caracter) şi tipuri structurate. Datele elementare sunt
unităţi indivizibile de informaţie, iar datele de tip structural sunt alcătuite prin
asamblarea datelor elementare. Printr-un tip de dată se înţelege în general atât
domeniul de valori posibile ale datelor, cât şi operaţiile specifice datelor de acel tip.
Introducerea şi utilizarea unei variabile în descrierea algoritmului corespunde
identificării şi manipulării unei date de un tip de dată presupus. Variabila are ataşat
un nume, o valoare curentă care aparţine domeniului tipului specificat şi implică
cunoaşterea mulţimii de operaţii posibile la care ia parte. În algoritmii informatici,
variabilele sunt toate datele de intrare, datele de ieşire (rezultatele finale ale
problemei) şi datele intermediare (acele date care corespund unor rezultate parţiale
ale problemei ce intervin în procese ulterioare pentru determinarea rezultatelor
finale).
În anumite situaţii, acţiunea algoritmilor este îndreptată asupra: fişierelor şi a
articolelor. Fişierul reprezintă o mulţime structurată şi omogenă de date şi articolul
este unitatea atomică a fişierului.
Obiectele pe care le utilizează un algoritm sunt supuse unor operaţii de
manipulare. Secvenţa de paşi descrisă de algoritm este parcursă într-o anumită
ordine dictată de operaţiile de control. Cele două categorii de operaţii formează
operaţiile principale ale algoritmilor informatici:
- Operaţii de intrare-ieşire – permit introducerea în memoria
sistemului de calcul (real sau virtual) a datelor de intrare şi respectiv,
redarea rezultatelor obţinute

4
Algoritmi şi structuri de date

- Operaţii de atribuire – prin intermediul acestor operaţii, unei


variabile i se atribuie valoarea unei alte variabile, a unei constante sau
rezultatul evaluării unei expresii
- Operaţii de calcul – sunt operaţiile executate cu ajutorul expresiilor
- Operaţii de decizie – condiţionează execuţia unui operaţii sau grupe
de operaţii în urma evaluării unor expresii la valorile logice –
adevărat, fals.
- Operaţii de apel – permit apelul unei instrucţiuni sau a unui grup de
instrucţiuni în vederea executării acesteia de un număr finit de ori
- Operaţii de salt – permit continuarea algoritmului dintr-un anumit
punct al acestuia
- Operaţii auxiliare – operaţii specifice lucrului cu baze de date sau
fişiere.

5. Descrierea algoritmilor. Scheme logice

Reprezentarea algoritmilor poate fi făcută în două maniere:


- grafic – prin intermediul schemelor logice
- textual – prin intermediul unor limbaje standardizate (limbaj
pseudocod, limbaj de programare)
Schemele logice sunt reprezentări grafice ale algoritmilor informatici construite
pe baza unei mulţimi de simboluri grafice conectate prin intermediul săgeţilor.
Mulţimea de simboluri grafice şi semnificaţia acestora este prezentată în
continuare:

a. Simbolurile delimitatoare: având rolul de a marca începutul şi sfârşitul


schemei logice, corespunde momentelor de start şi stop a algoritmului
descris corespunzător.

b. Simboluri de intrare-ieşire – corespunzătoare operaţiilor de intrare-ieşire


prin care se realizează comunicarea cu exteriorul sistemului de calcul
virtual.

5
Algoritmi şi structuri de date

c. Simboluri de atribuire - calcul – pentru reprezentarea operaţiilor de


calcul şi atribuire. Detaliile referitoare la operaţii sunt înscrise în interiorul
blocului.

d. Simbolul de decizie – utilizate pentru reprezentarea operaţiilor de decizie.


Sunt simboluri cu o singură intrare şi cel puţin două ieşiri.

e. Simbolul de procedură – se folosesc pentru reprezentarea operaţiilor de


apel. Sunt apelate proceduri sau module externe (care sunt descrise în altă
parte).

f. Simboluri auxiliare: Conectorii şi săgeţile – asigură consistenţa schemei


logice.

Utilizarea corectă a simbolurilor grafice permite descrierea oricărui algoritm


informatic. Se evidenţiază trei structuri fundamentale corespunzătoare: parcurgerii
secvenţiale a unei succesiuni de paşi, ramificării algoritmilor şi execuţiei repetate a

6
Algoritmi şi structuri de date

unei secvenţe de operaţii. Cele trei structuri fundamentale sunt: structura


secvenţială, structura alternativă, structura repetitivă, şi sunt descrise prin
blocuri de simboluri logice astfel:

Denumire Reprezentare grafică Semnificaţie


structură
Structura Execuţia secvenţială a
secvenţială operaţiilor descrise în
simbolurile de calcul:
S1, …Sn
Structura Execută secvenţa S1 dacă
alternativă condiţia Cond este adevărată
SAU execută secvenţa S2 dacă
Cond este falsă

Execută secvenţa S dacă


condiţia Cond este adevărată
SAU nu execută secvenţa S
dacă Cond este falsă.

Structura Structura repetitivă


repetitivă precondiţionată:
Cât timp condiţia Cond rămâne
adevărată repetă execuţia
secvenţei S
Structura repetitivă
postcondiţionată:
Repetă execuţia secvenţei S cât
timp condiţia Cond rămâne
adevărată

Observaţie: Diferenţa dintre structurile repetitive precondiţionată şi


postcondiţionată constă în poziţia simbolului de decizie corespunzător etapei de
verificare a condiţiei de continuare a execuţiei blocului:
- structura precondiţionată presupune iniţial verificarea condiţiei şi apoi
execuţia secvenţei
- structura postcondiţionată presupune execuţia secvenţei şi apoi
verificarea condiţiei
Această diferenţă va genera un număr minim de repetări a secvenţei S diferit
pentru cele două structuri: minim 0 execuţii pentru structura precondiţionată şi
minim 1 execuţie a secvenţei în structura postcondiţionată, în situaţia în care de la
prima evaluare a condiţiei aceasta este falsă..

6. Exemple de scheme logice

7
Algoritmi şi structuri de date

Exemplul 1. Algoritmul de determinare a soluţiilor ecuaţiei de gradul 2:


ax 2 + bx + c = 0 . Rezolvarea problemei presupune tratarea celor 4 cazurilor
identificate în funcţie de datele de intrare furnizate:
1. rădăcinile ecuaţiei sunt reale
2. ecuaţia de gradul II nu are rădăcini reale
3. ecuaţia este de gradul I
4. ecuaţia este imposibilă

Exemplul 2. Algoritmul de determinare a produsului primelor n numere naturale.

8
Algoritmi şi structuri de date

Exemplul 3. Algoritmul de determinare a celui mai mare divizor comun a două


numere:

7. Pseudocod

Pseudocodul este un limbaj standardizat intermediar între limbajul natural şi


limbajul de programare. Descrierea algoritmilor cu ajutorul limbajului pseudocod
înlătură din neajunsurile utilizării schemelor logice, fiind mult mai flexibil şi
natural decât acestea. În plus, descrierea în pseudocod permite convertirea cu
uşurinţă a algoritmilor astfel exprimaţi într-un limbaj de programare.

9
Algoritmi şi structuri de date

Limbajul pseudocod foloseşte două tipuri de propoziţii:


1. Propoziţiile standard: proprii limbajului pseudocod
2. Propoziţiile ne-standard: acele propoziţii ce descriu în limbaj uzual
operaţii ce urmează a fi detaliate, rafinate ulterior. Aceste propoziţii sunt marcate
prin simbolul #.
Există o concordanţă între simbolurile grafice ale schemelor logice şi
propoziţiile limbajului pseudocod. De asemenea, structurile fundamentale pot fi
descrise prin propoziţii proprii limbajului pseudocod.
Tabelul următor prezintă concordanţa dintre descrierea grafică şi propoziţiile
pseudocod pentru operaţii specifice algoritmilor:

Schema logică Pseudocod

Algoritmul NUME-ALGORITM este:

Sfârşit NUME-ALGORITM
sau SfAlgoritm

Citeşte a1,a2,…,an

Tipăreşte a1,a2,…,an

Limbajul pseudocod, pentru a uşura traducerea ulterioară a algoritmului în


limbaj de programare, permite folosirea unei propoziţii standard echivalentă uneia
dintre instrucţiunile populare ale limbajelor de programare. Aceasta propoziţie
corespunde unei structuri repetitive cu număr cunoscut de repetări şi se descrie
astfel:

Schema logică Pseudocod

Pentru var de la I la F cu pasul pas


Execută S
Sfârşit Pentru

- structura ciclică cu număr cunoscut


de repetări

10
Algoritmi şi structuri de date

Tabelul următor prezintă modul de descriere a structurilor fundamentale în


pseudocod:

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.

Structuri Câttimp Cond Câttimp condiţia Cond este


repetitive Execută S adevărată se repetă blocul
SfCâttimp S.
Repetă Se repetă blocul S până
Execută S când condiţia Cond devine
Pânăcând Cond adevărată.

8. Probleme propuse spre rezolvare

Să se descrie grafic şi textual (pseudocod) algoritmul de rezolvare a


problemei următoare:

Problema 1. Să se citească un număr de n valori şi să se determine cea mai mică


valoare citită.

Problema 2. Să se rezolve sistemul de 2 ecuaţii liniare cu 2 necunoscute.

Problema 3. Să se citească n valori şi să se calculeze media aritmetică a acestora.

9. Exemple de algoritmi descrişi în pseudocod


Structuri
Exemplul 1. Algoritmul de determinare a soluţiilor ecuaţiei de gradul 2:
decizionale.
ax + bx + c = 0
2
imbricate
Algoritm EcuaţieGradII este:
Citeşte a, b, c
Dacă a ≠ 0 atunci
Dacă b ≠ 0 atunci
x = -c / b
Tipăreşte “Sol. Ec. grad I”,x

11
Algoritmi şi structuri de date

Altfel
Tipăreşte “Imposibil de
rezolvat”
SfDacă
Altfel
Δ = b2-4ac
Dacă Δ≥0 atunci
x1 = ( −b − ∆ ) 2a
x 2 = ( −b + ∆) 2a
Tipăreşte x1,x2
Altfel
Tipăreşte “Rădăcini complexe“
SfDacă
SfDacă
Sfârşit EcuaţieGradII

Exemplul 2. Algoritmul de determinare a produsului primelor n numere naturale.

Algoritm Factorial este:


Citeşte n
P:=1
Structură Pentru i de la 1 la n cu pasul 1
repetitivă cu
Execută P:=P * i
număr
cunoscut de Sfârşit Pentru
repetări
Tipăreşte P
Sfârşit Factorial

Exemplul 3. Algoritmul de determinare a celui mai mare divizor comun a două


numere.

Algoritm Euclid este:


Citeşte a,b
rest = a modulo b
Câttimp (rest ≠ 0)
Structură a=b
repetitivă b = rest
precondiţionată rest = a modulo b
Sfârşit câttimp
Tipăreşte b

12
Algoritmi şi structuri de date

Sfârşit Euclid

10. Subalgoritmi

O problemă de dificultate sporită necesită uneori o împărţire în subprobleme de


dificultate mai mică în baza principiului divide et impera. Algoritmii de rezolvare a
subproblemelor rezultate sunt mai uşor de descris şi vor constitui subalgoritmi ai
algoritmului global de rezolvare a problemei considerate. Deseori se poate întâlni
situaţia ca o anumită secvenţă de operaţii să fie executată de mai multe ori pentru
date de intrare diferite. În aceste cazuri este utilă construirea unui subalgoritm
corespunzător secvenţei de operaţii şi apelarea acestuia în algoritmul global ori de
câte ori este nevoie.
Subalgorimii sunt practic algoritmi care rezolvă subprobleme ale unei probleme
date. Acest lucru indică faptul că orice subalgoritm verifică proprietăţile de
generalitate, finitudine, claritate, corectitudine ale algoritmilor informatici.
Caracterul de generalitate va fi asigurat prin parametrizarea subalgoritmilor.
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

Apelarea unui subalgoritm din algoritmul apelant se face prin propoziţia:


Cheamă NumeSubalgoritm (listă parametrii actuali)
Lista parametrilor formali va conţine nume generice ale datelor de intrare şi
datelor de ieşire ale subalgoritmului. Lista parametrilor actuali conţine numele
datelor concrete care se doresc a fi transmise subalgoritmului şi numele sub care se
reţin rezultatelor parţiale ale subalgoritmului.
La apelul unui subalgoritm parametrii formali (generici) din listă se vor înlocui
cu parametrii concreţi (actuali).
Exemplu: Să se descrie un algoritm de determinare a maximului dintre 4 valori
date, evidenţiind subalgoritmul de determinare a maximului dintre oricare două
valori.
Subalgoritm Maxim2(DI: x ,y; DE: Max)
Dacă x>y atunci
Max=x
Altfel
Max=y
SfDacă
Sfârşit Maxim2

Apelul repetat al Algoritm Maxim4 este:


subalgoritmului, pentru
diferiţi parametri 13
actuali
Algoritmi şi structuri de date

Citeşte a,b,c,d
Cheamă(a,b; max1)
Cheamă(c,d; max2)
Cheamă(max1,max2 ; maxim)
Tipăreşte maxim
Sfârşit Maxim4

Rezolvarea problemelor cu ajutorul calculatorului presupune:


1. Modelarea matematică a problemei
2. Alegerea unei metode de rezolvare
Modelarea matematică şi alegerea metodei de rezolvare se îmbină cu
conceperea algoritmului rezultând ceea ce numim proiectarea programului. În faza
de elaborare a algoritmului trebuie să se ţină seama de proprietăţile fundamentale
ale algoritmilor: generalitate, claritate, finitudine, corectitudine şi eficienţă.
Calitatea implementării software este dependentă de calitatea algoritmului elaborat.
De aceea, faza de dezvoltare a algoritmului de rezolvare a unei clase de probleme
trebuie abordată cu o deosebită atenţie.

14
Algoritmi şi structuri de date

II. ELEMENTELE LIMBAJULUI DE PROGRAMARE C

Limbajul de programare C a fost dezvoltat la începutul anilor ’70 de către Ken


Thompson şi Dennis M. Ritchie. Popularitatea acestui limbaj a fost câştigată
datorită avantajelor pe care le prezintă:
- programele scrise în C sunt portabile eficiente
- permite utilizarea principiilor programării structurate
- oferă facilităţi de manevrare a biţilor, octeţilor şi adreselor de memorie
- este un limbaj dedicat paradigmei programării procedurale

Prin limbaj de programare înţelegem totalitatea regulilor sintactice şi semantice


prin care se pot comunica unui computer algoritmii de rezolvare a problemelor.
Elementele unui limbaj de programare sunt: alfabetul, unităţile lexicale,
unităţile sintactice şi comentarii.

A. Alfabetul limbajului C este format din:

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

D. Comentarii: au rolul de a justifica textul sursă redactat şi de a oferi o


mai bună înţelegere a acestuia. Comentariile sunt marcate în programul
sursă prin // sau /*…*/
Exemple: //variabila i are semnificaţia unui contor
/* acest program determină
soluţiile ecuaţiei de gradul II*/

15
Algoritmi şi structuri de date

B. Unităţile lexicale ale limbajului C

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.

Variabile, tip de date


Variabilă:
- are un nume care o identifică (acest nume este exprimat printr-un
identificator)
- are asociată o locaţie de memorie în care se regăseşte valoare curentă,
conţinutul acestei locaţii de memorie se poate schimba
- are un anumit tip de date, care exprimă natura şi dimensiunea valorilor pe
care le ia
Regulă: într-un program C, toate datele (variabile) trebuie declarate pentru a
putea fi folosite. Prin declararea unei date se înţelege precizarea tipului datei
respective şi stabilirea identificatorului. Tipul datei ne spune domeniul valorilor
datei cât şi lungimea de reprezentare internă. Declararea datelor se face prin
instrucţiuni de declarare de tip de date:
<instrucţiune de declarare de tip>::= <tip><listă de identificatori>;
unde: <tip> poate fi un tip predefinit (vezi lista tipurilor predefinite în C), un tip
definit de programator sau tipul special void (tip nedefinit).

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

B.2. Cuvintele rezervate (cuvinte cheie) sunt identificatori care au o


semnificaţie specială. Orice cuvânt rezervat nu poate avea altă utilizare într-un
program decât cea predefinită. Mulţimea cuvintelor cheie ale limbajului C este
prezentată în tabelul următor:
Cuvintele cheie ale limbajului C
auto defaul float registe switch
t r
break do for return typedef
case double go to short union
char else if signed unsigned
const enum int sizeof void

16
Algoritmi şi structuri de date

continue extern long static volatile


struct while

Lista cuvintelor rezervate conţine şi setul de tipuri predefinite ale limbajului C.


Tipurile predefinite ale limbajului corespund tipurilor elementare de date: întreg,
real, caracter. Aceste tipuri sunt introduse prin cuvintele rezervate:

Tipul Domeniul de valori


Semnificaţie
predefinit
întreg reprezentat intern pe 16 biţi, [-32768,32767]
int
în cod complementar faşă de 2
întreg reprezentat intern pe 16 biţi, [-32768,32767]
short
în cod complementar faşă de 2
întreg reprezentat intern pe 32 biţi, [-2147483648,2147483647]
long
în cod complementar faşă de 2
întreg fără semn reprezentat pe 16 [0,65535]
unsigned
biţi
unsigned întreg fără semn reprezentat pe 32 [0,4294967295]
long biţi
caracter reprezentat intern prin [0.255]
char codul ASCII corespunzător pe 8
biţi
[- 3,4 ⋅10 38 , 3,4 ⋅10 38 ]
Real, reprezentat în virgulă flotantă, - valorile minime cuprinse în
float
simplă precizie – pe 32 biţi intervalul [- 3,4 ⋅10 −38 ,
3,4 ⋅10 −38 ] se confundă cu 0
[- 1.7 ⋅10 308 , 1.7 ⋅10 308 ]
- valorile minime cuprinse în
Real, reprezentat în virgulă flotantă,
double intervalul [- 1.7 ⋅10 −308 ,
dublă precizie – pe 64 biţi
1.7 ⋅10 −308 ] se confundă cu
0
Caracter, reprezentare internă fără [0,255]
unsigned
semn, pe 8 biţi, echivalent implicit
char
pentru char
signed Caracter, reprezentare internă cu [-128,127]
char semn, pe 8 biţi
- valoarea minimă absolută
reprezentabilă: 3,4 ⋅10 −4932
long Real, reprezentat în virgulă flotantă,
- valoarea maximă absolută
double dublă precizie – pe 80 biţi
reprezentabilă: 1.1 ⋅10 +4932

B.3. Constante

17
Algoritmi şi structuri de date

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
- Constante “şir de caracter”
- Constante numerice
- Constante simbolice

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 şir de caractere


Sunt formate dintr-o succesiune de caractere care se delimitează prin ghilimele:
“exemplu”. Fiecare caracter al şirului se reprezintă în memoria calculatorului pe un
octet, prin codul ASCII corespunzător. Sfârşitul secvenţei de coduri ale caracterelor
dintr-un şir este marcată în memorie de un octet special cu valoarea NULL,
corespunzător caracterului ‘\0’.
Observaţie: reprezentarea în memorie a lui ‘k’ este diferită de reprezentarea lui
“k”. Astfel, şirului “k” îi este alocat un spaţiu de 2 octeţi, ultimul fiind cel ce
marchează sfârşitul NULL.

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

18
Algoritmi şi structuri de date

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
[{+/-}]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>

19
Algoritmi şi structuri de date

Dacă o expresie nu este precedată de semn se consideră implicit semnul +


(pozitiv).

Exemple: +12.34, -0.56, +(a-2), etc.

Operatori aritmetici binari:


<OperatorAritmeticBinar>::= +| - | * | / | %
Au semnificaţia operaţiilor aritmetice: adunare, scădere, împărţire, inmulţire,
modulo şi se utilizează în structuri de forma:
<expresie1><OperatorAritmeticBinar><expresie2>
Expresiile 1, 2 pot fi reprezentate de constante, variabile, nume de funcţii sau
alte expresii aritmetice.
Exemple: a+b, (a+b) / 2, suma(a,b) % c , etc.

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

Diferenţa dintre preincrementare şi postincrementare:


Fie următoarele două expresii:
1. A:=++B
2. A:=B++
si B are valoarea iniţială 4.
Pentru prima expresie, în urma efectuării calculelor B va avea valoarea 5 şi A
va avea valoarea 5 dat fiind faptul că prima acţiune este aceea de incrementare
urmată de atribuire. În al doilea caz, B are valoarea 5 iar A=4, dat fiind faptul că
atribuirea se face înaintea incrementării.

Operatorii relaţionali:
<OperatorRelaţional>::= = = | != | < | <= | > | >=
Cu semnificaţia:
= = egal
!= diferit
<= mai mic sau egal
>= mai mare sau egal, etc.
20
Algoritmi şi structuri de date

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)
Exemplu: Fie expresia 2<3 ; în urma comparării operanzilor (constante
numerice în acest caz) se constată că valoarea logică este “adevăr” ceea ce
conduce la evaluarea expresiei 2<3 la valoarea 1 (Adevărat).

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

Operatorii la nivel de biţi:


Aceştia corespund operatorilor logici (boolean): ŞI, SAU, Negaţie, SAU
EXCLUSIV
<OperatoriBoolean>::= & | | | ~ | ^
& - ŞI
| - SAU
~ - Negaţia
^ - SAU EXCLUSIV

21
Algoritmi şi structuri de date

Operatorii la nivel de biţi se utilizează în manevrarea biţilor din reprezentările


binare ale operanzilor întregi sau caracter.

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

Operatori de asignare (atribuire):


<OperatorAsignare>::= = | += | -= | *= | /= | %= | >>= | <<= | &= | != | ^=
Cea mai simplă operaţie de asignare este cea formată prin construcţia:
<variabilă> = <expresie>
având semnificaţia următoare: expresia din partea dreaptă a operatorului de
asignare este evaluată iar rezultatul evaluării (o valoare) este atribuit variabilei din
partea stângă a operatorului =.
În cazul general, când avem asignări de forma:
<variabilă> <operator> = <expresie>
unde: <operator>::=+|-|*|/|%|>>|<<|&|!|^ , structură este echivalentă cu:
<variabilă>=<variabilă> <operator> <expresie>

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”

Operatorul secvenţial “,”:


Se foloseşte în construcţii de forma:
<expr1> , <expr2> , <expr3> , …. , <exprn>
Expresia globală de mai sus se evaluază de la stânga la dreapta, valoarea finală este
cea rezultată în urma evaluării ultimei sub-expresii exprn.

Operatorul de conversie explicită ( cast)

22
Algoritmi şi structuri de date

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.

C. Unităţile sintactice

Unităţile sintactice sunt elemente complexe alcătuite prin combinarea a două


sau mai multe unităţi lexicale.

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.

A. Instrucţiunea expresie: orice expresie urmată de caracterul ; devine


instrucţiune.
Sintaxa instrucţiuni expresie este următoarea:
<expresie>;
Exemplu: a=b+c;
B. Instrucţiunea bloc: este formată din simbolurile { } în interiorul cărora
sunt scrise alte instrucţiuni. Ordinea de execuţie a instrucţiunilor grupate
este ordinea apariţiei acestora în bloc (instrucţiunile din bloc se execută
secvenţial):
Sintaxa:
{
<instrucțiune_1>
<instrucțiune_2>

<instrucțiune_n>
}
Rolul acestei instrucţiuni este de a grupa mai multe instrucţiuni simple,
compuse sau alte instrucţiuni bloc.

23
Algoritmi şi structuri de date

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>

sau:
if ( <expresie> )
<instrucţiune_1>

Unde, instrucţiunile 1 sau 2 pot fi de asemenea instrucţiuni compuse sau bloc


de instrucţiuni.

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:

if (a!=0) If (a>=b) if (delta>=0)


b=1/a; maxim=a; {
else x1=(-b-sqrt(delta))/2*a;
maxim=b; x2=(-b+sqrt(delta))/2*a;
}

Sintaxa limbajului C permite ca în cadrul instrucţiunilor compuse ce constituie


cele două ramuri ale structurii alternative IF să existe alte structuri alternative IF. O
astfel de construcţie se denumeşte structură alternativă imbricată sau instrucţiuni IF
imbricate.

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ă:
24
Algoritmi şi structuri de date

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:
- Se evaluează expresia dintre paranteze la o valoare.
- Se “caută” secvenţial valoarea la care a fost evaluată expresia în lista
de valori val1, val2, …valn.
- Dacă nu se găseşte nici o valoare val1, val2,…valn egală cu valoarea la
care a fost evaluată expresia se va executa, dacă este precizată,
instrucţiunea default sau efectul instrucţiunii switch este nul
- Dacă s-a găsit o valoare egală cu valoarea expresiei se va executa
instrucţiunea corespunzătoare şi se va părăsi structura alternativă.

Exemplu: Afişarea numelui zilei din săptămână căreia îi corespunde un număr


1,2,...,7:

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

25
Algoritmi şi structuri de date

unde: <instrucţiune> poate fi o instrucţiune simplă, compusă, şi poate conţine


alte instrucţiuni while, caz în care spunem că avem o instrucţiune while imbricată

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.

Simplificat, o instrucţiune while are semnificaţie:


“Cât timp expresia este adevărată repetă corpul de instrucţiuni”

Observaţie: corpul instrucţiunii while este posibil să nu se execute nici o dată, în


situaţia în care expresia evaluată este de la început falsă.

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>);

<instrucţiune>: poate fi o instrucţiune simplă, compusă, şi poate conţine alte


instrucţiuni do…while, caz în care spunem că avem o instrucţiune repetitivă
imbricată.

Efectul instrucţiunii do…while:


- în primul pas se execută corpul instrucţiunii do…while
- Se evaluează expresia dintre paranteze

26
Algoritmi şi structuri de date

- dacă rezultatul evaluării expresiei este adevărat se reia procesul prin


execuţia corpului instrucţiunii, în caz contrar se încheie execuţia
instrucţiunii do…while
Semnificaţie:
“Execută blocul de instrucţiuni până când expresia devine falsă”
Observaţie: corpul unei instrucţiuni repetitive postcondiţionate se execută cel puţin
o dată. (chiar dacă expresia este falsă de la prima evaluare).

Exemplu 1. Citeşte un caracter de la tastatură până când se introduce caracterul


‘0’.
do
{
scanf(“%c”,&caracter);
}
while (caracter<>’0’);

Exemplu 2. Calculează suma unor numere întregi citite de la tastatură până la


introducerea unui număr negativ sau zero:

suma=0;
do
{
scanf(“%d”, &nr); //citeste numărul curent
suma=suma+nr;
} while (nr>0) ;

Exemplu 3: Programul următor citeşte un text (terminat prin Enter) şi afişează


numărul de vocale din compunerea textului.

#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>

27
Algoritmi şi structuri de date

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:
- În primul pas, se evaluează expresia1, respectiv se execută secvenţa de
iniţializare
- Se evaluează expresia2 (această expresie are acelaşi rol ca <expresie> din
structura while sau do…while )
- Dacă rezultatul evaluării anterioare este adevărat se execută corpul
instrucţiunii for şi apoi se execută secvenţa de actualizare specificată prin
expresie3
- Dacă rezultatul este fals, se încheie execuţia for.

Exemplu 1: Afişarea primelor 10 numere naturale.

for ( i=1; i<=10; i++)


printf(“\n %d”, i);

Exemplu 2: Calcularea lui n!

fact=1;
for (i=1;i<=n;i++)
{
fact=fact*i;
}

E. Apelul funcţiilor de intrare - ieşire


Operaţiile de intrare-ieşire (citirea datelor de intrare şi afişarea rezultatelor) nu
se realizează prin instrucţiuni ale limbajului C, ci prin apelarea unor funcţii
speciale, definite în bibliotecile de funcţii ale limbajului C.

Funcţiile printf, scanf

- printf şi scanf sunt funcţii predefinite de intrare–ieşire definite în biblioteca stdio


Pentru a putea folosi aceste funcţii într-un program C este necesară în prealabil
specificarea prin directivă de preprocesare a fişierului antet (header) ce conţine
prototipurile acestora:
#include <stdio.h> //la începutul programului sursă
28
Algoritmi şi structuri de date

Prin apelul funcţiei printf se realizează afişarea pe ecran a mesajelor, datelor,


valorilor unor variabile sau rezultate finale sau intermediare ale programului.

Sintaxa apelului funcţiei printf:


printf(“sir de formatare”, expr1, expr2, …)
unde:
- expr1, expr2, … sunt expresii separate prin virgulă şi pot reprezenta
variabile, constante sau orice expresie evaluabilă (aritmetică, logică,
relaţională, etc.)
- sir de formatare este încadrat de caracterele ghilimele “ ” şi reprezintă
forma sub care vor fi tipărite pe ecran valorile expresiilor.
Efect: prin apelul funcţiei printf se realizează afişarea pe ecran a datelor într-un
anumit format.
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”)

În general sir de formatare poate conţine:


- Text
- Specificatori de format
- Secvenţe escape
Secvenţele escape sau comenzi de control pot fi de două tipuri: secvenţe
negrafice şi secvenţe grafice sau afişabile. Numărul specificatorilor de format este
egal cu numărul de expresii separate prin virgulă. Fiecare specificator de format se
referă la modul de redare a valorii expresiei de pe poziţia corespondentă:

Secvenţele escape negrafice:


\n – trecere la linie nouă
\t – deplasare la dreapta cu un tab
\b – deplasare la stânga cu o poziţie
\r – revenire la începutul rândului
\f – trecere la pagina următoare
\a – semnal sonor

Secvenţele escape grafice:


\’ – afişare apostrof
\” – afişare ghilimele
\\ - afişare backslash
\xCC – afişare caracterul al cărui cod ASCII în hexazecimal este dat prin
secvenţa CC de cifre în baza 16.

Specificatorii de format
<specificator de format>::= % [-] [m.[n]] [l] <conversie>
<conversie>::= d|o|x|X|u|s|c|f|e|E|g|G

29
Algoritmi şi structuri de date

- caracterul – dacă apare semnifică cadrarea la stânga a rezultatului


corespunzător specificatorului curent
- m - specifică numărul minim de caractere ale datei ce va fi afişate
- .n - semnifică în cazul datelor numerice numărul de cifre după punctul
zecimal
- l - semnifică conversie din formatul intern long în formatul extern.

Conversie specifică tipul de conversie din formatul intern (reprezentarea


internă) în cel extern (cum va fi afişat):
d – întreg int cu semn
o – întreg în octal
x, X – întreg în hexazecimal (litere mici sau MARI pentru cifrele a,b,…f
respectiv A,B,…F)
u – întreg fără semn (unsigned)
c – din codul ASCII al reprezentării binare interne -> caracterul
s – din şirul de coduri ASCII -> şirul de caractere
f – conversie din float sau double în formatul extern zecimal
e,E - conversie din float sau double în formatul cu mantisă şi exponent
g,G – conversiile de la f şi E sau f şi e dar alegerea se face pentru numărul
minim de poziţii

Exemple:
printf(“valoarea intreaga a= %d”,a);

printf(“realul a= %5.2f”,a);

printf(“%d + %d = %d”,a,b,a+b);

printf(“suma este %4.4f” , suma);

Sintaxa apelului funcţiei scanf :


printf(“sir de formatare”, adr1, adr2, …)
Efect: prin apelul funcţiei scanf se realizează citirea de la tastatură datelor într-
un anumit format.
Şirul de formatare are aceeaşi semnificaţie ca şi la funcţia printf.
Şirul de adrese: adr1, adr2,…adrn se asociază specificatorilor de format
păstrând ordinea. O adresă se specifică prin precedarea numelui variabilei de
operatorul unar adresă notat &. Astfel var este numele unei variabile, iar &var este
adresa de memorie la care se stochează valoarea variabilei.
Excepţie: nu se utilizează operatorul adresă în cazul în care variabila este o
variabilă specială pointer, respectiv când numele variabilei este o adresă de
memorie.

Exemple:

30
Algoritmi şi structuri de date

scanf (“%d”, &x);

scanf (“%d %d %f”,&a,&b,&x);

scanf(“%d %d %d”, &x[0], &x[1], &x[2]);

scanf(“%s %s %d”, nume, prenume, &varsta);

Structura unui program C

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
considerate. Instrucţiunile programului pot fi instrucţiuni de declarare sau definire
a unor variabile, funcţii şi instrucţiuni care se regăsesc în corpul funcţiilor.
Structura unui program C este următoarea:

<program>::= [<directive de procesare>]


[<instrucţiuni de declarare a variabilelor globale>]
<funcţii>

Directivele de preprocesare: sunt numite şi comenzi de preprocesare şi apariţie


lor în program nu este obligatorie. Sunt marcate prin simbolul # (diez) care le
precede.
Exemplu:
/* includerea bibliotecii standard de funcţii de intrare-ieşire */
#include <stdio.h>

Instrucţiuni de declarare a variabilelor globale: opţional; Anumite variabile


sunt folosite în cadrul mai multor funcţii, având o utilizare globală., fapt pentru
care aceste variabile sunt declarate în secvenţa de declarare a variabilelor cu
caracter global (în afara oricăror funcţii ale programului).
Funcţii: sunt unităţi independente ale unui program prin care se reprezintă un
subalgoritm (procedura, modul) al unui algoritm de rezolvare a unei probleme date.
Clasificare funcţii:
1. Funcţii standard – au nume prestabilit, sunt constituite în biblioteci de
funcţii standard
2. Funcţii utilizator – au nume ales de către utilizator şi sunt construite de
către acesta
3. Funcţia principală (main)

Un program C conţine obligatoriu funcţia principală main. Putem avea mai


multe funcţii utilizator declarate şi definite sau alte funcţii standard ce sunt apelate
dar în prealabil au fost incluse, prin directive de preprocesare, bibliotecile din care
fac parte.

31
Algoritmi şi structuri de date

Sintaxa funcţiei main:


void main (void)
{
//declaraţii locale
//instrucţiuni

}

Exemplu de program C:
#include <stdio.h> //rezolvarea ec. de gradul II
#include <math.h>

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

32
Algoritmi şi structuri de date

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

III. FUNCTII. TRANSMITEREA PARAMETRILOR.


RECURSIVITATE.

Funcţia reprezintă o unitate de sine stătătoare a unui programului C, prin


intermediul căreia se descrie un subalgoritm de rezolvare a unei subprobleme.
Într-un capitol precedent am subliniat importanţa subalgoritmilor în descrierea
unor metode de rezolvare a problemelor. Programele C permit o traducere a
descrierilor algoritmice bazate pe subalgoritmi prin posibilitatea definirii unor
funcţii utilizator care tratează aspecte parţiale ale rezolvării. Un program C poate
conţine funcţia principală main dar şi alte funcţii definite de către programator sau
funcţii predefinite din bibliotecile standard.
Limbajul C este suplimentat prin biblioteci care conţin funcţii înrudite prin
intermediul cărora se realizează diferite operaţii. Aceste funcţii de biblioteci pot fi
apelate în programele dezvoltate dacă în prealabil s-a inclus biblioteca
corespunzătoare prin directive de preprocesare #include. (spre exemplu funcţiile
standard scanf şi printf ale bibliotecii stdio)

Funcţiile definite de utilizator au următoarea sintaxă:

<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:

Apelul unei funcţii se face prin construcţia:

34
Algoritmi şi structuri de date

NumeFuncţie(pa1, pa2, … , pan);

Categorii de funcţii:

1. Funcţie fără tip şi fără parametrii

Există posibilitatea definirii de către utilizator a unor funcţii care nu


returnează nimic şi lista parametriolor acestora este vidă:

void NumeFuncţie ()
{
… //corpul functiei
}

Apelul funcţiei se face printr-o construcţie de forma:

NumeFuncţie();

Exemplu: Se defineşte o funcţie Cifre care tipăreşte cifrele arabe. Funcţia


va fi apelată în funcţia main.

#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

Exemplu: Programul C conţine o funcţie CitireValoare, care citeşte o valoare


numerică întreagă de la tastatură şi returnează această valoare funcţiei main
apelantă, pe care aceasta o va afişa pe ecran.

#include <stdio.h>

int CitesteValoare()
{
int numar; //declaraţie de variabilă locală
scanf(“%d”,&numar);
return numar;
}

void main(void) void main(void)


{ //varianta compacta
int valoare; {
valoare=CitesteValoare();//apel printf(“valoarea este: %d”,
printf(“valoarea este: %d”, CitesteValoare());
valoare); }
}

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) }

//apelul din functia main


int nr1,nr2, min;
nr1=7; nr2=6;
min=minim(nr1,nr2); // la apel a va primi valoarea 7 şi b – valoarea 6
min=minim(nr2,nr1); // la apel a va primi valoarea 6 şi b – valoarea 7

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>

void putere(int n) //ridică la pătrat valoarea n şi afişează


rezultatul
{
n=n*n;
printf(“ valoarea lui n in functie este %d”,n); //n este 25
}
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 5 (!nu s-a
modificat parametrul actual)
}

Î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

Pointer = variabilă care are ca valori adrese de memorie.


Sintaxa de declarare a unei variabile pointer:
tip * p; // variabilă pointer spre tip
Prin această declaraţie s-a introdus o variabilă ale cărei valoare este adresa unei
zone de memorie ce poate reţine o dată de tipul tip.
Fie x o variabilă simplă de tipul tip:
tip x;
şi p un pointer (variabilă de tip pointer) care are ca valoare adresa variabilei x.
Pentru a face o atribuire unei variabile de tip pointer p se foloseşte construcţia:
p=&x; // semnificaţia: lui p i se atribuie adresa variabilei x.

37
Algoritmi şi structuri de date

Prin construcţia &x ne referim la adresa variabilei x.


Prin construcţia *p ne referim la valoarea variabilei x.

-
-
Variabila pointer p:
Operatorul * se numeşte operator de indirectare
Operatorul & se numeşte operator adresă
Adresa
Expresia *&x are aceiaşi semnificaţie cu expresia x.

Transmiterea parametrilor prin adresă

Transmiterea prin adresă se realizează prin variabile de tip pointer şi ne


asigură de modificarea valorii parametrilor actuali.
Transmiterea prin prin adresa este realizată astfel:

1. Parametrii formali ai funcţiei se declară ca fiind de tip pointer;


tip NumeFuncţie(tip1 *p1, tip2 *p2, … , tipn *pn)
{…};
2. La apel, argumentele funcţiei sunt adresele parametrilor actuali;
NumeFuncţie(&p1, &p2, … , &pn)

Exemplu:
#include <stdio.h>

void putere(int *p) //ridică la pătrat valoarea n şi afişează


rezultatul
{
*p=*p* *p;//ridică la pătrat valoarea referită de p: *p
printf(“ valoarea lui n in functie este %d”,*p); //n este 25
}

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

Exerciţiu: Să se scrie o funcţie care interschimbă valorile a două variabile


transmise ca parametrii.

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).

void interschimbare(int a, int void interschimbare(int *a,


b) int *b)
{ {
int auxiliar; int auxiliar;
auxiliar=a; auxiliar=*a;
a=b; *a=*b;
b=auxiliar; *b=auxiliar;
} }

//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:

1. Dezvoltaţi un program C care determină minimul dintr-un şir de numere


citite de la tastatură (fără a utiliza tablouri), punând în evidenţă funcţia care
determină minimul dintre două valori.
2. Scrieţi o funcţie care rezolvă ecuaţia de gradul II. Funcţia va avea 5
argumente: coeficienţii ecuaţiei (a,b,c) şi posibilele soluţii (x1,x2). Funcţia
returnează un număr întreg cu semnificaţia:
- -1, ecuaţia nu este de gradul II
- 0 , ecuaţia are soluţii complexe
- 1, ecuaţia are soluţii reale
Primii trei parametrii se transmit prin valoare, ultimii doi - prin adresă.

39
Algoritmi şi structuri de date

RECURSIVITATE

Recursivitatea se obţine prin instrucţiunea de apel a unei funcţii în corpul


definirii funcţiei respective:

<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.

Problemă 1: Să se calculeze P(n)=n! printr-o funcţie recursivă.


Analiza problemei: Pornind de la observaţia că produsul P(n)=1*2*…(n-1)*n se
mai poate formula ca:
P(n)=P(n-1) * n, vom defini o funcţie factorial care tratează problema
determinării lui P(k) pe baza formulei anterioare, presupunând că P(k-1) este
calculabil după acelaşi procedeu. P(k-1)=P(k-2)*(k-1).
Funcţia autoapelantă trebuie să conţină o condiţie de terminare a recursivităţii.
În problema calculului n!, condiţia de terminare a recursivităţii se deduce din
observaţia că 1! are valoarea 1, ceea ce nu mai necesită un calcul suplimentar.
Apelul iniţial al funcţiei factorial se va face pentru parametrul actual n-
reprezentând data de intrare a programului. Funcţia returnează la ieşirea din apel
rezultatul dorit n!.

int factorial(int k)
{
if (k>1)
return (k*factorial(k-1));
else
return 1;
}

//apelul functiei
int p;
p=factorial(n);

Execuţia pas cu pas:

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* (n-
Recursivitatea poate fi de mai multe feluri, în funcţie de numărul de apeluri

n
conţinute de funcţia (subalgoritmul) recursivă:
1. recursivitate liniară: funcţia conţine cel mult un autoapel -
exemplu funcţia factorial descrisă anterior
2. recursivitate neliniară: funcţia conţine două sau mai multe apeluri
recursive – exemplu fibonacci descris în continuare

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
}

Problema 3. Determinarea celui mai mare divizor comun a două numere a şi b.


(prin funcţie recursivă)
Analiza problemei:
cmmdc(a,b) poate fi:
1. a, dacă b = 0
2. cmmdc(b, a mod b), dacă b≠0
Prin a mod b se înţelege restul împărţirii întregi a lui a la b

Pe baza observaţiei precedente, funcţia recursivă cmmdc se construieşte astfel:


int cmmdc(int m, int n)
{
if(n==0)
return m;
return
cmmdc(n,m%n); //autoapel
}

Recursivitatea este o tehnică costisitoare datorită spaţiului de memorie blocat


pentru reţinerea variabilelor locale create la fiecare apel. Există diferite metode de
eliminare a recursivităţii din programele C, pentru a preîntâmpina acest neajuns.
Descriem în continuare procedura de eliminare a recursivităţii liniare:
1. Se utilizează o listă care se iniţializează ca fiind vidă. În această listă vor fi
salvaţi parametrii formali şi variabilele locale ale funcţiei recursive. Operaţiile
posibile cu lista folosită sunt: adăugarea unui nou element în capătul listei şi
extragerea unui element din capătul listei(vezi conceptul de stivă)
2. Câttimp condiţia de continuare a recursivităţii este adevărată execută:
- Adaugă (salvează) în listă valorile actuale pentru parametrii
funcţiei recursive si variabilele locale
- Execută instrucţiunile funcţiei recursive
- Modifica valorile argumentelor funcţiei recursive (actualizarea
parametrilor)
3. Dacă la terminarea pasului 2 lista nu este vidă, se extrage mulţimea de
variabile din capătul acesteia, se calculează valoarea dorită şi se executa
instrucţiunile ce apar după apelul funcţiei recursive, apoi se trece la pasul 2.
Dacă lista este vidă se termină execuţia algoritmului.

42
Algoritmi şi structuri de date

IV. TABLOURI. TEHNICI DE SORTARE.

Tablourile în limbajul C sunt variabile cu indici. Pot fi de două categorii,


tablouri unidimensionale (vectori) sau multidimensionale. La nivel abstract, un
tablou unidimensional se poate defini ca o listă ordonată de n valori de acelaşi tip:
x1,x2, … ,xn. Fiecare poziţie i din cadrul tabloului conţine elementul xi, respectiv o
valoare de tipul specificat.

Declararea unui tablou unidimensional în limbajul C se face astfel:


<tip> NumeTablou [NumarElemente];
Unde <tip> este tipul de date al elementelor tabloului, NumarElemente este un
număr natural sau o constantă cu valoare întreagă pozitivă prin care se precizează
numărul elementelor din tablou, NumeTablou este un identificator prin care este
denumit vectorul.
- Referirea unui element al tabloului se foloseşte numele tabloului şi indicele
corespunzător. Spre exemplu: int x[100];
- Pentru a referi elementul de pe poziţia i vom scrie x[i-1], dat fiind faptul că
primul element al şirului este x[0].
- Tablourilor li se asociază locaţii de memorie consecutive, de aceeaşi
lungime în care sunt stocate valorile fiecărui element.
- Observaţie: numele tabloului (spre ex. x) este o variabilă a cărei valoare
este adresa de memorie a primului element din şir (pointer).

Declararea tablourilor n-dimensionale:


<tip> NumeTablou [N1] [N2] …[Nn];
Unde N1, N2 … Nn reprezintă numărul de elemente pe fiecare dimensiune.
- Referirea unui element al tabloului multidimensional se face prin
identificatorul tabloului (numele) şi specificarea poziţiei pe fiecare
dimensiune:
- NumeTablou [i1] [i2] …[in].
Exemplu:
int A[20] [30];
//declararea unei matrice (tablou bi-dimensional) de 20 linii si 30 coloane.
- Primul element al tabloului bidimensional declarat mai sus este A[0]
[0], iar numele tabloului conţine adresa primului element memorat.

Observaţie: Numele unui tablou în C este de fapt un pointer, avînd ca valoare


adresa primului sau element. La transmiterea vectorilor ca parametrii ai unor
funcţii se va ţine cont de acest aspect.

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

Tablourile sunt structuri de date:


- compuse: sunt formate din mai multe elemente (de acelaşi fel)
- ordonate: există o relaţie de ordine dată de poziţia elementelor în
tablou
- omogene: toate elementele unui tablou au acelaşi tip
Tablourile unidimensionale vor fi denumite în continuare vectori.

Pointeri şi tablouri. Operaţii cu pointeri

Î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[0] tab[1]
Exemplu:
int tab[100];
//tab – este adresa elementului t[0]
Observaţie: Deoarece memoria alocată unui tablou este o zonă contiguă, respectiv,

tab
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:

Asupra pointerilor se pot efectua următoarele operaţii:


- incrementare/decrementare
- adunarea/scădere cu un întreg
- diferenţa a doi pointeri
Fie declaraţia următoare:
<tip> *p; //p este un pointer la tipul <tip>
Efectul operaţiilor de incrementare/decrementare este următorul:
p++ şi ++p echivalent cu: p=p+dim
p-- şi --p echivalent cu: p=p-dim
unde: dim reprezintă dimensiunea exprimată în octeţi a unei variabile de tipul
<tip>
Exemplu.
int *p;
p++;
//p se măreşte cu 2, deoarece o variabilă de tipul int se memorează pe 2 octeţi
Adunarea şi scăderea unui întreg
Fie: <tip> *p;
int k;
Expresiile: p+k, p-k sunt expresii valide în limbajul C, având semnificaţiile
următoare:

44
Algoritmi şi structuri de date

p+k - reprezintă o adresă de memorie; valoarea p+k este egală cu valoarea


lui p mărită cu k*dim , unde dim- dimensiunea exprimată în octeţi a tipului <tip>
p-k - are valoarea p micşorată cu k*dim , unde dim- dimensiunea exprimată
în octeţi a tipului <tip>
Accesarea elementelor unui tablou poate fi făcută şi prin operaţii cu variabile
pointer:

dim octeti dim


Exemplu:

tab[0]
int tab[100]; //tablou de 100 întregi
int i; tab[1]
(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]

Diferenţa a doi pointeri:


Două variabile de tip pointer prin care se referă elementele aceluiaşi tablou pot
fi scăzute. Fie tabloul tab şi doi pointeri p şi q care adresează elementele tab[i] şi

tab tab+1
tab[j]. Diferenţa p-q este un număr întreg k, reprezintând numărul de elemente
care desparte cele două adrese. Această dimensiune se poate determina prin
calculul elementar: k=(j-i).
tab+
Între adresele p şi q se află un număr de (j-i) elemente, respectiv, (j-i)*dim
octeţi, unde dim - dimensiunea necesară reprezentării unui element al tabloului.

dim

tab[i]
Şiruri de caractere .......
Operaţiile cu şiruri de caractere se efectuează prin apelul unor funcţii de
bibliotecă specifice. Prototipurile funcţiilor care manipulează şiruri de caractere se
află în fişierul antet string.h.

45
Algoritmi şi structuri de date

Orice şir de caractere se va păstra într-o zonă de memorie contiguă. Ne


reamintim că unei variabile de tipul char îi este necesară o zonă de memorie de 1
octet, pentru reţinerea codului numeric corespunzător (reprezentarea internă). Un
şir de caractere, declarat ca tablou unidimensional de tip char va ocupa o zonă de n
octeţi terminată printr-un octet suplimentar cu valoarea '\0’ – caracterul nul, prin
care s-a marcat terminarea şirului.
Ex: char sir[20];
Folosind relaţia dintre tablouri şi pointeri, putem utiliza în programe declaraţii
de forma:
char *psir;
Accesarea caracterelor din compunerea şirului va fi posibilă prin variabile
indexate sau prin operaţii cu pointeri:
sir[0] sau *psir – codul numeric (ASCII) al primului caracter din sir
sir[1] sau *(psir) - codul numeric al celui de-al doilea caracter din sir
sir sau psir – adresa primului caracter
sir+1 sau psir+1 – adresa celui de-al doilea caracter din sir

Pentru manipularea şirurilor de caractere, biblioteca standard string.h pune la


dispoziţie funcţii speciale dedicate operaţiilor specifice.

Lungime unui şir de caractere


Lungimea unui şir de caractere este definită prin numărul caracterelor care intră
în compunerea şirului. Funcţia standard strlen este utilă în determinarea lungimii
unui şir de caractere:
unsigned strlen(const char *s)
Exemplu:
char const *psir = “text”
unsigned l;
l= strlen(psir); // lui l i se va atribui 4
//numărul de caractere, excluzând marcatorul NULL)
Copierea unui şir de caractere
- este operaţia prin care un şir de caractere sursă este copiat într-o altă
zonă de memorie ataşată unui alt şir de caractere destinaţie
- funcţia specifică este: strcpy

char * strcpy(char *destinatie, const char *sursa)

Funcţia are ca efect copierea tuturor caracterelor în zona de memorie referită de


destinatie din zona de memorie referită de sursa. (inclusiv caracterul NULL)
Funţia strcpy returnează la revenire adresa de început a zonei în care s-a copiat
şirul (pointer-ul destinatie)
Exemplu: Interschimbarea a două şiruri de caractere:
void schimb(char a[20],char b[20])
{
char aux[20];

46
Algoritmi şi structuri de date
strcpy(aux,a);
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.

Compararea a două şiruri de caractere


Operaţia de comparare a două şiruri presupune verificarea codurilor ASCII ale
caracterelor din compunerea şirurilor. Compararea şirurilor de caractere este o
operaţie utilă în probleme care cer ordonarea lexicografică a unor secvenţe de text.
char * strcmp(const char *sir1,const char *sir2)
Funcţia strcmp returnează:
- valoare negativă, dacă sir1<sir2
- 0, dac ă sir1=sir2
- valoare pozitivă, dacă sir1>sir2

Exemplu: Program de generare a parolelor. Pentru un cuvânt dat, parola va fi


obţinută prin scrierea de la dreapta la stânga a caracterelor de pe poziţiile impare.

#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

Operaţiile specifice cu vectori sunt:


1. parcurgerea
a. pentru accesarea şi/sau modificarea elementelor (citirea şi
afişarea)
b. pentru numărarea elementelor ce verifică o condiţie
2. căutarea secvenţială
3. minimul/maximul dintr-un vector
4. inserarea şi ştergerea unui element pe o poziţie dată
5. concatenarea a doi vectori
6. interclasarea a doi vectori
7. sortarea
a. sortarea prin selecţie
b. sortarea prin numărare
c. sortarea prin metoda bulelor (prin interschimbare)

Alte variante de sortare vor fi descrise în capitolele dedicate metodelor de


programare (ex. sortarea rapidă şi sortarea prin interclasare)

1. Parcurgerea tablourilor

//Parcurgerea tablourilor –citirea şi afişarea unui vector

void citire_tablou(int t[], int *n)


{
int i;
printf(“dati dimensiunea tabloului:”); scanf(“%d”,n);
for(i=0;i<*n;i++)
scanf(“%d”, &t[i]);
}

void afisare_tablou(int t[], int n)


{
int i;
for(i=0;i<n;i++)
printf(“%d”, t[i]);
}

int tablou[30];
int dim_tablou10;
citire_tablou(tablou, &dim_tablou);
//apelul funcţiei de citire tablou
afisare_tablou(tablou, dim_tablou);
//apelul funcţiei de afişare tablou

Legătura dintre pointeri şi tablouri influenţează maniera de transmitere a


tablourilor ca parametri unei funcţii. Oferim o altă variantă de funcţie care citeşte

48
Algoritmi şi structuri de date

un tablou unidimensional, în care se folosesc expresii cu pointeri, iar parametrul


formal este declarat ca pointer:

void citire_tablou_II(int *pt, int *n)


{
int i, valoare;
printf(“dati dimensiunea tabloului:”); scanf(“%d”,n);
for(i=0;i<*n;i++)
{
scanf(“%d”, &valoare);
*(p+i)=valoare;
}
}

//Parcurgerea tablourilor –numărarea elementelor ce verifică o condiţie dată


int numara_pozitive(int t[], int n)
{
int i, contor;
contor=0;
for(i=0;i<n;i++)
if (t[i]>0)
contor++;
return contor;
}

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
}

3. Determinarea minimului/maximului dintr-un vector

Principiul este următorul:


- se presupune că elementul de pe prima poziţie din vector este minimul
- se parcurge vectorul element cu element (de la a 2-a poziţie, până la
ultima), comparându-se minimul presupus cu valoarea curentă din
vector
- dacă elementul curent este mai mic decât minimul presupus, se va
actualiza valoarea minimului, în caz contrar se trece la următorul
element fără a altera minimul presupus
49
Algoritmi şi structuri de date

Pentru determinarea maximului procedeul este similar, modificându-se doar


operatorul logic utilizat în condiţia de actualizare a maximului presupus.

Algoritmul descris în pseudocod pentru determinarea minimului dintr-un vector


x1,x2,...,xn este următorul:

Algoritm Minim este


Citeşte: x1,x2,...,xn, n //vectorul x de n elemente; dimensiunea n
Fie min=x1
Pentru i de la 2 la n //se parcurge vectorul
Dacă xi<min atunci
min=xi
SfDacă
SfPentru
Tipăreşte min
SfAlgoritm

// codul sursă C corespunzător algoritmului descris


//se citesc în prealabil dimensiunea şi elementele vectorului x
int x[100];
int i, min, n;
citire_tablou(x,&n);
min=t[0];
for(i=0;i<n;i++)
if (x[i]<min0)
min=x[i];
printf(”Minimul este:%d”, min);

4. Inserarea/ştergerea unui element

Inserarea unui element nou pe o poziţie k în vector presupune efectuarea


următoarelor operaţii:
- mărirea dimensiunii vectorului cu 1.
- mutarea cu o poziţie la dreapta a tuturor elementelor situate pe poziţiile
mai mari decât k
- inserarea propriu-zisă a noului element pe poziţia k.

Ştergerea elementului de pe poziţia k presupune operaţiile următoare:


- mutarea la stânga cu o poziţie a tuturor elementelor situate la dreapta
poziţiei k
- micşorarea dimensiunii vectorului cu 1

5. Concatenarea a doi vectori

Rezultatul concatenării a doi vectori de dimensiune n1, respectiv n2, este un al


treilea vector de dimensiune n1+n2, format prin copierea elementelor primului
vector urmată de copierea elementelor celui de-al doilea vector. Concatenarea este

50
Algoritmi şi structuri de date

o operaţie simplă fapt pentru care vor lăsa ca exerciţiu definirea unei funcţii C care
realizează această operaţie.
Observaţie: operaţia de concatenarea diferă semnificativ de operaţia de
interclasare.

6. Interclasarea a doi vectori

Interclasarea este procedeul prin care, pornind de la două secvenţe ordonate de


elemente se formează o secvenţă care conţine toate elementele primelor două şi
este ordonată.
Operaţia de interclasare se aplică asupra a doi vectori sortaţi rezultând un al
treilea vector cu proprietăţile:
- conţine toate elementele vectorilor iniţiali
- este ordonat

Fie X şi Y doi vectori ordonaţi crescător, de dimensiune n, respectiv m:


x[1],x[2],...,x[n]
y[1],y[2],...,y[m]
Se doreşte obţinerea vectorului ordonat z: z[1],z[2],...,z[p], format din toate
elementele celor doi vectori de intrare.
Principiul este de a completa element cu element vectorul rezultat Z, prin
copierea elementelor vectorilor X şi Y păstrând relaţia de ordine. Interclasarea se
realizează prin executarea următoarelor etape:
1. Cât timp mai există elemente de parcurs în ambii vectori: X,Y, aceştia
se parcurg secvenţial:
a. Se compară elementele curente ale celor doi vectori X şi Y, şi
cel mai mic dintre acestea se copiază în vectorul Z
2. Dacă au mai rămas elemente neparcurse în vectorul X, se vor copia în
Z
3. Dacă au mai rămas elemente neparcurse în vectorul Y, se vor copia în
Z

Parcurgerea vectorilor X,Y,Z presupune utilizarea a trei variabile cursor cu


semnificaţia poziţiei curente în vectorul corespunzător:
Fie i – cursorul care indică poziţia curentă în vectorul X
Fie j – cursorul care indică poziţia curentă în vectorul Y
Fie k – cursorul care indică poziţia curentă în vectorul Z

Algoritmul Interclasare este:


Citeşte n, x[1],x[2],...,x[n]
Citeşte m, y[1],y[2],...,y[m]
Fie i=1,j=1 //poziţiile de start în vectorii de intrare
Fie k=1 //poziţia de start în vectorul rezultat
Câttimp(i<=n şi j<=m)
Dacă x[i]<y[j] atunci
z[k]=x[i]

51
Algoritmi şi structuri de date

k=k+1 //trecere la următoarea poziţie în vectorul Z


i=i+1//trecere la următoarea poziţie în vectorul X
Altfel
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

Adesea, este necesară prelucrarea datelor structurate în tablouri


multidimensionale. Un caz particular este cel al tablourilor bidimensionale,
cunoscute sub denumirea de matrice. Structura de matrice este reprezentată în
matematică prin:
 x11 x12 ... x1n 
 
x x 22 ... x2n 
X =  21
... ... ... ... 
 
x xm2 ... x mn 
 m1

Toate elementele unei matrice sunt de acelaşi tip şi dimensiunile matricei se


referă la numărul de linii (m) şi de coloane (n). În limbajul C, declararea unei
structuri de tablou bi-dimensional se face prin construcţia:
tipElement NumeTablou [linii] [coloane], unde:
- tipElement este tipul de dat[ al elementelor
- linii şi coloane – specifică dimensiunea memoriei alocate tabloului
identificat prin NumeTablou
Un tablou bidimensional este reprezentat într-o succesiune de locaţii de
memorie referite prin acelaşi identificator (NumeTablou). Fiecare element al
tabloului este referit prin poziţia sa în cadrul şirului. Poziţia este precizată prin

52
Algoritmi şi structuri de date

două numere pozitive (indecşi), care reprezintă cele două dimensiuni (linie şi
coloană).

Prin declaraţia tipElement NumeTablou [linii] [coloane] s-au alocat în memorie


un număr de octeţi egal cu linii*coloane*sizeof(tipElement), necesari memorării
elementelor acestei structuri de date. Zonă de memorie rezervată tabloului este
contiguă.
Numele tabloului este adresa primului element memorat, accesat prin
construcţia: NumeTablou[0][0], astfel încît în prelucrările tablourilor, în mod uzual
se consideră numerotarea liniilor şi a coloanelor începând de la 0.
Accesarea elementului de pe linia i, coloana j se face prin construcţia
NumeTablou[i][j]. Ne reamintim că sintaxa NumeTablou[i] este echivalentă unei
operaţii cu pointeri exprimată prin expresia: NumeTablou+i. Într-o manieră
similară, expresia NumeTablou[i][j] poate fi exprimată printr-o operaţie cu
pointeri: NumeTablou+i+j.
Citirea elementelor unei matrice este posibilă prin citirea pe rînd a fiecărui
element din compunerea sa. În acest sens se vor parcurge mulţimile indecşilor de
linie şi coloană prin două instrucţiuni repetitive (ciclice) imbricate. Codul C de mai
jos are ca efect citirea unei matrice de numere întregi:

int mat[10][10];
int i,j;
printf(“\nIntroduceti nr. de linii ”);scanf(“%d”,&m);
printf(“\nIntroduceti nr. de coloane ”);scanf(“%d”,&n);

for(i=0; i<m; i++)


for(j=0; j<n; j++)
{
printf(“\n Matrice[%d][%d] = : ”,i,j);
scanf(“%d”,&mat[i][j]);
}
Afişarea valorilor unei matrici (în formatul uzual) este descrisă prin secvenţa
următoare :

printf(“\n Matricea este: ”);


for(i=0; i<m; i++)
{
for(j=0; j<n; j++)
printf(“%d”, mat[i][j]);
printf(“\n”);
}

În programele de prelucrare a matricelor este utilă implementarea funcţiilor


corespunzătoare operaţiilor de intrare –ieşire cu aceste structuri: citirea unei
matrice, afişarea unei matrice. Funcţiile respective vor avea ca parametri atât
dimensiunile tabloului (numărul de linii şi coloane) dar şi adresa primului element
(numele tabloului). Parcurgerea în cadrul funcţiilor a tablourilor cu ajutorul

53
Algoritmi şi structuri de date

indecşilor ascunde în fapt un calcul de adrese: cunoaşterea adresei de început a


zonei de memorie alocate tabloului permite accesarea tuturor elementelor sale.
În privinţa parametrilor formali, antetul funcţiei de citire poate fi exprimat:
void citire (tipElement NumeTablou[Mmaxim][Nmaxim], int linii, int coloane)

Exemplu: Programul următor determină suma şi produsul a două matrici:

#include <stdio.h>
#include <conio.h>
#define DimMax 10 //numărul maxim de elemente
//pe linii si coloane

void afisare_matrice(int n,int m,int a[DimMax][DimMax])


{
int i,j;
for(i=0;i<n;i++)
{ for(j=0;j<m;j++)
printf("%d ",a[i][j]);
printf("\n");
}
}
void citire_matrice(int *n,int *m,int a[DimMax][DimMax])
{
int i,j;
printf("\nIntroduceti nr. de linii n=");scanf("%d",n);
printf("\nIntroduceti nr. de coloane m=");
scanf("%d",m);
printf("\nIntroduceti elementele matricei\n");
for (i=0;i<*n;i++)
for(j=0;j<*m;j++)
{
printf("a[%d,%d]=",i,j);scanf("%d",&a[i][j]);
}
printf("\n");
}

void suma(int n,int m, int a[DimMax][DimMax],


int b[DimMax][DimMax], int c[DimMax][DimMax])
{
int i,j;
for(i=0;i<n;i++)
for(j=0;j<m;j++)
c[i][j]=a[i][j]+b[i][j];
}

void produs(int n,int m,int p, int a[DimMax][DimMax],


int b[DimMax][DimMax],int c[DimMax][DimMax])
{
int i,j,k;
int s;
for(i=0;i<n;i++)

54
Algoritmi şi structuri de date
for(j=0;j<p;j++)
{
s=0;
for(k=0;k<m;k++)
s=s+a[i][k]*b[k][j];
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

Sortarea reprezintă procedeul prin care o mulţime de elemente este aranjată


după o relaţie de ordine dată.

Sortarea prin selecţie


Fie x1,x2,...,xn un vector de n elemente.
Principiul este acela de a considera subvectorul xi,…,xn şi de a determina
minimul din această secvenţă, urmând ca minimul rezultat să se interschimbe cu
elementul xi. Procedeul se va repeta pentru oricare i=1,…,n-1.

Algoritm SortareSelecţie este:


Citeşte n, x1,x2,...,xn
Pentru i de la 1 la n-1
//determină poziţia şi valoarea minimului subvectorului xi,…,xn
pozmin=i
min=xi
Pentru j de la i+1 la n
Dacă xj <min atunci
pozmin=j
min=xj
SfDacă
sfPentru
//interschimbare xi cu min
xpozmin=xi
55
Algoritmi şi structuri de date

xi=min
SfPentru
SfAlgoritm

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 citireSir(int x[],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[],int n)


{
int i;
for(i=0;i<n;i++)
printf("%d ",x[i]);
}

void SSort(int x[],int n)


{
int i,j,aux,poz;
for(i=0;i<n;i++)
{
//caut in sirul i .... n elementul minim
aux=x[i];poz=i;
for(j=i+1;j<n;j++)
if (x[j]<aux)
{aux=x[j];
poz=j;}
//interschimb cu x[i]
x[poz]=x[i];
x[i]=aux;
}
}

void main()
{
int a[100],n;
citireSir(a,&n);
SSort(a,n);

56
Algoritmi şi structuri de date
tiparireSir(a,n);
}

Sortarea prin inserţie


Fie x1,x2,...,xn un vector de n elemente
Principiul acestei tehnici este acela de a privi tabloul ca fiind împărţit în două
subtablouri: x1,...,xi-1 şi xi,...,xn. Se presupune că primul subtablou este deja ordonat
şi se urmăreşte inserarea elementului xi în subtabloul ordonat astfel încât după
efectuarea inserţiei subtabloul rezultat să rămână ordonat. Acest procedeu se va
continua privind tabloul iniţial ca două subtablouri : x1,...,xi şi xi+1,...,xn. Cunoscând
că primul subtablou este deja ordonat (ne-am asigurat de acest lucru la operaţia de
inserare precedentă), se va continua cu inserarea elementului xi+1 în x1,...,xi şi
obţinerea subtabloului ordonat x1,...,xi+2. Procedura se repetă până când nu mai sunt
elemente de inserat în subtabloul stâng.
Inserarea unui element oarecare xi presupune determinarea poziţiei în care va fi
depus. Aceasta se rezumă la parcurgerea subtabloului în care se va face inserarea,
de la stânga la dreapta, şi determinarea primul element xk care este mai mare decât
xi. În acel moment k devine poziţia pe care va fi depus xi. Parcurgerea subtabloului
se poate face şi în sens invers, de la dreapta la stânga, însă în acest caz se va
determina k – poziţia primului element xk care verifică condiţia că este mai mic
decât xi.
Prima împărţire a tabloului este în punctul i=2, ceea ce produce un subvector
format din doar elementul x1 şi subvectorul x2,...,xn. Se poate observa că primul
subtablou este deja ordonat.

Algoritm SortareInserţie este:


Citeşte n, x1,x2,...,xn
Pentru i de la 2 la n
//inserez elementul xi în subvectorul stâng
x0=xi
j=i-1
Câttimp (xj>x0)
xj+1=xj
j=j-1
SfCâttimp
xj+1=x0
SfPentru
SfAlgoritm

Exemplu: Funcţia următoare realizează sortarea prin inserţie a unui vector.

void SInsert(int x[],int n)


{
int i,j,aux,k;
for(i=1;i<n;i++)
{
//caut pentru x[i] pozitia buna in subsirul din stanga

57
Algoritmi şi structuri de date
//1.....i-1
k=i-1; aux=x[i];//salvez valoarea curenta de inserat în aux
while ((aux<x[k])&& (k>=0))
//primul element mai mic decat aux
{x[k+1]=x[k];
k--;
}
// k+1 reprezinta pozitia pe care se insereaza aux
x[k+1]=aux;
}
}

Sortarea prin interschimbare (BubbleSort )

Metoda sortării prin interschimbare constă în parcurgerea repetată a vectorului


şi compararea pe rând a perechilor de elemente consecutive urmată de
interschimbarea valorilor acestora în situaţia în care relaţia de ordine dorită nu este
verificată.
La fiecare parcurgere se va presupune că vectorul este ordonat (marcarea acestei
presupuneri se face printr-un cod), însă la determinarea unei perechi de elemente
consecutive care necesită interschimbare, presupunerea este anulată.
Algoritmul se va termina în condiţiile în care la o parcurgere anterioară,
completă a vectorului, presupunerea s-a dovedit adevărată.

Algoritm SortareInterschimbare este


//ordonare crescătoare după valorile elementelor
Citeşte n, x1,x2,...,xn
Repetă
Ordonat=Adevărat
Pentru i de la 1 la n-1
Dacă xi>xi+1 atunci
Cheamă Interschimbare(xi,xi+1)
Ordonat=fals
SfDacă
SfPentru
Pânăcând (Ordonat=Adevărat)
SfAlgoritm

Exemplu: Funcţia următoare realizează sortarea prin interschimbare a unui vector.

void SBubble(int x[],int n)


{
int cod; //false sau adevarat
int i,j,aux,k;
do
{
cod=0; //presupun vectorul ordonat
for(i=0;i<n-1;i++)

58
Algoritmi şi structuri de date
{
if (x[i]>x[i+1])
{
//Interschimb vecinii
aux=x[i];
x[i]=x[i+1];
x[i+1]=aux;
cod=1;
//am gasit vecini care nu respecta
//relatia de ordine
}
}
}while (cod!=0);
}

Probleme propuse spre rezolvare:

1. Se dă o matrice cu numere întregi, de n linii şi m coloane. Să se determine


numărul elementelor pozitive din compunerea matricei.
2. Se dă un tablou bidimensional de numere întregi organizat în n linii şi n
coloane (matrice pătratică). Să se ordoneze crescător elementele de pe
diagonala principală a matricei.
3. Fiind dată o matrice A de n linii şi coloane, formată din numere reale, să
se construiască o matrice B de n linii şi m+1 coloane, ale cărei prime m
coloane sunt copiate din matricea A şi ultima coloană este formată din
valorile sumelor elementelor de pe fiecare linie a matricei A.
4. Să se determine inversa unei matrice.
5. Determinaţi cel mai mic număr pozitiv al unui vector de n numere întregi.

59
Algoritmi şi structuri de date

V. STRUCTURI. TIPURI DE DATE DEFINITE DE UTILIZATOR.

Datele pot fi clasificate în două categorii:


- Date simple, izolate: tipurile acestora sunt predefinite în limbajul C (ex.
int. double, float, char)
- Date grupate, sau structurate: tablourile şi structurile, care permit
prelucrarea globală dar şi individuală a fiecărui element al datei respective.

Structurile: reprezintă o modalitate de grupare a datelor care nu sunt neapărat


de acelaşi tip (spre deosebire de tablouri). Este necesară uneori referirea la o dată
care cuprinde mai multe elemente, exemplu data calendaristică , având
componentele: zi, luna, an.
Prin structură în limbajul de programare, înţelegem o colecţie de date
neomogene (de tipuri diferite), grupate sub un singur nume. Exista posibilitatea
accesului la fiecare componentă a structurii , cât şi posibilitatea referirii în
ansamblu a structurii definite.

Sintaxa declarării unei structuri:


struct NumeStructură
{
<instructiuni de declarare de tip>
} nume1, nume2, …, numeN;

unde:
<instructiune de declarare de tip>::= <tip> <lista identificatori>

Observaţie: Identificatorii: NumeStructură, nume1, …, numeN pot sa lipsească


dar nu toţi odată. Astfel:
- Dacă NumeStructură lipseşte, cel puţin nume1 trebuie să apară în construcţie
- Dacă nume1, …, numeN lipsesc, atunci este obligatoriu ca NumeStructură se
fie prezent.
- În situaţia în care NumeStructură este prezent, prin acest lucru se defineşte
un nou tip de dată denumit NumeStructură.
-Dacă nume1, … numeN sunt prezente ele reprezintă date de tipul nou introdus
NumeStructură.
- Dacă una dintre datele din compunerea unei structuri este de tipul pointer la
tipul de date introdus prin structura respectivă, numim structura recursivă:

struct NumeStructură //structura recursivă


{
struct NumeStruct * data; //pointer la tipul de date struct NumeStructură
<instructiuni de declarare de tip>
} nume1, nume2, …, numeN;

Exemple:

60
Algoritmi şi structuri de date

struct DataCalendaristică
{
int zi;
int luna;
int an;
} DataNasterii, DataAngajarii;

-prin această construcţie am introdus un tip nou (DataCalendaristică) şi am definit


două variabile de tipul nou introdus (DataNasterii şi DataAngajarii)

struct {
int zi;
int luna;
int an;
} DataNasterii, DataAngajarii;

-prin această construcţie NU am introdus un tip nou ci doar am definit două


variabile structurate (DataNasterii şi DataAngajarii).

struct DataCalendaristică{
int zi;
int luna;
int an;
}

struct DataCalendaristică DataNasterii, DataAngajarii;

- prin această construcţie am definit un tip nou (DataCalendaristică) şi ulterior s-au


declarat două variabile structurate (DataNasterii şi DataAngajarii) de tipul
DataCalendaristică.

Există posibilitatea declarării unor tablouri de date structurate. Spre exemplu,


avem nevoie într-un program de un şir de 100 date calendaristice, pe care dorim
ulterior să le prelucrăm. Cum se declară un tablou de date structurate?

struct NumeStructură
{
<instrucţiuni de declarare de tip>
}
struct NumeStructură TablouDeStructuri[N];

- prin această construcţie am declarat un tablou unidimensional de N date


structurate. Fiecare element al tabloului este reprezentat de o grupare de date.
Exemplu:
struct DataCalendaristică
{
int zi; int luna;int an;
}

61
Algoritmi şi structuri de date
struct DatăCalendaristică SirDate[100];

- fiecare element al tabloului SirDate are trei componente: zi, luna, an şi


reprezintă o dată calendaristică.

Există posibilitatea ca în cadrul definirii unei structuri de date să apară o


declaraţie a unei date de tip structurat. Spre exemplu, dorim să prelucram date
grupate de tipul Persoană. Pentru fiecare dată de acest tip avem mai multe
informaţii de identificare:nume, prenume, adresă, data naşterii. Se observă că data
naşterii la rândul său este o dată structurată (zi, lună, an).
Exemplu de structuri imbricate:
struct DataCalendaristică {
int zi; int luna; int an;
};
struct Persoana {
char nume[10]; char prenume[10]; char adresa[25];
struct DataCalendaristica dataNasterii;
};

//declaraţia unei date (Pers1) de tipul Persoana


struct Persoana Pers1;

Accesul la componentele unei structuri

Pentru a putea accesa componente ale unei date structurate se va utiliza


operatorul punct “.”. Dacă avem o structură cu numele NumeStructură şi o
componentă a sa cu numele NumeComponentă, referirea componentei respective
se face prin construcţia:
NumeStructură.NumeComponentă
Pentru structuri imbricate se foloseşte operatorul “.” de câte ori este
nevoie:Dacă dorim să accesăm anul naşterii persoanei Pers1 din exemplul
precedent, acest lucru se face printr-o construcţie de forma:
Pers1.dataNasterii.an
Fie structura:
struct DataCalendaristică
{ int zi, luna, an; }
struct DatăCalendaristică SirDate[100];

Pentru a accesa o componentă a unui element al unui tablou de date structurate


se folosesc construcţii de forma :
SirDate[24].zi , SirDate[1].luna , SirDate[k].an.

Pointeri la structuri

Declararea unui pointer la o structură NumeStructură se face prin construcţia:


struct NumeStructură *p;
În acest caz, p este o variabilă pointer, şi are ca valoare adresa unei date de tipul
NumeStructură.

62
Algoritmi şi structuri de date

Exemplu. struct DataCalendaristică *p;

Avem nevoie de pointeri la structuri în următoarea situaţie:


- În cazul în care avem o funcţie ce prelucrează o variabilă de tipul unei
structuri, transmisă ca parametru, şi dorim ca efectul modificărilor din
corpul funcţiei apelate să se regăsească şi în functia apelanta. Spre
exemplu, dorim să citim o dată de tip structură printr-o funcţie, iar
valorile citite în corpul funcţiei trebuie să fie vizibile în programul
apelant.
Exemplu:
void Citire(struct DataCalendaristică *p)
{ int ziuaN,lunaN,anulN;
scanf(“%d”,&ziuaN);
(*p).zi=ziuaN;
scanf(“%d”,&lunaN);
(*p).luna=lunaN;
scanf(“%d”,&anulN);
(*p).an=anulN;
}

Struct DataCalendaristica DC;


//Apelul functiei:
Citire(&DC);

Pentru simplificarea sintaxei codului sursă, construcţia (*p).NumeComponentă


se poate înlocui cu contrucţia: p->NumeComponentă.

Tipuri de date utilizator

Î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

În cazul tipurilor definite de utilizator, introduse prin declaraţii de structură,


putem asigna un nume noului tip printr-o construcţie de forma:
typedef struct NumeStructură
{
<instructiuni de declarare de tip>
} NumeTip;
Declararea unor variabile de tipul anterior se face astfel:
NumeTip data1, data2, … datan;

Exemplu1: Program care citeşte un şir de structuri de tipul Persoana şi afişează


acest şir ordonat după câmpul nume.

#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 citirePers(TPersoana *p)


{
char aux[40];
printf("\n Introduceti CNP="); scanf("%s",aux);
strcpy(p->cnp,aux);
printf("\n Introduceti Nume="); scanf("%s",aux);
strcpy(p->nume,aux);
printf("\n Introduceti Prenume="); scanf("%s",aux);
strcpy(p->prenume,aux);
printf("\n Introduceti zi nastere:");
scanf("%d",&p->datanasterii.zi);
printf("\n Introduceti luna nastere:");
scanf("%d",&p->datanasterii.luna);
printf("\n Introduceti an nastere:");
scanf("%d",&p->datanasterii.an);
}

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 sortNUME(TPersoana per[],int nr)


{
int cod,i;
TPersoana aux;
do{
cod=1;
for(i=0;i<=nr-2;i++)
if(strcmp(per[i].nume,per[i+1].prenume)>0)
{
//interschimb per[i] cu per[i+1]
aux=per[i];
per[i]=per[i+1];
per[i+1]=aux;
cod=0;
}
}while (cod==0);
}

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]);
}

Probleme propuse spre rezolvare:

3. Să se definească un nou tip de dată pentru entitatea Punct (3D), să


se denumească acest tip TipPunct şi să se declare 3 date de acest
tip utilizator.
4. Scrieţi un program care citeşte într-un tablou un număr de k
structuri de tipul utilizator Marfa. Câmpurile structurii sunt:
codMarfa, DenumireMarfa, PretMarfa, CantMarfa, Valoare. Se
vor calcula valorile mărfurilor (valoare=pret*cantitate) şi tabloul
se va ordona descrescător după valorile obţinute şi se va afişa pe
ecran. (valoarea produselor nu se va citi, aceasta urmînd a fi
calculată după formula dată)
5. Să se scrie un program care:
- Defineşte un nou tip de date Punct2D (puncte în plan)
- Defineşte un tip de date Triunghi

65
Algoritmi şi structuri de date

- Citeşte de la tastatură un tablou unidimensional (şir) de


date de tipul triunghi.

66
Algoritmi şi structuri de date

TIP ABSTRACT DE DATE

Prin tip de date se înţelege domeniul valorilor şi operaţiile specificate datelor de


acel tip. Tipurile de date se clasifică în tipuri elementare (ex. numeric real, numeric
întreg, caracter) şi structurate (omogene – tablouri, neomogene - structuri).
Majoritatea limbajelor de programare oferă posibilitatea manipulării datelor de tip
elementar dar şi introducerea de noi tipuri de date. Pentru tipurile de date definite
de utilizator, programatorul îşi va construi module specifice de prelucrare a datelor
de acel tip.
Prezentăm ca exemplu un modul C care conţine definirea tipului de date
abstract Rational.
Reamintim că un număr raţional este un număr care poate fi exprimat prin
fracţia m/n, unde m şi n sunt numere întregi. Reprezentarea unui număr raţional se
poate face printr-o structură cu două componente de tip întreg, semnificând întregii
m şi n ai fracţiei m/n. Identificăm următoarele operaţii cu numere raţionale:
- Iniţializare: stabileşte valorile implicite pentru m=0,n=1.
- Definire: stabileşte valorile pentru componentele m şi n.
- Modificare unei componente m sau n (setm, setn)
- Accesarea unei componente (getm, getn)
- Adunarea a două numere raţionale
- Înmulţirea a două numere raţionale
- Reducerea unui raţional (prin împărţirea lui m şi n la cel mai mare divizor
comun al celor doi întregi
- Testarea egalităţii a două numere raţionale
- Testarea relaţiei de ordine dintre două numere raţionale
- Inversul unui raţional

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;

void initializare(Rational *a)


{
a->m = 0;
a->n = 1;
}

void definire(Rational *a, int m_nou, int n_nou)


{ a->n = n_nou;

67
Algoritmi şi structuri de date
a->m = m_nou;
}

void setm(Rational *a, int m_nou)


{
a->m = m_nou;
}

void setn(Rational *a, int n_nou)


{
a->n = n_nou;
}

int getm(Rational a)
{ return a.m;
}

int getn(Rational a)
{ return a.n;
}

int cmmdc(int x, int y)


{ int aux;
if (x < y)
{
aux = x;
x = y;
y = aux;
}
while ( x != y)
if ( x > y)
x = x -y;
else
y = y -x;
return x;
}

void reduce(Rational *a)


{ int x; //cel mai mare divizor comun
x = cmmdc(a->m, a->n);
a->m = a->m / x;
a->n = a->n / x;
}

void print(Rational a)
{ printf("\n %d/%d ", a.m, a.n);
}

void inmultire(Rational a, Rational b, Rational *c)


{ c->m = a.m * b.m ;
c->n = a.n * b.n;

68
Algoritmi şi structuri de date
reduce(c);
}

void adunare(Rational a, Rational b, Rational *c)


{ c->m = (a.m * b.n + b.m * a.n);
c->n = a.n * b.n;
reduce(c);
}

int egalitate(Rational a, Rational b)


{if ((a.m*b.n)==(a.n*b.m))
return 1;
return 0;

Rational invers(Rational a)
{ Rational aux;
aux.m=a.n;
aux.n=a.m;
return aux;
}

int maiMic(Rational a, Rational b)


//verifica relatia de ordine a<b
{if (((a.n*b.n>0) &&(a.m*b.n<=a.n*b.m))||
((a.n*b.n<0) &&(a.m*b.n>=a.n*b.m)))
return 1;
return 0;
}

Problemă: Urmând exemplul prezentat anterior (TAD Rational) să se conceapă


tipul abstract de date Complex, dezvoltând o bibliotecă de funcţii specifice
operaţiilor cu numere complexe.

69
Algoritmi şi structuri de date

VI. LUCRUL CU FIŞIERE

Prin fişier se înţelege colecţie ordonată de elemente pe care le numim


înregistrări. Fişierele sunt păstrate pe suporturi externe reutilizabile: hard disk,
floppy disk, etc. Prelucrarea fişierelor implică un număr de operaţii specifice
acestora:

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

Operaţiile specifice fişierelor se realizează prin intermediul unor funcţii


standard existente în biblioteca limbajului C. Există două nivele de tratare a
fişierelor:
1. Nivelul inferior – face apel direct la sistemul de operare
2. Nivelul superior – face apel la proceduri specializate de prelucrare a
fişierelor, care utilizează structuri speciale de tipul FILE.

Nivelul inferior de prelucrare a fişierelor

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ţie: Indicatorii de mod pot fi combinaţi. Astfel pentru a crea un fişier în


care avem drepturi de scriere şi citire vom utiliza construcţia: S_IREAD|
S_IWRITE.
Funcţia creat returnează un număr întreg care are semnificaţia descriptorului de
fişier (dacă este pozitiv) sau eroare (dacă este -1).
Descriptorul de fişier este reprezintă un număr întreg pozitiv ataşat fişierului
prelucrat, prin intermediul căruia se identifică un fişier în operaţiile specifice
realizate asupra acestuia.
2. Deschiderea unui fişier
Deschiderea fişierului se realizează prin apelul funcţiei open al cărei prototip
este:
int open (const char* cale, int acces );
Utilizarea acestei funcţii presupune includerea fişierelor io.h şi fcntl.h:
#include <io.h>
#include <fcntl.h>

- cale – pointer la un şir de caractere prin care se specifică calea completă a


fişierului ce va fi deschis.
- acces – întreg prin care se specifică modul de acces la fişier:
 O_RDONLY – numai în citire
 O_WRONLY– numai în scriere
 O_RDWR – citire-scriere
 O_APPEND – adăugare la sfârşitul fişierului
 O_BINARY – prelucrare binară
 O_TEXT – prelucrare text

Valorile de acces pot fi combinate prin caracterul ‘|’ , spre exemplu:


O_RDONLY|O_BINARY
Funcţia returnează un întreg nenegativ (descriptorul de fişier) sau –1 în caz de
eroare.
Exemplu:
int df;
df=open(“nume.txt”,O_APPEND);

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

71
Algoritmi şi structuri de date

Exemplu:
int df;
df = open (“C:\\Borlandc\\bin\\text.cpp”, O_RDWR);

3. Citirea din fişier


Se realizează prin funcţia read, prototipul acesteia se află în fişierul io.h:
int read(int df, void *mem, unsigned lung);
unde:
- df – descriptorul ataşat fişierului (returnat de funcţia open)
- mem – pointer spre zona de memorie în care se va păstra înregistrarea
citită din fişier
- lung – lungimea înregistrării exprimată în număr de octeţi

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

Funcţia write returnează un întreg reprezentând numărul de octeţi scrişi din


fişier, în general numărul specificat de parametrul lung. Dacă cele două valori sunt
diferite, este semnalat prin acesta un caz de eroare.
Exemplu: scrierea într-un fişier a unei secvenţe de caractere

int df, i;
char text[30] = “Acest text se scrie in fisier!”;
df=open(“proba.txt”,O_APPEND);
write(df, text, 30);

72
Algoritmi şi structuri de date

5. Poziţionarea în fişier
Citirea şi scrierea în fişiere se face în mod secvenţial, ceea ce înseamnă că
înregistrările se scriu una după alta pe suportul fişierului şi citirea se face în
aceeiaşi ordine în care au fost scrise. Dacă dorim ca accesul la înregistrări să se
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ă

2 – deplasamentul se consideră de la sfârşitul fişierului


Funcţia returnează poziţia faţă de începutul fişierului.

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

Nivelul superior de prelucrare a fişierelor


Funcţii de manipulare a fişierelor sunt definite în bibliotecile standard ale
limbajului. Prototipurile funcţiilor se găsesc în fişierul antet stdio.h. Fişierul care
se prelucrează este identificat în mod unic printr-un pointer de fişier (uzual
denumim acest pointer de fişier pf).
În limbajul C există tipul de date FILE (fişier) prin care se defineşte structura
datelor dintr-un fişier. În declaraţiile de tip vom avea nevoie de o secvenţă:
FILE *pf;
1. Deschiderea unui fişier
Funcţia de deschidere a unui fişier se numeşte: fopen şi prototipul funcţiei este
următorul:
FILE *fopen(const char *nume, const char *mod);
În declaraţiile de tip vom avea nevoie de o secvenţă:
FILE *pf, fopen();
Argumentele funcţiei:
nume – şir de caractere prin care se identifică numele fişierului de pe disc
pe care dorim să-l accesăm.
mod – şir de caractere prin care se specifică modul de acces la fişier.
Modurile valide de deschidere a fişierului sunt:
“r” - deschidere pentru citire

73
Algoritmi şi structuri de date

“w” - deschidere pentru scriere (informaţia din fişier se pierde la


deschiderea în acest mod)
“a” - deschide pentru scriere (fără pierderea informaţiei din fişier) sau
creează fişier
“r+” - deschide pentru actualizare
“w+” - deschide sau creează fişier pentru actualizare (pierderea
informaţiei)
“a+” - deschide sau creează fişier pentru actualizare (fără pierderea
informaţiei anterioare)
Efectul apelării funcţiei: deschide fişierul specificat prin nume în modul de
acces dorit şi returnează un pointer spre structura FILE (FILE *pf) care ulterior se
utilizează pentru a identifica fişierul în operaţiile ce se efectuează asupra sa. În caz
de insucces, funcţia returnează NULL.
Exemplu:
FILE *pf; //pointer spre FILE

pf=fopen(“fis1.dat”, “r”);
// deschid fişierul fis1.dat pentru a putea citi informaţii
if (pf==NULL)
{
printf(“\n Nu s-a putut deschide fisierul”);
exit(1); //iesire din program
}

2. Închiderea fişierului
Funcţia de închidere a unui fişier se numeşte fclose şi are prototipul:
int fclose(FILE *pf);
Argumentul funcţiei:
pf – pointer spre FILE, identifică fişierul ce se închide
Funcţia returnează 0 în caz de succes sau EOF (end of file) în caz de insucces.
În declaraţiile de tip vom avea nevoie de o secvenţă:
int fclose();
3. Scrierea în fişier
Funcţia de scriere în fişier: fprintf are prototipul:
int fprintf(FILE *pf, const char* sir_format, lista_arg);
Argumentul funcţiei:
pf – pointer spre FILE, identifică fişierul ]n care se efectuează scrierea
sir_format, lista_arg - au aceiaşi semnificaţie ca şi în cazul funcţiei
printf
Funcţia returnează în caz de succes numărul de caractere scrise sau un număr
negativ în caz de insucces.
Exemplu:
fprintf(pf, “Rezultatul este: %d”, rezultat);

4. Citirea din fişier


Funcţia de citire din fişier: fscanf are prototipul:
74
Algoritmi şi structuri de date

int fscanf(FILE *pf, const char* sir_format, lista_arg);


Argumentul funcţiei:
pf – pointer spre FILE, identifică fişierul din care se efectuează citirea
sir_format, lista_arg - au aceiaşi semnificaţie ca şi în cazul funcţiei scanf
Funcţia returnează în caz de succes numărul de caractere citite sau un număr
negativ în caz de insucces.
Exemplu:
fscanf(pf, “%d”, &x);
5. Poziţionarea în fişier
În mod firesc accesul la înregistrările unui fişier se produce în mod secvenţial,
prin parcurgerea înregistrărilor de la începutul fişierului până la înregistrarea
dorită. Funcţia de bibliotecă fseek este utilă deoarece are ca efect poziţionarea în
fişier fără a fi necesară parcurgerea explicită a înregistrărilor anterioare celei
căutate.
Prototipul funcţiei este:
int fseek (FILE *pf, long increment, int mod)
mod poate fi:
0 – dacă increment – indică numărul de octeţi faţă de începutul fişierului
1 – dacă increment – indică numărul de octeţi faţă de poziţia curentă
2 – dacă increment – indică numărul de octeţi faţă de sfârşitul fişierului
În parcurgerea înregistrărilor dintr-un fişier, în mod frecvent este necesară
verificarea dacă s-a ajuns sau nu la sfârşitul fişierului. În acest scop se va folosi
funcţia: feof
int feof(FILE *stream);
Funcţia returnează 1 dacă s-a ajuns la sfârşitul fişierului, 0 dacă nu s-a ajuns
la sfârşitul fişierului şi -1 în caz de eroare.

Alte funcţii de prelucrare a fişierelor

Funcţiile fgetc şi fputc au prototipueile


int fgetc(FILE *pf);
int fputc(int c, FILE *pf);
Efectul: citirea/scrierea unui caracter din/în fişierul identificat prin pointerul pf.
Funcţia fgetc – returnează caracterul citit.
Exemplu: Programul citeşte caracter cu caracter un fişier şi afişează conţinutul
acestuia pe ecran:
#include <stdio.h>
void main(void)
{
int c;
FILE *ptr;

ptr = fopen("c:\\test.txt","r");

while ((c = fgetc(ptr)) != EOF)


{

75
Algoritmi şi structuri de date
printf("%c",c);
}
fclose(ptr);
}

Funcţiile fgets şi fputs au prototipurile


int *fgets(char *sir, int n, FILE *stream);
int fputs(const char *sir, FILE *stream);
Efectul: citirea/scrierea unui şir de caractere din-în fişierul identificat prin
pointerul pf.

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.

Inserarea pe o poziţie k a unei noi înregistrări presupune parcurgerea


următoarelor etape:
1. deschiderea fişierului iniţial în citire
2. crearea unu fişier auxiliar şi deschiderea sa în modul scriere
3. parcurgerea secvenţială fişierului iniţial şi copierea înregistrărilor în
fişierul auxiliar până la înregistrarea k
4. scrierea noii înregistrări în fişierul auxiliar
5. continuarea parcurgerii fişierului iniţial şi continuarea copierii
înregistrărilor în fişierul auxiliar
6. redenumirea fişierului auxiliar cu numele fişierului iniţial

Ştergerea înregistrării k dintr-un fişier presupune de asemenea folosirea unui


fişier auxiliar şi parcurgerea etapelor următoare:
1. crearea fişierului auxiliar şi deschidere sa în modul scriere
2. parcurgerea şi copierea înregistrărilor fişierului iniţial în fişierul auxiliar
până la înregistrarea de pe poziţia k-1
3. citirea înregistrării k fără a fi copiată în auxiliar
4. parcurgerea şi copierea înregistrărilor fişierului iniţial în fişierul auxiliar
de la înregistrarea k+1 până la sfârşit.

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.

Afişarea conţinutului unui fişier. Etape:


Deschide fişierul
Câttimp nu s-a ajuns la sfârşitul fişierului
Citeşte linia curenta
Afişare pe ecran linia citita

76
Algoritmi şi structuri de date

Sfcâttimp
Închidere fişier

Copierea conţinutului unui fişier (fis1.dat) într-un alt fişier (fis2.dat). Etape:
Creează fișierul fis2.dat
Deschide fişierul fis1.dat
Câttimp (nu s-a ajuns la sfârşitul fişierului fis1.dat)
Citeşte din fişierul fis1.dat
Scrie în fişierul fis2.dat
Sfcâttimp
Închidere fişierele.

Probleme:
6. 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.
7. 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.
8. 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

VII. ALOCAREA DINAMICA A MEMORIEI

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

blocului de memorie alocat; returnează 0 dacă nu există spaţiu liber de mărimea


solicitată.
Funcţia realloc:
void *realloc(void* adrBloc, size_t Dimensiune)
Funcţia încearcă să mărească sau să micşoreze un bloc (alocat dinamic)
la numărul de octeţi specificaţi de parametrul Dimensiune. Parametrul adrBloc
indică un bloc de memorie obţinut prin apelarea anterioară a funcţiei malloc,
calloc sau realloc. Dacă parametrul adrBloc este 0, efectul este identic cu al
funcţiei malloc. Dacă Dimensiune este 0 efectul apelului este identic cu cel al
funcţiei free.
Funcţia ajustează mărimea blocului alocat la Dimensiune şi returnează adresa
blocului realocat sau 0 - dacă nu se poate face realocarea.
Funcţia free:
void free(void * p)
Argumentul funcţiei este adresa zonei ce va fi eliberată. Efectul apelului
funcţiei free este de a elibera memoria a cărei adresă este p.

Exemplu:
tip *p;
p= (tip*)malloc(sizeof(tip)) ;
free(p);

Observaţie: operatorul sizeof returnează dimensiunea în octeţi a expresiei:


sizeof(expresie). Construcţia (Tip*) realizează o conversie explicită de tip: din tipul
pointer nedefinit (void *) spre tipul pointer la Tip.

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:

Program – Exemplu de alocarea dinamică a tablourilor.

#include <stdio.h>
#include <stdlib.h>

int *citire_vector(int *pDim)


{
int * vector;
int i;
printf("\nDati n:");scanf("%d", pDim);
if ((vector=(int *)calloc(*pDim,sizeof(int)))==NULL )
{
printf("Insucces la alocarea dinamica");
exit(1);
}
for (i=0; i<*pDim; i++)

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 afisare_vector(int *vector, int n)


{
int i;
for (i=0; i<n; i++)
{
printf("\nvector[%d]=%d", i+1, vector[i]);
}
}

void main(void)
{
int n;
int *vector;
vector=citire_vector(&n);
afisare_vector(vector, n);
}

Program 2 - operaţii cu şiruri de caractere. Programul ca aloca dinamic memorie


pentru un şir de caractere sir2 şi va efectua o copiere a şirului sir1 citit de la
tastatură în zona de memorie rezervată noului şir.

#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

VIII. LISTĂ SIMPLU ÎNLĂNŢUITĂ

Lista reprezintă o mulţime de dimensiune variabilă, formată din elemente de


acelaşi tip. Înţelegem prin listă – o mulţime dinamică şi omogenă, a cărei
dimensiune se modifică în timpul rulării programului.
Memorarea structurilor de date de tip listă se poate face în două maniere:
1. secvenţial - elementele listei sunt memorate la adrese consecutive
2. înlănţuit – elementele listei nu ocupă adrese consecutive, fiecare
element conţine pe lângă informaţia propriu-zisă şi o legătură spre
următorul element.
Memorarea secvenţială se poate produce prin folosirea structurilor de date de tip
tablou. Dinamica listei secvenţiale se realizează în această situaţie prin reţinerea
unui parametru suplimentar cu semnificaţia dimensiunii curente a tabloului. Pentru
listele organizate static, sub formă de tablou, ordinea elementelor este cea implicită
în tablou.
În ciuda simplităţii acestei abordări se preferă varianta elementelor înlănţuite şi
alocate dinamice. Argumentul folosirii listelor înlănţuite rezidă din necesitatea
economiei de memorie. Folosirea tablourilor forţează o declararea a numărului
maxim de elemente ceea ce rareori este cunoscut la momentul dezvoltării
programului. În consecinţă, folosirea unui tablou de o dimensiune mult mai mică
decât maximul declarat, permite manevrarea elementelor sale dar în acelaşi timp
produce o risipă inutilă a unui bloc memorie alocat şi neutilizat.
În spiritul economiei de memorie, abordarea structurilor de date de tip listă prin
liste înlănţuite alocate dinamic este avantajoasă. Organizarea acestor structuri de
date în C se face prin folosirea unor legături care nu sunt altceva decât pointeri
(adrese) spre următoarele elemente din listă. Pentru listele organizate dinamic, sub
formă înlănţuită, ordinea elementelor este dată de pointeri.Pointerii legătură din
compunerea unui element al listei indică adresele unor alte elemente de acelaşi tip.
Elementele unei liste se denumesc în mod uzual noduri. Prin folosirea pointer-ilor
(legături), structura unui nod al listei devine recursivă.
Liste înlănţuite pot fi: simplu, dublu sau multiplu înlănţuite. Clasificarea listelor
se face în funcţie de numărul legăturilor din compunerea unui element.
Operaţiile principale cu liste sunt următoarele:
1. parcurgere
2. creare
3. distrugere (ştergere)
4. adăugare nou element
5. ştergere element

Lista simplu înlănţuită


Structura nodului listei simplu înlănţuite:
Figura următoare prezintă în mod grafic structura unui nod al listei simplu
înlănţuite, punându-se în evidenţă cele două părţi ale nodului:
1. zona de informaţii, formată din unul sau mai multe câmpuri

81
Algoritmi şi structuri de date

2. legătura spre alt nod de acelaşi fel

info
O structură care descrie compunerea unui element de acest gen este următoarea:
struct nod
adr
{
//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


…….
Pentru implementarea operaţiilor specifice listelor înlănţuite este utilă
declararea a doi pointeri la tipul Tnod, cu semnificaţia adreselor primului şi
ultimului element din listă:
Tnod *prim, *ultim;

Parcurgerea listei

prim
Parcurgerea listei presupune accesarea sau prelucrarea elementelor listei în
ordinarea stabilită de legăturile conţinute de noduri. Cunoscând primul element
prim , acesta conţine adresa următorului element, care la rândul său conţine adresa
următorului, etc. În acest mod, prin urmărirea adreselor de legătură pot fi accesaţi
toţi membrii listei. Terminarea operaţiei de parcurgere a listei constă în accesarea
ultimului element, marcat prin adresă nulă a pointerului urm.

82
Algoritmi şi structuri de date

Considerând p adresa nodului curent din listă, trecerea la nodul următor se


obţine prin asignarea: p=p->urm;
Dacă nodul curent p este Null (p==0), se încheie parcurgerea.
Paşii parcurgerii unei liste sunt următorii:
1. iniţializează pointer-ul curent cu adresa primului nod : p=prim
2. câttimp (pointerul curent este diferit de 0: p!=0 ) execută
a. prelucrare nodul referit de p (accesare, modificare conţinut)
b. trecerere la următorul nod p=p->urm
Oferim în continuare o funcţie C prin care se parcurge lista simplu înlănţuită
gestionată de prim şi ultim şi sunt afişate informaţiile nodurilor traversate.

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
}
}

Crearea listei vide


Crearea unei liste înlănţuite care nu conţine niciun element presupune
iniţializarea celor doi pointeri prim şi ultim cu valoarea 0:
prim=ultim=0;
Crearea unei liste cu mai mult de 0 noduri presupune adăugarea în mod repetat
a noilor noduri până la întâlnirea unei condiţii de terminare a procedeului. Noile
noduri pot fi adăugate sistematic după ultimul element al listei, sau înaintea
primului nod. În procesul de adăugarea a unui nou nod se ţine cont de două
aspecte:
1. nodul nou trebuie alocat în memorie şi încărcat cu informaţie
2. anumite legături din listă trebuie refăcute pentru a asigura consistenţa
organizării
Prezentăm în continuare o funcţie C care are efectul alocării şi încărcării unui
nou nod de tipul Tnod. Utilitatea acestei funcţii o vom înţelege în construirea
funcţiilor de inserare - adăugare noduri la listă:
tnod * incarca_nod()
{
tnod *p;
p=(tnod*)malloc(sizeof(tnod)); //alocare memorie
printf("\n dati cheia"); scanf("%d",&p->cheie);
//citire cheie
… //citire alte informaţii conţinute de nod
return p; //returnarea adresei nodului încărcat
}

83
Algoritmi şi structuri de date

Inserarea unui nou element:

1. Înaintea primului nod


Etapele introducerii unui nou nod înaintea primului nod al listei gestionate prin
pointerii prim şi ultim sunt următoarele:
1. alocarea şi încărcarea noului nod:
2. stabilirea faptului că acest nou nod va adresa ca următor element
chiar pe nodul prim: nou->urm=prim
3. stabilirea noului nod ca prim element al listei: prim=nou
Observaţie: dacă lista este vidă în momentul încercării de a adăuga un 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 *p;
p=incarca_nod();
if (prim= =0) //lista vida
{prim=p;
1
ultim=p;

}
ultim->urm=0;
return; prim
p->urm=prim;
prim=p;
}

2. După ultimul nod


nou
Etapele introducerii unui nou nod după ultimul nod al listei gestionate prin
pointerii prim şi ultim sunt următoarele:
1. alocarea şi încărcarea noului nod:
2. stabilirea faptului că acest ultimul nod va adresa ca următor
element pe noul nod: ultim->urm=nou (1)
3. stabilirea noului nod ca ultim element al listei, şi marcarea acestuia
ca ultim nod din listă: ultim=nou; nou->urm =0; (2)
4. dacă lista este vidă în momentul încercării de a adăuga un nod nou,
se va trata distinct acest caz

84
Algoritmi şi structuri de date

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; ultim
}

3. Inserarea unui nod înaintea unui nod specificat


Acest tip de operaţie se realizează în două etape:
1. căutarea nodului înaintea căruia se va insera un nou nod
2. inserarea propriu-zisă a noului nod

Căutarea unui nod specificat prin cheie se realizează printr-un algoritm


elementar de parcurgere sistematică a listei până la întâlnirea nodului ce conţine
cheia căutată. În acest scop se poate construi o funcţie care returnează adresa
nodului căutat, şi 0 în caz de insucces (dacă nodul de cheie dată nu există în listă).
Argumentul funcţiei este valoarea cheii căutate.
Căutarea nodului înaintea căruia se va insera noul nod poate fi realizată în
aceiaşi funcţie în care se face inserarea. Procedura de inserare ţine cont de cazul
particular în care nodul specificat prin cheie este primul nod.
Stabilirea legăturilor dintre noduri trebuie făcută corect pentru a nu genera
pierderea unei sub-liste de noduri. În acest sens este necesară reţinerea în
permanenţă a adresei nodului precedent nodului curent, pentru ca ulterior noul nod
să poate fi înlănţuit între nodurile precedent şi curent. Imaginea următoare
sugerează etapele inserării noului nod, cunoscându-se adresa prev (nodul
precedent= şi adresa nodului curent p:
1. nodul nou va conţine adresa nodul curent: nou->urm=p;
2. precedentul nod va conţine adresa noului nod prev->urm=nou; -
atribuire prin care vechea înlănţuirea a nodurilor prev şi p se
pierde.

85
Algoritmi şi structuri de date

Funcţia următoare descrie operaţia de inserare înaintea unui nod căutat după
valoarea cheii:

void adaugare_inainte()
{
int valoare;

prev
printf("\ndati cheia de cautat:");scanf("%d",&valoare);
tnod *p,*prec, *nou; 2 curent
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ă

nou
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

4. După un nod stabilit de o cheie:


Operaţia de inserare după un nod specificat este similară celei anterioare. Cazul
particular al procedeului constă în găsirea ca nod de referinţă chiar ultimul nod al
listei, fapt pentru care operaţia se reduce la adăugarea după ultim (funcţie deja
construită).

86
Algoritmi şi structuri de date

În plus, reţinerea unei adresa a precedentului nodului curent nu mai este


necesară. În fapt, nodul nou se va insera între nodul găsit (curent) şi următorul nod
al nodului curent. Însă, prin maniera de înlănţuire, adresa următorului nod al
nodului găsit este cunoscută, fiind memorată ca legătură chiar în nodul găsit: p-
>urm .

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

p 2 p->urm
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
}
nou
{ //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
}

Ştergerea unui element


9. Ştergerea primului element al listei necesită eliberarea memoriei
dar şi actualizarea pointer-ului prim necesar în gestionarea
ulterioară a listei. Actualizarea prim-ului nod al listei se face prin
asignarea prim=prim->urm, prin care următorul nodului prim
(secundul) devine prim al listei

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
}

10. Ştergerea ultimului presupune refacerea legăturilor, eliberarea


unei zone de memorie şi actualizarea pointer-ului ultim (util în
gestionarea listei). În contradicţie cu operaţia de ştergere a
primului nod, ştergerea ultimului nod al listei necesită o parcurgere
în totalitate a listei, pentru a determina adresa precedentului
nodului ultim. Acest lucru este necesar pentru a realiza operaţia de
actualizare a pointer-ului ultim. Adresa următorului nod după prim
se poate determina în mod elementar prin câmpul urm, însă, adresa
nodului anterior ultimului se determină printr-un procedeu
suplimentar de traversare a listei, generând un cost suplimentar al
algoritmului.
Etapele ştergerii ultimului element al unei liste sunt:
a. traversarea listei pentru a determina adresa penultimului element
b. actualizarea ultimului nod
c. eliberarea memoriei
void stergere_ultim()
{
tnod *ultimvechi,*prec,*p;
p=prim;
if (p==0)
{
printf("\nlista este vida");return;
}
while (p!=ultim)
//traversarea listei şi reţinerea precedentului nodului
//curent

88
Algoritmi şi structuri de date
{
prec=p; //salvare precedent
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
}

11. Ştergerea unui element precizat


Ştergerea unui element precizat prin valoarea cheii se execută prin următorii
paşi:
- căutarea nodului p ce va fi şters şi reţinerea adresei precedentului
acestuia: prev
- refacerea legăturilor: prev->urm= p->urm (1)
- eliberarea memoriei
Cazurile particulare ale procedurii sunt tratate distinct prin procedurile specifice
de ştergere a primului sau ultimului nod.

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)

prev
{//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

89
Algoritmi şi structuri de date
free(pvechi); //eliberare memorie
return;
}
else
{//nu s-a găsit încă
prev=p; //salvarea adresei precedentului
p=p->urm; //trecere la următorul nod
}

}
}

12. Ş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

IX. LISTA DUBLU ÎNLĂNŢUITĂ

Lista dublu înlănţuită este formată din noduri care conţin:


- informaţie
- adresa următorului nod
- adresa precedentului nod

info
Avantajul utilizării listelor dublu înlănţuite rezultă din posibilitatea parcurgerii
(traversării) listei în ambele sensuri: de la primul la ultimul, respectiv, de la ultimul
la primul nod. Acest lucru permite o manipulare mai flexibilă a nodurilor listei
Structura unui nod al listei dublu înlănţuite este următoarea:
struct nod
{
//declaraţii de date – câmpuri ale informaţiei
struct nod *urm; //adresa următorului nod
struct nod *prec; //adresa nodului precedent
}
În exemplele următoare vom utiliza un tip de date utilizator prin care specificăm
tipul nodurilor listei:

typedef struct nod


{int cheie;
…..//alte câmpuri
struct nod *pre;
struct nod* urm;
}tnod;

Ca şi la lista simplu înlănţuită, principalele operaţii sunt:


- crearea;
- accesul la un nod; parcurgerea listei
- adăugarea unui nod;
- ştergerea unui nod,
- ştergerea listei.
Gestionarea unei liste dublu înlănţuite se face în maniera similară listelor simplu
înlănţuite prin adresele nodurilor prim şi ultim. În plus, nodul prim se marchează
prin stabilirea adresei precedentului său la Null: prim->prec=0.
tnod *prim,*ultim;
Figura următoare sugerează maniera de organizare a unei liste dublu înlănţuite
(alocate dinamic):

91
Algoritmi şi structuri de date

…….
Crearea listei dublu înlănţuită
Crearea listei vide presupune iniţializarea celor doi pointer de control prim
şi ultim cu valoarea 0 (Null): prim=ultim=0. Crearea unei liste cu mai mult de un
nod se rezumă la apelul repetat al subrutinelor de adăugare a unui nou nod (înaintea
primului sau după ultimul nod).

prim
Funcţia următoare este un exemplu prin care se poate crea o listă dublu
înlănţuită prin adăugarea noilor noduri după ultimul nod. Un caz particular al
procedurii de adăugare a noului nod este tratat distinct: în situaţia în care lista este
vidă, după adăugarea unui nod trebuie marcate nodurile prim şi ultim. Funcţia
incarca_nod este cea definită în capitolul dedicat listelor simplu înlănţuite.
void creare()
{
tnod *p;
int rasp;
//creare lista vida
prim=ultim=0;
printf("\nIntroduceti? (1/0)");scanf("%d",&rasp);
while (rasp==1)
{
p=incarca_nod(); //alocare memorie şi încărcare nod
if (prim==0)
{//creare primul nod
prim=p;ultim=p;
prim->pre=0; ultim->urm=0; //marcare capete listă
}
else
{
ultim->urm=p;
p->pre=ultim;
ultim=p;
ultim->urm=0;
}
printf("\nIntroduceti? (1/0)");scanf("%d",&rasp);
}
}

Parcurgerea listei dublu înlănţuită


Spre deosebire de listele simplu înlănţuite, listele dublu înlănţuite pot fi
parcurse în ambele sensuri. Prezentăm în continuare funcţii de afişare a informaţiei
nodurilor parcurse în două variante: prin folosirea legăturii următor (urm),
respectiv, succesor (prec).

92
Algoritmi şi structuri de date

void tiparireDirecta() void tiparireInversa()


{ {
tnod *p; tnod *p;
if (prim==0) if (prim==0)
{printf("\nLista e vida!"); {printf("\nLista e vida!");
return; return;
} }
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
} }
} }

ADĂUGAREA UNUI NOU NOD


Sunt tratate în continuare diferite modalităţi de adăugare a unui nou nod într-o
listă dublu înlănţuită:
13. Adăugare înaintea primului nod
Adăugarea unui nou nod înaintea primului nod ale listei presupune efectuarea
următoarelor operaţii:
1. alocarea şi încărcarea noului nod:
2. stabilirea faptului că acest nou nod va adresa ca următor element
chiar pe nodul prim: nou->urm=prim (1)
3. stabilirea faptului că nodul prim va referi ca precedent element pe
nodul nou: prim->prec=nou (2)
4. stabilirea noului nod ca prim element al listei: prim=nou (3)
5. marcarea nodului prim: prim->prec=0 (4)

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()
{

prim
tnod *nou; p=incarca_nod();

93
3
Algoritmi şi structuri de date
if (prim==0)
{
prim=nou;ultim=nou;
prim->prec=0;ultim->urm=0;
}
else
{
nou->urm=prim; //pasul 1
prim->prec=nou; //pasul 2
prim=nou; //pasul 3
prim->prec=0; //pasul 4
}
}

14. Adăugare după ultimul nod


Adăugarea unui nou nod după ultimul nod al listei presupune efectuarea
următoarelor operaţii:
- alocarea şi încărcarea noului nod:
- stabilirea faptului că acest nou nod va adresa ca precedent element
chiar pe nodul ultim: nou->prec=ultim (1)
- stabilirea faptului că nodul ultim va referi ca următor element pe nodul
nou: ultim->urm=nou (2)
- stabilirea noului nod ca ultim element al listei: ultim=nou (3)
- marcarea nodului ultim: ultim->urm=0 (4)

Cazul special al procedurii (lista este vidă) se tratează în mod diferit.

void adaugare_ultim()
{ …………….
tnod *nou;nou=incarca_nod();
if (prim==0)
{
prim=nou;ultim=nou;
prim->prec=0;ultim->urm=0;
}
else
{

94
Algoritmi şi structuri de date
nou->prec=ultim; //(1)
ultim->urm=nou; //(2)
ultim=nou; //(3)
ultim->urm=0; //(4)
}
}

15. Adăugare înaintea uni nod specificat prin cheie


Adăugarea unui nod înaintea unui nod specificat prin valoarea cheii, se
realizează prin două etape:
- căutarea nodului înaintea căruia se va face inserarea
- inserarea propriu-zisă
Reamintim că la operaţia similară pentru liste simplu înlănţuite era necesară
determinarea nodului precedent nodului precizat de cheie li acest lucru se realiza
printr-un pointer auxiliar în care se reţinea acea adresă. În cazul listelor dublu
înlănţuite lucrurile sunt simplificate datorită adreselor precedent conţinute de
fiecare nod, adrese utile în accesarea vecinilor (nodurile anterioare).
Dacă nodul căutat este chiar primul, problema se reduce la un caz particular
tratat prin funcţia adaugare_prim. Dacă lista este vidă sau nodul căutat nu s-a găsit,
nu se va adăuga un nou nod.

void adaugare_inainte(int valoare)


{
tnod *p,*nou; //p contine adresa nodului curent in
parcurgerea listei
p=prim;
while(p!=0)
{ //se parcurge direct lista, de la primul spre ultimul nod
if (p->cheie!=valoare)
p->prec
{//nu s-a gasit inca nodul de cheie data
p=p->urm; //trecere la urmatorul nod 2
}
else
{
//am gasit nodul p inaintea caruia se insereaza nou;
if (p!=prim)
{

95 nou
Algoritmi şi structuri de date
nou=incarca_nod();
nou->urm=p; //(1)
(p->pre)->urm=nou; //(2)
nou->pre=p->pre; //(3)
p->pre=nou; //(4)
return;
}
else
{
adaugare_prim();
return;
}

}
}
}//sfarsit functie

Observaţie: Pentru manevrarea legăturii următoare a nodului precedent celui


curent (notăm nodul curent p) este valabilă construcţia: (p->pre)->urm, unde (p-
>pre) este adresa precedentului nodului p.

16. Adăugare după un nod specificat prin cheie


Procedura de adăugare a unui nou nod după un nod precizat prin valoarea cheii
este simalră celei prezentate la punctul 3. Cazul particular este cel în care nodul
căutat este ultimul nod al listei, în această situaţie se va apela funcţia
adăugare_ultim. Dacă lista este vidă sau nu s-a găsit nodul căutat, nu are sens să
se opereze adăugarea unui nou nod.
Inserarea propriu-zisă a nodului nou înaintea nodului p presupune refacerea
legăturilor în aşa fel încât consistenţa listei să fie asigurată (să nu se piardă
secvenţe de noduri prin distrugerea accesului la ele ):
- nodul nou va avea ca legătură urm pe următorul nod al nodului p:
nou->urm=p->urm; (1)
- nodul p va fi urmat de nou
p->urm=nou; (2)
- precedentul nodului nou va fi nodul p
nou->pre=p; (3)
- nodul precedent al nodului următorul lui p devine nou:
(p->urm)->pre=nou; (4)

void adaugare_dupa(int valoare)


{
tnod *p,*nou;
//caut p si inserez nod
p=prim;
while(p!=0)
{
if (p->cheie!=valoare)

96
Algoritmi şi structuri de date
{
p=p->urm;
}
else
{
if (p==ultim)
{adaugare_ultim();return;}
else
{
nou=incarca_nod();
nou->urm=p->urm;
p->urm=nou;
nou->pre=p;
(p->urm)->pre=nou;
return;
}
}
}
}

ŞTERGEREA UNUI NOD


Ştergerea capetelor prim şi ultim ale unei liste dublu înlănţuite nu diferă prin
costul de calcul precum la listele simplu înlănţuite. Am văzul că în cazul listelor
simplu înlănţuite ştergerea ultimului nod necesita un efort computaţional mai mare.
Prin legătura prec a nodurilor unei liste dublu înlănţuite putem accesa nodurile
precedent, fapt pentru care, la ştergerea ultimului nod nu este necesară traversarea
completă a listei.
Ştergerea unui capăt al listei presupune:
- salvarea temporară a adresei capătului respectiv într-un pointer auxiliar
- actualizarea şi marcarea noului capăt
- eliberarea memoriei
Oferim în continuare două variante pentru funcţiile de ştergere a capetelor prim
şi ultim:

/*ştergere prim nod*/ /*ştergere ultim nod*/


void stergere_prim() void stergere_ultim()
{ {
tnod*primvechi; tnod*ultimvechi;
if (prim==0) //lista vidă if (ultim==0) //lista vidă
return; return;
else else
{//mai sunt noduri {
if(prim!=ultim) if (prim!=ultim)
{//salvare prim { //salvare ultim
primvechi=prim; ultimvechi=ultim;
//actualizare prim //actualizare ultim
prim=prim->urm; ultim=ultim->pre;
//marcare prim //marcare ultim
prim->pre=0; ultim->urm=0;
//eliberare memorie //eliberare memorie

97
Algoritmi şi structuri de date

free(primvechi); free(ultimvechi);
} }
else else
prim=ultim=0; prim=ultim=0;
} }
}//sfarsit functie }//sfarsit functie

Ştergerea unui nod oarecare


Ştergerea unui nod oarecare precizat prin valoarea cheii presupune:
- 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

Refacerea legăturilor constă din următoarele asignări:


- precedentul următorului lui p devine precedentul lui p
p->urm->pre=p->pre; (1)
- următorul precedentului lui p devine următorul lui p:
p->pre->urm=p->urm; (2)
2
Cazurile particulare se tratează separat: lista este deja vidă sau lista devine vidă
după ştergerea nodului.

void stergere_nod(int valoare)


p->prec
{
tnod *p,*pvechi;
if (prim==0) return; //lista este deja vida
if (prim==ultim && prim->cheie==valoare)
{//lista devine vida
prim=ultim=0;
return;
}
p=prim; p
98
Algoritmi şi structuri de date
while(p!=0)
{
if (p->cheie==valoare) //gasit
{
if (p==prim)
{stergere_prim();return;}
if (p==ultim)
{stergere_ultim();return;}
pvechi=p; //salvare adresă nod curent
p->urm->pre=p->pre; (1)
p->pre->urm=p->urm; (2)
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.

Exemplu:ştergerea listei prin apelul repetat al funcţiei de ştergere a primului nod.


void stergere_lista()
{
while(prim!=0)
stergere_prim();
}

99
Algoritmi şi structuri de date

X. LISTE CIRCULARE. STIVE. COZI

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.

LISTA SIMPLU ÎNLĂNŢUITĂ CIRCULARĂ

Î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

Operaţiile posibile cu listele circulare sunt aceleaşi ca cele specifice listelor


simplu înlănţuite:
- parcurgere
- creare
- distrugere (ştergere)
- adăugare nou element
- ştergere element
....
Crearea listei circulare simplu înlănţuite
Crearea listei vide se realizează prin iniţializarea pointer-ului pLC cu valoarea
Null:
pLC=0;
Crearea unei liste circulare care conţine cel puţin un element presupune o
operaţie repetitivă de adăugare a unui nou nod. Pentru nodul care se adaugă se va
aloca memorie în prealabil şi se acesta va încărca cu informaţii. Funcţia
incarca_nod prezentată în capitolele precedente poate fi utilizată în acest sens.
Adăugarea nodului nou se poate efectua în două maniere:
1. adăugarea înaintea nodului referit de pLC
2. adăugarea după nodul referit de pLC
Adăugarea unui nou nod înaintea nodului pLC necesită un efort computaţional
suplimentar prin parcurgerea listei circulare. Această parcurgere este necesară
pentru a determina adresa nodului precedent al nodului pLC în vederea refacerii
legăturilor şi asigurării consistenţei listei.
Acest aspect a fost evidenţiat în cazul listelor simplu înlănţuite, pentru care
operaţiile de inserare înaintea unui nod oarecare, respectiv, inserare după un nod
oarecare diferă semnificativ prin necesitatea parcurgerii complete a listei în primul
caz.

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);
}
}

Parcurgerea listei circulare simplu înlănţuite


Parcurgerea listei circulare se va face prin urmărirea legăturilor (adreselor)
conţinute de noduri, în aceiaşi manieră ca la listele simplu înlănţuite, printr-un
pointer auxiliar. Specificul listelor circulare implică o altă condiţie de oprire a
traversării listelor. Dacă pentru listele gestionate prin prim şi ultim această condiţie
era evidentă (pointer-ul auxiliar prin care se parcurge lista a ajuns la ultim), în
cazul listelor circulare condiţia se referă la revenirea în punctul de plecare. Iniţial,
pointer-ul conţine adresa cunoscută a unui nod oarecare: pLC. Nodurile următoare
se parcurg cât timp pointer-ul auxiliar nu va avea aceiaşi adresă de început: pLC
(nu a revenit la poziţia iniţială):

p=pLC;
do
{
//prelucrare nod referit de p
p=p->urm; //trecere la urmatorul nod
}while (p!=pLC);

Funcţia următoare afişează cheile nodurilor unei liste circulare:

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

Operaţia de căutare a unui nod specificat prin valoarea cheii presupune


parcurgerea listei circulare şi verificarea dacă nodurile conţin pentru câmpul cheie
valoarea dată. Funcţia prin care se realizează această operaţie va returna adresa
nodului găsit sau 0 în caz de insucces:

tnod* cauta(int valoare)


{
tnod *p; //pointer-ul auxiliar
p=pLC; //iniţializare p
if (p==0) return 0; //lista este vida
do
{
if (p->cheie==valoare)
return p; //s-a gasit nodul cautat
p=p->urm; //trecere la urmatorul nod
}while (p!=pLC);
return 0; //s-a incheiat cautarea si nodul nu s-a gasit
}

Inserarea nodurilor într-o listă circulară simplu înlănţuită

1. Inserarea înaintea unui nod specificat prin valoarea cheii


2. Inserarea după un nod specificat prin valoarea cheii

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:

void inserareInainte(int valoare)


{
tnod *nou;
tnod *prev; //precedentul nodului curent
tnod *p; //pointer care refera nodul curent
if (pLC==0)
return; //lista este vida, nu are sens continuare operatiei
p=pLC;
do //cautarea nodului p
{
prev=p; //retine precedentul nodului curent
p=p->urm; //trece la urmatorul nod

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.

2. Inserarea unui nod nou după un nod precizat de cheie presupune:


- căutarea nodului de cheie dată
- inserarea propriu-zisă
Dacă prima etapă s-a încheiat cu succes, se cunoaşte adresa nodului de cheie
dată p, dar şi adresa următorului nod (datorită legăturii urm) p->urm. Nodul nou
va fi inserat între cele două noduri de adrese cunoscute. Nu mai este necesară
determinare altei adrese decât cea a nodului căutat după valoarea cheii.
Funcţia următoare este o posibilă implementare a operaţiei discutate:
void inserareDupa(int valoare)
{
tnod *nou;
tnod *p; //pointer care refera nodul curent
if (pLC==0) return; //lista este vida, nu are sens continuare
operatiei
p=pLC;
do //cautarea nodului p
{
if (p->cheie==valoare) //s-a gasit nodul
break; //iesire din instructuinea repetitiva
//p este adresa nodului gasit
p=p->urm; //trece la urmatorul nod
}while(p!=pLC);
if (p->cheie!=valoare) //cautarea s-a incheiat cu Insucces
return;
//dacă s-a ajuns în acest punct, cautarea s-a incheiat cu
//Succes

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.

Ştergerea unui nod precizat de valoarea cheii

Operaţia de ştergere a nodului precizat printr-o cheie presupune:


- căutarea nodului şi reţinerea adresei precedentului său ()
- ştergerea nodului: refacerea legăturilor şi eliberarea memoriei
Cazurile particulare ale operaţiei se tratează diferit:
a. lista este vidă înainte ştergerii
b. lista devine vidă după ştergere
c. nodul de şters este chiar pLC
Convenim că în cazul particular c. (nodul ce se va şterge este chiar nodul referit
de pointer-ul pLC şi lista nu devine vidă), pLC va referi nodul precedent celui
şters.
O funcţie C care descrie operaţia de ştergere este următoarea:
void steregereNod(int valoare)
{
tnod *p,*prev;
//p - adresa nodului curent
//prev - adresa precedentului nodului curent
if (pLC==0) return; //lista este vida, cazul particular (a.)
p=pLC;
do //cautarea nodului p
{
prev=p; //retine precedentul nodului curent
p=p->urm; //trece la urmatorul nod
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) return; //nu s-a gasit nodul

//nodul gasit este referit de p, urmeaza etapa de stergere

if (p->urm==p) //lista are un singur nod - nodul care se va sterge (b.)


{
pLC=0; //lista devine vida
free(p); //eliberare memorie
}
else

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

Observaţie: în situaţia în care informaţia din noduri conţine adrese alocate


dinamic (prin apelul funcţiei malloc), eliberarea memoriei alocate unui nod p
trebuie să ţină cont şi de acest aspect, fapt pentru care, apelul funcţiei free(p) nu
este suficient. Din aceste considerente, o funcţie specială de eliberare a memoriei
alocate unui nod poate fi concepută. Spre exemplu:
typedef struct nod
{
int CNP; //câmp cu valori unice pentru nodurile listei
char *nume
struct nod *urm; //adresa următorului nod
}Persoana;
Alocarea memoriei pentru un nod de tipul Persoana (Persoana *p) necesită un
apel malloc pentru câmpul nume. Eliberarea memoriei alocate nodului p se va
executa corect prin funcţia următoare:
void eliberare_nod(Persoana *p)
{
free(p->nume);
free(p);
}

Ştergerea liste circulare simplu înlănţuite

Operaţia de distrugere a unei liste circulare se realizează prin ştergerea tuturor


nodurilor sale şi nu prin distrugerea adresei speciale pLC prin care se gestionează
lista.
Dacă nu este deja vidă, lista se parcurge şi noduri precedente nodului curent se
şterg până când lista devine vidă. O funcţie de ştergere a listei circulare gestionate
prin pLC este următoarea:
void stergere()
{
tnod *p; //nodul curent
tnod *prev; //precedentul nodului curent
if (pLC==0) return; //lista este deja vida

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ă.

LISTA DUBLU ÎNLĂNŢUITĂ CIRCULARĂ

Lista circulară dublu înlănţuită este gestionată printr-un pointer la un nod


oarecare. Structura nodului este cea prezentată la listele dublu înlănţuite şi conţine:
zona de informaţii, adresa precedentului şi adresa nodului următor.
Operaţiile specifice: creare, inserare nod, ştergere nod, ştergere listă,
parcurgere, căutare, sunt similare operaţiilor descrise cu liste circulare simplu
înlănţuite. Diferenţele semnificative apar la procedurile de inserare înaintea unui
nod precizat şi ştergerea unui nod oarecare, care se simplifică prin existenţa unei
legături spre nodurile precedente.
Transformarea unei liste dublu înlănţuite în listă circulară se realizează prin
legarea capetelor prim şi ultim, în ambele sensuri:

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

vârful vârful info

I Structura de date STIVĂ cu II. Structura de date STIVĂ cu alocare


alocare statică dinamică

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

de verificare:
- verifică dacă stiva este plină
info
În plus faţă de operaţiile enumerate anterior sunt posibile implementate operaţii

- verifică dacă stiva este goală


info
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 info
- baza stivei corespunde nodului prim şi vârful stivei corespunde

baza 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
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
Constatăm că spre deosebire de stive, ambele abordări sunt eficiente, astfel încât
alegerea oricărei variante este posibilă. Printr-o convenţie, adăugarea unui nod se
face după ultimul nod (coada) al listei, iar scoaterea din listă a unui nod este
implementată prin ştergerea nodului prim (cap).

109
Algoritmi şi structuri de date

XI. ARBORI

Un arbore reprezintă o mulţime nevidă şi finită de elemente de acelaşi fel, pe


care le denumim noduri:
A = { a 1 ,a 2 ,..., a n } , n > 0
Proprietăţile arborelui sunt:
1. există un singur nod numit rădăcină
2. nodurile diferite de rădăcină formează submulţimi disjuncte –
denumite subarbori.
3. Fiecare subarbore respectă proprietăţile 1 şi 2.
Arborii sunt structuri de date de natură dinamică şi recursivă (ca şi listele).
Reprezentarea grafică a unui arbore oarecare este ilustrată mai jos:

Nodurile arborelui pot fi:


- nodul rădăcină (fiecare arbore are un singur nod de acest tip)
- noduri interne
- noduri terminale (frunze)
Nodul rădăcină se distinge de celelalte noduri prin faptul că nu acesta nu are
succesori (noduri care îl preced ).
Nodurile interne sunt precedate de 1 singur nod şi pot fi urmate de 1 sau mai
multe noduri.
Nodurile terminale sunt precedate de 1 singur nod şi sunt urmate de 0 noduri.

Terminologia specifică încorporează denumirile de părinte pentru nodul care


preced alte noduri, fii, pentru nodurile care urmează un nod părinte şi fraţi – pentru
nodurile care au acelaşi părinte.

Nodurile unui arbore se caracterizează prin două mărimi:

110
Algoritmi şi structuri de date

- Nivelul nodului: reprezintă o valoarea naturală prin care se


identifică numărul de strămoşi până la nodul rădăcină.
o Nodul rădăcină este considerat pe nivelul 1
o Fiii nodului rădăcină sunt pe nivelul 2
o Fiii fiilor nodului rădăcină sunt pe nivelul 3
o etc.
- Ordinul nodului: este un număr natural şi reprezintă numărul
de descendenţi direcţi (fii) pe care îi are nodul respectiv. Nodurile
terminale au ordinul 0.

Nivelele nodurilor în arbore

Nodul Ordinul nodului


2
1 O(1)=2
2 O(2)=3
3 O(3)=0
4 O(4)=2
5 O(5)=0
6 O(6)=1
7 O(7)=0
8 O(8)=0
9 O(9)=0
4
Ordinul fiecărui nod din arborele prezentat ca exemplu
5 6
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.

Un nod al arborelui conţine:


d. zona de date
e. 1 sau mai multe legături spre fii săi

Dacă între fiii oricărui nod există o relaţie de ordine, spunem că arborele
respectiv este arbore ordonat.

7 111
8 9
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:

2
Figura 1 Arbore oarecare

După transformare, arborele devine binar:

5 6
112
Algoritmi şi structuri de date

Figura 2 Arbore binar


2
Parcurgerea arborilor

Parcurgerea (traversarea) arborilor presupune obţinerea unei liste a nodurilor


arborelui. În funcţie de ordinea în care sunt considerate nodurile arborelui, avem
mai multe tipuri de traversare a arborilor.

- Parcurgerea în adâncime (in depth): se vizitează subarborii descendenţi


ai nodului rădăcină, în aceiaşi manieră, apoi se vizitează nodul rădăcină.

5
Parcurgerea în adâncime a arborelui considerat ca şi exemplu în figura 1
produce următoarea listă a nodurilor: 7 5 6 2 3 4 1.
- Parcurgerea în lăţime (in breadth): se vizitează nodurile pe nivele,
pornindu-se de la nivelele cele mai de sus (nivelul 1 – rădăcina) spre
nivelele mai mari, şi pe fiecare nivel se vizitează nodurile într-o anumită
ordine, spre ex. de la stânga la dreapta. Parcurgerea în lăţime a arborelui
din figura 1 produce lista: 1 2 3 4 5 6 7.

ARBORI BINARI

Structura unui nod al arborelui binar este următoarea:


typedef struct nod
{
//informaţii 7
struct nod * st; //legătura spre subarborele stâng
6
struct nod * dr; //legătura spre subarborele drept
}Tnod;
unde:
st – este un pointer, a cărei valoare este adresa fiului stâng a nodului curent
dr – este un pointer, a cărei valoare este adresa fiului drept a nodului
curent

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;

Operaţiile specifice arborilor binari sunt:


1. crearea arborelui
2. inserarea unui nod
3. ştergerea unui nod
4. ştergerea arborelui
5. accesarea unui nod (căutarea)
6. parcurgerea unui arbore

Operaţia de creare a arborelui binar necesită iniţializarea nodului rădăcină:


rad=0 şi adăugarea (inserarea) ulterioară a noilor noduri după o anumită regulă.
Operaţiile de inserare şi accesare a nodurilor dintr-un arbore binar presupun
existenţa unui criteriu care se desprinde din specificaţiile problemei concrete de
rezolvat. Aceste criterii fiind variate, rezolvarea problemelor cu arbori binare se
poate simplifica prin considerarea unei funcţii verifică care are doi parametri:
pnod1 şi pnod2 – adresele a două noduri şi returnează valoare -1,0 sau 1 cu
semnificaţia:
A) -1 dacă pnod2 este adresa unui nod care poate fi accesat sau inserat în
stânga nodului adresat de pnod1
B) +1 dacă pnod2 este adresa unui nod care poate fi accesat sau inserat în
dreapta nodului adresat de pnod1
C) 0 dacă pnod2 referă un nod care NU poate fi accesat sau inserat în
subarborii stâng sau drept ai nodului referit de pnod2.
Prototipul acestei funcţii ajutătoare este:
int verifica(Tnod *pnod1 , Tnod * pnod2);
Inserarea unui nou nod presupune:
- determinarea locului în care va fi inserat noul nod
- alocarea de memorie şi încărcarea cu informaţii a nodului nou
- stabilirea legăturilor în arbore
În privinţa locaţiei în care poate fi inserat nodul, aceasta este dată de criteriul
specificat de problema concretă şi se disting următoarele cazuri:
- în poziţia rădăcinii
- ca nod terminal
- ca nod intern
Ştergerea unui nod oarecare presupune:
- determinarea locaţiei sale
- refacerea legăturilor în arbore
- eliberarea memoriei alocate nodului şters
Ştergerea întregului arbore necesită ştergeri repetate ale nodurilor sale până
când rădăcina devine vidă.

Parcurgerea unui arbore binare poate fi făcută în trei moduri:

114
Algoritmi şi structuri de date

1. parcurgerea în preordine: pentru fiecare nod curent se va


accesa/prelucra informaţia conţinută, subarborele stâng şi în final
subarborele drept. (fiecare subarbore va fi parcurs în aceiaşi manieră).
- Simplificat, parcurgerea în preordine a unui arbore
presupune parcurgerea în ordinea: R (rădăcină), S
(subarbore stâng), S (subarbore stâng). R S D
2. parcurgerea în postordine: pentru fiecare nod curent se va
parcurge subarborele stâng, subarborele drept şi în final se va
accesa/prelucra informaţia conţinută în nodul curent (fiecare subarbore
va fi parcurs în aceiaşi manieră).
- Simplificat parcurgerea în postordine înseamnă
traversarea arborelui în ordinea: S (stâng) D(drept)
R (rădăcină)
3. parcurgerea în inordine: pentru fiecare nod curent se va parcurge
subarborele stâng, nodul curent şi în final subarborele drept (fiecare
subarbore va fi parcurs în aceiaşi manieră).
- Simplificat: ordinea de traversare a arborelui este
S (stânga) R (rădăcină) D (dreapta).

Exemplu:

La parcurgerea în preordine a arborelui din figura anterioară se vor accesa


nodurile în ordinea: J H E A B F I G C D
La parcurgerea în postordine se vor accesa nodurile în ordinea:
ABEFHCGDIJ
La parcurgerea în inordine se vor accesa nodurile în ordinea:
AEBHFJICGD
Funcţiile de parcurgere a unui arbore binar în cele trei variante (preordine,
postordine, inordine) sunt date mai jos:

void preordine(tnod *p)


{
if p!=0
{
//vizitarea nodului curent (tipărirea cheii)

115
Algoritmi şi structuri de date
printf(“\t%d”, p->cheie);
//parcurgere subarbore stâng
//autoapelare funcţie preordine
preordine(p->st);
//parcurgere subarbore stâng
//autoapelare funcţie preordine
preordine(p->dr);
}
}

void inordine(tnod *p)


{
if (p!=0)
{
inordine(p->st);
printf(" %d",p->cheie);//vizitare nod
inordine(p->dr);
}
}

void postordine(tnod *p)


{
if (p!=0)
{
postordine(p->st);
postordine(p->dr);
printf(" %d",p->cheie);
}
}

Căutarea unui nod

Fiind dat un criteriu de căutare, căutarea într-un arbore binar presupune


parcurgerea nodurilor şi verificarea criteriului pentru fiecare nod vizitat. În cazul
în care s-a găsit un nod care respectă criteriul, procesul de parcurgere a arborelui se
va încheia. Dacă nu se găseşte niciun nod care verifică criteriul, acest lucru trebuie
semnalat.
În scopul definirii unei funcţii de căutare în arborele binar, se consideră funcţia
auxiliară prin care se verifică îndeplinirea sau neîndeplinirea criteriului de căutare
şi continuare căutării într-unul din subarborii stâng sau drept ai nodului curent. În
mod frecvent, criteriul se referă la îndeplinirea condiţiei de egalitate între o valoare
dată şi valoarea unui câmp din informaţia memorată în noduri. Pentru a generaliza,
considerăm funcţia verificare descrisă anterior prin care se decide dacă un nod
căutat pnod2 este găsit în arbore la adresa pnod1, sau se va continua căutarea în
subarborele stâng sau drept a nodului adresat de pnod1.
O funcţie de căutare se poate descrie ca mai jos. Funcţia returnează adresa
nodului găsit sau 0 în cazul în care nu se găseşte un nod care verifică criteriul
stabilit.

tnod *rad; //variabilă globală

116
Algoritmi şi structuri de date

tnod *cautare(tnod *pnod2)


{ // 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
}

Crearea unui arbore binar


Operaţia de creare a unui arbore binar presupune operaţii repetate de inserare a
unui nou nod ca nod terminat. Pentru fiecare nod care urmează să fie inserat este
necesară în prealabil determinarea poziţiei în care acesta va fi adăugat, respectiv,
părintele de care acesta se va lega. Nodul părinte se va determina printr-o
parcurgere parţială a arborelui. În realizarea acestei operaţii ne putem folosi de
funcţia auxiliară verifica sau o altă funcţie prin care se decide în care din subarbore
va fi continuată căutarea posibilului părinte.

ARBORI BINARI DE CĂUTARE

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

Exemplu: Figura alăturată ilustrează reprezentarea grafică a unui arbore binar de


căutare (valoarea cheii este precizată în fiecare nod):

117
Algoritmi şi structuri de date

4
Se poate observa o proprietate importantă a arborilor binari de căutare: la
parcurgerea în inordine se obţine lista nodurilor ordonate după valoarea cheii.
În plus, operaţia de căutare a unui nod specificat de valoarea cheii este simplificată
(de aici şi denumirea arborilor de căutare).
Căutarea unui nod după o cheie dată nu necesită parcurgerea întregului arbore.
Datorită specificului acestor arbori, valoarea cheii este comparată cu cheia
conţinută în rădăcină şi în funcţie de rezultatul comparaţiei, căutarea se va continua
doar într-unul dintre cei doi subarbori, celălalt subarbore fiind exclus din căutare.
Procedeul se va continua în mod similar pentru subarborele curent: se compară
2
valoarea cheii căutate cu cheia rădăcinii subarborelui şi în funcţie de rezultatul
comparaţiei se va continua căutarea doar într-unul dintre subarborii subarborelui 6
curent. Căutarea se va încheia în momentul în care un nod rădăcină a subarborelui
curent conţine cheia căutată sau dacă nu mai sunt noduri de parcurs şi cheia nu a
fost găsită.
Căutarea unei informaţii într-o structură de arbore binar de căutare este
eficientă, deoarece numărul nodurilor accesate se reduce prin excluderea acelor
subarbori a căror parcurgere ar fi inutilă.
Operaţiile de inserare şi ştergere executate asupra unui arbore binar de căutare
vor produce de asemenea un arbore binar de căutare. Astfel, în definirea unor
funcţii specifice care operează asupra arborilor de căutare se va ţine cont de
1 3
criteriul de ordine între cheile nodurilor ce formează arborele.
5
1. Creare arbore binar de căutare. Inserarea nodurilor.

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

Dacă există cel puţin un nod în arbore, se va căuta un părinte al nodului de


inserat. Acest părinte îndeplinească condiţiile:
- dacă cheia conţinută de părinte este mai mare decât cheia nodului nou,
atunci, părintele trebuie să aibă legătura stângă vidă, pentru a reuşi legarea nodului
nou în stânga sa (prin aceasta ne asigurăm că subarborele construit, al cărei
rădăcină este nodul părinte, este ordonat)
- dacă cheia conţinută de părinte este mai mică decât cheia nodului nou,
atunci, părintele trebuie să aibă legătura dreapă vidă

int inserare(tnod *nou)


{ tnod *p;
nou->st=nou->dr=0;
if (rad==0)
{//creare radacina
rad=nou;
rad->st=rad->dr=0;
return 1;
}
p=rad;
while(1)
{
if (nou->cheie<p->cheie)
if (p->st!=0)
p=p->st;
else
{p->st=nou;nou->st=nou->dr=0;return 1;}
else
if (p->dr!=0)
p=p->dr;
else
{p->dr=nou;nou->st=nou->dr=0;return 1;}
}
return 0; //nu s-a realizat operaţia, cod de eroare
}

Arborele binar de căutare se poate construi printr-o secvenţă:

printf("\ncate noduri introduceţi?"); scanf("%d",&n);


tnod *nou;
rad=0; //arbore vid
for (int i=0;i<n-1;i++)
{
nou=incarca_nod();
if (nou!=0) //s-a reușit încărcarea
{
j=inserare(nou);
if (j==0) //nu s-a reuşit inserarea
eliberare_nod(nou);//eliberare memorie alocată pt. nou
}
}

119
Algoritmi şi structuri de date

2. Căutarea unui nod de cheie precizată


Căutarea după valoarea cheii este operaţia specifică în arborele binar de căutare
prin care se determină adresa nodului a cărei cheie este egală cu o valoare dată.
Dacă nu se găseşte un astfel de nod, operaţia se încheie cu insucces.
Construim mai jos o funcţie care primeşte ca parametru valoarea cheii căutate
şi returnează:
- adresa nodului găsit, sau
- 0, în caz de insucces
tnod * cautare(int val)
{
tnod *p;
p=rad;
while(p!=0) //câttimp mai sunt noduri de vizitat
{
if (p->cheie==val)
return p; //s-a gasit nodul
if (val<p->cheie)
{ //continuare căutare în subarbore stâng
p=p->st; }
if (p->cheie<val)
{//continuare căutare în subarbore drept
p=p->dr; }
}
return p; //nu s-a găsit nodul, p este Null
}

3. Ştergere nod de cheie precizată

Ştergerea unui nod de cheie precizată, precum şi celelalte operaţii specifice


arborilor de căutare, va produce un arbore cu aceleaşi caracteristici (cheile
nodurilor rămân ordonate după ştergere).
Prima etapă a operaţiei de ştergere constă în determinarea nodului de cheie
precizată (nodul ce se va şterge). În acest scop este utilă o funcţie care caută cheia
în arbore, însă faţă de operaţia de căutare descrisă anterior, este nevoie
determinarea unui element suplimentar: adresa părintelui nodului ce va fi şters.
În funcţie de poziţia nodului de şters: p şi a părintelui său în arbore: parinte,
întâlnim următoarele cazuri:
a. nodul găsit este nod frunză (nod terminal) şi este situat în dreapta
părintelui său:
- se eliberează nodul
- se stabileşte legătura dreaptă a părintelui ca fiind nulă:
parinte->dr=0
b. nodul găsit este nod frunză (nod terminal) şi este situat în stânga părintelui
său:
- se eliberează nodul
- se stabileşte legătura stângă a părintelui ca fiind nulă:
parinte->st=0

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).

O altă manieră de stabilire a cazurilor de ştergere în arborele binar este dată de


ordinul nodului ce va fi şters:
Dacă nodul are ordinul 0 sau 1 (are maxim un subarbore) este util să grupăm
cazurile descrise mai sus a,b,c,d într-o singură tratare: părintele nodului p va
conţine adresa subarborelui lui p (stâng sau drept), chiar dacă acest subarbore este
vid (nodul p este terminal).
Dacă ordinul nodului p este 2, se va trata cazul e.

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:

Nodul Nodul
4 4
de sters 121 de sters
Algoritmi şi structuri de date

Nodurile predecesor şi succesor au proprietate că sunt nodurile de cheie


imediat mai mică, respectiv, imediat mai mare decât cheia nodului p, şi sunt noduri
cu un singur subarbore, fapt pentru care ştergerea nodului p se realizează prin:
- copierea informaţiei din predecesor/succesor în nodul p
- ştergerea predecesorului/succesorului nodului p- cazul ştergerii unui
nod cu un subarbore
Practic, ştergerea unui nod cu doi subarbori se transformă într-o căutare a altui
nod care are maxim un subarbore, urmată de ştergerea acestuia.
Exemplu:

4 3
Nodul
Figura 3 Înainte de ştergerea nodului Figura 4 După ştergerea nodului
denodul
Observaţii: Dacă sters
p ce va fi şters este părintele direct al predecesorului
său, atunci predecesorul este în stânga părintelui său (în stânga nodului p).
Dacă nodul p ce va fi şters nu este părintele direct al predecesorului său, atunci,
predecesorul este legat de părintele său direct prin legătura dreaptă.
Exemplu: În figura anterioară, nodul 4 nu este părintele direct al predecesorului
2 2 6
(nodul 3), astfel încât, predecesorul este legat în dreapta părintelui său direct –
nodul 2.
Ne imaginăm următoarea situaţie:

1 31 5
Predecesor
În acest caz, pentru nodul de şters se va actualiza legătura stângă. După operaţia
de ştergere, arborele devine: 3
Nodul
de sters
122
Algoritmi şi structuri de date

2
Funcţia de mai jos este o variantă de implementare a algoritmului de ştergere a
unui nod dintr-un arbore de căutare. Se acordă atenţie nodului rădăcină, acesta fiind
un nod important prin care se gestionează arborele. Dacă acest nod trebuie şters,
tratarea cazului se face în mod diferit pentru protejarea adresei de acces la arbore.

void stergere()//stergere nod de cheie data


{
tnod *p,*tatap,*predecesor,*tatapredecesor;
int cheie;
printf("\ndati cheia");scanf("%d",&cheie);
p=rad;
//caut nodul p; 1 5
while(p!=0)
{
if (p->cheie==cheie)
break;
if (cheie<p->cheie) {tatap=p;p=p->st;}
if (cheie>p->cheie) {tatap=p;p=p->dr;}
}//caut p si retine parintele sau
if (p==0) {printf("\n Nu exista cheia cautata!!!");return;}

//cazul I________________________nod frunza


if ((p->st==0)&&(p->dr==0))
{
if (p==rad) rad=0; //arborele devine vid
4
if (tatap->st==p) //cazul a
{tatap->st=0;
eliberare_nod(p);
return;}
if (tatap->dr==p) //cazul b
{tatap->dr=0;
eliberare_nod(p);
return;}
}
//cazul II________________________nod cu un subarbore

//cazul c, p are doar subarbore stang


if (p->dr==0)
{

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

//cazul e________________________nod cu 2 subarbori


//pas 1 caut predecesor si retin parintele predecesorului
tatapredecesor=p;
predecesor=p->st;
while(predecesor->dr!=0)
{
tatapredecesor=predecesor;
predecesor=predecesor->dr;
}//retin parintele predecesorului
if (tatapredecesor==p)
{//nodul de sters este parintele predecesorului
tatapredecesor->st=predecesor->st;
p->cheie=predecesor->cheie;
eliberare_nod(predecesor);
return;
}
if (tatapredecesor!=p)
{
tatapredecesor->dr=predecesor->st;
p->cheie=predecesor->cheie;
eliberare_nod(predecesor);
return;
}

124
Algoritmi şi structuri de date
}//sfîrşit functie stergere

4. Ştergere arbore binar de căutare

Ştergerea completă a arborelui binar constă în ştergerea tuturor nodurilor sale.


În acest scop se poate defini o funcţie recursivă prin care se parcurge arborele în
post ordine şi vizitarea nodului constă în eliberarea sa. Ultimul nod eliberat este
nodul rădăcină.
void stergere_arbore(tnod *p)
{
if (p!=0)
{
stergere_arbore(p->st);
stergere_arbore(p->dt);
eliberare_nod(p);
}
}

Apelul funcţiei este:


stergere_arbore(rad);

ARBORI BINARI ECHILIBRAŢI (AVL)

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.

Probleme propuse spre rezolvare

125
Algoritmi şi structuri de date

1. Se citeşte de la tastatură o expresie matematică în formă prefixata, sub forma


unui şir de caractere (operatorul este plasat în faţa operanzilor). Să se
construiască arborele corespunzător acestei expresii. Fiecare nod conţine un
operator sau un operand. Să se evalueze expresia.
2. Să se scrie o funcţie care numără într-un arbore binar ordonat câte noduri
conţin chei cu valori cuprinse în intervalul [a,b].
3. Scrieţi o funcţie care calculează nivelul şi factorul de echilibrare pentru oricare
nod al unui arbore binar.

126
Algoritmi şi structuri de date

XII. ELEMENTE DE GRAFURI. ALGORITMI.

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:

2
O muchie de la vârful x la vârful y este notata cu perechea ordonata (x, y), dacă
graful este orientat şi în mod uzual este folosit termenul de arc, si cu mulţimea
{x, y}, dacă graful este neorientat. În reprezentarea grafică, arcele (x,y) sunt
marcate prin săgeţi de la extremitatea iniţială x la cea finală y, iar muchiile prin
4
segmente.
Într-un graf orientat, existenţa unui arc de la vârful x la vârful y nu presupune şi
existenţa arcului de la y la x. În grafurile neorientate, dacă există muchie între x şi
y, atunci aceasta este şi muchie între vârfurile y şi x. Vârfurilor unui graf li se pot
ataşa informaţii numite uneori valori, iar muchiilor li se pot ataşa informaţii numite
costuri.

5
Următoarele noţiuni sunt specifice grafurilor:
Două vârfuri unite printr-o muchie se numesc adiacente.
Un drum este o succesiune de muchii de forma:
(x1, x2), (x2, x3), ..., (xn-1, xn) – în graf neorientat
3
sau de forma
{x1, x2}, {x2, x3}, ..., {xn-1, xn} – în graf neorientat
Un lanţ se defineşte ca o succesiune de vârfuri x1, x2, x3, … xn în care oricare
două vârfuri sunt adiacente.
Într-un drum simplu muchiile care îl compun sunt distincte.
Într-un drum elementar vârfurile care îl compun sunt distincte.
Lungimea drumului este egala cu numărul muchiilor care îl constituie.
Un lanţ elementar al grafului G care conţine toate vârfurile grafului se numeşte
lanţ hamiltonian. Determinarea unui lanţ hamiltonian al grafului este o problemă
foarte populară cunoscută ca Problema Comis Voiajorului rezolvată prin metoda
Greedy.
Un ciclu este un drum care este simplu şi care are drept capete un acelaşi vârf.
Un graf fără cicluri se numeşte graf aciclic.
Un subgraf al lui G este un graf G’=(X', Γ '), unde X' × X, iar Γ ' este formata
din muchiile din Γ care unesc vârfuri din X'.

127
Algoritmi şi structuri de date

Un graf parţial este un graf (X, Γ "), unde Γ " ⊆ Γ .


Un graf neorientat este conex, dacă între oricare doua vârfuri exista un drum.
Pentru grafuri orientate, aceasta noţiune este întărită: un graf orientat este tare
conex, dacă între oricare două vârfuri x si y exista un drum de la x la y si un drum
de la y la x.

Reprezentarea grafurilor

1. Prin matricea de adiacenta A, în care A[i, j] = 1 dacă vârfurile i si j sunt


adiacente, iar A[i, j] = 0 în caz contrar.
2. Prin liste de adiacenta: fiecare vârf i are ataşată lista de vârfuri adiacente
lui (pentru grafuri orientate, este necesar ca muchia sa plece din i).
3. Prin lista de muchii. Aceasta reprezentare este eficienta atunci când se
doreşte examinarea tuturor muchiilor grafului.
4. Prin matricea costurilor (grafuri neorientate etichetate) C în care C[i, j] ≠0
este costul asociat muchiei, iar C[i, j] = 0 semnifică faptul că nu există
muchie {i,j}.

Parcurgerea grafurilor

Parcurgerea unui graf presupune vizitarea intr-o anumita ordine nodurilor


grafului, o singura dată fiecare. În functie de ordinea de parcurgere a vârfurilor
exista 2 metode de parcurgere:

1. Metoda parcurgerii în lăţime - Breadth First


2. Metoda parcurgerii în adâncime - Depth First

1. Parcurgerea în lăţime Breadth First: se vizitează vârful stabilit iniţial xs,


apoi vecinii acestuia (vârfurile adiacente) apoi vecinii vecinilor lui xs, etc.
Procedura de parcurgere în lăţime funcţionează după următorul principiu: atunci
când s-a ajuns într-un vârf oarecare x nevizitat, îl marcăm si vizităm apoi toate
vârfurile adiacente lui x rămase nevizitate, apoi toate vârfurile nevizitate adiacente
vârfurilor adiacente lui x, etc. La parcurgerea în lăţime se foloseşte o structură de
coadă pentru a putea vizita toţi vecinii unui nod dat înainte de a-l marca drept
vizitat.

Subalgoritm BreadthFirst (x) este //x reprezintă vârful curent


C=Φ // iniţializare coada vidă
*Marchează x ca vizitat
Inserare(C,x) //inserare vârfului x în coada C
Câttimp (C≠Φ)
Scoate(C,y) //scoate din coadă vârful y
Pentru fiecare vârf z, adiacent lui y
Dacă z este nevizitat atunci
*Marchează z ca vizitat

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

2. Parcurgerea în adâncime presupune vizitarea vârfului iniţial xS şi marcarea sa


ca fiind vizitat, apoi se alege un vârf x, adiacent lui xS şi se aplică aceiaşi procedură
– recursiv, având ca punct de plecare vârful x. Procedura de parcurgere în
adâncime a vârfurilor unui graf se pretează la o implementare recursivă. La
terminarea procedurii curente (la revenirea din apelul recursiv), dacă exista un alt
vârf adiacent vârfului curent x, care nu a fost vizitat, apelam din nou procedura etc.
Dacă toate vârfurile adiacente lui x au fost marcate ca vizitate se termină vizitarea
vârfului x.

Subalgoritm DepthFirst (x) este:


*Marchează x ca vizitat
Pentru fiecare vârf y adiacent lui x
Dacă y este nevizitat atunci
Cheamă DepthFirst (y)
SfDacă
SfPentru
SfSubalgoritm

Observaţie: Varianta nerecursivă a subalgoritmului DepthFirst se realizează prin


utilizarea unei structuri de date de tip stivă.

Subalgoritmii BreadthFirst şi DepthFirst sunt apelaţi din algoritmul Parcurgere:

Algoritm Parcurgere(G) este


Pentru fiecare x din X
*Marchează x ca nevizitat
SfPentru
Pentru fiecare x din X
Dacă x este nevizitat atunci
Cheamă BreadthFirst (x) sau Cheamă
DepthFirst (x)
SfDacă
SfPentru
SfAlgoritm

129
Algoritmi şi structuri de date

În cazul unui graf neconex, se pune problema determinării componentelor sale


conexe:
O componenta conexa a grafului G=(X, M), este un subgraf G’=(X', M'), conex
şi maximal. Maximalitatea se referă la faptul că nu exista lanţ în graful G care sa
aibă o extremitate în X’ şi pe cealaltă în X\X’.
Un arbore este un graf neorientat, aciclic şi conex. Într-un arbore exista exact
un drum între oricare doua vârfuri.
Un graf parţial care este arbore se numeste arbore partial. Un arbore parţial
este un graf parţial fără cicluri.
Arborele parţial de cost minim (suma costurilor muchiilor este minimă) se
determină printr-un algoritm de tip Greedy: Algoritmul lui Kruskal.

Algoritmul de determinare a Arborele parţial de cost minim (APM).

Problema APM: Se dă un graf G=(X,Γ ) cu muchiile etichetate prin costuri


(datele de intarre sunt reprezentate prin matricea costurilor).Se cere determinarea
arborelui parţial de cost minim a grafului dat. (datele de ieşire sunt muchiile care
formează arborele parţial de cost minim)

Rezolvare: Algoritmul lui Kruskal este un algoritm cunoscut de rezolvare a


problemei enunţate şi este un algoritm de tip Greedy. Principiul acestui algoritm
este următorul:
Considerând graful G=(X,Γ ), A=(X, Γ ’) – arborele ce se determină
(reprezentat prin lista de muchii) şi n – numărul de vârfuri n=|X|:
- se porneşte cu arborele vid: Γ ’=Φ
- în mod repetat se alege muchia de cost minim a grafului G care nu
formează ciclu în arborele A şi se adaugă la Γ ’
- algoritmul se termină când au fost alese n-1 muchii

Algoritm Kruskal este:


Date de intrare: G=(X,Γ )
Fie Γ ’=Φ
Câttimp (|Γ ’|<n-1)
*Alege {i,j} de cost minim din Γ
Dacă Γ ’ ∪ {i,j} nu conţine cicluri
Γ ’=Γ ’ ∪ {i,j}
Γ =Γ \ {i,j}
SfDacă
SfCâttimp
Date de ieşire: A=(X,Γ ’)
SfAlgoritm

130
Algoritmi şi structuri de date

Observaţie: Mulţimea muchiilor grafului dat se poate ordona descrescător după


costuri şi se va parcurge în mod secvenţial, fără a mai fi necesară procedura de
alegere a muchiei de cost minim din graful G.

131
Algoritmi şi structuri de date

XIII.METODE DE ELABORARE A ALGORITMILOR. DIVIDE ET


IMPERA.

Metode Divide et Impera este o metodă de rezolvare a unor probleme şi este


inspirată de principiul “Împarte şi stăpâneşte”. În conformitate cu principiul
amintit, o problemă de complexitate mare se va împărţi în subprobleme de acelaşi
tip însă de dimensiuni mai mici ale căror rezolvare se va dovedi mai simplă. De
asemenea, subproblemele obţinute prin descompunerea problemei pot fi la rândul
lor împărţite în subprobleme mai mici. Etapa de împărţirea se consideră încheiată
când subproblemele devin elementare - rezolvabile imediat. Soluţiile
subproblemelor se vor combina pentru a alcătui soluţia globală a problemei.

Metoda descrisă poate fi aplicată în condiţiile în care problemele admit o


împărţire în subprobleme de acelaşi fel. Putem vorbi de un specific al problemelor
rezolvabile cu Divide et Impera. Acest specific este dat de următoarele afirmaţii:

- problema globală se poate împărţi în 2 sau mai multe subprobleme


asemănătoare de dimensiuni mai mici
- subproblemele obţinute prin împărţire sunt independente, ceea ce permite
ca soluţiile parţiale ale acestora să nu depindă unele de altele
- contextul problemei ne permite să identificăm condiţia ca o subproblemă
să fie considerată elementară şi să nu mai fie supusă unei împărţiri
ulterioare
- subproblemele obţinute prin împărţire admit aceiaşi metodă de rezolvare
fiind probleme de acelaşi fel.

Exemple de probleme celebre rezolvabile cu metoda Divide et Impera sunt:


problema Turnurilor din Hanoi, Sortarea rapidă, etc.

Fie P – problema globală, n – dimensiunea ei şi S – soluţia dorită. Fie n0 –


dimensiunea minimă a unei probleme pentru care aceasta devine elementară şi
admite o rezolvare imediată.
Deoarece toate subproblemele sunt tratate prin aplicarea aceluiaşi algoritm,
diferenţa făcându-se doar prin datele de intrare-ieşire şi dimensiunea
subproblemelor, precum şi datorită faptului că soluţiile obţinute sunt soluţii parţiale
ale problemei globale, descrierea potrivită a metodei este dată printr-un
subalgoritm autoapelabil. De altfel, implementarea în limbaj de programare se face
prin proceduri (funcţii) recursive.

Subalgoritm DivideEtImpera (P_formal, n_formal; S_formal) este:


Dacă n_formal< n0 atunci
#rezolvă imediat subproblema P_formal şi obţine soluţia S_formal
Altfel
#împarte problema P_formal de dimensiune n_formal în:

132
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

Propoziţiile nestandard din descrierea subalgoritmului nu pot fi detaliate decât în


funcţie de problema rezolvată.

Pentru a rezolva problema globală P, apelul subalgoritmului se va face pentru


parametrii actuali P, n şi S:

Cheamă DivideEtImpera(P,n;S)

Problema 1: Se cere determinarea maximului dintr-un vector de n valori numerice.

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:

x1 ,x2 ,x3 , …, xk+1 , xk , xk+1 ,…,xn-2 ,xn-1 ,xn

Maxim

x1 ,x2 ,x3 , …, xk+1 , xk xk+1 ,…,xn-2 ,xn-1 ,xn

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.

133
Algoritmi şi structuri de date

Am identificat în problema dată iniţial o problemă rezolvabilă prin Divide Et


Impera. Dimensiunea unei subprobleme este dată de numărul elementelor din
subşirul manevrat şi dimensiunea unei subprobleme elementare este 1. Combinarea
soluţiilor parţiale se face printr-o comparaţie simplă.
Subalgoritmul DivideEtImpera devine în acest caz:
Subalgoritm DivideEtImpera _Maxim(xinf , …, xsup ;Maxim_formal) este:
Dacă sup-inf <= 0 atunci
Maxim_formal= xinf
Altfel
mij= (sup+inf)/2
Cheamă DivideEtImpera_Maxim(xinf , …, xmij;Maxim1)
Cheamă DivideEtImpera_Maxim(xmij+1 , …, xsup,Maxim2)
Dacă Maxim1>Maxim2 atunci
Maxim_formal=Maxim1
Altfel
Maxim_formal=Maxim2
SfDacă
SfDacă
SfSubalgoritm

Pentru rezolvarea problemei complete apelul subalgoritmului devine:


Cheamă DivideEtImpera(x1 , … ,xn ;Maxim)
Program:
#include <stdio.h>
#define NMAX 20

void citire (int t[NMAX],int *n)


{int i;
printf("dati dimensiunea tabloului;");
scanf("%d",n);
for(i=0;i<*n;i++)
{
printf("\nT[%d]=",i);
scanf("%d",&t[i]);
}
}

int maxim(int t[NMAX], int inf,int sup)


{
int mij, max1,max2;
if ((sup-inf)<=0)
return t[inf];
mij=(sup+inf)/2;
max1=maxim(t,inf,mij);
max2=maxim(t,mij+1,sup);
if (max1>max2)
return max1;

134
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

Generalizând, pentru un număr oarecare n de discuri situate pe tija A,


rezolvarea problemei se reduce la parcurgerea următoarelor etape:
- mută primele n-1 discuri de pe A pe C, utilizând tija B ca intermediară
- mută discul rămas de pe tija A pe tija B

135
Algoritmi şi structuri de date

- mută cele n-1 discuri de pe C pe B, utilizând tija A ca intermediară

Observăm că problema s-a redus la două probleme de dimensiuni mai mici,


respectiv, de a efectua mutarea a n-1 discuri. Aceiaşi manieră de abordare a
problemei de dimensiune n-1 va reduce problema la mutarea a n-2 discuri, ş.a.m.d.
În cele din urmă, pentru cazul elementar al unui singur disc ce trebuie mutat, se va
aplica rezolvarea directă.
Subalgoritmul Hanoi va trata problema transferului a k discuri de pe tija X, pe
tija Y, folosind tija rămasă Z ca intermediară:

Subalgoritm Hanoi (k,X,Y,Z) este:


Dacă k=1 atunci
Mută discul de pe tija X pe tija Y //rezolvare directă, problemă
elementară
Altfel
Cheamă Hanoi(k-1,X,Z,Y)
Mută discul rămas de pe X pe Y //problemă elementară
Cheamă Hanoi(k-1,Z,Y,X)
SfDacă
SfSubalgoritm

Apelul subalgoritmului pentru rezolvarea problemei globale este:


Cheamă Hanoi(n,A,B,C)

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
}

Problema 3: Căutare Binară. Fiind dat un vector ordonat de n valori numerice, să


se determine poziţia pe care se regăseşte o valoare dată cheie.
Analiza problemei:
Căutarea unei valori cheie într-un vector ordonat de n valori se poate reduce la o
problemă mai simplă (de dimensiune mai mică) dacă ne folosim de informaţia că
vectorul este ordonat. Acest lucru ne permite să împărţim vectorul în două părţi

136
Algoritmi şi structuri de date

relativ egale, să comparăm cheia cu valoarea elementului de pe poziţia tăieturii şi


să decidem continuarea căutării într-unul dintre subvectorii rezultaţi, ignorând acea
parte care nu poate conţine cheia căutată. Subvectorul în care se continuă căutarea
poate fi la rândul său împărţit în două părţi pentru a obţine probleme mai mici.
Împărţirea se termină în condiţiile în care cheia a fost găsită sau subvectorul pe
care îl manevrăm nu poate fi împărţit (este format din maxim un element).
Decizia continuării căutării într-unui dintre subvectorii rezultaţi printr-o
împărţire precedentă se face pe baza unei comparaţii simple a elementului de pe
poziţia tăieturii, notat xmij, cu cheia căutată. Rezultatul comparaţiei poate fi
următorul:
- cheie egală cu valoarea elementului xmij – rezultă că am găsit poziţia mij a
cheii
- cheie mai mică decât xmij - rezultă că vom continua căutarea în prima parte
a vectorului, între elementele situate la stânga poziţiei mij
- cheie mai mare decât xmij – rezultă că vom continua căutarea între
elementele din dreapta poziţiei mij

Subalgoritmul de rezolvare a problemei de căutare binară se descrie astfel:


Subalgoritm CăutareBinară(xinf , …, xsup ,cheie;poz) este:
Dacă sup-inf <= 0 atunci
// vectorul curent conţine un singur element
Dacă xinf= cheie atunci
poz=inf
Altfel
poz= -1 // semnificaţia faptului că nu s-a găsit cheia
SfDacă
Altfel
// vectorul curent conţine mai mult de un element
mij= (sup+inf)/2
Dacă cheie=xmij atunci
poz=mij //am găsit poziţia cheii
Altfel
Dacă cheie<xmij atunci
Cheamă CăutareBinară(xinf , …, xmij ,cheie;poz)
Altfel
Cheamă CăutareBinară(xmij+1 , …, xsup ,cheie;poz)
SfDacă
SfDacă
SfSubalgoritm

Apelul Cheamă CăutareBinară(x1 , …, xn ,cheie;poz) rezolvă problema


globală. Dacă valoarea poz este egală cu -1 la terminarea apelului, cheia nu a fost
găsită în vectorul dat, în caz contrar, valoarea poz reprezintă poziţia pe care a fost
găsită cheia căutată.

137
Algoritmi şi structuri de date

Problema 4. Sortare rapidă (QuickSort). Algoritmul de sortare rapidă este


un exemplu tipic de algoritm Divide et Impera. Fiind dat un vector oarecare de n
elemente se cere ordonarea crescătoare a vectorului după valorile elementelor.
Ideea de bază a sortării rapide constă în împărţirea vectorului în doi
subvectori cu proprietatea că toate elementele primului sunt mai mici decât un
element pivot şi elementele celui de-al doilea subvector sunt mai mari decât
pivotul. Alegerea pivotului rămâne în sarcina programatorului, existând trei
variante plauzibile: pivotul este elementul median al subvectorului manevrat,
pivotul este elementul de pe prima poziţie, respectiv, pivotul este ales ca elementul
de pe ultima poziţie din subvector. În descrierea următoare, pivotul este ales ca
fiind elementul de pe poziţia mediană.
Împărţirea vectorului în două părţi care verifică proprietatea enunţată
necesită operaţii de interschimbare a valorilor elementelor de pe poziţii diferite.
Odată ce etapa de împărţire a fost parcursă, cei doi subvectori rezultaţi vor fi supuşi
aceluiaşi procedeu de împărţire. Problema devine elementară implicit dacă
subvectorul corespunzător are dimensiunea 1, fiind considerat ordonat. Combinarea
soluţiilor nu necesită o etapă distinctă, făcându-se în mod implicit, prin
interschimbările elementelor vectorului dat în apelurile recursive ale
subalgoritmului.
Împărţirea unui şir de elemente xinf, …,xsup se face parcurgând paşii:
1. iniţializează doi indici de parcurgere a subşirului în două sensuri: i –
indicele de parcurgere a şirului de la stânga la dreapta, respectiv, j –
indicele de parcurgere de la dreapta la stânga; iniţial, i=inf, j=sup.
2. stabileşte pivotul (elementul de pe poziţie mediană din şir)
3. câttimp pivotul este mai mare decât elementele de pe poziţiile parcurse de
la stânga la dreapta, efectuează avans la dreapta al indicelui i
4. cât timp pivotul este mai mare decât elementele de pe poziţiile parcurse de
la stânga la dreapta, efectuează avans la dreapta al indicelui i
5. dacă i<j interschimbă valorile de pe poziţiile i şi j şi actualizează indicii
prin avans la dreapta, respectiv la stânga
6. repetă paşii 3, 4 şi 5 până când valoarea indicelui i devine mai mare decât
valoarea lui j

După parcurgerea paşilor 1-6 se consideră subşirul xinf, …,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.

Subalgoritmul corespunzător metodei descrise anterior este următorul:

Subalgoritm QuickSort(xinf, …,xsup)este:


Fie i=inf şi j=sup
mij=(inf+sup)/2 //mij – reprezintă poziţia pivotului xmij
Repetă
Câttimp ( i<sup şi xi<xmij)
//avans la dreapta
i=i+1

138
Algoritmi şi structuri de date

SfCâttimp
Câttimp ( j>inf şi xj>xmij)
//avans la stânga
j=j-1
SfCâttimp
Dacă i<j atunci
Cheamă Interschimbare(xi,xj)
SfDacă
Pânăcând (i>j)
Dacă (i<sup) atunci
Cheamă QuickSort(xi, …,xsup)
SfDacă
Dacă (j>inf)
Cheamă QuickSort(xinf, …,xj)
SfDacă
SfSubalgoritm

Pentru rezolvare problemei globale se va efectua apelul:


Cheamă QuickSort (x1, …,xn)

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)

139
Algoritmi şi structuri de date
{//interschimb x[i] cu x[j]
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);
}

Problema 4. Sortarea prin interclasare este o tehnică de ordonare a unui


vector. Principiul de bază este acela al împărţirii vectorului iniţial în două părţi
egale (subvectori), prin sortarea părţilor rezultate şi ulterior interclasarea celor doi
subvectori ordonaţi, rezultând vectorul global ordonat. Fiecare dintre cei doi
subvectori obţinuţi la o împărţire precedentă, la rândul lor pot fi împărţiţi fiecare în
două părţi, urmând ca subvectorii de dimensiune mai mică să fie supuşi aceluiaşi
mecanism de ordonare prin interclasarea şi să obţinem subvectori ordonaţi, de
dimensiuni mai mari.
Operaţia de interclasare a doi vectori a fost discutată într-un capitol precedent.
Această operaţie presupune parcurgerea secvenţială a celor doi vectori ordonaţi şi
construirea celui de-al treilea vector prin copierea elementelor din cei doi vectori
cu restricţia de a păstra relaţia de ordine între elementele vectorului rezultat. În
algoritmul de sortare prin interclasare, vectorii ce vor fi interclasaţi sunt de fapt
subvectori ai aceluiaşi vector, ceea ce conduce la o procedură de interclasare uşor
diferită faţă de cea prezentată în capitolul III.
Observăm că problema generală permite o împărţire în subprobleme
independente mai mici rezolvabile prin aceiaşi tehnică, ceea ce ne permite o
abordare prin metoda Divide et Impera. Subproblemele se consideră elementare
dacă dimensiunea subvectorilor devine 1, ceea ce este echivalent afirmaţiei
“subvector ordonat”.
Subalgoritmul prin care se realizează sortarea prin interclasare este descris în
continuare:

140
Algoritmi şi structuri de date

Subalgoritm MergeSort(xinf , …, xsup) este:


Dacă sup-inf<1 atunci
// nu se efectuează nimic, se revine din apelul subalgoritmului (condiţia
// de terminare a recursivităţii), considerând că subvectorul curent de
// dimensiune 1 este ordonat
Altfel
Fie mij=(inf+sup)/2
Cheamă MergeSort(xinf , …, xmij)
Cheamă MergeSort(xmij+1 , …, xsup)
//interclasarea subvectorilor xinf , …, xmij şi xmij+1 , …, xsup
i=inf // i – indicele de parcurgere a subvectorului xinf , …, xmij
j=mij+1 // j – indicele de parcurgere a subvectorului xmij+1 , …, xsup
k=1 // k –parcurgerea vectorului rezultat y1 ,…, ydim
Câttimp (i<=mij şi j<=sup)
Dacă (xi<xj) atunci
yk=xi // se copiază din primul subvector
i=i+1
k=k+1
Altfel
Interclasare yk=xj // se copiază din al doilea subvector
j=j+1
k=k+1
SfDacă
SfCâttimp
Câttimp (i<=mij) //au mai rămas elemente în primul subcvector
yk=xi // se copiază din primul subvector
i=i+1
k=k+1
SfCâttimp
Câttimp (j<=sup) //au mai rămas elemente în primul subcvector
yk=xj // se copiază din al doilea subvector
j=j+1
k=k+1
SfCâttimp
// copierea vectorului y în vectorul xinf , …, xsup
Pentru i de la 1 la k-1
xi+inf-1=yi
SfPentru
SfDacă
SfSubalgoritm

Apelul Cheamă MergeSort(x1 , …, xn) va produce ordonarea vectorului global

141
Algoritmi şi structuri de date

Program C: Ordonarea unui vector prin metoda sortării prin interclasare.

#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);
}

142
Algoritmi şi structuri de date

XIV. METODE DE ELABORARE A ALGORITMILOR.GREEDY

Metoda Greedy (greedy = (en.)lacom) este o tehnică de rezolvare problemelor


de optimizare bazată pe principiul alegerii “lacome” a optimului local în vederea
determinării optimului global. Soluţia unei probleme abordată cu metoda Greedy se
construieşte treptat, prin alegeri corecte, optimale şi irevocabile ale soluţiilor
parţiale. Dezavantajul major al metodei constă în incapacitatea determinării soluţiei
optime globale pentru orice problemă.
Pentru a aplica metoda Greedy, problema abordată trebuie reformulată într-o
maniera şablon prin care se pot identifica datele de intrare sub forma unei mulţimi
A, datele de ieşire sub forma unei submulţimi B a mulţimii de intrare A şi anumite
condiţii specifice problemei care ne permit alegerea unei soluţii parţiale optimale:
Fie A o mulţime de n elemente: A={a1,a2,…,an}.
Se cere determinarea submulţimii B, B ⊆ A astfel încât B este maximală şi
verifică anumite condiţii.
Principiul de rezolvare:
- Se porneşte cu mulţimea vidă B={}, care ulterior va fi completată cu
elemente ale mulţimii A
- Se parcurge mulţimea A, element cu element, şi se verifică pentru fiecare
membru al mulţimii A condiţiile identificate din contextul problemei.
Dacă aceste condiţii sunt verificate, elementul respectiv va fi „înghiţit” de
mulţimea B
- Procedeul se încheie în două situaţii:
o S-a determinat mulţimea maximală B
o Nu mai sunt elemente de parcurs în mulţimea A

Există două variante ale algoritmului Greedy:


- cu ordonarea în prealabil a mulţimii A
- fără ordonarea mulţimii A
Descrierea algoritmică a metodei generale este prezentată în continuare, cu
menţiunea că propoziţiile nestandard (introduse prin prefixarea cu simbolul*) nu
pot fi rafinate decât pentru problema concretă de rezolvat:

Algoritm Greedy este: //varianta fără ordonarea mulţimii A


Date de intrare: A
Date de ieşire: B
Fie B={}
Câttimp (A≠Φ) şi (*B nu este optimă)
*Alege ai din A
Dacă B ∪ {ai} *este soluţie posibilă atunci
B = B ∪ {ai} sau * ai înlocuieşte un element din B
A=A \ { ai }
SfDacă
SfCâttimp

143
Algoritmi şi structuri de date

SfAlgoritm

Algoritm Greedy2 este: //varianta cu ordonarea mulţimii A după un criteriu


Date de intrare: A
Date de ieşire: B
Fie B={}
*Ordonează mulţimea A
Pentru i de la 1 la n
Dacă B ∪ {ai} este soluţie posibilă atunci
B = B ∪ {ai} sau ai înlocuieşte un element din B
SfDacă
SfPentru
SfAlgoritm

Problema 1. Problema monedelor.


Se consideră o mulţime infinită de monede de valori 1,k1,k2,...,kn-1 şi S o sumă
exprimabilă prin aceste monede. Se cere determinarea unei scheme de exprimare a
sumei S utilizând un număr optim (minim) de monede.

Analiza problemei:
Din problema formulată mai sus putem deduce mulţimea de intrare A ca fiind
mulţimea monedelor de valori 1,k1,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ă kj care este mai mică valoric decât
suma S
- Cât timp suma S rămâne pozitivă adaugă moneda kj la mulţimea B şi scade
valoarea monedei din suma S
- alege monede de valoare imediat mai mică decât kj, respectiv, kj-1
- Cât timp suma S rămâne pozitivă adaugă moneda kj-1 la mulţimea B şi
scade valoarea monedei kj-1din suma S
- alege monede de valoare imediat mai mică decât kj-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

Procedeul descris anterior corespunde unui algoritm Greedy: soluţia globală se


construieşte treptat prin alegeri succesive şi irevocabile ale unităţilor monetare ce
intră în alcătuirea schemei de exprimare a sumei S. Lăcomia procedeului este dată
de maniera de parcurgere a valorilor monetare, de la cele mai mari înspre cele mai
mici, ceea ce permite formarea unei mulţimi B de dimensiune minimă (cu cât

144
Algoritmi şi structuri de date

valorile monetare sunt mai mari, cu atât numărul monedelor prin care se exprima S
este mai mic).

Algoritmul GreedyUnităţiMonetare este:


Date de intrare: S,k,n
Date de ieşire: B
Fie B={}
*Caută cel mai mare j pentru care kj<=S
Câttimp (j≥0)
Câttimp (S- kj ≥ 0)
S=S- kj
B=B ∪ { kj }
SfCâttimp
j=j-1 //alege următoarea valoare monetară, mai mică
SfCâttimp

Datele de intrare ale algoritmului descris sunt suficiente pentru a defini


mulţimea A: cunoscând k, n şi faptul că numărul de monede de acelaşi tip este
nelimitat, mulţimea A se subînţelege ca fiind: A= {1,1,… , k,k,…,k2, k2,… … kn-
1 n-1
,k ,…..}

Problema 2. Problema Rucsacului. Se consideră o mulţime A de n obiecte


caracterizate fiecare prin capacitate şi importanţă: A= {a1,a2,…,an}:
∀ i =1,..., n , wi – importanţa obiectului ai
∀ i =1,..., n , ci – importanţa obiectului ai
Se cere determinarea unei scheme de încărcare optimă a unui rucsac de
capacitate maxim permisă C cu obiecte din mulţimea A: suma importanţelor
obiectelor selectate trebuie să fie maximă şi suma capacităţilor obiectelor selectate
nu trebuie să depăşească capacitatea maximă C.
Observaţie: Varianta fracţionară a problemei rucsacului permite ca anumite
obiecte să fie încărcate în fracţiuni, soluţia finală oferită de metoda Greedy fiind
optimul global. Pentru problema discretă a rucsacului (obiectele nu pot fi împărţite
pe fracţiuni), metoda Greedy nu determină întotdeauna optimul global.

Analiza problemei (varianta discretă): Strategia de încărcare a rucsacului


(iniţial gol) este de a introduce treptat în rucsac acel obiect din mulţimea obiectelor
disponibile, pentru care raportul capacitate – importanţă este minim, ceea ce
corespunde construirii unei soluţii parţiale optime. Practic, la fiecare pas de
construire a soluţiei vom prefera un obiect de importanţă mare şi capacitate mică.
Alegerea “lacomă” a obiectelor garantează obţinerea unei soluţii finale bune.
Faptul că asupra alegerilor efectuate nu se revine determină ca în multe situaţii
soluţia optimă globală să nu poată fi găsită.
Se observă că mulţimea obiectelor A se parcurge în ordine crescătoare după
raportul capacitate – importanţă, ceea ce ne conduce la construirea unui algoritm
Greedy în varianta a doua, cu ordonarea elementelor mulţimii A.

145
Algoritmi şi structuri de date

Algoritmul de rezolvare a problemei rucsacului, varianta discretă, este descris în


continuare:

Algoritm Rucsac este:


Date de intrare: A= {a1,a2,…,an}
//pentru care se cunosc wi şi ci , ∀i =1,..., n
Date de intrare: C –capacitatea rucsacului
Date de ieşire B //mulţimea obiectelor selectate în rucsac
Ordonează A , crescător după valorile rapoartelor wi/ci
Fie CB=0 //suma capacităţilor obiectelor selectate
Fie B={}
Pentru i de la 1 la n
Dacă ( CB + ci ≤ C ) atunci
B=B ∪ {ai}
CB =CB + ci
SfDacă
SfPentru
SfAlgoritm

Problema 3. Planificarea spectacolelor. Se consideră o mulţime de n activităţi


(spectacole) A= {a1,a2,…,an}, fiecare activitate ai fiind caracterizată prin ora de
debut si şi ora de terminare ti. Într-o sală de spectacole, se doreşte o selecţie a
activităţilor disponibile astfel încât să fie derulate, într-o singură zi cât mai multe
spectacole şi acestea să nu se intercaleze.
Se cere un orar al spectacolelor astfel încât numărul lor să fie maxim posibil şi
să nu existe suprapuneri.

Analiza problemei: Din analiza specificaţiilor problemei se deduce imediat


formularea specifică unei probleme rezolvabile prin metoda Greedy:
Se dă A= {a1,a2,…,an}
Se cere B ⊆ A astfel încât B este maximală şi verifică anumite condiţii.
Condiţiile se referă la ne-suprapunerea oricăror două activităţi planificate.
Ceea ce este dificil de stabilit, este maniera de alegere, la fiecare pas, a
următoarei activităţi din mulţimea activităţilor disponibile (neplanificate deja),
pentru a obţine în final o planificare optimă.
Identificăm trei variante de alegere a următoarei activităţi pe care o planificăm :
- în ordine crescătoare, după timpul de start si
- în ordine crescătoare, după durata activităţii: di = ti-si
- în ordine crescătoare, după timpul de terminare ti
Dintre cele trei variante enunţate, doar cea de-a treia se dovedeşte plauzibilă.
Pentru primele două cazuri vom demonstra ineficienţa prin contraexemple:
Contraexemplu 1.

146
Algoritmi şi structuri de date

s4 t4

s3 t3
s2 t2

s1 t1

Considerăm cazul particular descris în imaginea alăturată. Fiecare activitate este


reprezentată printr-un segment ale cărui capete marchează timpul de start şi de
terminare. Prin alegerea spectacolelor în ordinea crescătoare a timpului de debut, se
va alege ca primă activitate planificată: a1 şi se constată imposibilitatea selectării
unei alte activităţi fără să existe suprapuneri. Algoritmul ar furniza în acest caz
soluţia B={a1} cu toate că există o soluţie mai bună: B={a2,a3}.

Contraexemplu 2.
s4 t4

s3 t3
s2 t2

s1 t1

Prin alegerea în ordinea crescătoare a duratei activităţilor disponibile, în cazul


particular figurat în imaginea alăturată, prima activitate selectată este a1. Această
alegere se dovedeşte neinspirată deoarece nicio altă activitate nu va mai putea fi
planificată ulterior, datorită suprapunerilor. Soluţia ar fi în acest caz B={a1} cu
toate că există o soluţie mai bună: B={a2,a3}.

Algoritmul de rezolvare a problemei planificării spectacolelor implică


ordonarea mulţimii de intrare, crescător, după timpul de terminare a activităţilor:

Algoritm GreedySpectacole este:


Date de intrare: A= {a1,a2,…,an}// pentru care se cunosc si şi ti ,
∀i =1,..., n
Date de ieşire B
Fie B={}
*Ordonează crescător A după valorile ti
Pentru i de la 1 la n
Dacă *(nu există aj ∈ B, astfel încât ai se suprapune lui aj) atunci
B=B ∪ {ai}
SfDacă
SfPentru
SfAlgoritm

Observaţie: Două activităţi ai şi aj se consideră că nu se suprapun ddacă este


verificată condiţia:si>tj sau sj>ti.
147
Algoritmi şi structuri de date

XV. METODE DE ELABORARE A ALGORITMILOR.


BACKTRACKING.

Metoda Backtracking (căutare cu revenire) este o metodă generală de rezolvare


a unei clase de probleme de optimizare bazată pe principiul construirii soluţiilor, în
mod treptat, prin alegeri succesive ale soluţiilor parţiale. Spre deosebire de metoda
Greedy, bazată pe acelaşi principiu, Backtracking oferă posibilitatea revenirii
asupra unor decizii anterioare, fapt care conduce la obţinerea soluţiilor optime
globale în orice situaţie. În plus, dacă sunt mai multe soluţii, metoda le determină
pe toate.
Metoda căutării cu revenire realizează o explorare sistematică a spaţiului
soluţiilor posibile, construieşte soluţiile finale, fără a genera însă toate soluţiilor
posibile din care s-ar putea extrage doar acele soluţii ce îndeplinesc condiţii
specifice problemei. Un dezavantaj major al metodei este acela că este o metodă
relativ costisitoare în privinţa timpului de execuţie.
Problemele rezolvabile cu metoda Backtracking sunt probleme în care soluţia
este reprezentată sub forma unui vector:
x = { x1 , x 2 ,..., x n } ∈ S ,
∀ i =1,..., n , xi ∈ Ai
unde S = A1 × A2 ×.. × An , şi Ai sunt mulţimi finite nu neapărat distincte.
Cerinţa problemei este determinarea tuturor soluţiilor posibile, care satisfac
anumite condiţii specifice problemei (denumite condiţii interne).
O abordare simplistă ar genera toate soluţiile posibile - elementele produsului
cartezian S = A1 × A2 ×.. × An , şi ulterior s-ar verifica aceste soluţii în vederea
identificării acelora care verifică condiţiile interne. Abordarea aceasta este
neeficientă. Generarea tuturor soluţiilor implică memorie şi timp suplimentar.
Backtracking este o alternativă inspirată de rezolvare a problemelor de acest gen.
Metoda evită generarea tuturor soluţiilor posibile, soluţiile finale se obţin prin
alegerea succesivă de elemente din mulţimile A1 , A2 , …, An cu posibilitatea
revenirii asupra unei alegeri dacă aceasta nu a condus la obţinerea unei soluţii
finale.
Algoritmii Backtracking pornesc cu o soluţie iniţială reprezentată de vectorul
vid. Ulterior acesta va fi mărit prin adăugarea elementelor x k din mulţimea
corespunzătoare Ak , treptat pentru k=1,2,…. Dacă vectorul parţial
{ x1 , x 2 ,..., x k } verifică anumite condiţii de validare, deduse din condiţiile
interne, se va continua cu augmentarea vectorului curent prin adăugarea unui nou
element x k +1 din mulţimea corespunzătoare Ak +1 . Dacă vectorul nu
îndeplineşte condiţiile de validare se încearcă un nou element din mulţimea Ak şi
se reverifică condiţiile de validare. Există posiblitatea ca Ak să nu mai conţină
alte elemente care au rămas neverificate, ceea ce produce o revenire la o decizie
anterioară şi încercarea unui alt element pe poziţia anterioară k-1 din vector.

148
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

149
Algoritmi şi structuri de date

Cazul 2.2
… … … …
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

Tratarea nivelului k se face diferenţiat pentru cazurile următoare:


Cazul 1. { x1 , x 2 ,..., x k } verifică condiţiiile de validitate: în acest caz se va
trece la completarea nivelului următor al stivei cu un element x k +1 din Ak +1 .
(operaţie push). Dacă vectorul parţial verifică şi condiţiile de finalitate se va
furniza soluţia.
Cazul 2. { x1 , x 2 ,..., x k } NU verifică condiţiile de validitate şi:
Cazul 2.1. mai există elemente din Ak care nu au fost încercate, atunci:
*
- se alege alt element x k neîncercat din Ak şi se reverifică
condiţiile de validitate.
Cazul 2.2 Nu mai există elemente din Ak rămase neîncercate, atunci:
- se revine pe nivelul anterior k-1 (operaţie pop).

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 } .

Funcţie Valid(k) este:


//verifică dacă soluţia parţială { x1 , x 2 ,..., x k } din stivă este validă sau
nu
//întoarce Adevarat sau Fals în funcţie de rezultatul testului
SfFuncţie

Funcţie Final(k) este:


//verifică dacă soluţia parţială { x1 , x 2 ,..., x k } este finală sau nu
//întoarce Adevarat sau Fals în funcţie de rezultatul testului
SfFuncţie

Varianta Iterativă:

Algoritm BackTrackingIterativ este:


150
Algoritmi şi structuri de date

k=1 //iniţializarea nivelului stivei


Câttimp (k≥1)
Dacă ( ∃ x k ∈ Ak “neîncercat” ) atunci
Stiva [ k ] ← x k //operaţia push
Dacă Valid(k)=Adevarat atunci
Dacă Final(k)=Adevarat atunci
Tipăreşte “Soluţie:” Stiva Stiva [1],.., Stiva [k ]
Altfel
k=k+1 //trece la alt nivel
SfDacă
SfDacă
Altfel
Stiva [ k ] = Null //operaţia pop
k=k-1 //revine la nivelul precedent
SfDacă
SfCâttimp
SfAlgoritm

Varianta recursivă:

Subalgoritm Backtracking( k) este: //tratează nivelul k


Pentru fiecare x k ∈ Ak
Stiva [ k ] ← x k //operaţia push
Dacă Valid(k)=Adevarat atunci
Dacă Final(k)=Adevarat atunci
Tipăreşte “Soluţie:” Stiva Stiva [1],.., Stiva [k ]
Altfel
Cheamă Backtracking( k+1) //autoapel
SfDacă
SfDacă
SfPentru
SfSubalgoritm

Observaţie: În varianta recursivă, operaţiile pop (revenirea pe un nivel inferior al


stivei soluţie) se execută automat la revenirile din apelurile subalgoritmului.

Pentru furnizarea soluţiilor, se va apela:


Cheamă Backtracking (1).

Problema 1. Problema damelor. Se dă o tablă de şah de n linii şi n coloane.


Având n regine, se cer toate soluţiile de aranjare a reginelor pe tabla de şah, astfel
încât oricare două piese de acest tip să nu se atace.
Analiza problemei:
Pornind de la observaţia că două dame se atacă în condiţiile în care ambele se
situează: fie pe aceiaşi linie, fie pe aceiaşi coloană, fie pe o diagonală a tablei de

151
Algoritmi şi structuri de date

şah, putem deduce o variantă simplificată de reprezentare a unei soluţii prin


împiedicarea oricăror dame de a fi situate pe aceiaşi coloană. Considerăm astfel că
dama k este blocată să ocupe coloana k a tablei de şah. Astfel, oricare două dame
sunt poziţionate pe coloane distincte şi soluţia finală poate fi reprezentată printr-un
vector de n elemente Stiva [1],.., Stiva [ k ],..., Stiva [n ] astfel încât:
k– reprezintă indicele coloanei pe care se află dama k
Stiva[k] – elementul din stivă este indicele liniei ocupate de dama k
Practic, poziţia în stivă: k - corespunde atât indicelui de coloană cât şi
identificatorului damei, iar valoare de pe nivelul k din stivă coincide cu numărul
liniei ocupate de dama k.

1. Spaţiul soluţiilor posibile este produsul cartezian: A × A ×... × A ,


unde A={1,2,…,n}.
2. Soluţia este reprezentată printr-un vector (stivă).
3. Condiţiile ca o soluţie parţială Stiva [1],.., Stiva [k ] să fie validă se
reduc la a verifica dacă a k-a damă nu atacă oricare din damele
poziţionate pe primele k-1 coloane.
4. O soluţie este finală dacă s-a reuşit poziţionarea tuturor damelor,
respectiv, dimensiunea vectorului construit este n (s-a completat
nivelul n al stivei)
5. Dacă pe tabla de şah sunt dispuse k-1 dame astfel încât acestea nu se
atacă (soluţie parţială validă), se poate trece la poziţionarea celei de-a
k-a piesă.

Pe baza consideraţiilor 1-5 problema 1 se poate rezolva printr-un algoritm


Backtracking. Algoritmul general descris anterior nu se modifică, în schimb vor fi
detaliate funcţiile Valid şi Final:

Funcţie Valid(k) este


//verifică dacă a k-a damă atacă damele 1,2,…,k-1
Valid=Adevărat
Pentru i de la 1 la k-1
Dacă ( Stiva [ k ] = Stiva [i ] ) SAU ( (k-i)=( Stiva [ k ] − Stiva [i ] ) )
atunci
/ damele k şi i sunt situate pe aceiaşi linie sau pe aceiaşi diagonală
Valid=Fals
SfDacă
SfPentru
SfFuncţie

Funcţie Final(k) este:


Dacă k=n atunci
// am ajuns la nivelul n al stivei – s-au poziţionat toate cele n regine
Final=Adevărat
Altfel
Final=Fals

152
Algoritmi şi structuri de date

SfDacă
SfFuncţie

Rezolvare1: Programul C pentru rezolvarea problemei damelor (varianta


nerecursivă)

#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]);
}

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;

153
Algoritmi şi structuri de date
if (valid(k)==1) {gasit=1;break;}
}
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);
}

Rezolvare2: Programul C pentru rezolvarea problemei damelor (varianta


recursivă)

#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]);
}

154
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);
}

Problema 2. Se dă un labirint reprezentat sub forma de matrice cu m linii si n


coloane, ale cărei elemente sunt valori 1 sau 0. Fiecare element al matricei
reprezintă o camera a labirintului (1 pentru zid si 0 pentru drum liber). Intr-una din
camere, de coordonate Xstart si Zstart se afla o persoană. Determinaţi toate
drumurile prin care persoana va putea ieşi din labirint,dacă aceasta va putea efectua
doar mişcări spre Nord, Sud, Est şi Vest.

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

155
Algoritmi şi structuri de date

scurte decât altele. Condiţia ca un vector de perechi (i,j) să fie soluţie finală a
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

Funcţie Final (k)


Fie (i,j) –poziţia memorată în Stiva[k]
Dacă i=1 sau j=1 sau i=m sau j=n atunci
Final=Adevarat
Altfel
Final=Fals
SfDacă
SfFuncţie
Subalgoritm Labirint(k) este:
Pentru fiecare vecin posibil (i,j) al camerei salvate în Stiva[k-1]
Stiva[k]=(i,j) //operaţia push

156
Algoritmi şi structuri de date

Dacă Valid(k)=Adevărat atunci


Dacă Final(k)=Adevărat atunci
Tipăreşte “Soluţie:” Stiva Stiva [1],.., Stiva [k ]
Altfel
Cheamă Labirint(k+1)
SfDacă
SfDacă
SfPentru
SfSubalgoritm

Rezolvarea problemei labirintului se face prin iniţializarea stivei cu poziţia de


start: Stiva [0] = ( Xstart , Ystart ) şi apelul:Cheamă Labirint(1).

Problemă rezolvată PERMUTĂRI: Programul următor geneează permutările de n


prin metoda Backtracking - varianta recursivă.

#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);
}

157
Algoritmi şi structuri de date

XVI. METODE DE ELABORARE A ALGORITMILOR.PROGRAMARE


DINAMICA.

Metoda Programării dinamice se poate considera ca o îmbunătăţire a tehnicii


Greedy. În cazul metodei Greedy, alegerea unui element se face pe baza unui
criteriu de admisibilitate, însă la Programarea dinamică, acest criteriu este cel al
optimalităţii; folosind-se de un criteriu mai puternic, rezultatul final oferit de
metoda programării dinamice este întotdeauna optimul global.
Ca şi metoda Divide et impera, programarea dinamică presupune împărţirea
problemei în subprobleme, rezolvarea şi combinarea soluţiilor acestora pentru a
obţine rezultatul final. În situaţia în care subproblemele obţinute prin
descompunere conţin subprobleme comune, este preferabilă aplicarea metodei
Programării dinamice.
O particularitate a metodei constă în construirea si utilizarea unor tabele cu
informaţii. Tabelele sunt construite prin completarea unui element folosind
elemente completate anterior. Denumirea metodei provine din maniera de
completare dinamică a tabelei de informaţii intermediare.
Metoda programării dinamice se caracterizează prin principiile fundamentale:
- sunt evitate calculele repetate ale aceluiaşi subcaz, rezultatele intermediare
obţinute fiind memorate
- soluţia este construită în manieră bottom-up (de jos în sus): pornind de la
soluţiile celor mai mici subprobleme, prin combinarea acestora se va
ajunge treptat la soluţia globală
- problema abordată respectă principiul optimalităţii

Programarea dinamică este o metodă de rezolvare a problemelor de optimizare


pentru care soluţia este rezultatul unui şir de decizii optime. Problema de
optimizare rezolvabilă prin metoda programării dinamice este formulată astfel:

Fie un sistem care se poate afla într-una din stările:


S0, S1, S2 , …, Sn-1, Sn
şi fie şirul de decizii
D1, D2 , …, Dn-1, Dn
prin care sistemul este trecut prin stările date:
S 0  D1 → S 1  
D2
→ S 2 → ... → S n − 1  
Dn
→ Sn
Problema verifică unul dintre principiile optimalităţii:
Varianta 1: 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 Dk , …, Dn-1, Dn duce sistemul din starea
Sk-1 în starea finală Sn în mod optim.

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:

158
Algoritmi şi structuri de date

∀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.
Metoda înapoi – aplicată în cazul verificării principiului optimalităţii în
forma 2.

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ă.

Metoda înainte operează în următoarele 4 faze:


1. prin relaţiile de recurenţă se vor calcula optimele corespunzătoare stărilor
îndepărtate de starea finală în funcţie de optimele corespunzătoare stărilor
apropiate de starea finală (practic în această fază se completează tabela de
informaţii intermediare)
2. se determină deciziile care leagă optimele determinate în faza anterioară
3. se află optimul total
4. se determină soluţia problemei reprezentată de un şir de decizii constituit
prin compunerea deciziilor de la etapa 2, în mod directe, de la început
spre sfârşit (înainte)

Metoda înapoi este duală metodei descrise anterior şi operează în următoarele 4


faze:
1. prin relaţiile de recurenţă se vor calcula optimele corespunzătoare stărilor
apropiate de starea finală în funcţie de optimele corespunzătoare stărilor
depărtate de starea finală
2. se determină deciziile care leagă optimele determinate în faza anterioară
3. se află optimul total
4. se determină soluţia problemei reprezentată de un şir de decizii constituit
prin compunerea deciziilor de la etapa 2, în mod invers, de la sfârşit spre
început (înapoi)

Deşi pare o metodă generală de rezolvare a problemelor de optim,


aplicabilitatea programării dinamice este restrânsă doar la acele probleme pentru
care se poate demonstra principiul optimalităţii.

Problema 1 Subşir crescător de lungime maximă (metoda înainte)


Se dă A = { X 1 , X 2 ,..., X n } o mulţime (şir) neordonată de elemente.
Se cere determinarea lui B, B ⊆ A , şir ordonat crescător, de lungime maximă.
Exemplu: în şirul 7 1 4 2 5 3, cel mai lung subşir crescător este 1,2,3

159
Algoritmi şi structuri de date

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:

Subşiru Lungimea
l
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

Generalizând, dacă i1 , i 2 ,..., i n este subşirul optimal crescător de lungime n


care începe cu i1 : atunci:
- i 2 , i 3 ,..., i n este subşirul optimal optimal de lungime n-1 care începe cu
i2
- i 3 , ,..., i n este optimal de lungime n-2 care începe cu i3
- …..
- i k , i k +1 ,..., i n este subşirul optimal de lungime maximă care începe cu
ik, ∀k >1
Prin reformularea observaţiilor anterioare se identifică principiul optimalităţii în
varianta 1, fapt pentru care se va aplica metoda înainte:

Etapa_1&2: se vor calcula pe baza relaţiilor de recurenţă, pentru fiecare


element al şirului iniţial lungimea celui mai mare subşir crescător care se poate
forma începând de la el.

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:

L(k):={1+maxL(i) , astfel încât Xi >=Xk, i={1, 2, …, n}} şi k ∈ {1, 2, …, n} }

160
Algoritmi şi structuri de date

Algoritm Determinare_Lungimi este:


L(n)=1
Pentru k=n-1 la 1 cu pas -1
L(k)=1
Caută i>k astfel încât i ≤ n şi X i ≥ X k
Dacă s-a găsit (i) atunci
L(k)=L(i)+1
SfDacă
SfPentru
SfAlgoritm

Etapa3: Se determină maximul din tabela lungimilor L. Fie acesta L(imax). În


acest caz:
- L(imax) – reprezintă lungimea subşirului crescător de lungime maximă
(lungimea soluţiei)
- imax – reprezintă indicele din şirul iniţial de la care începe subşirul soluţie

Etapa4: Se determină soluţia problemei (subşirul crescător de lungime


maximă), prin construire, pornind de la elementul de pe poziţia imax până la
elementul de pe poziţia n, reţinându-se doar elementele care verifică relaţia de
ordine.

Problema 2: Problema Robotului (metoda înapoi)

Se dă o tablă împărţită în m linii şi n coloane:


 a11 a12 ... a1n 
 
a a22 ... a2 n 
A =  21
... ... ... ... 
 
a am 2 ... amn 
 m1
Fiecare căsuţă a tablei conţine obiecte de valori diferite (numere naturale):
aij ∈N .
Un robot situat în căsuţa de start a11 va traversa tabla pentru a ajunge la căsuţa
finală amn realizând doar mişcările posibile:
- la dreapta cu o căsuţă: ( aij →ai ,( j +1) )
- în jos cu o căsuţă: ( aij →a(i +1), j )
şi colectează obiectele din căsuţele traversate.

Să se determine secvenţa de mişcări (drumul) a robotului astfel încât suma


valorilor colectate la ieşire să fie maximă.

Exemplu:
Pentru următorul caz:

161
Algoritmi şi structuri de date

2 0 7 1
4 0 3 0
0 5 1 0
2 4 1 2

Soluţia imediată este parcurgerea căsuţelor: (1,1)-(2,1)-(2,2)-(3,2)-(4,2)-(4,3)-


(4,4), robotul colectează obiecte de valoare totală: 18 reprezentând suma maximă a
sumelor colectate prin parcurgerea oricăror drumuri de la căsuţa iniţială la căsuţa
finală.
Se observă că suma maximă cu care ajunge robotul în căsuţa finală poate fi
obţinută fie prin mutarea anterioară de pe coloana din stânga (n-1), fie prin mutarea
anterioară din celula de pe linia (m-1). Astfel, dacă suma finală a valorilor este
maximă, atunci prin scăderea valorii comune conţinută în amn , înseamnă că şi
drumurile parţiale efectuate până în celula am−1,n sau an ,m −1 sunt de sumă
maximă parţială.
Generalizând:
- Dacă în celula ai , j robotul a ajuns cu transport maxim de la celula
iniţială a11 , efectuând ultima mutare dreapta (robotul ajunge în celula
ai , j din celula ai , j −
1 ) atunci în celula ai , j −
1 robotul a ajuns cu sumă

maximă posibilă de la a11 la ai , j −1 .


- Dacă în celula ai , j robotul a ajuns cu transport maxim de la celula
iniţială a11 , efectuând ultima mutare jos (robotul ajunge în celula ai , j
din celula ai −1, j ) atunci în celula ai −1, j robotul a ajuns cu sumă
maximă posibilă de la a11 la ai −1, j .

Observaţie: Ultima formulare este chiar Principiul optimalităţii exprimat în


varianta 2:
Dacă secvenţa de celule parcurse de la celula : (1,1) la celula (m,n)
este de transport maxim: a11 , …,, aik −11 , jk −1 , aik , j k , … , am ,n
Atunci ∀k < n , secvenţa de celule parcurse de la (1,1) la ( ik ,
j k ) este de transport maxim: a11 , …,, ai ,j
k −11 , aik , j
k −1 k

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}

162
Algoritmi şi structuri de date

Pe baza relaţiei de recurenţă se va construi o tabelă suplimentară care reţine


valorile sumelor maxime pe care le poate colecta robotul până în fiecare celulă a
tablei iniţiale:

 S11 S12 ... S1n 


 
S S 22 ... S2n 
S =  21
... ... ... ... 
 
S Sm2 ... S mn 
 m1

Algoritm DeterminareSumeMaxime este:


S(0,1):=0
S(1,0):=0
Pentru i de la 1 la m
Pentru j de la 1 la n
Dacă S(i-1,j)> S(i,j-1) atunci
S(i,j):= S(i-1,j)+ ai,j
Altfel
S(i,j):= S(i,j-1)+ ai,j
SfDacă
SfPentru
SfPentru
SfAlgoritm

Se constată că ultima valoare determinată, S m ,n reprezintă chiar suma


maximă a drumului complet al robotului, iar soluţia problemei–vectorul format din
căsuţele (i,j) traversate cu transport maxim, poate fi determinată printr-o
parcurgerea inversă a matricei S :

Algoritm RefacereDrum este:


Vector(1)⇐ (m,n) // Reţine căsuţa finală (m,n)
Fie i=m, j=n, k=2
Câttimp ((i,j)≠(1,1))
Dacă S i −1, j > S i , j −1 atunci
Vector(k)⇐ (i-1,j) //Reţine căsuţa (i-1,j),
i=i-1
k=k+1
Altfel
Vector(k)⇐ (i,j-1) //Reţine căsuţa (i,j-1)
j=j-1
k=k+1
SfDacă
SfCâttimp
//Vectorul de căsuţe parcurs invers este soluţia problemei.
Pentru i de la k la 1 cu pas -1

163
Algoritmi şi structuri de date

Tipăreşte Vector(i)
SfPentru
SfAlgoritm

Observaţie: Tehnica descrisă pentru rezolvarea problemei robotului este


Programarea dinamică, varianta metodei înapoi.

Probleme:

1. Scrieţi un program de rezolvare a problemei robotului.


2. Scrieţi programul de rezolvare a problemei 1: Subşir crescător de lungime
maximă.
3. Scrieţi un program care determină cel mai lung subşir comun al două şiruri
date.

164
Algoritmi şi structuri de date

BIBLIOGRAFIE:

1. Liviu Negrescu, Limbajele C si C++ pentru incepatori Vol. I (p.1 si 2) -


Limbajul C , Editura Albastră, Cluj-Napoca, 2002.
2. Brian Kernighan, Dennis Ritchie, The C Programming
Language, Second Edition, Prentice Hall, 1988.
3. Bazil Pârv, Alexandru Vancea, Fundamentele limbajelor de
programare, Editura Albastră, Cluj Napoca, 1996.
4. Knuth, Donald E., Arta programarii calculatoarelor: Algoritmi
fundamentali. vol.1 , Editura Teora, Bucureşti, 1999.
5. Doina Logofătu, Algoritmi fundamentali în C++. Aplicaţii, Editura
Polirom, Iaşi, 2007.
6. Frenţiu M, Pârv B., Elaborarea programelor. Metode şi tehnici
moderne, Editura PROMEDIA, Cluj-Napoca, 1994.
7. Petrovici V., Goicea F., Programare în limbajul C, Editura Tehnică,
Bucureşti, 1993.

165

S-ar putea să vă placă și