Sunteți pe pagina 1din 200

Structuri de

date
Cuprins
INTRODUCERE ................................................................................ 6
CAPITOLUL 1. STRUCTURI DE DATE şi COLECŢII DE DATE ….7
1.1 Generalităţi şi definiţii……………………………………………7
1.1.1 Noţiunea de dată ....................................................... ..……7
1.1.2 Definiţia noţiunii structură de date ..................................... 7
1.1.3 Clasificări ale structurilor de date ........................................ 9
1.2 Colecţii de date………………………………….……….…......11
1.2.1 Exemple de colecţii: .......................................................... 11
1.2.2 Caracteristicile generale ale colecţiilor ............................. 12
1.2.3 Operaţii specifice colecţiilor .............................................. 12
1.2.4 Parcurgerea colecţiilor ...................................................... 13
1.2.5 Criterii de clasificarea a colecţiilor .................................... 13
1.3 Tipuri de date fundamentale în C/C++…………………… …16
1.3.1 Tipuri simple de date......................................................... 16
1.3.2 Pointeri .............................................................................. 19
1.4 Structura de date de tip tablou ............................................ 29
1.4.1 Modul de declarare ........................................................... 29
1.4.2 Prelucrări uzuale efectuate asupra structurii de date de tip
tablou ............................................................................. …..30
1.4.3 Utilizarea funcţiilor din biblioteca <algorithm> .................. 35
1.4.4 Utilizarea containerului <vector> ...................................... 41
A. Prezentarea generală a variabilelor de tip vector .......... 41
B. Utilizarea variabilelor de tip vector ................................. 42
C. Funcții conținute de un vector ........................................ 44
1.4.5 Containerului <string> ....................................................... 48
A. Utilizarea variabilelor de tip string .................................. 48
B. Proprietăți generale ale variabilelor de tip string ............ 49
C. Citirea și afișarea unui string .......................................... 50
D. Funcții conținute de un string ......................................... 50
1.5 Structura de date de tip listă înlănţuită .............................. 57
1.5.1 Generalităţi........................................................................ 57
1.5.2 Liste dinamice simple ....................................................... 58
1.5.3 Liste dinamice duble ......................................................... 64
1.5.4 Containerul list .................................................................. 70
A. Prezentarea generală a variabilelor de tip list ................ 70
B. Utilizarea variabilelor de tip list ....................................... 71
C. Operații implementate pentru variabilele de tip list ........ 72
D. Funcții conținute de o variabilă de tip list ....................... 72
E. Accesarea elementelor unei liste cu iteratori .................. 75
1.6. Structura de date de tip stivă ............................................. 80
1.6.1. Generalităţi....................................................................... 80
1.6.2. Implementarea unei stive folosind structura de date de tip
tablou unidimensional .......................................................... 80
1.6.3. Implementarea unei stive folosind structura de date de listă
înlănţuită .............................................................................. 83
1.6.4. Containerul stack ............................................................. 86
A. Prezentarea generală a variabilelor de tip stack ............ 86
B. Utilizarea facilităților oferite de containerul stack ........... 86
C. Operații implementate pentru variabilele de tip stack .... 87
D. Funcții conținute de o variabilă de tip stack ................... 88
1.6.5. Aplicaţie a structurii de date de tip stivă: forma fără
paranteze a unei expresii matematice (forma poloneză) ... 88
1.7 Structura de date de tip coadă ............................................ 95
1.7.1 Generalităţi........................................................................ 95
1.7.2. Implementarea unei cozi folosind structura de date de tip
tablou unidimensional .......................................................... 96
1.7.3. Implementarea unei cozi folosind structura de date de listă
înlănţuită .............................................................................. 98
1.7.4. Containerul queue.......................................................... 100
A. Prezentarea generală a variabilelor de tip queue ........ 101
B. Utilizarea variabilelor de tip queue ............................... 102
C. Operații implementate pentru variabilele de tip queue.103
D. Funcții conținute de o variabilă de tip queue ............... 103
1.7.5. Aplicaţii ale structurii de tip coadă ................................. 104
A. Sortarea radix (sortarea cu găleți) ................................ 104
B. Algoritmul Lee .............................................................. 106
1.8. Cozi cu două capete (cozi duble) ..................................... 109
1.8.1 Generalităţi; containerul <deque> .................................. 109
1.8.2 Utilizarea variabilelor de tip deque .................................. 109
1.8.3. Operaţii implementate pentru variabile de tip deque ..... 111
1.8.4. Funcţii conţinute de o variabilă de tip deque ................. 111
1.9 Probleme propuse .............................................................. 113
CAPITOLUL 2. GRAFURI NEORIENTATE.................................. 121
2.1 Introducere în teoria grafurilor .......................................... 121
2.2. Definiţia noţiunii de graf ................................................... 122
2.3. Noţiuni de bază pentru grafuri neorientate ..................... 124
2.4. Exemple de grafuri particulare……………………………..126
2.5. Lanţ, ciclu, conexitate……………………………………..…129
2.6. Metode de reprezentare pentru grafuri neorientate….,...134
2.6.1 Matricea de adiacenţă .................................................... 134
2.6.2 Liste de adiacenţă ........................................................... 135
2.6.3 Lista de muchii ................................................................ 136
2.7 Traversarea grafurilor ........................................................ 142
2.7.1 Traversarea în adâncime (DFS) ..................................... 142
2.7.2 Traversarea în lăţime (BFS)............................................ 145
2.7.3 Aplicaţii ale algoritmilor de traversare ............................. 152
A. Determinarea muchiilor critice dintr-un graf conex ....... 152
B. Determinarea unui ciclu eulerian întrun graf eulerian ... 154
2.8 Probleme propuse .............................................................. 159
CAPITOLUL 3. GRAFURI ORIENTATE .................................. .... 163
3.1 Introducere .......................................................................... 163
3.2 Noţiuni de bază ................................................................... 164
3.3. Drum, circuit, tare-conexitate ........................................... 165
3.4 Metode de reprezentare a digrafurilor .............................. 167
3.4.1 Matricea de adiacenţă .................................................... 167
3.4.2 Liste de adiacenţă ........................................................... 168
3.4.3 Lista de arce ................................................................... 169
3.5 Drumuri de cost minim în grafuri orientate ...................... 175
3.5.1 Reprezentarea grafurilor ponderate ................................ 176
3.5.2 Algoritmul Dijkstra ........................................................... 177
3.5.3 Algoritmul Roy-Floyd....................................................... 181
3.6 Flux maxim într-o reţea de transport ................................ 186
3.6.1 Definiţii ............................................................................ 186
3.6.2 Determinarea fluxului maxim într-o reţea de trasport ..... 187
3.7 Sortarea topologică ............................................................ 194
3.8 Probleme propuse .............................................................. 198
INTRODUCERE

Lucrarea reprezintă o abordare a principalilor algoritmi din teoria


grafurilor prin utilizarea facilităŃilor bibliotecilor moderne conŃinute de
limbajul C++ care implementează containere (cunoscute în literatura de
specialitate ca ansamblul de biblioteci STL) precum şi a bibliotecii de
algoritmi. Am pus accentul pe utilizarea STL datorită în primul rând a
ideii de reutilizare a soft-ului deja scris, ceea ce constituie „moda zilei”.
Aceste biblioteci a fost incluse în standardele C++ în 1998. Modul de
utilizare pentru containerele STL este la nivelul standardului adoptat de
comitetul care se ocupă acest aspect în anul 2005.
Lucrarea este structurată în trei capitole. În primul am prezentat
structurile de date uzuale, cu accent pe implementările din STL. În
fiecare subcapitol sunt incluse programe care ilustrează modul de
utilizare şi facilităŃile oferite de containerele implementate din STL. În
capitolul doi sunt prezentate principalele noŃiuni din teoria grafurilor
neorientate şi sunt implementaŃi algoritmii cei mai importanŃi din acest
domeniu prin utilizarea facilităŃilor din STL. Capitolul trei prezintă
noŃiunile de bază şi algoritmii cei mai importanŃi pentru grafuri orientate.
Cele mai importante secvenŃe conŃinute de algoritmii
implementaŃi la capitolele doi şi trei sunt explicate succint. Se face şi o
analiză a complexităŃii algoritmilor ca urmare a utilizării diverselor
structuri de date. De asemenea se fac trimiteri la studii care prezintă
modul în care, prin utilizarea unor structuri de date avansate,
complexitatea iniŃială a acestor algoritmi este îmbunătăŃită.
În scrierea programelor incluse în lucrare am urmărit în primul
rând o lizibilitate cât mai mare. Astfel am preferat să inserăm în text
programe complete, chiar dacă aceasta a presupus apariŃia aceleiaşi
secvenŃe (cum ar fi cea de citire a unor date sau cea de afişare a unor
informaŃii) în multe programe. Tot pentru o lizibilitate ridicată, am
urmărit ca funcŃiile definite în programele din text să fie declarate cu un
număr cât mai mic de parametri.
Lucrarea este utilă celor care doresc să studieze teoria grafurilor
mai ales prin prisma celor mai importanŃi algoritmi şi a implementării
concrete a acestora.
Într-un viitor apropiat ne propunem să continuăm problematica
specifică teoriei grafurilor prin publicarea unei lucrări în care să
analizăm grafurile neorientate de tip arbore şi arborii cu rădăcină. În
această lucrare urmează să folosim facilităŃile STL stabilite prin
standarde mai recente (2011 şi 2014).
Autorul

6
CAPITOLUL 1
STRUCTURI DE DATE şi COLECłII DE DATE

1.1 GeneralităŃi şi definiŃii

1.1.1 NoŃiunea de dată

În informatică prin dată, se înŃelege un model de reprezentare a


informaŃiilor despre obiectele supuse prelucrării pe calculator. Acest
model trebuie să fie accesibil atât utilizatorului cât şi componentelor
calculatorului.

Datele se pot clasifica în funcŃie de obiectele pe care le reprezintă,


astfel:

• date elementare sau scalare care se prezintă sub forma de


entităŃi indivizibile;
• colecŃii de date, care se prezintă sub forma unor mulŃimi de date
elementare între care se definesc şi se descriu anumite relaŃii.

Organizarea datelor presupune următorii paşi:

• identificarea datelor;
• clasificarea şi descrierea proprietăŃilor, a caracteristicilor datelor;
• gruparea datelor în colecŃii de date destinate prelucrării
automate;
• reprezentarea externă pe suporturi tehnice;
• identificarea, definirea şi descrierea procedurilor de prelucrare
automată.

EficienŃa prelucrării automate a datelor depinde în mare măsură, de


organizarea internă şi externă a acestora şi de stabilirea unor structuri
de date care să corespundă cerinŃelor de prelucrare.

1.1.2 DefiniŃia noŃiunii structură de date

În informatică, o structură de date este o metodă sistematică de


stocare a informaŃiilor şi datelor într-un calculator, în aşa fel încât
respectivele informaŃii să poată fi folosite în mod eficient. Totodată o

7
structură de date presupune şi că între aceste informaŃii s-au definit o
serie de relaŃii cum ar fi:

• de echivalenŃă;
• de ordine parŃială sau de ordine totală

RelaŃiile sunt concretizate în operaŃii. Pentru realizarea acestor


operaŃii o structură de date presupune existenŃa un grup de operatori
de bază cu o anumită semantică (sintaxă) care duc la un mecanism de
selectare şi identificare.

Structurile de date sunt utilizate în diferite situaŃii ca de exemplu:

• Memorarea unor date din realitate;


• Instrumente ale programatorilor;
• Modelarea unor situaŃii din lumea reală.

În proiectarea multor tipuri de programe, alegerea structurii de date


este principalul obiectiv al specificaŃiilor de implementare. ExperienŃa în
construirea sistemelor informatice mari a arătat că dificultatea
implementării precum şi calitatea şi performanŃa produsului final depind
în mare măsură de alegerea structurilor de date. Dacă tipurile de
structuri de date au fost alese în mod avantajos, algoritmii ce vor trebui
utilizaŃi devin de multe ori aproape evidenŃi. Câteodată însă situaŃia
este mai complicată; atunci structurile de date sunt alese pe baza
necesităŃilor sarcinilor principale.
Există tipuri de structuri de date care sunt foarte specializate pe
anumite sarcini sau aplicaŃii; altele sunt utile pentru o paletă mult mai
largă de aplicaŃii. De exemplu:

• arborii B sunt foarte potriviŃi pentru implementarea bazelor de


date;
• tabele de rutare se folosesc îndeosebi pentru reprezentarea
interconectării elementelor din reŃelele de calculatoare;
• tablourile şi listele sunt utile în aplicaŃii diverse.

Deseori o alegere bine făcută a structurii de date va permite şi


implementarea unui algoritm eficient. O structură de date bine
concepută permite efectuarea unei varietăŃi de operaŃii de bază,
utilizând puŃine resurse (de exemplu ocupă un spaŃiu mic de memorie
sau ocupă pentru un interval mic de tip unitatea centrală
(microprocesorul) ceea ce rezultă dintr-un timp mic de execuŃie).

8
Structurile de date se implementează utilizând tipuri de date,
toate facilitate de către un limbaj de programare. Pentru multe metode
formale de proiectare şi limbaje de programare, factorul organizatoric
cheie sunt structurile de date şi nu algoritmii. Majoritatea limbajelor
dispun de un mod care permite reutilizarea structurilor de date şi pentru
alte aplicaŃii, prin ascunderea detaliilor de implementare, sigure şi
verificate, în spatele unor interfeŃe controlate. De exemplu, în limbajul
C++ se utilizează în acest scop tipuri obiectuale implementate prin
structuri sau prin clase.
Exemple de structuri de date (logice) uzuale: tablouri, stive, cozi,
liste, grafuri, arbori, tabele de dispersie.
Din cauză că structurile de date au o importanŃă atât de mare,
multe dintre ele sunt incluse în bibliotecile standard ale multor limbaje
de programare şi medii de dezvoltare, cum ar fi

• Standard Template Library (STL) în C++,


• Java Collections Framework.

1.1.3 Clasificări ale structurilor de date

Vom presupune că o structură de date este alcătuită din


componente. Acestea se pot identifica şi selecta prin diverse metode,
de exemplu un nume de identificare sau o poziŃie pe care o ocupă în
cadrul structurii, potrivit relaŃiei de ordine stabilită.

A. După tipul componentelor

• structuri de date omogene, care conŃin elemente de acelaşi tip;


• structuri de date eterogene, care conŃin componente de tipuri
diferite.

Dacă o structură se poate descompune în structuri de acelaşi tip,


atunci structura respectivă este recursivă.

B. După zona de memorie ocupată

• structuri de date interne; au caracter temporar, deoarece sunt


reprezentate în memoria internă a calculatoarelor (care este
volatilă);

9
• structuri de date externe: au un caracter relativ permanent,
deoarece sunt memorate pe suport extern. Aceste structuri pot
cuprinde:
o fişiere de date
o baze de date
o bănci de date

C. Din punct de vedere al modului de alocare a zonelor de


memorie ocupate

• structuri de date statice, la care alocarea zonelor de memorie


necesare păstrării temporare a datelor este făcută în momentul
compilării programului şi rămâne aceeaşi pe toată durata de
execuŃie a programului respectiv;
• structuri de date dinamice, la care alocarea zonelor de memorie
se face numai în momentul execuŃiei programului, când în
program se consideră că este momentul necesar, ele putând fi
modificate, eliberate sau realocate pe toata durata de execuŃie a
programului respectiv.

D. După nivelul de structurare a datelor:

• structura logică, care se referă la modul de ordonare a datelor,


la operatorii de prelucrare a datelor;
• structura fizică, reprezentând modul de implementare, de
reprezentare a datelor pe suporturi externe.

10
1.2 ColecŃii de date

ColecŃiile (sau containerele) sunt structuri de date care


păstrează obiecte asemănătoare, în număr finit.

1.2.1 Exemple de colecŃii:

Vectorul (vector) – este grup de elemente de acelaşi tip accesate


direct, printr-un indice întreg. Ne reamintim că un tablou static conŃine
un număr fixat de elemente, alocate la compilare. FaŃă de acesta,
colecŃia vector permite verificarea încadrării indicilor în limite, alocarea
dinamică de memorie, etc. Adăugarea unui element are complexitate
O(1), pe când ştergerea şi căutarea O(n) (unde n reprezintă numărul
de elemente din vector).

Şirul de caractere (string) este un vector de caractere, cu operaŃii


specifice: determinarea lungimii şirului, compararea a două şiruri,
copierea unui şir, concatenarea unui şir la altul, căutarea unui subşir,
etc.

Lista (list) este o colecŃie omogenă, cu număr de elemente variabil


in limite foarte largi. Elementele listei sunt accesibile secvenŃial cu
complexitate O(n). Adăugarea şi ştergerea de elemente se fac eficient
cu complexitate O(1).

Stiva (stack) – este o colecŃie cu acces restrâns la unul din capete


(denumit vârful stivei). OperaŃiile specifice: punerea (push) şi
scoaterea unui element din stivă (pop) se fac eficient cu complexitate
O(1).

Coada (queue) – este o listă la care inserările se fac pe la un capăt


(spatele cozii), iar ştergerile se fac pe la celălalt capăt (faŃa cozii).
Cozile păstrează elementele in ordinea sosirii.

MulŃimea (set) – este o colecŃie de valori unice. Permite operaŃii


eficiente (cu complexitate O(log(n)) de inserare, ştergere, test
incluziune şi operaŃii specifice mulŃimilor ca intersecŃia, reuniunea,
diferenŃa etc.

Coada prioritară (priority queue) – este o colecŃie coadă în care


elementele au asociate priorităŃi şi la care ştergerea elementului cu
prioritate maximă se face in O(1), iar inserarea unui element se face

11
corespunzător priorităŃii lui în O(log(n)). Inserarea elementelor în
coada prioritară se face în ordinea priorităŃii, astfel încât să fie extras
întotdeauna elementul cel mai prioritar. Serviciul de urgenŃă al
spitalelor foloseşte ca model coada prioritară. Cozile prioritare sunt
folosite la planificarea joburilor într-un sistem de operare (jobul cel mai
prioritar va fi primul executat).

DicŃionarul (map) – reprezintă o colecŃie indexată de elemente;


elementele pot fi oarecare în timp ce indecşii trebuie să fie o valori
aparŃinând unui tip pe care sunt definite relaŃii de ordine.

1.2.2 Caracteristicile generale ale colecŃiilor

• pot fi copiate.
• numărul maxim de elemente pe care îl pot conŃine
(„capacitatea”);
• numărul curent (actual) de elemente conŃinute (cardinalul);
• modificabile sau nemodificabile: suportă/nu suportă operaŃii de
modificare (de exemplu adăugare unui element, eliminarea unui
element);
• sunt sau nu sunt imutabile, adică permit/nu permit modificarea
elementelor colecŃiei;
• permit sau nu permit acces aleatoriu: accesul aleatoriu se referă
la faptul ca timpul de acces este acelaşi pentru toate elementele
colecŃiei.

1.2.3 OperaŃii specifice colecŃiilor

O colecŃie (sau un container) implementează următoarele operaŃii:


• crearea unei colecŃii noi vide;
• ştergerea tuturor obiectelor colecŃiei;
• raportarea numărului de obiecte din colecŃie (size);
• inserarea unui nou obiect în colecŃie;
• scoaterea unui obiect din colecŃie;
• accesul la un obiect din colecŃie;
• obŃinerea adresei primului element din colecŃie (begin)
• obŃinerea adresei care marchează sfârşitul colecŃiei (end)

12
Mai pot fi definite operaŃii precum:
• test dacă colecŃia este vidă: de regulă denumit isEmpty;
• modificarea unui element din colecŃie;
• copierea unei colecŃii;

1.2.4 Parcurgerea colecŃiilor

Traversarea sau parcurgerea colecŃiei presupune enumerarea


sistematică a tuturor elementelor colecŃiei, folosind in acest scop un
iterator sau enumerator. El poate fi văzut ca un pointer la oricare
element din colecŃie.
Parcurgerea unei colecŃii cu un iterator se implementează prin trei
funcŃii care asigură:
• poziŃionarea iteratorului pe primul element;
• poziŃionarea iteratorului pe următorul (precedentul) element din
colecŃie (de regulă prin incrementare sau decrementare);
• detectarea sfârşitului colecŃiei (după ultimul element).

1.2.5 Criterii de clasificarea a colecŃiilor

a) După numărul de succesori al unui element:


• ColecŃii liniare (de exemplu vectorul); o colecŃie liniară conŃine
elemente ordonate prin poziŃie. Astfel există primul element al
colecŃiei, al doilea element,..., ultimul element.
• ColecŃii neliniare (arborescente, recursive) de tipul arborilor;
elementele sunt identificate fără o relaŃie poziŃională.

b) Metoda de acces la elemente (pentru colecŃiile liniare):


• ColecŃii cu acces direct – orice element poate fi selectat fără a
accesa in prealabil elementele care îl preced (vectorii);
• ColecŃii cu acces secvenŃial – accesul la un element se face
pornind de la primul element al colecŃiei prin deplasare către
elementul căutat (listele liniare)

c) ColecŃiile neliniare se clasifică în:


• ColecŃii ierarhice în care elementele sunt partiŃionate pe niveluri.
Fiecare element de pe un nivel are mai mulŃi succesori pe
nivelul următor (de exemplu un arbore: acesta este structura

13
ideală care descrie sistemul de fişiere cu directoare şi
subdirectoare sau diagramele de organizare ale firmelor).
• ColecŃii grupuri – sunt colecŃii neliniare in care elementele nu
sunt ordonate în nici un fel. Exemple:
o mulŃimea reprezintă un grup. OperaŃiile specifice sunt:
reuniunea, intersecŃia, diferenŃa, testul de apartenenŃă,
relaŃia de incluziune.
o graful este o structură de date care modelează relaŃiile
între obiecte prin două mulŃimi: o mulŃime de vârfuri şi o
mulŃime de muchii care conectează aceste vârfuri.
d) Unicitatea elementelor din colecŃie:
• ColecŃii cu elemente distincte: mulŃimea
• ColecŃii cu elemente multiple: lista, vectorul

e) PrezenŃa sau absenŃa unei "chei":


• ColecŃii cu cheie – dacă o parte din fiecare element (denumită
cheie) este relevantă pentru accesul la un element din colecŃie.
Cheile se compară folosind operatori relaŃionali.
• ColecŃii fără cheie

f) Posibilitatea definirii unei relaŃii de egalitate între valorile


elementelor sau între valorile cheilor, deci a unei operaŃii de
căutare în colecŃie:
• ColecŃii cu operaŃie de egalitate două elemente din colecŃie sunt
egale dacă toate componentele lor sunt egale
• ColecŃii fără operaŃii de egalitate (secvenŃe de elemente)

g) ExistenŃa unei relaŃii de ordine între elemente:


• ColecŃii ordonate (sortate) cu elementele sortate printr-o relaŃie
de ordine. De exemplu elemente şiruri de caractere sortate
lexicografic (alfabetic). Un element dintr-o colecŃie sortată poate
fi accesat rapid, folosind relaŃia de ordine pentru a-i determina
poziŃia;
• ColecŃii neordonate – între elementele acestora nu există nici o
relaŃie de ordine.
De observat că nu orice tip de elemente este ordonabil; de aceea una
din proprietăŃile unui container poate fi sortabilitatea lui. (De exemplu, o
listă de figuri geometrice nu este sortabilă). O colecŃie sortată trebuie
să aibă definită fie egalitatea cheilor, fie a elementelor.

h) PosibilităŃi de acces la elementele colecŃiei:


• cu acces la orice element din colecŃie pe baza poziŃiei în colecŃie

14
• cu acces la orice element din colecŃie pe baza valorii
elementului
• cu acces limitat la primul şi/sau la ultimul element din colecŃie

i) Limitarea numărului de elemente din colecŃie:


• ColecŃii cu dimensiune limitată;
• ColecŃii cu dimensiune nelimitată

j) Utilizarea colecŃiei:
• ColecŃii pentru memorarea temporară a unor date (de tip buffer),
care au un conŃinut foarte volatil: stive, cozi, mulŃimi;
• ColecŃii de căutare, cu un conŃinut mai stabil şi cu operaŃii
frecvente de căutare: liste, dicŃionare, arbori. Uneori se
consideră că o structură de date generală este o colecŃie de
înregistrări (structuri). Pentru o structură de date abstracte
folosită în căutare se evidenŃiază un câmp discriminant, folosit la
identificarea unică a fiecărei înregistrări denumit cheie (key) sau
cheie de căutare (search key). Cheia poate fi şi o combinaŃie a
două câmpuri din înregistrare (de ex. concatenarea a două
şiruri, cum ar fi numele şi prenumele).

k) Natura elementelor componente:


• omogenă: toate componentele sunt de un acelaşi tip, deci este o
colecŃie omogenă.
• eterogenă date care pot fi de tipuri diferite (prin pointeri către
void)

15
1.3 Tipuri de date fundamentale în C/C++

1.3.1 Tipuri simple de date

Un tip de date este de fapt o mulŃime de valori pentru care s-a


adoptat un anumit mod de reprezentare în memoria calculatorului si
o mulŃime de operatori care pot fi aplicaŃi acestor valori. MulŃimea de
valori este dată de lungimea zonei de memorie ocupată de date
aparŃinând acelui tip. În general, lungimea zonei de memorare pentru
un tip de date este dependentă de calculatorul pe care s-a implementat
compilatorul. Putem considera că tipurile de date fundamentale ale
limbajului C/C++ sunt următoarele:
- int
- char
- double
- float
- void
- bool
- pointer
În completare există un număr de modificatori (sau calificatori),
care se pot aplica unora dintre tipurile de bază. Modificatorii sunt
short, long, signed şi unsigned. Ei se pot aplica tipurilor de bază
char, int sau double. Astfel, se obŃin tipuri derivate de
date. short şi long se referă la mărimea diferită a plajei de valori, iar
datele de tip unsigned sunt pozitive. IntenŃia autorilor limbajului C a
fost ca int să reflecte mărimea cea mai "naturală" pentru tipul de
calculator pe care se lucrează iar short şi long să furnizeze lungimi
diferite de întregi,. Fiecare compilator este liber să implementeze
short şi long în funcŃie de hardware-ul existent pe tipul de calculator
pentru care este scris respectivul compilator; evident că va fi
respectată o regulă de bun simŃ: short nu este mai lung decât long.
ToŃi acesti calificatori pot aplicaŃi tipului int. Calificatorii signed (care
este implicit) şi unsigned se aplică tipului char. Calificatorul long se
aplica tipului double. Dacă într-o declaraŃie se omite tipul de bază,
implicit, acesta va fi int. Dacă în programe se fac declarări în care
apare un tip şi unul sau mai mulŃi modificatori, ordinea acestora poate fi
oricare: de exemplu, putem scrie oricare dintre următoarele declarări:
short unsigned int x;
unsigned short int x;

16
int unsigned short x; etc.
Tabelul următor prezintă lungimea zonei de memorie ocupată de
fiecare tip de dată în medii de dezvoltare a aplicaŃiilor (IDE) scrise pe
32 biŃi implementate pe calculatoare compatibile IMB-PC şi intervalul
de valori aferent:
Dimensiune
Tip Domeniu
în bi i
-
int 32
2147483648..2147483647
char 8 -128...127
double 64 +/-1.7×10308
float 32 +/-3.4×1038
unsigned
8 0...255
char
unsigned
16 0..4294967295
int
short int 16 -32768..32767
short
unsigned 16 0...65535
int
-
long int 32
2147483648..2147483647
unsigned
32 0..4294967295
long
long long
64 -263...263-1
int
long long
unsigned 64 0...264-1
int
long
80 1.1×104932
double
bool 8 true, false
Tabelul 1.1 Tipuri de date în C

17
În fişierele header <climits> sunt definite constante simbolice
(cum ar fi: INT_MAX, INT_MIN, LONG_MAX, LLONG_MAX etc) care au ca
valoare limitele inferioară şi superioară ale intervalului de valori pentru
unele dintre tipurile de date întregi de mai sus. Numele unora dintre
aceste constante poate să difere în funcŃie de mediul de programare
utilizat. Astfel, pentru valoarea maximă a tipului long long int în
unele implementări este definită constanta simbolică I64_MAX.
Fără a detalia foarte mult modul de reprezentare a datelor reale
(de tip float sau double), ne reamintim faptul că, pentru acestea,
este importantă şi precizia de reprezentare. Deoarece calculatorul
poate reprezenta doar o submulŃime finită de valori reale, în anumite
cazuri, pot apare erori importante. Numerele reale pot fi scrise sub
forma:
N = mantisă×bazăexponent
unde:
- mantisa este un număr frac ionar normalizat (în faŃa virgulei se
afla 0, iar prima cifra de după virgula este diferita de zero);
- exponentul este un număr întreg.
- baza = 2 (deoarece forma interna de reprezentare a numerelor
este binară.
În memorie sunt reprezentate mantisa şi exponentul. Numărul de cifre
de după virgulă determină precizia de reprezentare a tipului real. Astfel,
într-un IDE în care se utilizează 23 cifre (binare) pentru reprezentarea
mantisei, două valori reale care în baza 10 diferă la a 7-a cifră vor avea
aceeaşi reprezentare şi deci vor fi considerate egale. Aceasta este
situaŃia uzuală pentru reprezentarea datelor de tip float. Pentru
datele de tip double, precizia este 14 cifre zecimale, iar pentru cele de
tip long double, precizia este de 20 cifre zecimale. Fişierul header
<cfloat> conŃine definiŃii pentru diverse constante simbolice
corespunzătoare valorilor limită care caracterizează cele trei tipuri reale
din tabelul 1.1

Lungimea zonei de memorie ocupate de o dată de un anumit tip


(pe câŃi octeŃi este memorată data) poate fi aflată cu ajutorul
operatorului sizeof.
Exemplu:
cout << "Un int este memorat pe";
cout << sizeof(int)<<"octeti.\n";
InstrucŃiunea are ca efect afişarea pe monitor a mesajului:
Un int este memorat pe 4 octeti.

18
1.3.2 Pointeri

A) GeneralităŃi

Pointerii reprezintă caracteristica cea mai puternică a limbajului


de programare C/C++. Variabilele de tip pointer reŃin adrese de
memorie. Pot, de exemplu, să păstreze adrese de memorie ale altor
variabile care conŃin valori de diverse tipuri.
Astfel, putem spune că:
- un nume de variabilă reprezintă accesul direct la o valoare
(referire directă);
- un pointer referă indirect o valoare.
Referirea unei valori printr-un pointer se numeşte indirectare sau
dereferenŃiere.

Declararea unei variabile de tip pointer se face cu sintaxe de genul:


tip * nume;
unde tip poate fi orice tip din C/C++ (inclusiv void).
Exemplu:
double *x, *y; (1.1)
În acest exemplu sunt declarate variabilele x şi y. Ambele sunt
de tip pointer către valori de tip double, adică x şi y pot reŃine adrese
de memorie ale unor valori de tip double. În limbajul informatic acest
fapt se poate exprima şi astfel: x şi y „pointează” spre double.
Pot fi declaraŃi pointeri care să pointeze către orice tip de dată
(nu doar spre unul singur cum este cazul celor două variabile din
exemplul anterior care pot pointa doar spre double). Aceştia sunt
pointerii către void.
Există pointerii constanŃi: valoarea acestora nu se poate
modifica. Ei pot fi declaraŃi explicit astfel sau pot avea această
proprietate implicit prin natura declaraŃiei. Un exemplu de declarare a
unui pointer constant:
const int *p;

B) OperaŃii cu pointeri

i afişarea
O expresie de tip pointer poate fi afişată aşa cum afişăm valori
aparŃinând unor tipuri întregi sau reale; presupunând că am declarat o
variabilă pointer p instrucŃiunea următoare are ca efect afişarea

19
adresei conŃinută de variabila p. În implementările uzuale la data scrierii
cărŃii, valoarea este afişată în baza 16 pe şase cifre.
cout << *p;

ii atribuiri
Un pointer poate fi asignat altui pointer doar dacă cei doi
pointează spre acelaşi tip. În caz contrar, trebuie aplicată o operaŃie de
conversie explicită (type cast) pentru ca tipul valorii pointerului din
dreapta semnului = să fie adus la tipul pointerului din stânga. ExcepŃie
de la această regulă face pointerul void* care este un tip generic şi
poate reprezenta orice tip de pointer fără a mai fi nevoie de conversie
de tip (type cast).
Exemplu:
int *p1, *p2; (1.2)
double *q;
p1 = p2; //atribuire corectă
q = p2; //atribuire incorectă
q = (double *)p2; //atribuire corectă
Un pointer poate primi şi una dintre valorile 0 sau NULL. Un pointer cu
valoarea 0 sau NULL nu pointează către nicio zonă de memorie.
Constanta NULL este declarată în fişierul header <iostream> şi în alte
câteva biblioteci standard. Atribuirea valorii NULL unui pointer este
echivalentă cu atribuirea valorii 0 deoarece 0 este convertit automat
către o adresă de tipul pointerului.

iii operatorul & (operatorul adresă)


Acesta este unar şi returnează adresa operandului său.
Exemplu:
int y = 5; (1.3)
int *yPtr;
yPtr = &y;
Prin ultima instrucŃiune, adresa de memorie a variabilei y este
atribuită variabilei y.

iv operatorul *
În contextul pointerilor operatorul * este denumit operator de
indirectare sau de dereferenŃiere. El ne oferă accesul la zona de
memorie a cărei adresă o conŃine pointerul căruia îi este aplicat.
În contextul instrucŃiunilor din exemplul anterior, instrucŃiunea
cout << *yPtr << endl;

20
tipăreşte valoarea zonei de memorie conŃinută de pointerul yPtr
(secvenŃa va afişa valoarea 5).
Un pointer dereferenŃiat poate fi folosit în partea stângă a unei
instrucŃiuni de asignare (in terminologia limbajului C/C++, o construcŃie
sintatctică care poate să apară în stânga a unei atribuiri se numeşte
lvalue):
De exemplu:
*yPtr = 17;
Prin această operaŃie (în contextul instrucŃiunilor din exemplul (1.3)),
valoarea 17 este asignată variabilei y.
DereferenŃierea nu se poate aplica pointerilor spre void.

v operatorii new şi delete


Cei doi operatori permit alocare şi dezalocarea de memorie. Ei
sunt specifici pentru C++. Zona de memorie unde se produc efectele
acestor doi operatori se numeşte memorie dinamică.
Operatorul new permite alocarea de memorie pentru variabile.
Variabilele respective sunt denumite variabile dinamice deoarece ele
„apar” prin efectul unor instrucŃiuni şi nu al unor declarări. Prin opoziŃie,
variabilele care „apar” prin declarări sunt denumite variabile statice.
Evident că atunci când facem alocarea de memorie pentru o variabilă
dinamică trebuie să îi precizăm tipul. Astfel, sintaxele care are ca efect
alocarea de memorie pentru o variabilă dinamică (şi deci „apariŃia” ei)
sunt următoarele:

A) new tip;
Această expresie are ca efect apariŃia unei variabile dinamice de
tipul precizat. Mai mult, valoarea expresiei este adresa unde s-a făcut
alocarea, iar tipul acestei adrese este pointer spre tipul din expresie. În
cazul în care nu se poate face această alocare, valoarea expresiei este
NULL. În mod normal, această adresă este reŃinută într-o variabilă de
tip pointer, astfel încât să putem accesa variabila dinamică care a
apărut (Variabillele dinamice NU au nume, deci le vom accesa prin
pointeri cu condiŃia să le ştim adresele!).
Exemple:
int *p1,*p2;
double *q;
p1 = new int; //apare o variabilă dinamică de tip int iar
adresa ei este reŃinută de p1. Accesarea acestei variabile dinamice se
va face cu sintaxa *p1!

21
q = new double; //apare o variabilă dinamică de tip double
iar adresa ei este reŃinută de q.

p2 = new char; //eronat! new char generează o adresă


spre char, dar p2 poate reŃine adrese ale unor informaŃii de tip int!

B) new tip(expresie);
Efect similar cu forma A) cu deosebirea că variabila dinamică
pentru care s-a făcut alocarea este iniŃializată cu valoarea expresiei.

C) new tip[nr_elem];
Această expresie are ca efect apariŃia a nr_elem variabile
dinamice, toate având tipul precizat. Aceste elemente vor ocupa poziŃii
consecutive în memoria dinamică. Mai mult, valoarea expresiei este
adresa unde se află primul dintre elementele alocate. În cazul în care
nu se poate face această alocare, valoarea expresiei este NULL. În
mod normal, această adresă este reŃinută într-o variabilă de tip pointer,
iar elementele alocate vor putea fi accesate asemănător cu elementele
unui tablou, deoarece variabilei pointer i se pot aplica parantezele
pătrate (vezi şi paragraful C de la acest subcapitol referitor la legătura
dintre pointeri şi tablouri)!
Exemplu:
int *p;
p = new int[100]; // s-au alocat 100 elemente de tip int
aflate în poziŃii consecutive în memorie, astfel încât vom putea accesa
aceste elemente cu sintaxe de genul p[0], p[1],..., p[i],... etc.

Operatorul delete determină dezalocarea unei zone de


memorie. Dacă în zona respectivă de memorie se află o variabilă
dinamică, atunci putem afirma (cu ghilimelele de rigoare) că aceasta
„dispare”. Dimensiunea zonei de memorie dezalocată rezultă din
dimensiunea tipului spre care pointează p. Utilizarea operatorului
delete se face cu sintaxele:

D) delete p; //unde p este de tip pointer.


Efectul acestei instrucŃiuni este că zona de memorie a cărei
adresă era reŃinută de p este dezalocată – adică este marcată ca fiind
liberă. ConŃinutul zonei de memorie care a fost dezalocată este
nedefinit!

22
E) delete [] p; //unde p este pointer către un tablou
Efectul acestei instrucŃiuni este că zona de memorie ocupată de
tabloul spre începutul căreia pointează p este dezalocată.

vi operatorul ->
Acest operator se poate aplica numai pointerilor care reŃin
adrese ale unor variabile care au câmpuri (struct, class, union). El
oferă accesul la câmpurile respectivelor variabile. Să presupunem că
avem următoarele declarări:
struct STUDENT
{
char nume[20], prenume[30];
double notaexamen, notalaborator;
} *p;
În acest context, următoarele instrucŃiuni reprezintă alocarea de
memorie pentru o variabilă dinamică de tip STUDENT şi citirea de la
tastatură a valorilor câmpurilor acesteia:
p = new STUDENT;
cin >> p->nume >> p->prenume;
cin >> p->notaexamen >> p->notalaborator;

vii – operaŃii aritmetice cu pointeri


Asupra pointerilor pot fi realizate operaŃii de:
- adăugare sau scădere a unui întreg (cu simbolurile + , += , - , -
=)
- incrementare (++)/decrementare (--)
- diferenŃa a doi pointeri.
Pentru a exemplifica semnificaŃia acestor operaŃii să presupunem că
declarăm tabloul int v[5] al cărui prim element este plasat de
compilator la adresa de memorie 22FF50. Pentru un calculator pe care
întregii sunt reprezentaŃi pe 4 octeŃi (bytes), cele cinci elemente ale
tabloului sunt plasate la adresele de memorie din figura de mai jos
astfel:

22FF50 22FF54 22FF58 22FF5C 22FF60


v[0] v[1] v[2] v[3] v[4]
vPtr

23
Pointerul vPtr poate fi iniŃializat cu adresa primului element al tabloului
folosind secvenŃa următoare:
int *vPtr;
vPtr = &(v[0]);
Adresa celui de-al doilea element al tabloului este &(v[1]).
Spre deosebire de operaŃiile matematice în care adunarea 22FF50+2
are ca rezultat valoarea 22FF52, în aritmetica pointerilor adăugarea
unui întreg la o adresă de memorie are ca rezultat o nouă adresă de
memorie. Aceasta este egală cu adresa iniŃială la care se adaugă un
număr de locaŃii de memorie egal cu valoarea întregului înmulŃită cu
dimensiunea obiectului la care referă pointerul.
Exemplu:
vPtr += 2;
are ca rezultat valoarea 22FF50 + 2 × 4 = 22FF58 (presupunem
că valorile int sunt reprezentate pe 4 octeŃi). În urma acestei operaŃii,
vPtr va pointa către v[2].
Similar, pentru un pointer către double, aceeaşi operaŃie are ca rezultat
valoarea 22FF50 + 2 × 8 = 22FF60.
Incrementarea unui pointer are acelaşi efect cu adunarea valorii 1 la
acel pointer.
Rezultatul diferenŃei între doi pointeri are sens dacă cei doi pointeri
pointează către elementele aceluiaşi tablou. În acest caz, rezultatul
este egal cu numărul de elemente aflat între adresele pointate de cei
doi pointeri (rezultatul poate fi pozitiv sau negativ!)

C) pointeri şi tablouri

Limbajul C a fost astfel conceput încât să existe o legătură directă între


tablouri şi pointeri. Astfel
- numele unui tablou reprezintă în limbajul C/C++ un pointer
constant;
- în C putem indexa pointerii pentru a accesa elemente aflate în
zona de memorie spre care pointează respectivul pointer (la fel
ca la tablouri).
Exemplu: presupunând declarările
int v[5]; (1.4)
int *vPtr = v;
elementul v[3] poate fi referit (accesat) şi prin expresiile:
*(vPtr + 3)
*(v + 3)

24
Valoarea 3 din aceste expresii se numeşte offset la pointer, iar o
expresie care accesează un element al unui tablou în acest mod se
numeşte notaŃie offset sau notaŃie pointer.
ObservaŃie: fără paranteze, expresia *vPtr + 3 ar fi avut ca efect
adunarea valorii 3 la expresia *vPtr, adică la v[0].
În schimb, numele unui tablou este un pointer constant şi nu poate fi
folosit în operaŃii care i-ar schimba conŃinutul.
Exemple
v += 3; (1.5)
v = vPtr;
sunt operaŃii invalide.
Pentru pointeri se pot folosi indici la fel ca şi pentru tablouri.
Exemplu: în instrucŃiunile de mai jos vom citi de la tastatură o valoare
naturală n, vom aloca în memorie spaŃiu pentru un tablou cu n
elemente şi în cazul în care alocarea s-a efectuat cu succes vom citi de
la tastatură valori pentru aceste n elemente.
int * ptr=NULL, n, i; (1.6)
cin >> n;
if (n>0)
{ ptr = new int[n];
if (ptr != NULL)
{ for (i=0; i<n; i++)
cin >> ptr[i];
}
else
cout << „spatiu de memorie insuficient”;
}

Legătura dintre tablouri şi pointeri stă la baza declarării parametrilor „de


tip tablou” pentru funcŃii. Când declarăm un parametru de tip tablou de
fapt declarăm un pointer către zona de memorie unde se află
respectivul tablou.
De exemplu următoarele declarări ale antetului unei funcŃii cu doi
parametri: n de tip int şi a de tip tablou având 100 elemente de tip
int sunt echivalente:
void f(int a[100], int n)
void f(int a[], int n)
void f(int *a, int n)

În exemplul care urmează vom aplica proprietăŃile pointerilor pentru a


utiliza câteva funcŃii din biblioteca <algorithm>. Este vorba de
25
funcŃiie sort, max_element şi replace. Majoritatea funcŃiilor din
biblioteca <algorithm> implementează algoritmi fundamentali
(căutare, numărare, minim, maxim, sortare, interclasare) şi au ca
parametri adresa unde începe zona de memorie în care se află stocate
elementele asupra cărora vrem să fie aplicată funcŃia şi adresa unde se
află primul element asupra căruia NU vrem să mai fie aplicată funcŃia
precum şi alŃi parametri după caz. O altă caracteristică a majorităŃii
acestor funcŃii este că au mai multe sintaxe de utilizare care diferă prin
numărul de parametri.
Cele trei funcŃii enumerate mai sus le vom utiliza sub următoarele
forme:
- sort(adr1, adr2); funcŃia sortează crescător elementele
aflate în memorie între adresele adr1 (inclusiv) şi adr2
(exclusiv). Pentru a putea aplica acastă funcŃie toate elementele
din respectiva zonă de memorie trebuie să fie de acelaşi tip şi
pentru ele să fie implementat operatorul <.
- max_element(adr1, adr2); returnează adresa unde apare
cel mai mare element dintre elementele aflate în memorie între
adresele adr1 (inclusiv) şi adr2 (exclusiv). CondiŃiile de aplicare
sunt similare cu cele de la sort.
- replace(adr1, adr2, val1, val2); înlocuieşte cu
valoarea val2 toate elementele aflate în memorie între adresele
adr1 (inclusiv) şi adr2 (exclusiv) egale cu val1.

//program 1.1: exemple de utilizare a bibliotecii


algorithm
//cu ajutorul pointerilor
#include <iostream>
#include <algorithm>
#include <cstdlib>
using namespace std;
//funcŃie de generare aleatoare a unui tablou
//cu valori din intervalul [0;dom-1]
void gen_aleatoare(int *a, int n, int dom)
{ for (int i=1;i<=n;i++)
a[i] = rand()%dom;
}
//funcŃie de afişare a valorilor unui tablou pe
monitor
void afişare_e(int a[], int n)
{ for (int i=1;i<=n;i++)
26
cout << a[i] <<" ";
cout << endl;
}
int main()
{ int n=20, valv, valn, a[1000];
int *p=a;
gen_aleatoare(a,n,12);
cout<<"sirul initial de elemente:\n";
afişare_e(p,n);
//sortare crescătoare a elementelor a3, a4,...,a7
sort(a+3,a+8);
cout<<"sirul sortat intre pozitiile 3 si 7\n";
afişare_e(a,n);
//inlocuim valoarile egale cu a[1] cu -a[1]
//varianta eronata
//replace(a+1,a+n+1,a[1],-a[1]);
// afişare_e(a,n);
//varianta corecta
valv = a[1];
cout<<"sirul in care valorile egale cu";
cout<< " primul element au fost inlocuite\n";
replace(a+1,a+n+1,valv,-valv);
afişare_e(a,n);
//inlocuirea unei valori din tablou cu alta valoare
//ambele citite de la tastatură
cout << "dati o val. din sir si val. cu care va fi
inlocuita...";
cin>> valv >> valn;
replace(a+1,a+n+1,valv,valn);
cout << "sirul rezultat\n";
afişare_e(a,n);
cout << "adresa maximului:";
cout << max_element(a+1,a+n+1)<<endl;
cout << "valoarea maximului: ";
cout << * max_element (a+1,a+n+1)<<endl;
cout<< "pozitia maximului:";
cout << max_element(a+1,a+n+1)-a<< endl;
return 0;
}

Rezultatele afişate de program în fereastra consolă:


27
sirul initial de elemente:
5 11 10 4 5 4 6 6 10 8 5 5 1 3 1 11 7 2 3 0
sirul sortat intre pozitiile 3 si 7
5 11 4 4 5 6 10 6 10 8 5 5 1 3 1 11 7 2 3 0
sirul in care valorile egale cu primul element au
fost inlocuite
-5 11 4 4 -5 6 10 6 10 8 -5 -5 1 3 1 11 7 2 3 0
dati o val. din sir si val. cu care va fi inlocuita
-5 -55
sirul rezultat
-55 11 4 4 -55 6 10 6 10 8 -55 -55 1 3 1 11 7 2 3 0
adresa maximului: 0x22ef90
valoarea maximului: 55
pozitia maximului: 1

Tipurile de date fundamentale sunt tipuri de date simple: o variabilă


aparŃinând unui tip de acest fel poate reŃine la un moment dat o singură
valoare.

28
1.4 Structura de date de tip tablou

Tablourile reprezintă o structură de date cu următoarele caracteristici:

• este omogenă (toate elementele sunt de acelaşi tip);


• zona de memorie ocupată este contiguă (adică este formată din
octeŃi consecutivi);
• zona de memorie ocupată este fixă (odată stabilită, dimensiunea
ei nu poate fi modificată);

Putem clasifica tablourile după diverse criterii. Cel pe care îl vom


analiza în cele ce urmează este dat de

1.4.1 Modul de declarare

În funcŃie de modul de declarare putem considera că există:

a) tablouri statice: tablourile de acest tip le declarăm prin:

tip nume[număr_de_elemente];

În implementări C mai vechi, „număr_de_elemente” trebuie să fie o


constantă (numerică sau simbolică):

int a[101]; (1.7)


sau
const int nrelem = 100; (1.8)
int a[nrelem+1];

În implementări C noi (de exemplu Code Blocks) se pot face si astfel


de declaraŃii:

int n; (1.9)
cin >> n;
int a[n];

Numărul de elemente pe care le va avea un astfel de tablou este egal


cu valoarea pe care o are variabila n în acel moment. SecvenŃa (1.9)
trebuie tratată cu mare atenŃie deoarece este posibil ca alocarea de

29
memorie pentru tabloul a pe să nu fie efectuată din varii motive (cel
mai probabil dintr-o valoare prea mare citită pentru n).

b) tablouri dinamice: declarările în acest caz sunt de genul:

tip * nume; (1.10)


nume = new tip[numar_elemente];

Un exemplu concret:

int *p; (1.11)


p = new int[1000];
Evident în acest caz sunt permise şi sintaxe în care numărul de
elemente pe care le va avea tabloul este stabilit prin instrucŃiuni ale
programului şi nu prestabilit, ca în exemplul următor:

int *p, n; (1.12)


cin >> n;
p = new int[n];

Accesarea elementelor unui tablou se face prin indici. Aceasta este


posibil datorită faptului că elementele tabloului ocupă poziŃii
consecutive.

În limbajul C/C++ prin legătura care există între tablouri şi pointeri,


accesarea elementelor unui tablou se poate face şi prin pointeri,
utilizând operaŃiile aritmetice ce se pot efectua cu pointerii. Astfel dacă
avem următoarea declarare a unui tablou

double a[10001];

atunci *(a+i) reprezintă elementul din poziŃia i, adică a[i]


(0≤i≤10000).

1.4.2 Prelucrări uzuale efectuate asupra structurii de


date de tip tablou

A. Atribuirea de valori pentru elementele tabloului

• citire de la tastatură
• citire dintr-un fişier
• atribuire de valori după o formulă
30
• atribuire de valori aleatoare
• atribuirea de valori luate dintr-un alt tablou

B. Afişarea valorilor elementelor unui tablou

C. OperaŃii fundamentale efectuate cu informaŃiile conŃinute de un


tablou

• inserare
• eliminare
o a unui element determinat prin poziŃia din tablou
o a unui element determinat prin valoarea sa
o a tuturor elementelor egale cu o valore dată
• căutare
• sortare
• efectuarea unor calcule:
o minim/maxim
o statistici: contorizare, însumare
o verificare (dacă elementele tabloului au o anumită
proprietate)
• interclasare

D. OperaŃii diverse

• inversarea ordinii elementelor;


• determinarea de secvenŃe de lungime maximă de elemente
având o anumită proprietate

Programul următor implementează unele dintre aceste operaŃii. Câteva


precizări:
• operaŃiile sunt implementate prin funcŃii în care tipul tabloului
este generic;
• prima declaraŃie #define asociază construcŃiei sintactice
template <class tipelem> (utilizată în cazul funcŃiilor
generice – vezi Anexa 1) numele GENERIC utilizat apoi în
declararea majorităŃii funcŃiilor din program;
• tablourile utilizate in program sunt declarate cu 5 elemente în
plus faŃă de dimensiunea introdusă de la tastatură pentru a
permite eventuale inserări.

31
• tablourile sunt prelucrate începând cu elementul din poziŃia 0.
Pozi ia 4 reprezintă deci a cincea poziŃie din tablou.

//program 1.2 operatii uzuale pentru tablouri


#include<iostream>
#include<fstream>
#include<cstdlib>
#include<string>
#define GENERIC template <class tipelem>
#define dimmax 1000
using namespace std;

//citirea tabloului de la tastatura


template <class tipelem>
void citire_t(int nrelem, tipelem a[])
{ for (int i=0;i<nrelem;i++)
cin >> a[i];
}
//afişarea elementelor tabloului
GENERIC void afişare_e(int nrelem, tipelem a[])
{
for (int i=0;i<nrelem;i++)
cout << a[i] << ' ';
cout << endl << endl;
}
//citirea tabloului din fisier
GENERIC void citire_f(int &nrelem, tipelem a[], char
numef[101])
{ ifstream fisier(numef);
nrelem=0;
while (fisier >> a[nrelem])
nrelem++;
}
//generare tablou cu valori aleatoare
GENERIC void generare_aleatoare(int nrelem, tipelem
a[], int dom)
{ for (int i=0;i<nrelem;i++)
a[i] = (tipelem)(rand()%dom/1.1);
}
//inserare in tablou pe o pozitie data
GENERIC void inserare(int &nrelem, tipelem a[], int
poz, tipelem val, int nrmaxelem)
{if (poz<0 || poz > nrelem)
{ cout << "pozitie aiurita\n";
return ;
32
}
if (nrelem > nrmaxelem)
{ cout << "nu mai e loc...\n";
return ;
}
for (int i=nrelem; i>=poz;i--)
a[i+1] = a[i];
a[poz] = val;
nrelem++;
}
//eliminarea din tablou a unui element dintr-o
pozitie data
GENERIC void eliminare(int &nrelem, tipelem a[], int
poz)
{if (poz<0 || poz > nrelem)
{ cout << "pozitie aiurita\n";
return ;
}
for (int i=poz; i<nrelem; i++)
a[i] = a[i+1];
nrelem--;
}
int main()
{ int nrelem1,nrelem2,nrelem3;
cout << "citire de la tastatura pentru un
tablou\n";
cout << "dati nr elem pentru un tablou de tip
int...";
cin >> nrelem1;
int x[nrelem1+5];
citire_t(nrelem1,x);
afişare_e(nrelem1,x);
cout << endl;

cout << "generare aleatoare\n";


cout << "dati nr elem pentru un tablou de tip
double...";
cin >> nrelem2;
double a[nrelem2+5];
generare_aleatoare(nrelem2, a, 200);
afişare_e(nrelem2,a);
cout << endl;

cout << "citire dintr-un fisier...\n";


string s[dimmax+5];
33
citire_f(nrelem3,s,"cuvinte.in");
cout<< "tabloul citit contine " << nrelem3 << "
elemente:"<< endl;
afişare_e(nrelem3,s);

cout << "tabloul de int dupa inserarea valorii -45


in pozitia 4\n";
inserare(nrelem1,x,4,-45,dimmax);
afişare_e(nrelem1,x);

eliminare(nrelem2,a,4);
cout << "tabloul de double dupa eliminarea valorii
din pozitia 4\n";
afişare_e(nrelem2,a);
return 0;
}
Rezultatele afişate de program în fereastra consolă:
citire de la tastatura pentru un tablou
dati nr elem pentru un tablou de tip int... 5
dati elementele tabloului...
12 23 34 45 56

generare aleatoare
dati nr elem pentru un tablou de tip double...7
37.2727 60.9091 121.818 90.9091 153.636 112.727
70.9091

citire dintr-un fisier...


tabloul citit contine 4 elemente:
ianuarie februarie martie aprilie

tabloul de int dupa inserarea valorii -45 in


pozitia 4
12 23 34 45 -45 56

tabloul de double dupa eliminarea valorii din


pozitia 4
37.2727 60.9091 121.818 90.9091 112.727 70.9091

34
1.4.3 Utilizarea funcŃiilor din biblioteca <algorithm>

Vom rezolva câteva dintre operaŃiile fundamentale enumerate


mai sus şi prin utilizarea unor funcŃii din biblioteca <algorithm>.
Pentru aceasta este utilă o succintă prezentare a sintaxei
funcŃiilor pe care le vom folosi. La toate aceste funcŃii, primii doi
parametri reprezintă adresa primului element căruia vrem să îi fie
aplicată funcŃia şi respectiv adresa primului element căruia nu vrem să
îi mai fie aplicată funcŃia. Aceşti doi parametri îi vom nota în cele ce
urmează cu adr1 şi adr2. Aşa cum aminteam în subcapitolul anterior,
multe dintre aceste funŃii au mai multe sintaxe de utilizare care diferă
prin numărul de parametri. Vom prezenta pentru fiecare dintre aceste
funcŃii sintaxa care corespunzătoare utilizării funcŃiei în exemplul dat la
finalul subcapitolului. Deoarece prezentarea care urmează este
succintă, nu am intrat în amănuntele referitoare la condiŃiile de
aplicabilitate a acestor funcŃii. Vom presupune aceste condiŃii implicite
în contextul unei utilizării de „bun simŃ”. (De exemplu, pentru a utiliza
funcŃiile de căutare, este evident că tipul valorii pe care o căutăm şi
tipul elementelor din tabloul în care căutăm trebuie să fie identic sau
compatibil iar pentru acest tip trebuie să fie implementat operatorul de
testare a egalităŃii: == ). InformaŃii mai detaliate referitoare la funcŃiile
din biblioteca <algorithm> se găsesc la numeroase adrese de
internet; recomandăm site-ul cplusplus.com.

copy(adr1, adr2, adr3);


Copiază conŃinutul zonei de memorie dintre adresele adr1 şi adr2 în
zona de memorie care începe la adr3. Returnează un pointer care
reprezintă adresa unde se termina zona în care s-a făcut copierea,
ceea ce ne permite să determinăm câte elemente sunt în structura de
date în care s-a făcut copierea.

fill(adr1, adr2, val);


Atribuie elementelor din zona de memorie cuprinsă între adresele adr1
şi adr2 valoarea val. Tipul valorii val trebuie să fie compatibil cu
al elementelor din tablou.

random_shuffle(adr1, adr2);
Rearanjează aleator valorile elementelor din zona de memorie dintre
adresele adr1 şi adr2.

35
remove(adr1, adr2, val);
Elimină toate elementele din zona de memorie dintre adresele adr1 şi
adr2 care sunt egale cu val. Returnează un pointer care reprezintă
noua valoare a adr2, ceea ce ne permite să determinăm câte
elemente au rămas în tablou.

find(adr1, adr2, val);


Returnează un pointer la primul element din tablou egal cu val dacă
acesta există sau valoarea adr2 dacă val nu apare în tablou.

sort(adr1, adr2);
Sortează crescător valorile elementelor din zona de memorie cuprinsă
între adresele adr1 şi adr2. Complexitate O(n×log n).

count(adr1, adr2, val);


Returnează numărul de elemente egale cu val.

merge(adr1,adr2,adr3,adr4,adr5);
Interclasează valorile elementelor din zonele de memorie cuprinse între
adresele adr1 şi adr2, cu cele dintre adresele adr3 şi adr4;
rezultatul interclasării va fi depus în memorie începând de la adresa
adr5. FuncŃia returnează un pointer care reprezintă valoarea adresei
unde se termină zona de memorie unde a fost depus rezultatul
interclasării ceea ce ne permite să determinăm câte elemente conŃine
tabloul rezultat prin interclasare.

reverse(adr1, adr2);
Inversează ordinea valorilor elementelor din zona de memorie cuprinsă
între adresele adr1 şi adr2.

binary_search(adr1, adr2, val);


Căutare binară. Apelabilă numai pentru tablouri sortate crescător şi
care au elemente pentru care este implementat operatorul <.
Returnează valoarea 1 dacă val apare în tablou sau 0 dacă val nu
apare în tablou.

lower_bound(adr1, adr2, val);


Căutare binară. Apelabilă în acelaşi condiŃii ca funcŃia anterioară. Dacă
val apare în tablou returnează adresa primei apariŃii a acesteia. Dacă
nu apare, returnează adr2.

36
upper_bound(adr1, adr2, val);
Căutare binară. Apelabilă în acelaşi condiŃii ca funcŃiile anterioare.
Dacă val apare în tablou returnează adresa de după ultima apariŃie a
acesteia. Dacă nu apare, returnează adr2.
Programul următor ilustrează utilizarea acestor funcŃii.

//program 1.3 tablouri si functii din <algorithm>


#include <iostream>
#include <cstdlib>
#include <algorithm>
#define GENERIC template <class tipelem>
using namespace std;

GENERIC void citire_t(int n, tipelem tab[])


{
for (int i=0;i<n;i++)
cin >> tab[i];
}

GENERIC void afişare_e(int n, tipelem tab[])


{
for (int i=0;i<n;i++)
cout << tab[i] << ' ';
cout << endl << endl;
}

//atribuire de valori dupa o formula data


//de exemplu elementul din pozitia i are valoarea
i+1
GENERIC void construire_formula(int n, tipelem
tab[])
{ for (int i=0;i<n;i++)
tab[i] = i+1;
}
//atribuirea de valori aleatoare elementelor unui
tablou
GENERIC void generare_aleatoare(int n, tipelem
tab[], int domeniu)
{
for (int i=0;i<n;i++)
tab[i]=(tipelem)((rand()%domeniu/1.1));
}

int main()
{ int n,n1;
37
cout << "dati nr elem din tabl cu care lucrati:";
cin >> n;
int a[n+5], a1[n+5];
double b[n+5];
cout << "umplere cu o valoare data\n";
fill(a,a+n,30);
afişare_e(n,a);

cout << "umplerea unui tablou cu valorile din


primul tablou\n";
copy(a,a+n,a1);
n1 = n;
afişare_e(n,a1);

cout << "rearanjare aleatoare a valorilor\n";


construire_formula(n,a1);
cout << "tablou initial\n";
afişare_e(n1,a1);
random_shuffle(a1,a1+n1);
cout << "tablou dupa rearanjare\n";
afişare_e(n1,a1);
cout<<"inserare prin utilizarea functiei copy\n";
int pozitie, valoare;
cout<<"dati pozitia unde inserati;
cout<<" si valoarea de inserat";
cin >> pozitie >> valoare;
copy(a1+pozitie,a1+n1,a1+pozitie+1);
a1[pozitie] = valoare;
n1++;
afişare_e(n1,a1);

cout << "eliminare\n";


cout << "dati valoarea de eliminat: ";
cin >> valoare;
int *sfârsit;
sfârsit = remove(a1,a1+n1,valoare);
if (n1 != sfârsit - a1)
{ n1 = sfârsit - a1;
afişare_e(n1,a1);
}
else
cout << "valoarea nu apare in tablou\n";

cout << "cautare secventiala\n";


cout << "dati valoarea de cautat: ";
38
cin >> valoare;
int *rez;
rez = find(a1,a1+n1,valoare);
if (rez == a1+n1)
cout << "nu apare"<<endl;
else
cout<<"apare in pozitia " <<rez-a1<<endl;

cout << "contorizare\n";


cout << "dati valoarea de numarat: ";
cin >> valoare;
cout << "nr. aparitii pentru " << valoare<<" = ";
cout << <<count(a1,a1+n1,valoare)<<"\n";
cout << "interclasare\n";
int nr1, nr2, nr3, *p;
cout << "dati numarul de elemente ale celor doua";
cout<<" tablouri pe care le vom interclasa ";
cin >> nr1 >> nr2;
int v1[nr1+5], v2[nr2+5], v3[nr1+nr2+10];
generare_aleatoare(nr1,v1,30);
generare_aleatoare(nr2,v2,50);
sort(v1,v1+nr1);
sort(v2,v2+nr2);
cout << "primul tablou\n";
afişare_e(nr1,v1);
cout << "al doilea tablou\n";
afişare_e(nr2,v2);
p = merge(v1,v1+nr1,v2,v2+nr2,v3);
nr3 = p - v3;
cout << "vector rezultat prin interclasare\n";
afişare_e(nr3,v3);
cout<<endl<<"acest tablou are "<<nr3;
cout <<" elemente"<<endl;
cout << "cautare binara\n";
cout << "dati valoarea de cautat: ";
cin >> valoare;
cout << "rezultatul cautarii binare: ";
cout << binary_search(v3,v3+nr3,valoare)<<endl;
//determinam poz primei aparitii a valorii in tablou
p = lower_bound(v3,v3+nr3,valoare);
if (p == v3+nr3)
cout << "nu apare"<<endl;
else
cout<<"prima aparitie e in pozitia "<<p-v3;
cout<<endl;
39
//determ. poz ultimei aparitii a valorii in tablou
p = upper_bound(v3,v3+nr3,valoare);
if (p == v3+nr3)
cout << "nu apare"<<endl;
else
cout<<"ultima aparitie e in pozitia "<<(p-v3)-1;
cout << endl;

cout << "inversarea ordinii elementelor\n";


reverse(v3,v3+nr3);
afişare_e(nr3,v3);
return 0;
}
Rezultatele afişate de program în fereastra consolă:
dati nr de elem din tabl cu care lucrati: 14
umplere cu o valoare data
30 30 30 30 30 30 30 30 30 30 30 30 30 30

umplerea unui tablou cu valorile din primul tablou


30 30 30 30 30 30 30 30 30 30 30 30 30 30

rearanjare aleatoare a valorilor:


tablou initial
1 2 3 4 5 6 7 8 9 10 11 12 13 14
tablou dupa rearanjare
13 2 10 3 1 12 8 4 5 7 9 6 11 14

inserare prin utilizarea functiei copy


dati pozitia unde inserati si valoarea de inserat 4
-45
13 2 10 3 -45 1 12 8 4 5 7 9 6 11 14

eliminare
dati valoarea de eliminat: 1
13 2 10 3 -45 12 8 4 5 7 9 6 11 14

cautare secventiala
dati valoarea de cautat: 7
apare in pozitia 9
contorizare
dati valoarea de numarat: 8
nr. aparitii pentru 8 = 1
interclasare
dati numarul de elemente ale celor doua tablouri 10
15
40
primul tablou
0 1 1 5 9 19 21 22 24 24
al doilea tablou
2 10 14 15 16 17 19 19 23 29 34 38 40 42 44

vector rezultat prin interclasare


0 1 1 2 5 9 10 14 15 16 17 19 19 19 21 22 23 24 24
29 34 38 40 42 44
acest tablou are 25 elemente

cautare binara
dati valoarea de cautat: 19
rezultatul cautarii binare: 1
prima aparitie e in pozitia 11
ultima aparitie e in pozitia 13
inversarea ordinii elementelor
44 42 40 38 34 29 24 24 23 22 21 19 19 19 17 16 15
14 10 9 5 2 1 1 0

1.4.4 Utilizarea containerului <vector>

Datorită importanŃei pe care o prezintă tablourile în rezolvarea


unei palete foarte mari de aplicaŃii, în ansamblul de biblioteci STL a
limbajului C++ a fost inclus un container care implementează vectori.
Vom prezenta modul de utilizare a acestui container si vom ilustra
rezolvarea operaŃiilor fundamentale pentru tablouri (enumerate în
subcapitolul 1.4.2) pe baza facilităŃilor oferite de acest container.

A. Prezentarea generală a variabilelor de tip vector

Vectorii sunt containere secveŃiale care reprezintă tablouri.


Aceştia conŃin:
• un tablou ale cărui elemente pot fi de orice tip din limbajul
C/C++;
• funcŃii care implementează operaŃiilor de bază aplicabile
tablourilor;
• informaŃii referitoare la tabloul conŃinut de vector; dintre
acestea unele ne sunt complet inaccesibile iar altele le
putem vizualiza şi le putem modifica numai prin
intermediul funcŃiilor conŃinute de un vector.

Dimensiunea tabloului conŃinut de un vector este dinamică,


adică se poate modifica pe parcursul programului.
41
Un vector ocupă o zonă contiguă de memorie. Aceasta are ca
efect posibilitatea de accesare a acestor elemente prin indici.
Intern vectorii sunt implementaŃi având la bază un tablou alocat
dinamic (vezi subcapitolul 1.4.1). Schimbarea dimensiunii alocate se
face după următorul algoritm: presupunem că la un moment dat, printr-
o alocare dinamică anterioară, tabloul conŃinut de un vector are o
capacitate dată ce permite memorarea unui număr de elemente. În
momentul în care se apelează o funcŃie care adaugă un element
suplimentar în tablou şi prin aceasta s-ar depăşi capacitatea de
memorare se realizează următoarele operaŃii:
• se face o nouă alocare dinamică a unui tablou de capacitate mai
mare în altă zonă de memorie;
• se copiază elementele existente în tabloul vechi în poziŃiile
corespunzătoare din acest nou tablou;
• se adaugă în acest tablou nou elementul suplimentar;
• se eliberează zona de memorie ocupată de tabloul vechi;

Deoarece efectuarea acestor paşi la fiecare adăugare a unui nou


element ar fi o operaŃie cu complexitatea O(n), atunci când se alocă
noul tablou (prin n am notat numărul de valori care urmează să fie
reŃinute de vector), acesta va avea o capacitate de două ori mai mare
decât tabloul anterior (în CodeBlocks sau MinGW). Aceasta duce la
complexitatea Θ(1) pentru operaŃia de adăugare a unui element la
sfârşitul unui vector.

B. Utilizarea variabilelor de tip vector

În primul rând trebuie inclusă biblioteca corespunzătoare din STL


denumită vector:
#include <vector>

Apoi se declară o variabilă vector cu sintaxa:


vector <tip_elemente> nume;

Variabila vector va avea numele din declaraŃie anterioară iar


elementele tabloului conŃinut de vector vor avea tipul dintre
parantezele unghiulare. Tabloul conŃinut de această variabilă va fi vid
(nu are alocată memorie pentru niciun element).

De exemplu:

vector <int> v1; (1.13)

42
Prin aceasta am declarat o variabilă v1 care conŃine un tablou de valori
int.

vector <double> a,b; (1.14)

DeclaraŃie pentru două variabile a şi b care conŃin fiecare câte un


tablou de valori reale.

vector <int> T[100]; (1.15)

DeclaraŃie pentru variabilă T care este un tablou de 100 de vectori –


practic o matrice cu 100 linii, în care liniile au dimensiuni variabilă.

Există şi alte posibilităŃi de declarare, de exemplu:

vector <tip_elemente> nume(nre,val);

Prin această declarare, variabila declarată va conŃine nre


elemente de tipul precizat între parantezele unghiulare. Toate aceste
elemente au valoarea val.
De exemplu:

vector <int> v2(10,100); (1.16)


/*variabila v2 care conŃine un tablou de 10 valori
întregi egale cu 100.*/

Cum aminteam mai înainte, o caracteristică importantă a unui


vector este că elementele tabloului conŃinut pot fi accesate cu
indici. Primul element din tabloul conŃinut de orice vector are
indexul 0. Pentru exemplele de mai înainte, sintaxele pe care le vom
putea utiliza sunt:

v1[20] sau T[3][20]

Pentru variabile de tip vector sunt implementate următoarele


operaŃii:

• atribuiri: acestea sunt permise numai pentru vectori care


conŃine elemente de acelaşi fel. Pentru exemplele anterioare,
putem scrie în program instrucŃiunea:

v1 = v2;

43
prin aceasta se face atât alocarea de memorie pentru tabloul
din v1 cât şi atribuirile corespunzătoare.
În schimb atribuirea

a = v2;
este eronată.

• comparări cu operatorii ==, !=, >, >= etc. Şi acestea sunt


permise numai pentru vectori care conŃine elemente de
acelaşi fel. Compararea se face lexicografic. CondiŃia care
trebuie îndeplinită pentru a putea utiliza aceşti operatori este
ca elementele tablourilor conŃinute de cei doi vectori să poată
fi comparate cu respectivii operatori.

C. FuncŃii conŃinute de un vector

Oricare dintre aceste funcŃii se apelează cu sintaxe de genul:

variabila_vector.nume_funcŃie(parametri)

begin()
Returnează un pointer la primul element din tabloul inclus in vector.

end()
Returnează un pointer la sfârşitul tabloului (adresa de după ultimul
element din tablou)

size()
Returnează numărul de elemente din tablou (valoare de tip unsigned)

max_size()
Returnează numărul maxim de elemente pe care le-ar putea avea
tabloul.

capacity()
Returnează numărul de elemente alocate pentru tablou în acel
moment.

empty()
Testează dacă vectorul este vid

44
push_back(val)
Adaugă un element care va conŃine valoarea val la sfârşitul tabloului.
Face alocarea de memorie (după mecanismul prezentat la 7.4.1) şi
incrementează numărul de elemente conŃinut de vector (furnizat de
funcŃia size()).

pop_back()
Elimină ultimul element din tablou.

insert(adrbegin, adrbegin2, adrend2)


Inserează elementele aflate între adresele adrbegin2 şi adrend2 în
vectorul curent începând de la adresa adrbegin.

insert(adrbegin, nre, val)


Inserează în vectorul curent începând de la adresa adrbegin un
număr de nre elemente fiecare dintre acestea având valoarea val.

erase(adr1, adr2)
Elimină elementele aflate între adresele adr1 şi adr2 din vectorul
curent

erase(adr)
Elimină elementul aflat la adresa adr din vectorul curent.

Programul următor exemplifică utilizarea unora dintre funcŃiile


prezentate mai înainte. Cele două funcŃii declarate în program (una
pentru generarea aleatoare a unui vector şi cealaltă pentru afişarea
elementelor conŃinute de tabloul vectorului) sunt sunt generice (tipul
elementelor din vector nu este unul concret).

//program 1.4 utilizarea containerului vector


#include <vector>
#include <iostream>
#include <algorithm>
#include <cstdlib>
using namespace std;
template <class tipelem>
void generare_aleat(vector <tipelem> &v, int n,
int dom)
{ v.erase(v.begin(),v.end());
for (int i=0;i<n;i++)
{ v.push_back((tipelem)(rand()%dom/1.1));
}
}
45
template <class tipelem>
void afişare_e(vector <tipelem> v)
{cout<< "dimensiune vector = " << v.size() << endl;
for (unsigned int i=0;i<v.size();i++)
cout << v[i] << ' ';
cout << endl<<endl;
}

int main()
{vector <int> v1,v2,v3(13,7);
vector <double> a;
cout<<"nr elemente din v1 = "<<v1.size()<<endl;
//dimensiune 0
cout<<"nr elemente din v3 = "<<v3.size()<<endl;
//dimensiune 13
cout << "ilustrarea modului de alocare a memoriei";
cout << "prin operatia push_back\n";
for (int i=1;i<=20;i++)
{ v2.push_back(i);
cout<<v2.size()<<'-'<<v2.capacity()<<'\t';
}

generare_aleat(v1,12,19);
cout << "vector aleator de intregi"<<endl;
afişare_e(v1);
sort(v1.begin(), v1.end());
cout << "vectorul anterior sortat"<<endl;
afişare_e(v1);

v1.pop_back();
cout << "vectorul dupa pop_back"<<endl;
afişare_e(v1);
int poz, val;
cout<<"dati pozitia de unde vreti sa eliminati";
cout << "un element...";
cin >> poz;
if (poz<0 || poz>v1.size()-1)
cout << "las-o pe alta data\n";
else
v1.erase(v1.begin()+poz);
cout << "vector dupa eliminare"<<endl;
afişare_e(v1);
cout << "dati pozitia de unde vreti sa inserati";
cout << "un element si valoarea acestuia...";
46
cin >> poz >> val;
if (poz<0 || poz>v1.size())
cout << "las-o pe alta data\n";
else
v1.insert(v1.begin()+poz,1,val);
cout << "vector dupa inserare"<<endl;
afişare_e(v1);
generare_aleat(a,10,11);
cout << "vector aleator de numere reale"<<endl;
afişare_e(a);
return 0;
}

Rezultatele afişate de program în fereastra consolă:


nr elemente din v1 = 0
nr elemente din v3 = 13
ilustrarea modului de alocare a memoriei prin
operatia push_back
1-1 2-2 3-4 4-4 5-8 6-8 7-8 8-8 9-16 10-16
11-16 12-16 13-16 14-16 15-16
16-16 17-32 18-32 19-32 20-32
vector aleator de intregi
dimensiune vector = 12
2 16 6 12 15 9 1 2 0 9 4 5

vectorul anterior sortat


dimensiune vector = 12
0 1 2 2 4 5 6 9 9 12 15 16

vectorul dupa pop_back


dimensiune vector = 11
0 1 2 2 4 5 6 9 9 12 15

dati pozitia de unde vreti sa eliminati un


element...4
vector dupa eliminare
dimensiune vector = 10
0 1 2 2 5 6 9 9 12 15

dati pozitia de unde vreti sa inserati un element si


valoarea acestuia... 4 44
vector dupa inserare
dimensiune vector = 11
0 1 2 2 44 5 6 9 9 12 15

47
vector aleator de numere reale
dimensiune vector = 10
4.54545 7.27273 5.45455 6.36364 2.72727 6.36364
8.18182 1.81818 6.36364 6.36364

1.4.5 Containerului <string>

Containerul string facilitează lucrul cu şiruri de caractere. El


poate fi privit ca o extensie a containerului de tip vector. A fost
implementat având în vedere importanŃa şirurilor de caractere în
aplicaŃii precum şi nevoia de a pune la dispoziŃia programatorilor în
C++ a unor mijloace simple de tratare a acestor şiruri. Denumirile
funcŃiilor asociate sunt asemănătoare cu cele din limbajul Pascal.

A. Utilizarea variabilelor de tip string

În primul rând trebuie inclusă biblioteca corespunzătoare denumită


<string>:

#include <string>

Apoi se declară variabile de tipul string cu sintaxa:

string numevar;

Prin aceasta este declarată o variabilă cu numele numevar. Această


variabilă conŃine un tablou ale cărui elemente au tipul char şi câteva
funcŃii care implementează operaŃiile uzuale care se efectuează cu
şiruri de caractere (şi pe care le vom prezenta în continuare). Prin
declaraŃia de mai înainte şirul de caractere conŃinut de acea variabilă
va fi vid şi mai mult, nu are alocată memorie pentru vreun element.

Exemple:

string s1;
Prin aceasta am declarat o variabilă s1care conŃine şir de caractere
vid.

string c[100];

48
Aici am declarat o variabilă c care este un tablou de 100 de string -
uri – practic o matrice în care cele 100 linii pot conŃine şiruri de
caractere de lungimi diferite.

Există şi alte posibilităŃi de declarare, de exemplu:

string nume(nre,val);

Prin această declarare, variabila declarată va conŃine nre caractere


toate având valoarea val. De exemplu prin declararea

string s2(10,’#’);
variabila s2 va conŃine un şir de 10 caractere #.

Elementele unui şir de caractere conŃinut de o variabilă de


tipul string pot fi accesate prin indici (similar cu modalitatea de
accesare a caracterelor conŃinute de şirurile de caractere din limbajul C
standard). Primul caracter din string are indexul (indicele, poziŃia) 0.
Pentru declarările de mai înainte, sintaxele pe care le vom putea utiliza
sunt:
s1[20] sau c[3][20]
Evident că vom putea utiliza astfel de sintaxe numai după ce s-a făcut
alocarea de memorie pentru respectivele elemente.

B. ProprietăŃi generale ale variabilelor de tip string

Pentru variabile de tip string sunt implementate următoarele


operaŃii:

• atribuiri: Pentru exemplele anterioare, putem scrie în


program instrucŃiunea:
s1 = s2;
prin aceasta se face atât alocarea de memorie pentru tabloul
de caractere din s1 cât şi copierea tuturor caracterelor din
s2.

• comparări cu operatorii ==, !=, >, >= etc. Compararea se


face lexicografic (alfabetic).

• concatenări: acestea sunt implementate prin operatorul +.


Pot fi concatenate:
o două variabile de tip string;
49
o o variabilă de tip string cu o variabilă de tip
char.

De exemplu, pentru variabila s1 declarată mai înainte,


atribuirea
s1 = s1 + ’.’;
adaugă un punct la sfârşitul şirului de caractere conŃinut de
s1. Facem observaŃia că prin atribuire se face şi alocarea
de memorie corespunzătoare caracterului suplimentar pe
care îl va conŃine variabila s1 din exemplul dat mai
înainte.

C. Citirea şi afişarea unui string

Pentru citire de la tastatură sau dintr-un fişier avem la dispoziŃie


următoarele posibilităŃi:

• dacă şirul pe care urmează să îl citim nu conŃine spaŃii:


cin >> s;

• dacă acesta conŃine spaŃii, putem utiliza funcŃia:


getline(cin,s);

De remarcat ca prin citire se face si alocarea automata de memorie


pentru tabloul cu elemente de tip char care stă la baza string-
ului.

Pentru afişarea unui string se poate utiliza, indiferent de conŃinut,


sintaxa:

cout << s;
Dacă citirea sau scrierea se face dintr-un sau într-un fişier text, în
loc de cin sau cout se va utiliza numele variabilei asociată fişierului.

D. FuncŃii conŃinute de un string

Prezentăm câteva dintre funcŃiile conŃinute de o variabilă de tip


string. Acestea se apelează cu sintaxe similare cu funcŃiile conŃinute
containerul vector:

variabila_string.nume_funcŃie(parametri)

50
În prezentarea care urmează, string-ul care apare în faŃa numelui
funcŃiei (şi pe care nu îl vom mai scrie) îl vom denumi string curent
(eventual şir curent).

size()
Returnează numărul de elemente din stringul curent (valoare de tip
unsigned).

length()
Returnează numărul de elemente din stringul curent (valoare de tip
unsigned).

clear()
Şterge conŃinutul stringuluui curent care devine string vid.

empty()
Testează dacă şirul este vid. Dacă da returnează 1, dacă nu,
returnează 0.

erase(poziŃie, nrc)
Şterge din poziŃia dată un număr de caractere dat.

erase(poziŃie)
Şterge din poziŃia dată toate caracterele până la sfârşitul şirului.

substr(poziŃie, nrc)
Returnează un subşir al şirului conŃinând un număr de caractere
corespunzătoare cu numărul de caractere dat care începe de la poziŃia
dată.

substr(poziŃie)
Returnează un subşir al şirului începând din poziŃia dată până la
sfârşitul şirului.

find(şir)
Returnează poziŃia primei apariŃiii a şirului de caractere “şir” în
stringul curent.
Dacă nu apare ne returnează o valoare mai mare decât dimensiunea
şirului curent.

find(caracter)
Returnează poziŃia primei apariŃii a şirului de caractere “şir” în
stringul curent.

51
Dacă nu apare ne returnează o valoare mai mare decat dimensiunea
şirului curent.

find(şir, poziŃie) şi
find(caracter, poziŃie)
Similare cu cele anterioare cu deosebirea că încep căutarea din poziŃia
dată ca al doilea parametru.

rfind()
FuncŃie similară cu find() dar care caută începând de la dreapta
(sfârşitul) stringului.

find_first_of(şir)
Returnează poziŃia primei apariŃii în stringul curent a unui caracter
din şirul “şir”.

find_first_not_of(şir)
Returnează poziŃia primei apariŃii în stringul curent a unui caracter
care nu apare în şirul dat ca parametru al funcŃiei (notat „şir”).

insert(poziŃie,şir)
Inserează în poziŃia dată şirul dintre paranteze.

insert(pozitie,nr,caracter)
Inserează în poziŃia dată caracterul dat de nr ori.

În continuare vom rezolva câteva probleme tipice şirurilor de


caractere utilizând stringuri.

1. Fişierul cuvinte.in conŃine mai multe linii; fiecare linie


conŃine câte un cuvânt format din litere ale alfabetului englez. Ne
propunem să scriem cuvintele din fişier în ordine alfabetică în fişierul
cuvinte.out. Cuvintele vor fi afişate câte unul pe o linie a fişierului
cuvinte.out.
În rezolvarea acestei probleme vom pleca de la faptul că pentru
string-uri este implementat operatorul <. Aşa că vom citi cuvintele
din fişier şi le vom reŃine într-o structură de tip tablou sau vector. Apoi
vom folosi funcŃia sort din biblioteca <algorithm> pentru a ordona
cuvintele. În final vom afişa tabloul/vectorul rezultat. Deoarece enunŃul
nu precizează numărul maxim de cuvinte din fişierul de intrare, vom
folosi pentru a reŃine cuvintele un vector ale cărui elemente vor fi de
tipul string. Aceasta deoarece când declarăm o variabilă de tip

52
vector putem să nu precizăm un număr maxim de elemente pe care
aceasta l-ar putea reŃine.

//program 1.5: sortare alfabetica a unui sir de


cuvinte
#include <fstream>
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
using namespace std;

int main()
{ vector <string> cuvinte;
string aux;
ofstream fout("cuvinte.out");
ifstream fin("cuvinte.in");
if (fin == NULL)
{ cout << "fisier de intrare inexistent!";
return 1;
}

while (fin >> aux)


cuvinte.push_back(aux);

sort(cuvinte.begin(),cuvinte.end());

for (int i=0;i<cuvinte.size();i++)


fout << cuvinte[i] << endl;
return 0;
}

2. În informatică, noŃiunea de parsare se referă la parcurgerea


şi analizarea unui text, cu identificarea atomilor care îi corespund. Vom
scrie un program care rezolvă următoarea cerinŃă: citim un şir de
caractere format din cuvinte. Fiecare cuvânt este format din litere ale
alfabetului englez iar cuvintele sunt separate prin câte un spaŃiu. Să
afişăm cuvintele care alcătuiesc şirul citit, câte unul pe o linie a
ecranului.
Cu alte cuvinte vom parsa textul citit (reŃinut în cazul nostru într-
un string). Atomii care îl formează sunt cuvinte şi între doi atomi se
află un spaŃiu.
Algoritmul de parsare se bazează pe următoarea idee: vom parcurge
secvenŃial şirul iniŃial, vom identifica categoriile de caractere pe care le

53
putem întâlni şi vom stabili ce paşi trebuie să efectuăm pentru fiecare
dintre aceste categorii. De regulă vom utiliza un atom curent şi în
funcŃie de tipul caracterului pe care suntem la un moment dat, vom
modifica acest atom curent în mod corespunzător.
În cazul problemei noastre atomul curent va fi un cuvânt curent iar în
text putem întâlni două categorii de caractere:
• literă; în acest caz suntem „în interiorul” unui cuvânt din textul
citit. Litera respectivă trebuie adăugată la cuvântul curent (prin
concatenare);
• spaŃiu: în acest caz s-a terminat un cuvânt iar acest cuvânt l-am
reŃinut în cuvântul curent. Vom adăuga cuvântul curent la o
structură (un vector) în care vom reŃine toate cuvintele din şitul
citit, apoi vom „reseta” cuvântul curent deoarece urmează să
reŃinem în el următorul cuvânt din şirul citit.
Pentru a nu „rata” ultimul cuvânt din text, vom adăuga un spaŃiu la
sfârşitul şirului citit.

//program 1.6: parsarea unui sir de nume


#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main()
{ vector <string> cuvinte;
string text,cuvcurent;
getline(cin,text);
text += ' '; //adaugam un spatiu
for (int i=0;i<text.size();i++)
{ if (text[i] != ' ')
cuvcurent += text[i];
if (text[i] == ' ')
{ cuvinte.push_back(cuvcurent);
cuvcurent.erase(0);
}
}
cout<<"fraza are "<< cuvinte.size() << " cuvinte\n";
for (int i=0;i<cuvinte.size();i++)
cout << cuvinte[i] << endl;
return 0;
}

3. Vom parsa un text care conŃine numere. Aceasta poate fi util


deoarece în cazul în care trebuie să citim multe numere dintr-un fişier

54
text, citirea fiecărui număr în parte este o operaŃiune lentă. Este mult
mai rapid să citim din fişier un bloc de caractere de dimensiune mare
(ideal ar fi să stabilim dimensiunea acestui bloc în funcŃie de diverse
caracteristici ale hard-diskului cum ar fi capacitatea unei piste, a unui
sector, a memoriilor de tip cache ş.a.) şi apoi să parsăm blocul
respectiv.
Problema pe care o vom rezolva are următorul enunŃ: citim un şir de
caractere alcătuit din numere naturale separate prin câte un spaŃiu. Să
calculăm şi să afişăm suma numerelor din şirul citit.
De exemplu, dacă citim şirul
12 4 8 1 6
programul va afişa rezultatul 31.
Ideea de rezolvare a acestei probleme este similară cu cea
descrisă la exemplul anterior. DiferenŃa este că atomul curent va fi un
număr şi nu un şir de caractere. Adăugarea caracterului curent la
numărul curent nu se va face printr-o simplă concatenare ci prin
determinarea valorii numerice corespunzătoare caracterului citit
(deductibilă din codul ASCII al caracterului: de exemplu; '0' are codul
48, '1' are codul 49, '2' are codul 50 etc.) urmată de aplicarea formulei:
număr_curent = număr_curent *10 + cifra;

//program 1.7: parsarea unei sir de numere


#include <iostream>
#include <string>
using namespace std;
int main()
{ string text;
int suma=0, numarcurent=0;
getline(cin,text);
text += ' '; //adaugam un spatiu
for (int i=0;i<text.size();i++)
{ if (text[i] != ' ')
numarcurent=numarcurent*10+(text[i]-0');
if (text[i] == ' ')
{ suma = suma + numarcurent;
numarcurent = 0;
}
}
cout << "suma = " << suma << endl;
return 0;
}
O altă Idee de rezolvare este să tratăm stringul de intrare într-un mod
identic cu cel din programul 1.6; prin aceasta vom construi un vector
de string-uri, în care fiecare string va conŃine un număr dintre cele

55
din şirul de intrare (deci va fi format numai din cifre). Apoi să convetim
fiecare string în valoarea numerică pe care o reprezintă cu funcŃii
predefinite pentru conversie – în cazul exemplului nostru cu funcŃia
stoi, aceasta are ca parametru un string şi returnează valoarea
numerică corespunzătoare. Însă pentru a putea utiliza această funcŃie
trebuie să lucrăm într-un mediu de dezvoltare a aplicaŃiilor C++ apărut
după 2011.

56
1.5 Structura de date de tip listă înlănŃuită

1.5.1 GeneralităŃi

Lista reprezintă o structură de date cu următoarele caracteristici:

• este omogenă (toate elementele sunt de acelaşi tip);


• este secvenŃială;
• este liniară (conŃine elemente ordonate prin poziŃie);
• zona de memorie ocupată nu este contiguă;
• zona de memorie ocupată nu este fixă (dimensiunea ei se poate
modificat prin instrucŃiuni);

Elementelor unei liste conŃin:


• informaŃia care ne dorim să o memorăm denumită informaŃie
specifică sau propriu-zisă;
• o informaŃie de legătură.

ConŃinutul listei se poate schimba prin operaŃii pe care le vom


considera fundamentale:
• adăugarea de noi elemente la sfârşitul listei;
• inserarea de noi elemente în orice loc din listă;
• ştergerea de elemente din orice poziŃie a listei;
• modificarea unui element dintr-o poziŃie dată;

Printre operaŃiile care schimbă structura listei vom considera şi


iniŃializarea unei liste ca o listă vidă. Alte operaŃii sunt cele care nu
modifică structura listelor dar furnizează informaŃii despre ele. Dintre
acestea menŃionăm:
• localizarea elementului din listă care satisface un anumit criteriu;
• determinarea numărului de elemente din lista (a lungimii listei).

Putem să luăm în considerare şi alte operaŃii:


• separarea unei liste în două sau mai multe subliste în funcŃie de
îndeplinirea
unor condiŃii;
• crearea unei liste în ale cărei elemente se regăseşte informaŃia
specifică aflată în elementele altei liste şi care îndeplinesc unul
sau mai multe criterii;
• crearea unei liste ordonate după anumite criterii;
• interclasarea a două liste.
Cele mai multe dintre aceste operaŃii se bazează pe parcurgerea listei
element cu element.
57
Listele se pot clasifica după diverse criterii.
• după modul în care se face alocarea memoriei în:
o liste alocate static: elementele listei sunt de fapt
elementele unui tablou
o liste alocate dinamic: memoria alocată este exact cea
corespunzătoare numărului de elemente aflate în listă la
un moment dat.

• după modul în care le putem parcurge, în


o liste simple sau unidirecŃionale;
o liste duble sau bidirecŃionale

1.5.2 Liste dinamice simple

Un element al unei liste dinamice simple este alcătuit din două


informaŃii:

• informaŃia propriu-zisă (cea pe care vrem să o reprezentăm prin


această structură de date);
• informaŃia care reŃine adresa următorului element din listă
(informaŃia de legătură); dacă nu există un element următor
atunci această informaŃie va avea o valoare particulară care să
specifice acest fapt.

Pentru a efectua diversele operaŃii fundamentale asupra structurii


de date de tip listă, este necesar să cunoaştem adresa primului
element al listei. Când implementăm respectivele operaŃii prin funcŃii,
este convenabil este ca adresa primului element să fie dată sub forma
unui parametru formal în cazul în care nu folosim o modalitate prin care
această adresă este „văzută” implicit în interiorul funcŃiilor.
Dacă vrem să implementăm în limbajul C++ structura de date de tip
listă, vom declara tipul de date pentru elementele unei liste ca
structură. Aceasta va avea două câmpuri corespunzătoare celor două
tipuri de informaŃii pe care vrem să le reŃinem.
• Pentru a da un caracter general declaraŃiilor pe care le vom
face, vom declara o structură generică în care câmpul care
reŃine informaŃia propriu-zisă este de tip şablon (template).
• Celălalt câmp care reŃine adresa următorului element va fi de
tip pointer spre un element de tipul structurii pe care o
declarăm. Astfel în definirea tipului structurii apare chiar tipul
pe care îl definim. Acesta este un exemplu tipic date
58
recursive. Valoarea acestui câmp în ultimul element va fi
NULL.

O posibilitate ar fi următoarea:

template <class T> struct tipelem


{
T info;
tipelem <T> *urmt;
};

MenŃionăm faptul că în toate sintaxele care vor urma, atunci


când vrem să utilizăm pentru elementele unei liste tipul generic din
declaraŃia anterioară vom folosi sintaxa tipelem <T>.

În continuare vom implementa sub forma unor funcŃii câteva


dintre operaŃiile enumerate la începutul capitolului. Majoritatea
operaŃiilor se bazează pe parcurgerea listei. Algoritmul de parcurgere
presupune următorii paşi:
1. atribuim unui pointer adresa primului element;
2. prelucrăm informaŃia specifică din elementul
pointat de respectivul pointer;
3. deplasăm pointerul la următorul element din
listă dându-i ca valoare informaŃia aflată în
câmpul de legătură.
4. repetăm paşii 2. şi 3. până când pointerul
primeşte valoarea NULL.

Exemplificăm acest algoritm printr-o funcŃie care afişează valoarea


specifică memorată în elementele unei liste:

template <class T> void afişare(tipelem <T> * first)


{
tipelem <T> * p = first;
if (first == NULL)
{
cout << "lista este vida...";
return ;
}
while (p!=NULL)
{
cout << p->info << ' ';
p = p->urmt;

59
}
cout << endl;
}

Urmează o funcŃie care inserează un element nou în faŃa primului


element al unei liste:
template <class T> void
inslainceput(tipelem<T>*&first, T val)
{
/*1*/ tipelem <T> * p = new tipelem <T>;
/*2*/ p->info = val;
/*3*/ p->urmt = first;
/*4*/ first = p;
}
Ilustrăm efectul funcŃiei prin următoarele patru desene care corespund
celor patru instrucŃiuni numerotate din această funcŃie:

1 2
first first

val
0 0

p p
3 4
first

val val
0 0

p p first
Fig. 1.1 Efectul instrucŃiunilor 1, 2, 3, 4 din funcŃia adginceput

Remarcăm că pentru a rula corect şi în cazul în care lista este vidă


este necesar ca la primul apel al funcŃiei valoarea parametrului first
să fie NULL.

60
În continuare o funcŃie care elimină primul element dintr-o listă.

template <class T> void elimprimul(tipelem


<T>*&first)
{ if (first == NULL)
{ cout << "nu avem ce elimina = lista e vida\n";
return;
}
tipelem<T> *p = first;
/*!*/ first = first->urmt;
delete p;
}
Observăm că dacă nu am memora adresa primului element într-o
variabilă pointer (am notat-o cu p) şi am executa direct instrucŃiunea
notată cu semnul exclamării am pierde adresa unde se afla elementul
pe care ne propusesem să îl ştergem. El nu va mai face parte din listă,
dar va ocupa în continuare zona de memorie în care se afla (ceea ce
nu este de dorit!).
Urmează o funcŃie prin apelul căreia vrem să eliminăm un
element din listă despre care cunoaştem o informaŃie (de exemplu
adresa zonei pe care o ocupă în memorie sau informaŃia specifică pe
care o conŃine). Dacă adresa este identică cu a primului element vom
apela funcŃia anterioară. Altfel va trebui să cunoaştem adresa
elementului care precede elementul pe care vrem să-l eliminăm, pentru
a putea face legătura între acest element precedent şi elementul care
urmează după cel pe care vrem să-l eliminăm. De exemplu, să
presupunem că într-o listă ilustrată prin figura următoare, vrem să
eliminăm elementul de la adresa adr. Paşii pe care trebuie să îi
parcurgem sunt următorii:
• să poziŃionăm un pointer (notat în figură p) pe elementul care îl
precede pe cel de la adresa adr. Aceasta o vom face prin
parcurgerea listei (vezi figura 1.2/5)
• să facem legătura între elementul din listă pointat de p şi cel
care se află după cel pointat de adr (6)
• să eliberăm zona de memorie ocupată de elementul pointat de
adr. (7)

61
5 6
adr adr

val p val p
0 0

first first
7
adr

val p
0

first
Fig. 1.2 Eliminarea elementului pointat de adr dintr-o listă – funcŃia elimadr

template <class T>


void elimadr(tipelem<T> *adr,tipelem<T>*&first)
{ if (adr == NULL)
{ cout << "lista este vida...";
return;
}
if (first == adr)
{ elimprimul(first);
return ;
}
tipelem<T> *p = first; //parcurgerea listei
while (p->urmt != adr)
{ p = p->urmt;
if (p == NULL)
{cout << "adresa data nu e in lista\n";
return ;
}
}
/*6*/ p->urmt = adr->urmt;
/*7*/ delete adr;
}

62
În continuare vom analiza operaŃiunea de inserare într-o listă a unui
element după un element a cărui adresa o cunoaştem. Efectul
instrucŃiunilor 8,...,11 este ilustrat în figura 1.3.
template <class T> void
insertadr(tipelem<T> *adr, tipelem<T>*first, T val)
{ if (first == NULL)
{ cout<<" lista e vida\n";
return;
}
if (adr == NULL)
{ cout << "adresa dupa care inseram e NULL\n";
return;
}
/*8*/ tipelem<T> * q = new tipelem<T>;
/*9*/ q->info = val;
/*10*/ q->urmt = adr->urmt;
/*11*/ adr->urmt = q;
}

8 9
adr adr

q q

val
val
0 0

first first
10 11
adr adr

q q

val val
0 0

first first
Fig. 1.3 Efectul instrucŃiunilor 8,...,11 din funcŃia insertadr

Încheiem cu un exemplu de utilizare a celor prezentate mai


înainte. InformaŃia specifică este de tip double.

63
//program 1.8: functia main() pentru liste simple
tipelem <double> *primul1 = NULL;
int main()
{cout << "lista initiala\n";
for (int i=1;i<=10;i++)
inslainceput(primul1,i/2.0);
afişare(primul1);
cout << "lista dupa eliminarea primului element\n";
elimprimul(primul1);
afişare(primul1);
cout << "lista dupa eliminarea celui de-al treilea
element\n";
elimadr(primul1->urmt->urmt, primul1);
afişare(primul1);
cout << "lista dupa inserarea valorii 177 dupa al
doilea element\n";
insertadr(primul1->urmt, primul1, 177.0);
afişare(primul1);
}

Rezultatele afişate de program în fereastra consolă:

lista initiala
5 4.5 4 3.5 3 2.5 2 1.5 1 0.5
lista dupa eliminarea primului element
4.5 4 3.5 3 2.5 2 1.5 1 0.5
lista dupa eliminarea celui de-al treilea
element
4.5 4 3 2.5 2 1.5 1 0.5
lista dupa inserarea valorii 177 dupa al doilea
element
4.5 4 177 3 2.5 2 1.5 1 0.5

1.5.3 Liste dinamice duble

Un element al unei liste dinamice duble este alcătuit din trei


informaŃii:
• informaŃia propriu-zisă (cea pe care vrem să o reprezentăm prin
această structură de date);
• informaŃia care reŃine adresa următorului element din listă; dacă
nu există un element următor atunci în această informaŃie va
avea o valoare particulară care să specifice acest fapt;
64
• informaŃia care reŃine adresa elementului precedent din listă;
dacă nu există un element precedent atunci în această
informaŃie va avea o valoare particulară care să specifice acest
fapt.

Declararea tipului elementelor unei liste duble o vom face după


acelaşi principiu ca în cazul listelor simple.
Pentru a efectua diversele operaŃii fundamentale asupra structurii
de date de tip listă dublă, este necesar să cunoaştem şi să actualizăm
în permanenŃă adresa primului element al listei. Pentru reducerea
complexităŃii unora dintre algoritmi este convenabil să reŃinem şi să
actualizăm şi adresa ultimului element din listă. Aceasta va avea ca
efect faptul că majoritatea funcŃiilor pe care le vom scrie în continuare
vor avea ca parametri doi pointeri corespunzători acestor două
informaŃii, pe lângă alŃi parametri corespunzători operaŃiunii care se
doreşte a fi implementată. FuncŃiile pe care le prezentăm în continuare
sunt după cum urmează:
• două funcŃii care afişează informaŃia specifică reŃinută de
elementele unei liste duble, câte o funcŃie pentru fiecare sens
de parcurgere; vom putea apela aceste funcŃii numai pentru
liste pentru care informaŃia specifică aparŃine unui tip pentru
care este definit operatorul >> pentru afişare;
• o funcŃie care inserează un element în faŃa primului element
dintr-o listă dublă; valoarea inserată va fi transmisă ca
parametru formal;
• o funcŃie care inserează un element după ultimul element
dintr-o listă dublă; valoarea inserată va fi transmisă ca
parametru formal;
• o funcŃie care elimină primul element dintr-o listă dublă;
• o funcŃie care elimină ultimul element dintr-o listă dublă;
• o funcŃie care elimină elementul de la o adresă transmisă
prin parametru;
• o funcŃie care inserează un element precizat prin adresa lui o
valoare; adresa elementului după care inserăm şi valoarea
sunt date ca parametri;

În funcŃia main() vom declara o listă cu informaŃia specifică de tip


double şi vom apela aceste trei funcŃii. Pentru a simplifica sintaxele,
am preferat să asociem prin #define construcŃiei sintactice
template <class T> specifice şabloanelor, numele FGENERICA

65
//program 1.9: liste duble
template <class T> struct tipelem
{ T info;
tipelem <T> *urmt, *prec;
};
#define FGENERICA template <class T>
FGENERICA void afiseazasd(tipelem<T> * first)
//afişare stanga->drepta
{ tipelem<T> *p;
for (p=first;p!=NULL;p=p->urmt)
cout << p->info << ' ';
cout << endl;
}
FGENERICA void afiseazads(tipelem<T> * last)
//afişare drepta->stanga
{ tipelem<T> *p;
for (p=last; p!=NULL; p=p->prec)
cout << p->info << ' ';
cout << endl;
}
FGENERICA void adglainceput(tipelem<T> *&first,
tipelem<T> *&last, T val)
{
/*12*/ tipelem<T> *p = new tipelem<T> ;
/*13*/ p->info = val;
/*14*/ p->urmt = first;
/*15*/ p->prec = 0; //0 este echivalent cu NULL
if (first != NULL)
{
/*16!*/ first->prec = p;
/*17*/ first = p;
}
else
{
first = last = p;
}
}
/*Remarcăm în funcŃia adginceput tratarea separată a cazului în care
lista este vidă. Aceasta deoarece dacă lista este vidă, first are
valoarea NULL iar operaŃiunea de dereferenŃiere făcută prin
instrucŃiunea numerotată cu 16 în cazul în care este aplicată unui
pointer spre NULL este greşită şi produce o eroare de rulare! Efectul
instrucŃiunilor 12,...,17 este ilustrat în figura 1.4:

66
12 13
first first

0 0
... ...

val

p p

14 15
first first

0 0
... ...

val 0 val

p p

16 17
first

... ...

0 val 0 val

p first p

Fig. 1.4 Efectul instrucŃiunilor 12,...,17 din funcŃia adginceput pentru o listă dublă

FGENERICA void adglasfârsit(tipelem<T> *&first,


tipelem<T> *&last, T val)
{ tipelem<T> *p;
p = new tipelem<T>;
p->info = val;
p->urmt = NULL;
p->prec = last;
if (last != NULL)
{ last->urmt = p;
last = p;
}
else
{ last = first = p; }
}
FGENERICA void elimprimul(tipelem<T> *&first,
tipelem<T> *&last)
{ if (first == NULL)
{cout << "lista vida...";return;}
tipelem<T> *p;
p = first;
first = first->urmt;
delete p;
67
if (first != NULL)
first->prec = NULL;
else
last = 0;
}

FGENERICA void elimultimul(tipelem<T> *&first,


tipelem<T> *&last)
{ if (first == NULL)
{ cout << "lista vida..."; return; }
tipelem<T> *p;
p = last;
last = last->prec;
delete p;
if (last != NULL)
last->urmt = NULL;
else
first = NULL;
}

FGENERICA void eliminadr(tipelem<T> *&first,


tipelem<T> *&last, tipelem<T> *adr)
{ if (first == NULL)
{cout << "lista e vida, deci...";return;}
if (adr == first)
{
elimprimul(first,last);return ;
}
if (adr == last)
{
elimultimul(first,last); return ;
}
tipelem<T> *p, *q;
p = adr->prec;
q = adr->urmt;
p->urmt = q;
q->prec = p;
delete adr;

}
FGENERICA void insertadr(tipelem<T> *&first,
tipelem<T> *&last, tipelem<T> *adr, T val)
{ if (adr==NULL)
{ cout << "adresa data este NULL...";return;
}
68
if (first==NULL)
{cout << "lista vida, nu avem dupa cine insera";
return;
}
tipelem<T> *p = adr->urmt;
tipelem<T> *elemnou = new tipelem<T>;
elemnou->info = val;
elemnou->urmt = p;
elemnou->prec = adr;
adr->urmt = p->prec = elemnou;
}

tipelem <int> *prim1, *ultim1;


int main()
{ prim1= ultim1= NULL;
for (int i=1;i<=20;i++)
adglasfârsit(prim1, ultim1, i);
cout<< "lista initiala de la stanga la dreapta\n";
afiseazasd(prim1);
cout<< "lista initiala de la dreapta la stanga\n";
afiseazads(ultim1);
elimprimul(prim1,ultim1);
cout<< "lista dupa eliminarea primului element\n";
afiseazasd(prim1);
elimultimul(prim1,ultim1);
cout<<"lista dupa eliminarea ultimului element\n";
afiseazasd(prim1);
cout<<"lista dupa eliminarea elementului din";
cout<< " poz.4\n";
eliminadr(prim1,ultim1,prim1->urmt->urmt->urmt);
afiseazasd(prim1);
cout<<"lista dupa inserarea valorii 543 dupa";
cout<< " poz.4\n";
insertadr(prim1,ultim1,prim1->urmt->urmt->urmt,543);
afiseazasd(prim1);
return 0;
}

Rezultatele afişate de program în fereastra consolă:


lista initiala de la stanga la dreapta
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
lista initiala de la dreapta la stanga
20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
lista dupa eliminarea primului element
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
69
lista dupa eliminarea ultimului element
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
lista dupa eliminarea elementului din poz.4
2 3 4 6 7 8 9 10 11 12 13 14 15 16 17 18 19
lista dupa inserarea valorii 543 dupa poz.4
2 3 4 6 543 7 8 9 10 11 12 13 14 15 16 17 18 19

Observăm existenŃa cazurilor particulare ”lista vidă” sau „lista care


conŃine un singur element” care trebuie tratate separat. O soluŃie
convenabilă pentru evitarea secvenŃelor de tratare a acestor cazuri o
reprezintă adăugarea unor elemente de tip santinelă la cele două
capete ale listei duble. Astfel în locul atribuirilor
prim1= ultim1= NULL;
vom avea o secvenŃă prin care alocăm memorie pentru cele două
elemente, facem legăturile între ele şi să atribuim celor doi pointeri
adresele acestor două elemente. Această abordare va avea şi
avantajul că valorile pentru cei doi pointeri nu se vor mai modifica prin
prelucrările efectuate de funcŃiile pe care le vom scrie (cu excepŃia
evidentă a unei funcŃii care să elimine complet din memorie lista
respectivă).

1.5.4 Containerul list

Datorită numeroaselor aplicaŃii în care este utilă folosire listelor


înlănŃuite, în ansamblul de biblioteci STL a fost creată şi o bibliotecă
care implementează containerul listă dublă denumit list.

A. Prezentarea generală a variabilelor de tip list

Variabilele de tip list sunt liste dublu înlănŃuite. Ele conŃin


elementele unei astfel de liste, adresele primului şi ultimului element
din listă, alte informaŃii referitoare la listă (de exemplu numărul de
elemente) precum şi o serie de funcŃii care implementează operaŃiile
fundamentale asupra unei structuri de date (eliminare, inserare,
adăugare etc.).
Din punct de vedere al accesului pe care îl avem la elementele
unei variabile de tip list, singura informaŃie pe care o putem accesa
este informaŃia propriu-zisă reŃinută de o astfel de listă. Legăturile
dintre elemente ne sunt inaccesibile. Aceste legături se modifică
automat în momentul în care apelăm funcŃii care modifică structura
listei.

70
Parcurgerea unei liste de tip list se face cu un tip de pointeri
specializaŃi pentru astfel de operaŃii denumiŃi iteratori. În cazul
variabilelor de tip list, ei pot fi direcŃi (pentru parcurgere de la „stânga
la dreapta”) sau inverşi/reversibili (pentru parcurgere de la „dreapta la
stânga”). Pentru a trece de la un element la altul aceşti pointeri pot fi
incrementaŃi sau decrementaŃi.

B. Utilizarea variabilelor de tip list

În primul rând trebuie inclusă biblioteca corespunzătoare din STL


denumită list:
#include <list>

Apoi se declară variabile de tip list cu sintaxa:


list <tip_elemente> lista1, lista2;

Variabilele de tip listă vor avea numele din declaraŃia anterioară iar
elementele acestor liste duble vor avea informaŃia propriu-zisă de tipul
precizat între parantezele unghiulare. Listele conŃinute de aceste
variabile vor fi vide.

De exemplu:
list <int> L1;
reprezintă declararea unei variabile L1 care conŃine o listă cu valori
int

list <double> a,b;


reprezintă dedclararea a două variabile denumite a şi b care conŃin
fiecare câte o listă de valori reale

list <int> A[100];


reprezintă declararea unei variabile A care este un tablou de 100 de
liste duble – practic o matrice în care liniile au dimensiuni variabile şi
diferite una de cealaltă.

Există şi alte posibilităŃi de declarare, de exemplu:


list <tip_elemente> nume(nre,val);

Prin această declarare, variabila declarată va conŃine nre


elemente de tip tip_elemente toate reŃinând valoarea val.
De exemplu:
list <int> L2(10,100);

71
este declararea unei variabile denumită v2 care conŃine o listă cu 10
elemente cu informaŃia propriu-zisă de tip int. În toate cele 10
elemente această informaŃie este egală cu 100.

C. OperaŃii implementate pentru variabile de tip list:

• atribuire. Acesta este permise numai pentru liste care


conŃine elemente de acelaşi fel. Pentru exemplele anterioare,
putem scrie în program instrucŃiunea:
L1 = L2;
prin aceasta se face atât alocarea de memorie pentru
elementele listei din L1 cât şi atribuirile corespunzătoare. În
schimb atribuirea
a = L2;
este eronată.

• comparări cu operatorii ==, !=, >, >= etc. Şi acestea sunt


permise numai pentru liste care conŃin elemente de acelaşi
fel. Compararea se face lexicografic.

D. FuncŃii conŃinute de o variabilă de tip list

Oricare dintre aceste funcŃii se apelează cu sintaxe de genul:


variabila_list.nume_funcŃie(parametri)

begin()
Returnează un pointer la primul element din listă.

end()
Returnează un pointer la sfârşitul listei (adresa de după ultimul element
din listă).

rbegin()
Returnează un pointer la ultimul element din listă.

rend()
Returnează un pointer la marcajul de sfârşit de listă aflat la „capătul din
stânga” al listei (util pentru parcurgerea „dreapta-stânga”).

size()
Returnează numărul de elemente din listă (valoare de tip unsigned)

max_size()
72
Returnează numărul maxim de elemente pe care le-ar putea avea lista.

empty()
Testează dacă lista este vidă.

push_back(val)
Adaugă un element care va conŃine valoarea val la sfârşitul listei.

pop_back()
Elimină ultimul element din listă.

push_front(val)
Inserează un element care va conŃine valoarea val la începutul listei.

pop_front()
Elimină primul element din listă.

insert(adr, adrbegin2, adrend2)


Inserează elementele aflate între adresele adrbegin2 şi adrend2
în lista curentă începând de la adresa precizată prin pointerul adr.
Valorile conŃinute de acele elemente trebuie să fie de acelaşi tip cu
cele conŃinute de lista curentă.

insert(adr, nre, val)


Inserează în lista curentă înaintea elementului de la adresa adr un
număr de nre elemente fiecare dintre acestea având valoarea val.

insert(adr, val)
Inserează în listă înaintea elementului de la adresa adr un element
având valoarea val.

În toate cele trei forme de utilizare ale funcŃiei insert, pointerul


adr va pointa pe elementul pe care pointa şi înainte de apel.

erase(adr1, adr2)
Elimină elementele aflate între adresele adr1 şi adr2 din lista
curentă.

erase(adr)
Elimină elementul aflat la adresa adr din lista curentă.

FuncŃia erase returnează în ambele forme de utilizare un


interator (pointer) la primul element aflat după ultimul element eliminat
73
din listă. Dacă au fost eliminate toate elementele dintre adresa dată ca
parametru şi sfârşitul listei, funcŃia returnează o valoare identică cu
valoarea returnată de funcŃia end().

clear()
Elimină toate elemente din listă.

remove(val)
Elimină toate elementele din listă care conŃin valoarea val.

remove_if(nume_funcŃie_predicat_unară)
Elimină toate elementele din listă pentru care funcŃia „predicat”
returnează valoarea true. O funcŃie predicat unară se declară având
un singur parametru de tipul informaŃiei propriu-zise reŃinută de
elementele din listă şi returnează valoarea true dacă parametrul
satisface condiŃia dorită şi false în caz contrar. De exemplu, dacă
dorim să eliminăm toate valorile pare dintr-o listă, putem declara funcŃia
predicat astfel:
bool par(int x)
{ if (x%2 == 0) return true;
else return false;
}
iar apelul funcŃiei remove_if se va putea face astfel:
L1.remove_if(par);

sort()
Sortează crescător elemente din listă în cazul în care pentru tipul
acestora este definit operatorul <.

sort(nume_funcŃie_comparare)
Sortează crescător elemente din listă în cazul în care pentru tipul
acestora NU este definit operatorul <. În acest caz, prin funcŃia de
comparare stabilim cum vrem să se facă sortarea. Această funcŃie
trebuie să aibă doi parametri de tipul informaŃiei propriu-zisă conŃinută
de elementele listei şi să returneze valoarea true (sau 1) dacă primul
parametru e „mai mic” decât al doilea.

reverse()
Inversează ordinea elementelor din listă.

unique()
Elimină dublurile dintr-o listă sortată în cazul în care pentru tipul
acestora este definit operatorul ==.

74
unique(nume_funcŃie_predicat_binară)
Elimină dublurile dintr-o listă sortată în cazul în care pentru tipul
acestora NU este definit operatorul ==. FuncŃia predicat binară se
declară având doi parametri de tipul elementelor din listă şi returnează
valoarea true dacă cei doi parametri „sunt egali” şi false în caz
contrar.

merge(list L2)
Interclasează elementele listei curente cu elementele din lista L2. Cele
două liste trebuie să conŃină elemente de acelaşi tip iar respectivele
elemente trebuie sa fie sortate. Rezultatul interclasării se va afla în lista
curentă. Este necesar ca pentru elemente din cele două liste să fie
definit operatorul <.

merge(list L2, nume_funcŃie_comparare)


Interclasează elementele listei curente cu elementele din lista L2. Cele
douăp liste trebuie să conŃină elemente de acelaşi tip iar respectivele
elemente trebuie sa fie sortate. Rezultatul interclasării se va afla în lista
curentă. Această formă a funcŃiei merge se utilizează când pentru
elemente din cele două liste nu este definit operatorul <. FuncŃia de
comparare se declară şi se utilizează la fel ca în cazul funcŃiei sort.

front()
Returnează valoarea din primul element din listă.

back()
Returnează valoarea din ultimul element din listă.

E. Accesarea elementelor unei liste cu iteratori

Parcurgea secvenŃială a unei liste pentru implementarea unor diverşi


algoritmi se face utilizând iteratori. Aceştia se declară astfel:
• în cazul unui iterator direct:
list <tip_elem>::iterator nume_iterator;
• în cazul unui iterator reversibil (care ne oferă posibilitatea să
parcurgem lista în sens invers):
list<tip_elem>::reverse_iterator nume_iterat;

Dacă dorim să declarăm funcŃii în care tipul informaŃiei reŃinută în listă


este generic, atunci declarările precedente trebuie precedate de
cuvântul rezervat typename. Astfel iteratorul direct îl vom declara
astfel:
typename list <tip_elem>::iterator nume_iterat;
75
Vom exemplifica utilizarea iteratorilor prin declararea a două funcŃii
pentru afişarea informaŃiilor conŃinute de o listă. În prima funcŃie vom
parcurge lista de la „stânga la dreapta” iar în a doua funcŃie în sens
invers.
template <class tipelem>
void afişaresd(list<tipelem> L)
{ typename list <tipelem> :: iterator it;
for (it = L.begin(); it !=L.end(); it++)
cout << *it << ' ';
cout << endl;
}

template <class tipelem>


void afişareds(list<tipelem> L)
// de la „dreapta la stânga”
{ typename list <int> :: reverse_iterator it;
for (it = L.rbegin(); it !=L.rend(); it++)
cout << *it << ' ';
cout << endl;
}

În încheiere un program care ilustrează utilizarea containerului list:


//program 1.10 containerul list
#include <iostream>
#include <list>
#include <string>
using namespace std;
bool par (int x) //functie predicat
{ if (x%2 == 0) return true;
else return false;
}
bool fc(int a, int b) //functie pentru sort
{ if (a%2==1 && b%2==0) return 0;
else return 1;
}
template <class tipelem>
void afişaresd(list<tipelem> L)
{ typename list <tipelem> :: iterator it;
for (it = L.begin(); it !=L.end(); it++)
cout << *it << ' ';
cout << endl;
}
template <class tipelem>
void afişareds(list<tipelem> L)
76
// afişare de la dreapta la stanga
{ typename list <int> :: reverse_iterator it;
for (it = L.rbegin(); it !=L.rend(); it++)
cout << *it << ' ';
cout << endl;
}

list <int> L1,L2;


int main()
{ cout << "comparare liste:" << (L1 >= L2) << endl;
for (int i=0;i<10;i++)
L1.push_back(i);
cout << "Lista 1 afişata stanga->dreapta\n";
afişaresd(L1);
cout << "Lista 1 afişata dreapta->stanga\n";
afişareds(L1);
L2 = L1; //atribuire!
cout<<"Lista 2 generata prin copierea ";
cout << "listei initiale\n";
afişaresd(L2);
afişareds(L2);
cout << "Lista 1 rezultata prin eliminarea";
cout << valorilor pare\n";
L1.remove_if(par);
afişaresd(L1);

cout << "Inserarea valorii 15 in pozitia 2\n";


list <int> :: iterator it, it1;
it = L1.begin();
it++;
it = L1.insert(it,15);
//cout << *it << endl;
afişaresd(L1);

cout << "Inserarea listei in lista 1\n";


it++;
L1.insert(it,L2.begin(),L2.end());
//cout << *it << endl;
afişaresd(L1);

cout << "Inserarea valorii 77 in lista 1\n";


it = L1.begin();
it++;
L1.insert(it,4,77);
//cout << *it << endl;
77
afişaresd(L1);

cout << "Lista 1 sortata\n";


L1.sort();
afişaresd(L1);

cout << "Interclasarea celor doua liste\n";


L1.merge(L2);

cout << "Rezultatul functiei back() ";


cout << "dupa adaugarea valorii 1114\n";
L1.push_back(1114);
cout << L1.back() << endl;

cout << "Lista 1 dupa aplicarea ";


cout << "functiei unique()\n";
L1.unique();
afişaresd(L1);

cout << "Inserare dupa fiecare valoare para\n";


for (it = L1.begin(); it!=L1.end(); it++)
if ((*it) % 2 == 0)
{
it1 = it;
it1++;
L1.insert(it1,(*it)/2);
if (it1 == L1.end())
break;
it = it1;
}
afişaresd(L1);

cout << "Aranjarea valorilor pare apoi impare ";


cout << "cu functia sort\n";
L1.sort(fc);
afişaresd(L1);

list <string> L3;


string aux;
cout << "Introduceti patru cuvinte...";
for (int i=0;i<4;i++)
{
cin >> aux;
L3.push_front(aux);
}
78
cout << "Lista cu cele 4 cuvinte\n";
afişaresd(L3);

return 0;
}

Rezultatele afişate de program în fereastra consolă:


comparare liste:1
Lista 1 afişata stanga->dreapta
0 1 2 3 4 5 6 7 8 9
Lista 1 afişata dreapta->stanga
9 8 7 6 5 4 3 2 1 0
Lista 2 generata prin copierea listei initiale
0 1 2 3 4 5 6 7 8 9
9 8 7 6 5 4 3 2 1 0
Lista 1 rezultata prin eliminarea valorilor pare
1 3 5 7 9
Inserarea valorii 15 in pozitia 2
1 15 3 5 7 9
Inserarea listei in lista 1
1 15 0 1 2 3 4 5 6 7 8 9 3 5 7 9
Inserarea valorii 77 in lista 1
1 77 77 77 77 15 0 1 2 3 4 5 6 7 8 9 3 5 7 9
Lista 1 sortata
0 1 1 2 3 3 4 5 5 6 7 7 8 9 9 15 77 77 77 77
Interclasarea celor doua liste
Rezultatul functiei back() dupa adaugarea valorii
1114
1114
Lista 1 dupa aplicarea functiei unique()
0 1 2 3 4 5 6 7 8 9 15 77 1114
Inserare dupa fiecare valoare para
0 0 1 2 1 3 4 2 5 6 3 7 8 4 9 15 77 1114 557
Aranjarea valorilor pare apoi impare cu functia sort
1114 4 8 6 2 4 2 0 0 557 77 15 9 7 3 5 3 1 1
Introduceti patru cuvinte... ana are mere rosii
Lista cu cele 4 cuvinte
rosii mere are ana

79
1.6. Structura de date de tip stivă

1.6.1. GeneralităŃi

Stiva reprezintă o structură de date în care informaŃia este


accesată după principiul ultimul_venit = primul_plecat (Last In First
Out - LIFO). Aceasta se realizează prin faptul că elementele pot fi
inserate şi şterse din stivă pe la un singur capăt denumit vârful stivei.

Principalele caracteristici ale unei stive sunt următoarele:

• este omogenă (toate elementele sunt de acelaşi tip);


• poate fi statică sau dinamică (aceasta depinde de modalitatea
de implementare aleasă);
• accesul la elementele stivei este limitat la ultimul element intrat
în structură.

Putem implementa o stivă bazându-ne pe o structură de date de uz


general, de exemplu un tablou sau o listă simplă sau dublă. Indiferent
de metoda aleasă, trebuie implementate operaŃiile specifice pentru o
stivă. Acestea sunt:
• inserarea unui element – operaŃie denumită tradiŃional push;
• eliminarea unui element - operaŃie denumită tradiŃional pop;
• testarea dacă stiva este goală sau conŃine elemente;
• determinarea valorii elementului aflat în vârful stivei;
Este important ca toate aceste operaŃii (sau celesă fie de complexitate
O(1).

Pe lângă acestea, se pot implementa şi alte câteva operaŃii:


• determinarea numărului de elemente din stivă;
• golirea completă a stivei;
• determinarea numărului maxim de elemente pe care le poate
reŃine stiva.

1.6.2. Implementarea unei stive folosind structura de


date de tip tablou unidimensional

Vom implementa stiva ca o structură generică care va conŃine:


• tabloul unidimensional pentru reŃinerea valorilor inserate în stivă;

80
• vârful stivei – o dată (un câmp) de tip int care va reprezenta
poziŃia din tablou unde urmează să fie inserat un nou element;
• şapte funcŃii care implementează operaŃiile enumerate mai sus.
În exemplul care urmează, numărul maxim de elemente din stivă este
stabilit la 10000 printr-o declaraŃie de tip #define.

//program 1.11: stiva implementata cu tablou static


#include <iostream>
#include <string>
#define nmax 10000
using namespace std;

template <class tipg> struct stiva


{ tipg st[nmax]; // elementele stivei
int varf; //varful stivei=pozitia
//unde se insereze un element
void initializare()
{ varf = 0; }

void push(tipg val)


{ if (varf == nmax)
{ cout << "stiva e plina! \n";
return ;
}
st[varf] = val;
varf++;
}

bool egoala()
{ if (varf == 0) //return varf==0;
return true;
else
return false;
}

void pop()
{ if (egoala())
{ cout << "stiva e goala!\n";
return ;
}
varf--;
}

tipg top()

81
{ if (!egoala())
return st[varf-1];
else
cout << "stiva e goala!\n";
}

int nrelem()
{ return varf; }

void golire()
{ varf = 0; }

int capacitatemax()
{ return nmax; }
};

int main()
{ stiva <int> st1;
stiva <string> st2;
st1.initializare();
cout<< "sitva 1 este goala (1=da):";
cout << st1.egoala() <<"\n";
cout << "numarul maxim de elemente din stiva =";
cout << st1.capacitatemax() << "\n";
for (int i=1;i<=10;i++)
st1.push(10*i);
cout << "stiva contine ";
cout <<st1.nrelem()<<" elemente "<<"\n";
cout<< "in varful stive este valoarea ";
cout <<st1.top()<<"\n";
st1.pop();
cout << "stiva contine " <<st1.nrelem();
cout <<" elemente "<<"\n";
cout << "in varful stive este valoarea ";
cout <<st1.top()<<"\n";
cout << "golirea si afişarea stivei:" << endl;
while (!st1.egoala())
{ cout << st1.top() << ' ';
st1.pop();
}
cout << '\n';
string cuvant;
st2.initializare();
cout << "Tastati patru cuvinte...";
for (int i=1;i<=4;i++)
82
{
cin >> cuvant;
st2.push(cuvant);
}
cout<<"cuvintele in ordinea inversa a citirii\n";
while (!st2.egoala())
{ cout << st2.top() << ' ';
st2.pop();
}
return 0;
}

Rezultatele afişate de program în fereastra consolă:


sitva 1 este goala (1=da):1
numarul maxim de elemente din stiva =10000
stiva contine 10 elemente
in varful stive este valoarea 100
stiva contine 9 elemente
in varful stive este valoarea 90
golirea si afişarea stivei:
90 80 70 60 50 40 30 20 10
Tastati patru cuvinte...cuvintele in ordinea inversa
a citirii
verzi mere are ana

1.6.3. Implementarea unei stive folosind structura de


date de listă înlănŃuită

Vom implementa stiva sub forma unei structuri în care vom cuprinde:
• o structură reprezentând tipul elementelor listei;
• un pointer care va reŃine adresa primului element din listă;
• funcŃiile enumerate mai înainte.
OperaŃiunile de inserare şi eliminare sunt de fapt cele de inserare a
unui element în faŃa primului element dintr-o listă şi respectiv
eliminarea primului element dintr-o listă.

//program 1.12: stiva implementata cu lista simpla


//inlantuita
#include <iostream>
#include <string>
using namespace std;

83
template <class tipinfo> struct stiva
{ struct element
{ tipinfo info;
element * urmt;
};
element *varf;

void initializare()
{ varf = NULL; }

void push(tipinfo val)


{ element *p;
p = new element;
if (p == NULL)
{
cout << "spatiu memorie insuficient\n";
return;
}
p->info = val;
p->urmt = varf;
varf = p;
}

bool egoala()
{ if (varf==NULL) return true;
else return false;
}

void pop()
{ if (egoala())
{ cout << "stiva goala!\n";
return;
}
element *p;
p = varf;
varf = varf->urmt;
delete p;
}

tipinfo top()
{ if (!egoala()) return varf->info;
else
cout << "stiva e goala! \n";;
}

84
int nrelem()
{ int ct=0;
element * p;
for (p=varf; p!=NULL; p=p->urmt)
{ ct++; }
return ct;
}

void clear()
{ while (!isempty())
pop();
}
};
int main()
{stiva <int> st1;
stiva <string> st2;
st1.initializare();
cout << "sitva este goala (1=da):";
cout << <<st1.egoala()<<endl;
for (int i=1;i<=10;i++)
st1.push(10*i);
cout<< "stiva contine "<<st1.nrelem();
cout << <<" elemente "<<endl;
cout << "in varful stive este " <<st1.top()<< endl;
st1.pop();
cout<<"stiva contine "<<st1.nrelem();
cout <<" elemente "<<endl;
cout << "in varful stive este " <<st1.top()<< endl;
cout << "golirea si afişarea stivei:" << endl;
while (!st1.egoala())
{ cout << st1.top() << ' ';
st1.pop();
}
cout << '\n';
string nume;
st2.initializare();
cout << "Tastati patru cuvinte...";
for (int i=1;i<=4;i++)
{ cin >> nume;
st2.push(nume);
}
cout << "numele in ordinea inversa a citirii\n";
while (!st2.egoala())
{ cout << st2.top() << '\n';
st2.pop();
85
}
return 0;
}

Rezultatele afişate de acest program în fereastra consolă sunt aceleaşi


cu cele afişate de programul anterior.

1.6.4. Containerul stack

Datorită numeroaselor aplicaŃii în care este utilă folosirea


stivelor, în ansamblul de biblioteci STL a fost creată şi o bibliotecă care
implementează containerul stivă denumit stack.

A. Prezentarea generală a variabilelor de tip stack

Implicit variabilele de tip stack sunt implementate având la


bază cozi cu două capete (denumite şi deque de la acronimul
expresiei double-ended queue). Prin modul de declarare, se poate
stabili ca la baza variabilei de tip stivă să stea un alt container (un
vector sau o listă).
Spre deosebire de implementările anterioare în care am fi putut
accesa oricare element din tabloul sau lista simplă pe care le-am folosit
ca element de bază al stivelor implementate, variabilele de tip stack
nu oferă decât accesul la elementul aflat în vârful stivei (permis de
funcŃiile specifice unei stive).

B. Utilizarea facilităŃilor oferite de containerul stack

În primul rând trebuie inclusă biblioteca corespunzătoare din STL


denumită stack:
#include <stack>

Apoi putem declara o variabilă stack cu sintaxa:


stack <tip_elemente> nume_variabilă;

Variabila de tip stivă va avea numele din declaraŃie anterioară iar


informaŃiile pe care le vom putea insera în această stivă vor avea tipul
precizat între parantezele unghiulare. Stiva astfel declarată va fi vidă.
De exemplu prin:
stack <int> sti1, sti2;
declarăm două variabile st1 şi st2 care conŃin fiecare o stivă cu valori
int.
86
stack <string> sts3;
reprezintă declararea unei variabile sts3 care conŃine o stivă de şiruri
de caractere.
stack <int> A[100];
reprezintă declararea unei variabile A care este un tablou de 100 de
stive.

Există şi alte posibilităŃi de declarare, de exemplu:

stack<tip_elm,tip_containr> var_stiva(var_containr);

Prin această, stiva declarată va conŃine elementele containerului


precizat între parantezele unghiulare. tip_elm şi tipul elementelor din
container trebuie să fie compatibile.
De exemplu secvenŃa de declaraŃii şi instrucŃiuni:
vector <int> v;
for (int i=0;i<10;i++)
v.push_back(i);
stack <int, vector <int> > stv4(v);
va avea următorul efect: stiva stv4 va conŃine elementele aflate în
vectorul v. În vârful stivei va fi valoarea 9.

C. OperaŃii implementate pentru variabile de tip stack

• atribuire. Aceasta este permisă numai pentru stive care


conŃin elemente de acelaşi fel. Pentru exemplele anterioare,
putem scrie în program instrucŃiunea:
sti1 = sti2;
prin aceasta se face atât alocarea de memorie pentru
elementele stivei din st1 cât şi atribuirile corespunzătoare. În
schimb atribuirile
sts3 = sti2;
sau
sti1 = stv4;
sunt eronate.

• comparări cu operatorii ==, !=, >, >= etc. Şi acestea sunt


permise numai pentru stive care conŃin elemente de acelaşi
fel. Compararea se face dpdv. lexicografic.

87
D. FuncŃii conŃinute de o variabilă de tip stack

Oricare dintre aceste funcŃii se apelează cu sintaxe de genul:


variabila_stiva.nume_funcŃie(parametri)

size()
Returnează numărul de elemente din stivă (valoare de tip unsigned)

empty()
Testează dacă stiva este vidă. Returnează 1 sau 0.

push(val)
Inserează un element care va conŃine valoarea val în vârful stivei.

pop()
Elimină elementul din vârful stivei.

top()
Returnează valoarea elementului din vârful stivei.

1.6.5. AplicaŃie a structurii de date de tip stivă: forma


fără paranteze a unei expresii matematice (forma
poloneză)

Această notaŃie a fost introdusă de matematicianul polonez J.


Lukasiewicz în 1926 [12]. Algoritmul de transformare a unei expresii cu
paranteze în forma poloneză stă la baza evaluării de către calculator a
expresiilor aritmetice care apar în programe. Forma poloneză are două
variante: forma prefixată şi forma postfixată.

A. Forma poloneză prefixată, notată în continuare cu fppre, poate fi


obŃinută dintr-o expresie aritmetică, prin aplicarea următoarelor reguli:

1. Dacă expresia este formată dintr-un singur operand, fppre


coincide cu acel operand.

2. Pentru o expresie de forma


E=a op b, avem
fppre(E) = op a b.

De exemplu, pentru câteva expresii simple din limbajul C, avem:

88
Expresie C Forma poloneză prefixată
a+b +ab
a-b -ab
a*b *ab
a/b /ab
a%b %ab

3. Pentru o expresie cu paranteze de forma E=(E1), forma


poloneză prefixată asociată lui E coincide cu forma poloneză
prefixată asociată expresiei E1 (perechea de paranteze
exterioare nu contează):
fppre(E) ≡ fppre(E1)

4. Pentru o expresie compusă de forma E = E1 op E2, unde E1


şi E2 sunt expresii oarecare, avem relaŃia
fppre(E1 op E2) = op fppre(E1) fppre(E2) .

B. Forma poloneză postixată, notată cu fppost, poate fi obŃinută


dintr-o expresie aritmetică, prin aplicarea următoarelor reguli:

1. Dacă expresia este formată dintr-un singur operand, fppost


coincide cu acel operand.

2. Pentru o expresie de forma


E=a op b, avem
fppost(E) = a b op.
De exemplu, pentru câteva expresii simple din limbajul C, avem:

Expresie C Forma poloneză postixată


a+b ab+
a-b ab-
a*b ab*
a/b ab/
a%b ab%

3. Pentru o expresie cu paranteze de forma E=(E1), forma


poloneză postixată asociată lui E coincide cu forma poloneză
postixată asociată expresiei E1 (perechea de paranteze
exterioare nu contează):
fppost (E) ≡ fppost(E1)

89
4. Pentru o expresie compusă de forma E=E1 op E2, unde E1 şi
E2 sunt expresii oarecare, avem relaŃia
fppost (E1 op E2)= fppost(E1) fppost(E2) op

Remarcăm că ordinea în care se efectuează operaŃiile dintr-o expresie


poloneză este dată de ordinea în care simbolurile acestor operaŃii
(operatorii) apar în expresie.

C. Algoritmul de transformare a unei expresii aritmetice cu


paranteze în expresia poloneză postixată

Vom presupune că expresia este formată din următoarele elemente:


• operanzi (pentru o simplificare a algoritmului vom
presupune ca aceştia sunt forma i dintr-o singura literă);
• operatorii
• paranteze rotunde închise sau deschise.
Vom mai presupune că expresia este corectă dpdv matematic!

Algoritmul este unul de parsare şi foloseşte următoarele structuri de


date:
• un string (sau un şir de caractere) pentru expresia cu
paranteze;
• o stivă de caractere;
• un string (sau un şir de caractere) în care vom construi
expresia poloneză.
Paşii algoritmului sunt următorii:

90
1) Parcurgem în ordine toate caracterele expresiei.
Fie expr[i] caracterul curent
a. dacă expr[i] este operand, aceasta se
adaugă la forma poloneză;
b. dacă expr[i] este ( aceasta se pune pe
stivă
c. dacă expr[i] este ) atunci se scoate câte
un caracter de pe stivă şi se adaugă la
forma poloneză până când în varful stivei
apare ( . Aceasta se elimină din stivă
(dar evident nu se adaugă în forma
poloneză).
d.dacă expr[i] este operator atunci
d1.acesta se pune pe stivă dacă
i. stiva este goală;
ii. în vârful stivei este o (
iii. în vârful stivei este un
operator cu prioritate mai
mică;
d2.în caz contrar (deci când în vârful
stivei este un operator cu prioritate
mai mare sau egală) acesta este scos de
pe stivă şi adăugat la forma poloneză,
după care se reia pasul d.
2) După ce am parcurs expresia iniŃială,
eventualele caractere rămase pe stivă sunt
succesiv scoase de pe stivă şi adăugate în
forma poloneză.

Ca o simplificare a algoritmului, putem elimina pasul 2) dacă iniŃial


vom încadra expresia dată între paranteze rotunde.
Vom ilustra comportarea algoritmului pentru două expresii
simple. În tabelele următoare, parcurgerea expresiei iniŃiale este
ilustrată prin sublinierea caracterului curent. Coloanele „Stiva” şi
„Poloneză” (pentru forma poloneză), ilustrează actualizarea conŃinutului
celor două structuri corespunzător caracterului curent (iniŃial acestea
sunt vide). Prin vfS am notat vârful stivei.

91
Expresie Stiva Poloneză ObservaŃii
a+b*c
a+b*c a
a+b*c +←←vfS a
a+b*c +←←vfS ab
a+b*c *←←vfS ab Pasul d1, cazul iii
+
a+b*c *←←vfS abc
+
+←←vfS abc* Golirea stivei – pasul 2) din
algoritm
abc*+ Continuă golirea stivei

Expresie Stiva Poloneză


a*(b+c)/d
a*(b+c)/d *←←vfS a
a*(b+c)/d *←←vfS a
a*(b+c)/d (←←vfS a
*
a*(b+c)/d (←←vfS ab
*
a*(b+c)/d +←←vfS ab Pasul d1, cazul ii
(
*
a*(b+c)/d +←←vfS abc
(
*
a*(b+c)/d * abc+ Se goleşte sitva până la întâlnirea
parantezei deschise
a*(b+c)/d / abc+* Ramura d2 din algoritm
a*(b+c)/d / abc+*d
abc+*d/ Golirea stivei – pasul 2) din
algoritm
Programul următor implementează algoritmul descris mai sus. Pentru
simplitate, programul este scris pentru expresii care conŃin doar
adunări şi înmulŃiri. Completarea cu secvenŃele pentru tratarea şi a
altor operaŃii (scăderi, împărŃiri, ridicări la putere etc.) este însă uşor de
realizat odată ce s-a înŃeles programul iniŃial. Pentru comoditate am
folosit containerul stack. Expresia iniŃială este dată în variabila expr
de tip string. Forma poloneză va fi determinată tot într-o variabilă de
tip string denumită polo.

92
//program 1.13 determinarea formei poloneze
//postfixată
#include <iostream>
#include <stack>
#include <string>
using namespace std;
stack <char> st;
string expr, polo;
int main()
{ expr = "a+b/(c-d*(x+y-z)+t)";
expr = '('+expr +')'; //incadrare intre paranteze
for (int i=0;i<expr.size(); i++)
{ if (expr[i]>='a'&&expr[i]<='z')
polo += expr[i];
if (expr[i] == '(' )
st.push('(');
if (expr[i] == ')' )
{
while (st.top() != '(' )
{ polo += st.top();
st.pop();
}
st.pop();
}
if (expr[i] == '+'||expr[i] == '-')
if (st.empty()||st.top() == '(' )
st.push(expr[i]);
else
{ polo += st.top();
st.pop();
i--;
}
if (expr[i] == '*'||expr[i] == '/')
if(st.empty()||st.top()=='('
||st.top()=='+'||st.top()=='-' )
st.push(expr[i]);
else
{ polo += st.top();
st.pop();
i--;
}
}
cout<<"forma poloneza corespunzatoare expresiei\n";
cout << expr << " este "<< polo << endl;
return 0;}
93
Rezultatul programul afişat în fereastra consolă este:

forma poloneza corespunzatoare expresiei


(a+b/(c-d*(x+y-z)+t)) este abcdxy+z-*-t+/+

94
1.7 Structura de date de tip coadă

1.7.1 GeneralităŃi

Coada reprezintă o structură de date în care informaŃia este


accesată după principiul primul_venit = primul_plecat (First In First
Out - FIFO). Aceasta se realizează prin faptul că dacă ne imaginăm
această structură având două capete, inserarea unui element se face
la unul dintre capete iar ştergerea la celalalt capăt.

Principalele caracteristici ale unei cozi sunt următoarele:

• este omogenă (toate elementele sunt de acelaşi tip);


• poate fi statică sau dinamică (aceasta depinde de modalitatea
de implementare aleasă);
• accesul la elementele cozii este limitat la primul şi la ultimul
element intrate în structură;

Putem implementa o coadă bazându-ne pe o structură de date de


uz general, de exemplu un tablou sau o listă simplă sau dublă.
Indiferent de metoda aleasă, trebuie implementate operaŃiile specifice
pentru o coadă. Acestea sunt:
• inserarea unui element – operaŃie denumită tradiŃional push;
• eliminarea unui element - operaŃie denumită tradiŃional pop;
• testarea dacă coada este goală sau conŃine elemente;
• determinarea valorii elementului aflat în prima poziŃie din coadă;
Pe lângă acestea, se pot implementa şi alte câteva operaŃii:
• determinarea valorii elementului aflat în ultima poziŃie din coadă;
• determinarea numărului de elemente din coadă;
• golirea completă a cozii;
• iniŃializarea cozii;
• determinarea numărului maxim de elemente pe care le poate
reŃine coada.

Este important ca aceste operaŃii (sau cel puŃin cele din primul grup) să
fie de complexitate O(1).

95
1.7.2. Implementarea unei cozi folosind structura de
date de tip tablou unidimensional

Vom implementa coada ca o structură generică care va conŃine:


• tabloul unidimensional pentru reŃinerea valorilor inserate în
coadă;
• începutul şi sfârşitul cozii– două date (valori) de tip int care vor
reprezenta poziŃia din tablou de unde urmează să fie eliminat un
element şi respectiv poziŃia unde urmează să fie inserat un nou
element;
• nouă funcŃii care implementează operaŃiile enumerate mai sus.
În exemplul care urmează, numărul maxim de elemente din coadă este
stabilit la 10000 printr-o declaraŃie de tip #define.

//program 1.14 coada implementata cu tablou static


#include <iostream>
#include <string>
#define nmax 10000
using namespace std;

template <class tipg> struct coada


{tipg c[nmax]; // elementele cozii
int prim, ultim; //primul,ultimul element din coada
void initializare()
{ prim = ultim = 0; }
void push(tipg val)
{ if (ultim == nmax)
{ cout << "coada e plina! \n";
return ;
}
c[ultim] = val;
ultim++;
}
bool egoala()
{ if (ultim == prim) return true;
else return false;
}
void pop()
{ if (egoala ())
{ cout << "coada e goala!\n";
return ;
}
prim++;
}
96
tipg fata()
{ if (!egoala()) return c[prim];
else cout << "coada e goala!\n";
}
tipg spate()
{ if (!egoala()) return c[ultim-1];
else cout << "coada e goala!\n";
}

int nrelem()
{ return ultim - prim; }

void goleste()
{ prim = ultim = 0; }

int capacitatemax()
{ return nmax; }
};
int main()
{ coada<int> q1;
coada <string> q2;
q1.initializare();
cout << "coada 1 este goala (1=da):";
cout << q1.egoala() <<"\n";
cout << "numarul maxim de elemente din coada =";
cout << q1.capacitatemax() << "\n";
for (int i=1;i<=10;i++)
q1.push(10+i);
cout << "coada contine " <<q1.nrelem();
cout <<" elemente "<<"\n";
cout << "in prima pozitie a cozii este ";
cout << q1.fata()<<"\n";
cout << "in ultima pozitie a cozii este ";
cout <<q1.spate()<<"\n";
q1.pop();
cout << " coada contine " <<q1.nrelem();
cout << " elemente "<<"\n";
cout << "in prima pozitie a cozii este ";
cout << q1.fata()<<"\n";
cout << "in ultima pozitie a cozii este ";
cout << q1.spate()<<"\n";
cout << "golirea si afişarea cozii:" << endl;
while (!q1.egoala())
{ cout << q1.fata() << ' ';
q1.pop();
97
}
cout << '\n';
string cuvant;
q2.initializare();
cout << "Tastati 4 cuvinte...";
for (int i=1;i<=4;i++)
{ cin >> cuvant;
q2.push(cuvant);
}
cout << "numele in ordinea citirii\n";
while (!q2.egoala())
{ cout << q2.fata() << ' ';
q2.pop();
}
return 0;
}

Rezultatul programul afişat în fereastra consolă este:


coada 1 este goala (1=da):1
numarul maxim de elemente din coada =10000
coada contine 10 elemente
in prima pozitie a cozii este 11
in ultima pozitie a cozii este 20
coada contine 9 elemente
in prima pozitie a cozii este 12
in ultima pozitie a cozii este 20
golirea si afişarea cozii:
12 13 14 15 16 17 18 19 20
Tastati 4 cuvinte...numele in ordinea citirii
ana are mere galbene

1.7.3. Implementarea unei cozi folosind structura de


date de listă înlănŃuită

Vom implementa coada într-un mod asemănător cu modul de


implementare de la coadă dar cu următoarea diferenŃă: tipul struct
pentru elementele listei care vor reŃine informaŃiile inserate în coadă va
fi declarat înafara tipului struct prin care vom defini coada propriu-
zisă. Acest tip struct va conŃine doi pointeri către primul şi ultimul
element din lista pe baza căreia construim coada, precum şi funcŃiile
care implementează operaŃiunile specifice cozii.
OperaŃiunile de inserare în coadă este cea de inserare a unui element
în faŃa primului element dintr-o listă. OperaŃiunea de eliminare din
98
coadă este cea de adăugare a unui element la sfârşitul cozii.
Complexitatea O(1) pentru operaŃiile fundamentale este asigurată de
reŃinerea în permanenŃă a adreselor primului şi ultimului element din
coadă în cei doi pointeri.

//program 1.15 coada implementata cu lista liniara


#include <iostream>
#include <string>
using namespace std;
template <class tipinfo> struct element
{ tipinfo info;
element * urmt;
};
template <class tipinfo> struct coada
{ element <tipinfo> *prim, *ultim;
void initializare()
{ prim = ultim = NULL; }
void push(tipinfo val)
{ element <tipinfo> *p=NULL;
p = new element <tipinfo>;
if (p == NULL)
{ cout << "spatiu memorie insuficient\n";
return;
}
p->info = val;
p->urmt = NULL;
if (ultim != NULL) // lista nevida
{ ultim->urmt = p;
ultim = p;
}
else // lista vida
ultim=prim = p;
}
bool egoala()
{ if (prim == NULL) return true;
else return false;
}
void pop()
{ if (egoala())
{ cout << "coada e goala!\n";
return;
}
element <tipinfo> *p = prim;
prim = prim->urmt;
delete p;
99
}
tipinfo fata()
{ if (!egoala()) return prim->info;
else cout << "coada e goala \n";
}
tipinfo spate()
{ if (!egoala()) return ultim->info;
else cout << "coada e goala \n";
}
int nrelem()
{ int ct=0;
element <tipinfo> *p;
for (p=prim; p!=NULL; p=p->urmt)
{
ct++;
}
return ct;
}
void clear()
{ while (!egoala())
pop();
}
};
int main()
{ coada <int> q1;
coada <string> q2;
q1.initializare();
cout << "coada 1 este goala (1=da):";
cout << q1.egoala() <<"\n";
for (int i=1;i<=10;i++)
q1.push(10+i);
cout << "coada contine " <<q1.nrelem();
cout <<" elemente "<<"\n";
cout << "in prima pozitie a cozii este ";
cout << q1.fata()<<"\n";
cout << "in ultima pozitie a cozii este ";
cout << q1.spate()<<"\n";
q1.pop();
cout << "dupa o operatiune pop" << endl;
cout << "coada contine " <<q1.nrelem();
cout << " elemente "<<"\n";
cout << "in prima pozitie a cozii este ";
cout << q1.fata()<<"\n";
cout << "in ultima pozitie a cozii este ";
cout << q1.spate()<<"\n";
100
cout << "golirea si afişarea cozii:" << endl;
while (!q1.egoala())
{ cout << q1.fata() << ' ';
q1.pop();
}
cout << '\n';
string nume;
q2.initializare();
for (int i=1;i<=4;i++)
{ cin >> nume;
q2.push(nume);
}
cout << "numele in ordinea citirii\n";
while (!q2.egoala())
{ cout << q2.fata() << ' ';
q2.pop();
}
return 0;
}
Rezultatele afişate de acest program în fereastra consolă sunt aceleaşi
cu cele afişate de programul anterior.

1.7.4. Containerul queue

Datorită numeroaselor aplicaŃii în care este utilă folosirea cozilor,


în ansamblul de biblioteci STL a fost creată şi o bibliotecă care
implementează containerul de tip coadă denumit queue.

A. Prezentarea generală a variabilelor de tip queue

Implicit variabilele de tip queue sunt implementate având la


bază cozi cu două capete (denumite şi deque de la acronimul expresiei
double-ended queue). Prin modul de declarare, se poate stabili ca la
baza variabilei de tip coadă să stea un alt container (un vector sau o
listă).
Spre deosebire de implementările anterioare în care am fi putut
accesa oricare element din tabloul sau lista simplă care stau la baza
cozilor implementate, variabilele de tip queue nu oferă decât accesul la
funcŃiile specifice unei cozi.

101
B. Utilizarea variabilelor de tip queue

În primul rând trebuie inclusă biblioteca corespunzătoare din STL


denumită queue:
#include <queue>

Apoi se declară o variabilă list cu sintaxa:


queue <tip_elemente> nume_variabilă;

Variabila de tip coadă va avea numele din declaraŃie anterioară iar


informaŃiile pe care le vom putea insera în această coadă vor avea tipul
precizat între parantezele unghiulare. Coada astfel declarată va fi vidă.
De exemplu:
queue <int> qi1, qi2;
//două variabile qi1 şi qi2 care conŃin fiecare o
//coadă cu valori int

queue <string> qs3;


//o variabilă qs3 care conŃine o coadă de şiruri de
//caractere
queue <int> A[100];
//o variabilă A care este un tablou de 100 de cozi

Există şi alte posibilităŃi de declarare, de exemplu:

queue<tip_elm,tip_containr>var_coada(var_containr);

Prin această declarare, variabila de tip coada declarată va


conŃine elementele containerului precizat între parantezele unghiulare.
tip_elem şi tipul elementelor din container trebuie să fie compatibile
dpdv al atribuirii.
De exemplu secvenŃa de declaraŃii şi instrucŃiuni:
vector <int> v;
for (int i=0;i<=8;i++)
v.push_back(i);
queue <int, vector <int> > qv4(v);
va avea următorul efect: coada qv4 va conŃine elementele introduse în
vectorul v. În prima poziŃie din coadă va fi valoarea 0 iar în ultima
valoarea 8.

102
C. OperaŃii implementate pentru variabile de tip queue

• atribuire. Acesta este permise numai pentru cozi care conŃin


elemente de acelaşi fel. Pentru exemplele anterioare, putem
scrie în program instrucŃiunea:
qi1 = qi2;
prin aceasta se face atât alocarea de memorie pentru
elementele cozii din qi1 cât şi atribuirile corespunzătoare.
În schimb atribuirile
qs3 = qi2;
sau
qi1 = qv4;
sunt eronate.

• comparări cu operatorii ==, !=, >, >= etc. Şi acestea sunt


permise numai pentru cozi care conŃin elemente de acelaşi
fel. Compararea se face dpdv lexicografic.

D. FuncŃii conŃinute de o variabilă de tip queue

Oricare dintre aceste funcŃii se apelează cu sintaxe de genul:


variabila_coadă.nume_funcŃie(parametri)

size()
Returnează numărul de elemente din stivă (valoare de tip unsigned)

empty()
Testează dacă stiva este vidă. Returnează 1 dacă e goală sau 0 dacă
nu e goală.

push(val)
Inserează în coadă un element care va conŃine valoarea val.

pop()
Elimină un element din coadă (conform principiului FIFO).

front()
Returnează valoarea elementului din faŃa cozii.

back()
Returnează valoarea ultimului element din coadă.

103
1.7.5. AplicaŃii ale structurii de tip coadă

A. Sortarea radix (sortarea cu găleŃi)

Această metodă de sortare se poate aplica pentru a sorta un şir


de valori preferabil naturale, eventual întregi. Pentru a uşura
înŃelegerea metodei vom prezenta algoritmul pentru un şir de valori
naturale fiecare dintre ele având cel mult trei cifre în reprezentarea în
baza 10. Vom analiza apoi modul în care metoda poate fi extinsă
pentru numere de mai multe cifre.
Metoda se bazează pe un număr de cozi egal cu numărul de
cifre în care vom considera că sunt prezentate numere din şir. Dacă
considerăm numerele în baza 10, atunci vom utiliza 10 cozi pe care le
vom nota q0, q1,...,q9.
Vom considera că şirul pe care vrem să îl sortăm crescător este
reŃinut într-un tablou notat în continuare tab.
Algoritmul este următorul:

1. Parcurgem tabloul de valori. Fiecare element al


tabloului este introdus în coada cu numărul
egal cu ultima cifră (cifra unităŃilor) a
respectivului element. De exemplu dacă numărul
are cifra unităŃilor 0 va fi introdus în coada
notată q0, dacă cifra unităŃilor e 1 va fi
introdus în coada q1 etc.
2. Reconstruim tabloul tab astfel: la început vor
fi plasate valorile extrase din coada q0 prin
golirea acesteia, urmate de cele extrase din
coada q1, apoi cele extrase din q2 etc.
3. Parcurgem din nou tabloul de valori. Fiecare
element al tabloului este introdus în coada cu
numărul egal cu cifra zecilor a respectivului
element. De exemplu dacă numărul are cifra
zecilor 0 va fi introdus în coada notată q0,
dacă cifra zecilor e 1 va fi introdus în coada
q1 etc.
4. Reconstruim tabloul tab astfel: la început vor
fi plasate valorile extrase din coada q0 prin
golirea acesteia, urmate de cele extrase din
coada q1, apoi cele extrase din q2 etc.
5. Parcurgem din nou tabloul de valori. Fiecare
element al tabloului este introdus în coada cu

104
numărul egal cu prima cifră (cifra sutelor) a
respectivului element.
6. Reconstruim tabloul tab în acelaşi mod ca la
paşii 2 sau 4. După refacerea tabloului prin
extragerea tuturor valorilor din cele zece
cozi, tabloul va fi sortat crescător.

Vom ilustra comportarea algoritmului pe următorul exemplu:


tab = 287, 548, 21, 7, 519, 238, 1, 727, 241,
80, 539.

După pasul 1 conŃinutul cozilor va fi următorul:


q0: 80
q1: 21, 1, 241
q7: 287, 7, 727
q8: 548, 238
q9: 519, 539
Celelalte cinci cozi vor fi goale.

După pasul 2 conŃinutul tabloului va fi următorul:


tab 80 21,1,241 287,7,727 548,238 519,539
provenienŃă q0 q1 q7 q8 q9

După pasul 3 conŃinutul cozilor va fi următorul:


q0: 1, 7
q1: 519
q2: 21, 727
q3: 238, 539
q4: 241, 548
q8: 80, 287
Celelalte patru cozi vor fi goale.

După pasul 4 conŃinutul tabloului va fi următorul:


tab 1,7 519 21,727 238,539 241,548 80,287
provenienŃă q0 q1 q2 q3 q4 q8

După pasul 5 conŃinutul cozilor va fi următorul:


q0: 1, 7, 21, 80
q2: 238, 241, 287
q5: 519, 539, 548
q7: 727
Celelalte şase cozi vor fi goale.

După pasul 6 conŃinutul tabloului va fi următorul:


105
tab 1,7,21,80 238,241,287 519,539,548 727
provenienŃă q0 q2 q5 q7
Observăm că elementele din tablou sunt sortate crescător.

Dacă numerele din şir au mai mult de trei cifre se repetă paşii de
mai sus luând în considerare cifrele de la rangurile următoare (cifrele
miilor, zecilor de mii etc.).
Dacă şirul de valori are n elemente, atunci complexitatea
algoritmului va fi O(n), dar numărul de paşi este direct proporŃional
cu numărul maxim de cifre pe care îl au valorile din şir şi cu timpul
necesar accesării unei cifre dintr-un număr (în baza 10 acest acces se
face prin împărŃiri).
Există diverse variante mai rapide ale acestui algoritm. De
exemplu, putem considera numere din şir ca fiind date într-o bază
superioară, de exemplu 10000. Aceasta va presupune utilizarea a
10000 de cozi, dar numărul de distribuiri a numerelor în cozi urmată de
refacerea şirului cu valorile extrase din cozi se va reduce de patru ori.
Faptul că se utilizează de 10000 de ori mai multe cozi nu afectează
mărimea zonei de memorie ocupată dacă se utilizează cozi
implementate pe baza unei structuri de date dinamice.

B. Algoritmul Lee

A fost descris pentru prima dată de C. Y. Lee [11]. Mai este


cunoscut şi ca algoritmul de explorare a vecinătăŃilor sau al propagării
valurilor. El se aplică în probleme în care trebuie determinat un drum
de lungime minimă într-o configuraŃie dată (un labirint, o schemă pentru
circuit integrat) între unul sau mai multe puncte de plecare şi unul sau
mai multe puncte finale.
Într-o formă simplificată algoritmul este următorul:

1. Selectăm punctul de plecare şi îl marcăm cu 1.


2. i = 1
3. repetă // propagarea valurilor
a. marchează cu valoarea i+1 toŃi vecinii
nemarcaŃi care sunt accesibili dintr-un
punct marcat cu valoarea i
b. i = i+1
până când am atins punctul final sau nu mai
există puncte care pot fi marcate
//determinarea rutei
4. punctul curent = punctul de plecare
106
5. repetă
a. determinăm un punct marcat cu o valoare
mai mică cu 1 decât marcajul punctului
curent
b. adăugăm acest punct rutei
c. mutăm punctul curent în acest punct
până când ajungem în punctul de plecare.

Vom dezvolta algoritmul pentru a rezolva o problemă de tip labirint.


Se presupune că se dau:
- un labirint sub forma unei matrice conŃinând valorile 0 care
corespund poziŃiilor libere şi 1 corespunzătoare obstacolelor.
- o poziŃie de intrare în acest labirint
- o poziŃie de ieşire
Mai presupunem că avem un pion/o piesă/etc. care poate fi mutată prin
matrice după o anumită regulă – de exemplu sus/jos/stânga/dreapta cu
o poziŃie.
Trebuie să determinăm un drum pe care urmându-l, piesa ajunge de la
intrare la ieşire printr-un număr minim de mutări.
Vom presupune că există cel puŃin un drum de la intrare la ieşire. Dacă
această condiŃie nu este îndeplinită, algoritmul oferă informaŃiile
necesare pentru a determina acest fapt.
Structurile de date prin care vom implementa algoritmul sunt
următoarele:
• două matrice:
o matrice notată A pentru reprezentarea labirintului;
o o matrice notată M în care vom reŃine marcaje
conform algoritmului descris mai înainte;
completarea acestei matrice este scopul
algoritmului!
• o coadă notată Q în care vom reŃine poziŃii din labirint
Algoritmul propus este următorul:

107
1. Citim datele de intrare (preferabil dintr-un
fişier text)
2. Marcăm toate elementele din M cu o valoare
arbitrară VA
3. Bordăm matricea A cu 1 (poziŃii ocupate)
4. Inserăm poziŃia de plecare în Q
5. M[poziŃia de plecare] = 1
6. Repetă
a. extragem în PC poziŃia curentă din Q
b. pentru fiecare poziŃie PV vecină cu PC
i. dacă A[PV]==0 şi M[PV]== VA atunci
1. i1. M[PV] = M[PC]+1
2. i2. Inserăm PV în Q
3. sfârşit_dacă
ii. sfârşit_pentru
7. până când Q este vidă sau PV este poziŃia
finală
Determinarea drumului de lungime minimă se face conform
algoritmului prezentat la începutul subcapitolului.
Observăm că nu este neapărată nevoie de două matrice – am
putea folosi o singură matrice în care să atribuim elementelor care
corespund poziŃiilor cu obstacole o valoare convenabilă, de exemplu
valoarea negativă -1, iar elementelor corespunzătoare poziŃiilor libere o
valoare arbitrară convenabilă evident diferită de -1.
Există şi alte variante ale algoritmului – de exemplu poziŃiile pe
unde trece pionul conŃin anumite valori şi trebuie găsită o rută de cost
minim sau maxim.
Complexitatea algoritmului este O(n) unde prin n am notat numărul
de poziŃii din labirint (egal cu numărul de elemente din matricea care
descrie labirintul).

108
1.8. Cozi cu două capete (cozi duble)

1.8.1 GeneralităŃi; containerul <deque>

Cozile cu două capete (denumite uzual deque – acronim de la


denumirea în engleză: double_ended_queue) sunt containere la care
inserarea şi extragerea de elemente se poate face la ambele capete.
Principalele caracteristici ale unei cozi duble sunt următoarele:
• este omogenă (toate elementele sunt de acelaşi tip);
• poate fi dinamică sau statică
• oferă acces la oricare element
FuncŃional o coadă dublă este asemănătoare cu un container de tip
vector. Totuşi există câteva diferenŃe:
• operaŃiunile de inserare şi eliminare a unui element au
complexitate O(1) indiferent la care dintre cele două capete
sunt efectuate;
• inserarea unui element nu necesită realocarea de memorie
specifică containerului vector.
• elementele unui deque nu ocupă o zonă contiguă de memorie.
Aceasta presupune un mecanism mai complicat care să permită
accesul la oricare element.
• operaŃiunile de inserare şi eliminare efectuate în alte poziŃii
decât capetele cozii duble sunt mai ineficiente decât în cazul
listelor înlănŃuite.

1.8.2 Utilizarea variabilelor de tip deque

În primul rând trebuie inclusă biblioteca corespunzătoare din STL


denumită deque:
#include <deque>

Apoi se declară o variabilă deque cu sintaxa:


deque <tip_elemente> nume;

109
Variabila va avea numele din declaraŃie anterioară iar elementele
tabloului conŃinut de coada dublă vor avea tipul dintre parantezele
unghiulare. Cozile duble conŃinute de aceste variabile vor fi vide (nu
are alocată memorie pentru niciun element).

De exemplu:
deque <int> dq1;
//o variabilă dq1 care conŃine o coadă dublă de
//valori int

deque <double> dqa,dqb;


//două variabile dqa şi dqb care conŃin fiecare câte
//coadă dublă de valori reale

deque <int> A[100];


//o variabilă A care este un tablou de 100 de cozi
//duble practic o matrice în care liniile au
//dimensiuni variabile şi diferite una de cealaltă.

Există şi alte posibilităŃi de declarare, de exemplu:


deque <tip_elemente> nume(nre,val);

Prin această sintaxă, variabila declarată va conŃine nre


elemente de tip tip_elemente toate având valoarea val.
De exemplu:
deque <int> dq2(10,100);
//variabila dq2 care conŃine o coadă dublă cu 10
//valori întregi egale cu 100.

Cum aminteam mai înainte, o caracteristică importantă a unei


cozi duble este că elementele conŃinute pot fi accesate cu indici. Pentru
exemplele de mai înainte, sintaxele pe care le vom putea utiliza sunt:
dq1[20] sau A[3][20]

110
1.8.3. OperaŃii implementate pentru variabile de tip
deque

• atribuiri: acestea sunt permise numai pentru cozi duble care


conŃine elemente de acelaşi fel. Pentru exemplele anterioare,
putem scrie în program instrucŃiunea:
dq1 = dq2;
prin aceasta se face atât alocarea de memorie pentru coada
dublă din dq1 cât şi atribuirile corespunzătoare. În schimb
atribuirea
dqa = dq2;
este eronată.

• comparări cu operatorii ==, !=, >, >= etc. Şi acestea sunt


permise numai pentru cozi duble care conŃine elemente de
acelaşi fel. Compararea se face dpdv lexicografic.

1.8.4. FuncŃii conŃinute de o variabilă de tip deque

Oricare dintre aceste funcŃii se apelează cu sintaxe de genul:


variabila_deque.nume_funcŃie(parametri)

Prezentăm (ca şi în cazul altor containere) doar câteva dintre funcŃiile


disponibile.

begin()
Returnează un pointer la primul element din deque.

end()
Returnează un pointer la sfârşitul cozii duble (adresa de după ultimul
element din deque)

rbegin()
Returnează un pointer la ultimul element din deque.

rend()
Returnează un pointer la marcajul de sfârşit de listă aflat la „capătul din
stânga” al cozii duble (util pentru parcurgerea „dreapta-stânga”).

size()
Returnează numărul de elemente din deque (valoare de tip
unsigned)

111
max_size()
Returnează numărul maxim de elemente pe care le-ar putea avea
coada dublă.

empty()
Testează dacă coada dublă este vidă

push_back(val)
Adaugă un element care va conŃine valoarea val la sfârşitul tabloului.

pop_back()
Elimină ultimul element din tablou.

push_front (val)
Adaugă un element care va conŃine valoarea val la începutul cozii
duble.

pop_front()
Elimină primul element din coada dublă.

front()
Returnează valoarea din primul element din coada dublă.

back()
Returnează valoarea ultimului element al cozii duble.

112
1.9 Probleme propuse

Problema 1: ScrieŃi ce valori afişează pe ecran următorul program:


#include <iostream>
#include <vector>
using namespace std;
void f(vector <int> v)
{ for (int i=0;i<v.size();i++)
cout << v[i] << ' ';
cout << endl;
}
int main()
{ vector <int> v1,v2(4,7);
v1 = v2;
cout << v1.size() << endl;
f(v1);
v1.pop_back();
v1.insert(v1.begin()+1,2,3);
f(v1);
}

Problema 2: ScrieŃi ce valori afişează pe ecran următorul program:


#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
vector <int> v1,v2,v3(11,0);
int main()
{ for (int i=1;i<=6;i++) v1.push_back(i);
for (int i=1;i<=6;i++) v2.push_back(2*i);
v1.pop_back();
merge(v1.begin(),v1.end(),v2.begin(),v2.end(),
v3.begin());
for (int i=0;i<v3.size();i++)
cout << v3[i] << " ";
cout << endl;
cout << find(v3.begin(),v3.end(),8) - v3.begin();
}

113
Problema 3: Ce afişează instrucŃiunile notate cu 1 şi 2 din programul
alăturat?
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{ int a1[20], n=10, x=20;
for (int i=0;i<n;i++)
a1[i] = i;
fill(a1, a1+4, x);
/*1*/for (int i=0;i<n;i++) cout << a1[i] << ' ';
cout << endl;
sort(a1+2, a1+n-2);
/*2*/for (int i=0;i<n;i++) cout << a1[i] << ' '; }

Problema 4: ScrieŃi o funcŃie având doi parametri: n=un număr natural


(2≤n≤100) si X=un tablou unidimensional care conŃine n numere
întregi. FuncŃia va elimina din X toate valorile pare şi va furniza prin n
numărul de elemente rămas în tablou.

Problema 5: ScrieŃi o funcŃie având un parametru V de tip vector de


numere întregi. FuncŃia va elimina din V toate valorile impare şi va
furniza tot prin V vectorul rezultat în urma acestei prelucrări.

Problema 6: ScrieŃi ce afişează pe ecran următorul program:


#include <iostream>
#include <string>
using namespace std;
string cuv = "examen";
int main()
{ cout << cuv.find('a')<<endl;
for (int i=0; i<cuv.size(); i++)
cout << cuv.substr(i)<<endl;
}

Problema 7: Se consideră programul alăturat:


#include <iostream>
#include <string>
using namespace std;
string s,v= "aeiou";
int main()
114
{ int i;
getline(cin,s);
for (i=0;i<s.size();i++)
if (v.find(s[i]) < v.length())
s.erase(i,1);
cout << s << ' ' << s.length();
}
Ce va afişa acest program dacă pentru variabila de tip string s se
citesc următoarele şiruri:
a) "abecedar"
b) "copiii dau teza"

Problema 8: Fişierul text studenti.in conŃine pe prima linie un


număr natural n ( 3 ≤ n ≤ 100) iar pe fiecare dintre următoarele n linii,
separate prin câte un spaŃiu, un nume format numai din literele
alfabetului englez şi o valoare numerică întreagă reprezentând nota
finală a studentului.
ScrieŃi un program în care să rezolvaŃi următoarele cerinŃe:
• DeclaraŃi un tip struct denumit STUDENT care să permită
reprezentarea celor două informaŃii;
• DeclaraŃi o structură de date care să permită reŃinerea
informaŃiilor din fişierul studenti.in;
• AfişaŃi în ordine alfabetică a numelor informaŃiile din fişierul
text dat. Pentru aceasta declaraŃi o funcŃie de comparare astfel
încât ordonarea alfabetică să poată fi efectuată prin apelul
funcŃiei sort.

Problema 9: Fişierul cuvinte.in conŃine un număr oarecare de


cuvinte (cel mult un milion) dispuse pe cel puŃin o linie. Între două
cuvinte aflate pe aceeaşi linie se află cel puŃin un spaŃiu. ScrieŃi un
program care afişează pe ecran cuvintele din fişierul cuvinte.in în
ordinea crescătoare a lungimii lor. Cuvintele care au aceeaşi lungime
vor apare în ordine alfabetică.

Problema 10: ScrieŃi un program în limbajul C/C++ care citeşte de la


tastatură un singur şir format din cel mult 20 de caractere care
reprezintă numele şi prenumele unei persoane. Între nume şi prenume
se află un număr oarecare de caractere spaŃiu (cel puŃin unul). Atât
numele cât şi prenumele sunt formate numai din litere ale alfabetului
englez. Programul construieşte în memorie şi afişează pe ecran un al

115
doilea şir de caractere, care să conŃină prenumele, urmat de exact un
spaŃiu şi apoi numele din şirul citit iniŃial.
Exemplu: dacă se citeşte şirul:
Popescu Vasile
se va construi şi apoi se va afişa pe ecran şirul
Vasile Popescu

Problema 11: ScrieŃi un program în limbajul C/C++ care citeşte de la


tastatură un singur şir, format din cel mult 20 de caractere, care
reprezintă numele şi un singur prenume al unei persoane. Între nume şi
prenume se află un număr oarecare de caractere spaŃiu (cel puŃin
unul). Atât numele, cât şi prenumele, sunt formate numai din litere mici
ale alfabetului englez. Programul construieşte în memorie şi afişează
pe ecran un alt şir de caractere, care să conŃină iniŃiala prenumelui
(prima literă a prenumelui), urmată de un caracter punct, de exact un
spaŃiu şi de numele din şirul citit iniŃial. Toate literele din şirul afişat vor
fi, de asemenea, litere mici.
Exemplu: dacă se citeşte şirul:
popescu vasile
se va construi şi apoi se va afişa pe ecran şirul
v. popescu

Problema 12: Ce afişează instrucŃiunile notate cu /*1*/ şi /*2*/ din


programul alăturat?
#include <list>
#include <iostream>
using namespace std;
bool f1(int x)
{ if (x > 5 ) return false;
else return true;
}
void f2(list<int> L)
{ list<int>::iterator it;
for (it=L.begin();it!=L.end();it++)
cout << *it << " ";
cout << endl;
}
int main()
{ list <int> L1; int i,n=10;
for (i=1; i<=n; i++)
if (i%2==0) L1.push_back(i);
else L1.push_front(i);
116
/*1*/ f2(L1);
L1.remove_if(f1);
/*2*/ f2(L1);
}

Problema 13: ScrieŃi ce valori afişează pe ecran programul alăturat:


#include <iostream>
#include <list>
using namespace std;
void F(list<int>L)
{ list<int> :: iterator it;
for (it=L.begin();it!=L.end();it++)
cout << *it << ' ';
cout << endl;}
int main()
{ list <int> L1(3,6),L2(5,1);
cout << (L1 > L2) << endl;
L1.insert(L1.begin(),L2.begin(),L2.end());
F(L1);
L1.unique();
F(L1);
}

Problema 14: ScrieŃi ce valori afişează pe ecran următorul program,


dacă pentru n se citeşte valoarea 12:
#include <iostream>
#include <queue>
#include <stack>
queue <int> q1,q2; stack <int> s1; int i,n;
int main()
{ cin >> n;
for (i=1; i<=n; i++) q1.push(i);
while (!q1.empty())
{ if (q1.front()%3 == 0) s1.push(q1.front());
else q2.push(q1.front());
q1.pop();
}
cout << q1.size() <<' '<< q2.size()<<' ';
cout << s1.size()<<endl;
while (!q2.empty())
{ cout << q2.front()<<' ';
q2.pop();
}
117
cout << endl;
while (!s1.empty())
{ cout << s1.top()<<' ';
s1.pop();
}
}

Problema 15: ScrieŃi o funcŃie care are ca parametru adresa primului


element dintr-o listă înlănŃuită dublă sau un parametru de tip list.
Tipul elementelor listei poate fi oarecare (preferabil să îl declaraŃi
generic). Se ştie că lista transmisă ca parametru are cel puŃin trei
elemente. Dacă lista are un număr par de elemente, funcŃia va elimina
din listă cele două elemente de la mijlocul listei. Dacă lista are un
număr impar de elemente, funcŃia va elimina din listă elementul aflat la
mijlocul listei. De exemplu, dacă lista iniŃială conŃine valorile numerice
întregi
10 20 30 40 50 60
după aplicarea funcŃiei lista va conŃine valorile
10 20 50 60
Dacă lista iniŃială conŃine valorile numerice întregi
10 20 30 40 50
după aplicarea funcŃiei lista va conŃine valorile
10 20 40 50

Problema 16: ScrieŃi ce valori afişează pe ecran următorul program,


dacă pentru n se citeşte valoarea 8:
#include <stack>
#include <queue>
#include <iostream>
using namespace std;
queue <int> q1,q2; stack <int> s1; int i,n;
int main()
{ cin >> n;
for (i=1; i<=n; i++)
{ q1.push(i);
s1.push(i);
}
while (!q1.empty())
{ q2.push(q1.front()); q1.pop();
q2.push(s1.top()); s1.pop();
}
cout << q1.size() <<' '<< q2.size()<<' ';
cout << s1.size()<<endl;

118
while (!q2.empty())
{ cout << q2.front()<<' ';
q2.pop();
}
}

Problema 17: ScrieŃi expresia poloneză postfixată corespunzătoare


expresiilor de mai jos:
1: a + (b-x)
2: a + (g*b + d - (x*y-e) - c*d/e ) + f

Problema 18: Ce valoare au expresiile următoare date în forma


poloneză postfixată ştiind că a=1,b=7,c=3,d=2,e=1 ?
1: ab+c-
2: abc+de*-*ae*-

Problema 19: CompletaŃi programul 1.13 prin care se determină


forma poloneză a unei expresii, presupunând că în expresia iniŃială
apar şi următorii operatori:
% cu semnificaŃia din limbajul C;
^ simbolizând ridicarea la putere.

Problema 20: ConcepeŃi un algoritm pentru transformarea în formă


poloneză postfixată a unei expresii matematice în care apar operatorii
aritmetici uzuali, paranteze rotunde şi paranteze pătrate.

Problema 21: Următoarele valori sunt sortate cu algoritmul Radix


Sort prezentat în secŃiunea 1.7.5.A:
71 318 29 5 4 11 197 619 59 1 327 151 413
58 17 25
ScrieŃi care sunt valorile care vor intra în coada notată q1 în descrierea
din respectiva secŃiune, în ordinea dată de algoritm.

Problema 22: ImplementaŃi algoritmul Radix Sort considerând


numerele scrise in baza 10000, utilizând deci 10000 cozi dinamice.

Problema 23: Fişierul text numere.in conŃine pe prima linie două


numere naturale n şi k separate de un spaŃiu (3≤ ≤n≤
≤1000000,
2≤≤k≤
≤n), iar pe a doua linie un şir de n numere naturale x1, x2, ..., xn
separate prin câte un spaŃiu, fiecare număr din acest şir având cel mult
trei cifre. ScrieŃi un program care citeşte numerele din fişier şi
determină cel mai mic indice i pentru care suma secvenŃei de k

119
numere din şir care începe din poziŃia i, (deci suma numerelor xi, xi+1,
..., xi+k-1) este maximă. Programul va afişa pe ecran valoarea sumei
maxime şi poziŃia din şir unde începe respectiva secvenŃă.
Exemplu: dacă fişierul numere.in conŃine valorile
8 3
2 9 4 7 5 2 9 9
programul va afişa valorile
20 2 (suma maximă a trei numere aflate în poziŃii alăturate din şir se
obŃine pentru numerele 9, 4, 7). ConcepeŃi pentru rezolvarea acestei
probleme un algoritm cu complexitatea O(n).

Problema 23: ProiectaŃi o structură de date prin care să se poată


reprezenta eficient matricele rare. Acestea sunt matrice în care
numărul de valori nenule este mult mai mic în comparaŃie cu numărul
de valori nule. Rezultă că o reprezentare adecvată a acestor matrice
presupune să reŃinem numai informaŃiile referitoare la elementele
nenule care apar. Astfel, sub forma unor triplete, pentru fiecare element
nenul vom reŃine poziŃia unde acesta apare şi valoarea lui.
ImplementaŃi sub forma unor funcŃii:
- citirea de valori pentru o matrice rară;
- adunarea şi înmulŃirea pentru matricele rare reprezentate astfel;
- afişarea acestor matrice în formatul standart (pe linii şi coloane);

120
CAPITOLUL 2.

GRAFURI NEORIENTATE

2.1 Introducere în teoria grafurilor

În multe situaŃii avem de rezolvat diverse probleme pentru


configuraŃii alcătuite dintr-un set de elemente care sunt interconectate
prin legături. Astfel de configuraŃii pot fi reprezentate prin grafuri.
Se consideră că prima lucrare aparŃinând acestui domeniu este
lucrarea „Solutio problematis ad geometriam situs pertinentis”
cunoscută mai ales sub numele „Problema celor şapte poduri din
Konigsberg” [18] scrisă în 1735 de celebrul matematician Leonhard
Euler. Privită iniŃial ca un capitol al matematicii, această teorie s-a
dezvoltat în paralel cu combinatorica; a apărut un set de termeni
specifici şi ca urmare a apariŃiei calculatoarelor şi mai ales a reŃelelor
de calculatoare, un număr impresionant de algoritmi care rezolvă o
multitudine de probleme practice. Chiar noŃiunea de graf a căpătat mai
multe sensuri. Astfel teoria grafurilor a permis rezolvarea unui mare
număr de probleme din domenii diferite: teoria circuitelor electrice,
teoria reŃelelor de transport, teoria informaŃiilor, cibernetică, teoria
mulŃimilor sau alte discipline abstracte. De remarcat este faptul că
discipline foarte variate ajung să utilizeze teoreme analoage: de
exemplu noŃiunea de „matrice de incidenŃă” introdusă de Kirchhof
pentru studiul circuitelor electrice a fost reluată de către Henri Poincoré
în topologie.

121
2.2. DefiniŃia noŃiunii de graf

O definiŃie matematică a unui graf este următoarea:

Se numeşte graf G un ansamblul format din două mulŃimi notate


V şi E, unde V este o mulŃime finita si nevidă de elemente, iar E o
mulŃime de perechi cu elemente distincte din mulŃimea V. Elementele
mulŃimii V se numesc noduri sau vârfuri iar mulŃimea V se mai numeşte
si mulŃimea nodurilor sau vârfurilor. Elementele mulŃimii E se numesc
muchii iar mulŃimea E se numeşte mulŃimea muchiilor grafului. Dacă
perechile mulŃimii E sunt neordonate (adică perechea (a,b) este
identică cu perechea (b,a)) graful este neorientat. Dacă perechile sunt
ordonate, graful este orientat.

Formal, definiŃia unui graf neorientat poate fi scrisă astfel:


V = {x1 , x2 ,..., xn }
G (V , E ) = 
 E= {( a, b) / a, b ∈ V , (a, b) = (b, a )}
iar definiŃia unui graf orientat poate fi scrisă astfel:
V = {x1 , x2 ,..., xn }
G (V , E ) = 
 E = {(a, b) / a, b ∈ V , (a, b) ≠ (b, a )}

O definiŃie intuitivă este următoarea:

Un graf este o mulŃime de obiecte (numite noduri) legate între


ele printr-o mulŃime de muchii. Dacă muchiilor le sunt atribuite direcŃii
graful este orientat. În caz contrar, graful este neorientat. Un graf poate
fi reprezentat geometric ca o mulŃime de puncte legate între ele prin
linii. În general se notează cu n numărul de noduri şi cu m numărul de
muchii.

fig. 2.1: graf neorientat fig.2.2: graf orientat

122
Evident că în aplicaŃiile cu grafuri este util ca nodurile să fie
identificate printr-un număr, literă etc. Aceste notaŃii ale nodurilor se
numesc etichete. Uzual, etichetele sunt numere naturale
consecutive începând cu 1. În toate capitolele care urmează vom
considera că etichetele nodurilor grafurilor respectă această
ipoteză.

123
2.3. NoŃiuni de bază pentru grafuri neorientate

• nod sau vârf este un dintre element al mulŃimii de obiecte V:


• muchie [xi,xj] este o legătură dintre două noduri sau un
element al mulŃimii E;
• noduri adiacente sau noduri vecine sunt două noduri între
care există o muchie;
• muchii incidente sunt două muchii care au un capă (nod)
comun;
• muchie incidentă la un nod este o muchie care are unul dintre
capete în nodul respectiv;
• gradul unui nod x notat d(x) este egal cu numărul de noduri
adiacente cu nodul x;
• nod izolat este un nod cu gradul 0;
• nod terminal (sau nod frunză) este un nod cu gradul 1.

ProprietăŃi ale gradelor nodurilor unui graf


• 0 ≤ d(x) ≤ n-1
• d(1) + d(2) + ... + d(n) = 2× ×m
• Dacă un graf neorientat G are n noduri, n≥
≥3, atunci cel puŃin 2
noduri au grade egale.
• Pentru orice graf neorientat numărul nodurilor de grad impar
este par.

Să exemplificăm câteva dintre noŃiunile definite mai sus pentru


graful din figura 2.3:
• V = {1,2,3,4,5,6,7}, deci n=7;
• m=8 iar
E={(1,2),(2,3),(3,1),(4,3),(3,5),(4,5),(4,6),
(4,1),(1,6)};
• nodurile 3 şi 4 sunt adiacente;
• muchiile (2,3) şi (3,5) sunt incidente;
• d(1)=3, d(2)=2, d(3)=4, d(4)=4, d(5)=2,
d(6)=1
• nodul 7 este izolat;
• nodul 6 este frunză;

124
1 7
2
4 6

3 5
fig. 2.3 graf neorientat

125
2.4. Exemple de grafuri particulare

1) graf complet Kn

Este un graf în care oricare două noduri sunt adiacente.

Proprietate
n × (n − 1)
În graful complet Kn , m = = Cn2
2
Pe baza acestei formule se poate demonstra că numărul total de
n ( n −1)
grafuri neorientate cu n noduri este 2 2

2) graf bipartit

Este un graf în care există o posibilitate de a împărŃi nodurile în


două submulŃimi V1 şi V2 astfel încât să fie îndeplinite următoarele
proprietăŃi:
• V1 ∩ V 2 = Ø
• V1 U V 2 = Ø
• orice nod din V1 este adiacent numai cu noduri din V2 şi
orice nod din V2 este adiacent numai cu noduri din V1
Figura 2.4 ilustrează un astfel de graf:

1
4
2
3

fig. 2.4 graf bipartit

3) graf bipartit complet Kp,q

Este un graf bipartit în care (păstrând notaŃiile de la graful


bipartit):
• V1 conŃine p noduri iar V2 conŃine q noduri;
• fiecare nod din V1 este adiacent cu toate nodurile din V2
şi (evident) fiecare nod din V1 este adiacent cu toate
nodurile din V2.
126
Proprietate
În graful bipartit complet Kp,q , m = pq

4) graf regulat
Este un graf în care toate nodurile au acelaşi grad. Figura 2.5 ilustrază
un astfel de graf:

1
4
2
3

fig. 2.5 graf regulat

5) graf planar
Este un graf care poate fi desenat fără ca pe desen să se
intersecteze muchii.

Teorema 4 (Kuratowski [10])


Un graf este planar dacă şi numai dacă nu conŃine subdiviziuni
ale grafurilor K5 şi K3,3. O subdiviziune a unui graf este un graf
obŃinut prin inserarea de
noduri în interiorul muchiilor.

6) graf complementar al unui graf dat


Un graf complementar al unui graf dat este graful care conŃine
toate muchiile posibile pe care graful dat nu le conŃine.
Dacă graful dat are n noduri şi m muchii, graful complementar are n
n × (n − 1)
noduri şi – m muchii.
2

1 1

2 2
4 4
5
3 5 3

fig. 2.6 graf iniŃial fig. 2.7: graful complementar al grafului


iniŃial

127
7) grafuri izomorfe
Două grafuri sunt izomorfe dacă unul se poate obŃine din celălalt
printr-o renumerotare a nodurilor. Grafurile din figuria 2.8 sunt
izomorfe.
3
1

2 2
4

4 3 1
fig. 2.8 grafuri izomorfe

8) subgraf
Un subgraf al unui graf dat se obŃine eliminând din graful iniŃial
unul sau mai multe noduri şi a toate muchiile incidente acestor noduri.

3 3
1

2 2
4 4

6 5 6 5
fig. 2.9 graf iniŃial fig. 2.10: subgraf al grafului iniŃial
rezultat prin eliminarea nodului 1

9) graf parŃial
Un graf parŃial al unui graf dat se obŃine prin eliminarea din
graful iniŃial a uneia sau mai multor muchii.
3 3
1 1

2 2
4 4

6 5 6 5
fig. 2.11: graf iniŃial fig. 2.12: graf parŃial al grafului iniŃial
rezultat prin eliminarea muchiilor [6,1],
[2,1], [5,6]

128
2.5. LanŃ, ciclu, conexitate

• lanŃ într-un graf neorientat este o succesiune de vârfuri cu


proprietatea că între oricare două noduri alăturate din respectiva
succesiune există o muchie.
• lanŃ simplu este un lanŃ în care muchiile care-l formează sunt
diferite între ele.
• lanŃ multiplu este un lanŃ care nu este simplu;
• lanŃ elementar este un lanŃ în care oricare vârf care îl formează
apare în acel lanŃ o singură dată.
• lanŃ neelementar este un lanŃ în care cel puŃin un vârf apare de
cel puŃin două ori.
• lungimea unui lanŃ este egală cu numărul de muchii care îl
alcătuiesc. Dacă o muchie este parcursă de mai multe ori (în
cazul lanŃurilor multiple, ea se numără de fiecare dată când e
parcursă).

• ciclu într-un graf neorientat este un lanŃ în care primul nod şi


ultimul nod sunt identice. Într-un ciclu muchiile care îl formează
sunt distincte.
• ciclu elementar este un ciclu în care oricare vârf care îl
formează apare o singură dată (cu excepŃia primului care apare
încă odată la sfârşitul succesiunii de vârfuri.);
• ciclu neelementar este un ciclu în care cel puŃin un vârf apare
de cel puŃin două ori.
• lungimea unui ciclu este egală cu numărul de muchii care îl
alcătuiesc.

Folosind figura 2.13 vom exemplifica unele dintre aceste noŃiuni:

2
4 6

3 5
fig. 2.13 graf neorientat
• lanŃ elementar de lungime 3: [1,3,4,6];
• lanŃ neelementar de lungime 4: [4,1,2,4,5[;
• ciclu elementar de lungime 5: [2,3,5,4,1,2];
• ciclu neelementar de lungime 6: [1,2,3,4,5,3,1];
129
Un graf conex este un graf în care există cel puŃin un lanŃ între
oricare două noduri.
Dacă un graf nu este conex, el este format din componente
conexe. O componentă conexă este un subgraf maximal conex al unui
graf dat. Graful din fig. 2.15 are trei componente conexe.

fig. 2.14: graf conex fig. 2.15: graf neconex

Graf eulerian

LanŃ eulerian într-un graf neorientat este un lanŃ simplu care conŃine
toate muchiile grafului. În graful din figura 2.16, lanŃul
[1,2,6,5,4,6,1,3] este eulerian.

3
1

2
4

6 5
fig. 2.16: graf cu lanŃ eulerian

Ciclu eulerian este un ciclu care conŃine toate muchiile grafului.


În graful din figura 2.17 ciclul:
[1,2,3,4,10,6,7,5,11,7,8,9,7,10,1]
este ciclu eulerian.

130
2 8 9
1
3 10 7

4 5
6 11
fig. 2.17: graf cu ciclu eulerian

Un graf eulerian este un graf care conŃine un ciclu eulerian.

Teorema 5
Un graf este eulerian dacă şi numai dacă oricare vârf al său are
gradul par iar toate muchiile lui se află în aceeaşi componentă conexă.
Deci graful poate fi eulerian şi dacă conŃine noduri izolate.

Graf hamiltonian

LanŃ hamiltonian într-un graf neorientat este un lanŃ simplu care


conŃine toate nodurile grafului. În graful din figura 2.18, lanŃul
[3,1,2,6,5,4] este hamiltonian.

3
1

2
4

6 5
fig. 2.18: graf cu lanŃ hamiltonian

Ciclu hamiltonian este un ciclu elementar care trece prin toate


nodurile grafului.
În graful din figura 2.19 ciclul [1,2,6,5,4,3,1] este ciclu
hamiltonian.

131
3
1

2
4

6 5
fig. 2.19: graf cu ciclu hamiltonian

Un graf hamiltonian este un graf care conŃine un ciclu hamiltonian.

Teorema 6
Un graf este hamiltonian dacă este conex şi dacă oricare vârf al
său are gradul cel puŃin egal cu n/2.
De-a lungul timpului, pentru determinarea unui ciclu hamiltonian printr-
un algoritm cu complexitate cât mai bună s-a depus un efort
remarcabil. Evident putem determina un astfel de ciclu generând câte o
permutare a mulŃimii de noduri şi verificând dacă ea corespunde uni
ciclu, dar o astfel de rezolvare are complexitatea O(n!). Richard
Belmann în [1] prezintă o soluŃie de complexitate O(n2×2n).

Graf neorientat de tip arbore

Un graf neorientat este arbore dacă este conex şi aciclic. Figura 2.20
ilustrează un arbore cu 6 noduri.

3
1

2
4

6 5
fig. 2.20: graf neorientat de tip arbore
Pentru acest tip de grafuri există câteva teoreme importante:

1. Orice arbore cu n noduri are n-1 muchii.


2. Orice arbore cu cel puŃin două noduri are cel puŃin două
frunze.
3. În orice arbore există un unic lanŃ între oricare două noduri.

132
4. Orice muchie eliminăm dintr-un arbore, graful care rezultă
este neconex.
5. Orice muchie adăugăm într-un arbore, graful care rezultă va
conŃine un ciclu.
6. Orice graf conex care are n noduri şi n-1 muchii este arbore.
7. Orice graf aciclic care are n noduri şi n-1 muchii este arbore.

Rezultă că arborii sunt structuri optimale: ele asigură conexitate cu un


număr minim de muchii.
Pentru a verifica daca un graf este arbore cel mai convenabil
este sa folosim teorema 6.
Un studiu mult mai amănunŃit a acestor tipuri de grafuri ne
propunem să îl realizăm într-o lucrare viitoare.

Graf neorientat de tip pădure

Un graf neorientat este de tip pădure dacă este neconex, dar fiecare
componentă conexă este un subgraf de tip arbore. Figura 2.21
ilustrează un graf neorientat de tip pădure cu 9 noduri.

1 7
8
2
9 4 6

3 5
fig.2.21 graf neorientat de tip pădure

133
2.6. Metode de reprezentare pentru grafuri neorientate

Aceste metode sunt utile în scrierea de programe care


implementează algoritmi pentru grafuri. În aceste situaŃii, metoda de
reprezentare prin desen nu este practică.

2.6.1 Matricea de adiacenŃă

Se notează de regulă cu A. Pentru un graf cu n noduri, această matrice


are n linii şi n coloane. Elementele ei primesc valori după formula:
0 pentru i = j

ai , j = 1 daca exista muchia [i, j]
0 daca nu exista muchia [i, j]

De exemplu, pentru graful din figura 2.19, matricea de adiacenŃă este


011001
100001
A= 1 001 00
001011
000101
110110
ProprietăŃile matricelor de adiacenŃă:
• diagonala principală conŃine numai valoarea 0;
• este simetrică (faŃă de diagonala principală)
• gradul nodului x este egal cu numărul de valori egale cu 1 din
linia x sau coloana x
• pentru un nod izolat, linia şi coloana corespunzătoare din
matricea de adiacenŃă conŃin numai valoarea 0
• numărul de valori egale cu 1 din matrice este egal cu 2×m;

Complexitatea unor operaŃii uzuale:

OperaŃie Complexitate
Test dacă două noduri sunt adiacente O(1)
Determinarea tuturor nodurilor adiacente cu un nod dat O(n)
Determinarea muchiilor grafului O(n2)

Matricele de adiacenŃă sunt uşor de utilizat în programe, dar folosesc


ineficient memoria.
O structura de date potrivită pentru a reprezenta o matrice de
adiacenŃă o reprezintă tablourile bidimensionale.

134
2.6.2 Liste de adiacenŃă

Pentru fiecare nod se construieşte o listă cu nodurile adiacente


acestuia.
De exemplu, pentru graful din figura 2.19, listele de adiacenŃă sunt
următoarele:
1: 2,6,3
2: 1,6
3: 1,4
4: 3,5,6
5: 6,4
6: 2,1,5,4

ProprietăŃile listelor de adiacenŃă:


• gradul unui nod este egal cu numărul de valori din lista lui;
• numărul total de valori care apar în toate listele este egal cu 2m.

În programe este util ca valorile din liste pot fi sortate, mai ales dacă
configuraŃia grafului nu se modifică pe parcursul programului.

Complexitatea unor operaŃii uzuale:

OperaŃie Complexitate
Test dacă două noduri sunt adiacente O(n) sau O(log n)
dacă listele sunt
sortate
Determinarea tuturor nodurilor adiacente cu O(n)
un nod dat
Determinarea muchiilor grafului O(m)

Listele de adiacenŃă sunt relativ uşor de utilizat în programe. Ele


folosesc memoria mult mai eficient decât matricile de adiacenŃă. Sunt
foarte utile mai ales dacă graful are multe noduri cu grade mici.
Ca structuri de date potrivite pentru a reprezenta listele de adiacenŃă
enumerăm:
• tablou de vectori
• tablou de liste
• cozi cu două capete

135
2.6.3 Lista de muchii

Se construieşte o listă cu muchiile grafului.


De exemplu, pentru graful din figura 19, lista de muchii este
următoarea:

[1,2],[1,6],[1,3],[2,6],[3,4],[5,4],[4,6],[5,6]

ProprietăŃile listei de muchii:


• numărul total de perechi care apare este egal cu m;
• gradul unui nod este egal cu numărul de apariŃii ale nodului
respectiv în perechile din listă.

În programe este util ca valorile din cadrul unei perechi să fie sortate iar
întreaga listă să fie sortată după prima valoare din fiecare pereche iar
la egalitatea valorilor din prima poziŃie după a doua valoare din
pereche.

Complexitatea unor operaŃii uzuale:

OperaŃie Complexitate
Test dacă două noduri sunt adiacente O(m) sau O(log m)
dacă sortăm lista
Determinarea tuturor nodurilor adiacente cu O(m)
un nod dat
Determinarea muchiilor grafului O(m)

Lista de muchii este dificil de utilizat în programe. Ea însă foloseşte


memoria eficient. Din acest motiv, când grafurile sunt date în fişierele
de intrare, ele sunt de regulă date în acastă formă.
Ca structuri de date potrivite pentru a reprezenta listele de adiacenŃă
enumerăm:
• tablou sau vector de structuri cu două câmpuri;
• tablou sau vector de perechi.

Programul 2.1 exemplifică o posibilitate de a construi cele trei forme


de reprezentare pentru un graf dat printr-un fişier text în care pe prima
linie vom avea două numere naturale reprezentând valorile lui n şi m iar
pe următoarele m linii câte două valori reprezentând muchiile grafului.
Structurile de date utilizate pentru cele trei metode de reprezentare
sunt următoarele:

136
• pentru matricea de adiacenŃă, o matrice de valori de tip bool. În
declararea acesteia am presupus că valoarea lui n nu
depăşeşte 100, deci matricea este declarată cu 101 linii şi 101
coloane (în programe nu vom folosi linia şi coloana 0);
• pentru listele de adiacenŃă un tablou de 101 cozi cu două
capete;
• pentru lista de muchii un vector de perechi, în care pentru tipul
perechilor am utilizat un tip structură predefinit (denumit pair).
Acesta este un tip structură în care cele două câmpuri care îl
alcătuiesc au numele first şi second iar tipul acestor câmpuri
poate fi orice tip din C şi se precizează prin paranteze
unghiulare (ca la toate sintaxele care apar la utilizarea tipurilor
generice).
FuncŃiile implementate în program realizează:
• construirea celor trei modalităŃi de reprezentare a grafului pentru
valorile citite dintr-un fişier text al cărui nume este dat prin unul
dintre parametri funcŃiilor respective;
• afişarea fiecărei structuri care reprezintă graful;
• determinarea dacă două noduri date ca parametri sunt
adiacente.

//prog. 2.1 modalitati de reprezentare a grafurilor


#include <iostream>
#include <fstream>
#include <deque>
#include <algorithm>
#include <vector>
#define nmax 101
using namespace std;

bool A[nmax][nmax];
vector <pair <int,int> > LM;
deque <int> LA[nmax];

void citirematricead(bool A[nmax][nmax], int &n, int


&m, char numefisier[])
{ ifstream fin (numefisier);
fin >> n >> m;
for (int i=1;i<=n;i++)
for (int j=1;j<=n;j++)
A[i][j]=0;
for (int i=1;i<=m;i++)
{ int x,y;
fin >> x >> y;
137
A[x][y] = A[y][x] = 1;
}
fin.close();
}

void afisarematricead(bool A[nmax][nmax], int n)


{ for (int i=1;i<=n;i++)
for (int j=1;j<=n;j++)
{
cout << A[i][j]<<' ';
if (j==n)
cout << endl;
}
cout << endl;
}
bool adiacentematricead(bool A[nmax][nmax], int n,
int v1, int v2)
{ if (v1<1 ||v1>n )
return false;
if (v2<1 ||v2>n )
return false;
return A[v1][v2];
}

void citirelistead(deque <int> LA[], int &n, int &m,


char numefisier[])
{ ifstream fin (numefisier);
fin >> n >> m;
for (int i=1;i<=m;i++)
{
int x,y;
fin >> x >> y;
LA[x].push_back(y);
LA[y].push_back(x);
}
for (int i=1;i<=n;i++)
sort(LA[i].begin(), LA[i].end());
fin.close();
}
void afisaredeq(deque <int> q)
{ for (int i=0;i<q.size();i++)
cout << q[i] << ' ';
cout << endl;
}
void afisarelistead(deque <int> LA[], int n)
138
{ for (int i=1;i<=n;i++)
{ cout << i << " : ";
afisaredeq(LA[i]);
}
}

bool adiacentelistead(deque <int> LA[], int &n, int


v1, int v2)
{ if (v1<1 ||v1>n ) return false;
if (v2<1 ||v2>n ) return false;
return binary_search(LA[v1].begin(),
LA[v1].end(), v2);
}

void citirelistam(vector <pair <int,int> > &LM, int


&n, int &m, char numefisier[])
{ ifstream fin (numefisier);
fin >> n >> m;
for (int i=1;i<=m;i++)
{ pair <int,int> aux;
fin >> aux.first >> aux.second;
if (aux.first > aux.second)
swap(aux.first,aux.second);
LM.push_back(aux);
}
fin.close();
}
void afisarelistam(vector <pair <int,int> > LM)
{ int i;
for (i=0;i<LM.size()-1; i++)
cout << LM[i].first << '-' <<
LM[i].second<<" ; ";
cout << LM[i].first << '-' << LM[i].second <<
endl;
}
bool adiacentelistam(vector <pair <int,int> > LM,
int &n, int v1, int v2)
{ if (v1<1 ||v1>n ) return false;
if (v2<1 ||v2>n ) return false;
pair <int,int> aux;
aux.first=v1, aux.second=v2;
return binary_search(LM.begin(), LM.end(), aux);
}
int main()
{ int n,m,v1,v2;
139
cout << "MATRICEA DE ADIACENTA\n";
citirematricead(A,n,m,"graf.in");
afisarematricead(A,n);
cout<<"dati 2 noduri (intre 1 si "<<n << "): ";
cin >> v1 >> v2;
bool rez = adiacentematricead(A,n,v1,v2);
cout << "nodurile "<<v1 << " si " << v2;
if (rez)cout << " sunt adiacente" << endl;
else cout << " nu sunt adiacente" << endl;
cout << "----------------------------------\n";
cout << "LISTELE DE ADIACENTA\n";
citirelistead(LA,n,m,"graf.in");

afisarelistead(LA,n);
cout<< "dati 2 noduri (intre 1 si " << n << "): ";
cin >> v1 >> v2;
bool rez1 = adiacentelistead(LA,n,v1,v2);
cout << "nodurile "<<v1 << " si " << v2;
if (rez1) cout << " sunt adiacente" << endl;
else cout << " nu sunt adiacente" << endl;
cout << endl;
cout << "-------------------------------------\n";
cout << "LISTA DE MUCHII \n";
citirelistam(LM,n,m,"graf.in");
sort(LM.begin(), LM.end());

afisarelistam(LM);
cout<< "dati 2 noduri (intre 1 si " << n << "): ";
cin >> v1 >> v2;
bool rez3 = adiacentelistam(LM,n,v1,v2);
cout << "nodurile "<<v1 << " si " << v2;
if (rez3)
cout << " sunt adiacente" << endl;
else
cout << " nu sunt adiacente" << endl;
}
Dacă în fişierul de intrare introducem datele pentru graful din figura
2.19, programul va afişa în fereastra consolă următoarele:
MATRICEA DE ADIACENTA
0 1 1 0 0 1
1 0 0 0 0 1
1 0 0 1 0 0
0 0 1 0 1 1
0 0 0 1 0 1
1 1 0 1 1 0
140
dati 2 noduri (intre 1 si 6): 2 4
nodurile 2 si 4 nu sunt adiacente
----------------------------------------------
LISTELE DE ADIACENTA
1 : 2 3 6
2 : 1 6
3 : 1 4
4 : 3 5 6
5 : 4 6
6 : 1 2 4 5
dati 2 noduri (intre 1 si 6): 2 5
nodurile 2 si 5 nu sunt adiacente
----------------------------------------------
LISTA DE MUCHII
1-2 ; 1-3 ; 1-6 ; 2-6 ; 3-4 ; 4-5 ; 4-6 ; 5-6
dati 2 noduri (intre 1 si 6): 2 1
nodurile 2 si 1 sunt adiacente

141
2.7 Traversarea grafurilor

Traversarea unui graf, operaŃiune cunoscută şi sub numele de


parcurgere a unui graf reprezintă o vizitare a tuturor nodurilor
accesibile prin aplicarea algoritmul utilizat. Ordinea în care nodurile
sunt vizitate este dată de algoritmul utilizat. Indiferent de modalitatea
de traversare, nodurile care vor fi vizitate au proprietatea că sunt în
aceeaşi componentă conexă cu nodul de start. Astfel, printr-o
traversare putem determina dacă un graf este conex şi în cazul în care
nu este conex putem să îi determinăm componentele conexe.
Există două modalităŃi de traversare:
• în adâncime;
• în lăŃime.
Ambele modalităŃi se bazează pe:
• o metodă de reprezentare a grafului;
• o modalitate de marcare a nodurilor vizitate pentru a evita
intrarea în cicluri infinite;
În descrierea ambelor modalităŃi de parcurgere vom folosi
termenul de „nod părinte” a unui nod vizitat. Nodul x este părinte
pentru nodul y dacă în traversarea grafului în nodul y s-a ajuns
parcurgând muchia [x,y] (adică „venind” din nodul x).

2.7.1 Traversarea în adâncime (DFS)

Această modalitate de traversare se bazează pe următorul algoritm:

1. Parcurgerea începe cu nodul iniŃial,


denumit nodul de start, care este marcat
ca şi vizitat.
2. Se vizitează apoi primul vecin nevizitat
al nodului de start şi se marchează ca
vizitat.
3. Se vizitează în continuare primul vecin
nevizitat al primului vecin al nodului de
start, şi aşa mai departe (cu alte cuvinte
„mergând în adâncime”), până când ajungem
într-un nod care nu mai are vecini
nevizitaŃi.
4. Când ajungem într-un astfel de nod,
revenim la nodul său părinte. Dacă acest
nod mai are vecini nevizitaŃi, alegem

142
primul vecin nevizitat al său şi continuăm
parcurgerea în acelaşi mod. Dacă nici
acest nod nu mai are vecini nevizitaŃi,
revenim în nodul său părinte şi continuăm
în acelaşi mod, până când toate nodurile
accesibile din nodul de start sunt
vizitate.

O observaŃie importantă referitoare la ambele metode de


traversare este următoarea: ordinea în care sunt vizitate nodurile
grafului nu este unică dacă nu stabilim un criteriu referitor la ordinea în
care căutăm vecinii unui nod. Dacă însă stabilim un astfel de criteriu –
de exemplu căutăm vecinii unui nod în ordinea crescătoare a
etichetelor lor (în exemplele noastre a numerelor pe care acestea le
au), ordinea în care sunt vizitate nodurile devine unică.
O altă observaŃie importantă referitoare la traversarea unui graf
(indiferent că este cea în adâncime sau cea în lăŃime) este că în cazul
în care graful este conex, muchiile parcurse în algoritm formează un
arbore (vezi capitolul anterior) denumit „arbore de parcurgere” . Putem
să ataşăm sensuri muchiilor acestui arbore având în vedere sensul în
care le parcurgem în timpul desfăşurării algoritmului de traversare. O
formă echivalentă a acestui arbore este un tablou de predecesori notat
în general cu T în care în T[x] reŃine predecesorul nodului x în
desfăşurarea algoritmului de traversare.

Să aplicăm această modalitate de traversare pentru graful din figura


2.22:

1 7
8
2
9 4 6

3 5
fig.2.22 graf neorientat pentru ilustrarea metodelor
de traversare a grafurilor

Vom vizita vecinii unui nod în ordinea crescătoarelor a numerelor


acestora.
Dacă alegem ca nod de plecare nodul 1, atunci nodurile vor fi vizitate
în următoarea ordine:
1, 2, 3, 5, 6, 7, 4, 9, 8.

143
Arborele de parcurgere rezultat în urma acestei traversări este
următorul:
1 7
8
2
9 4 6

3 5
fig.2.23 arbore de parcurgere la traversarea în
adâncime a grafului din figura 2.22 pornind din nodul 1

Tabloul de predecesori are următoarea configuraŃie:


T[2]=1 T[3]=2 T[4]=7 T[5]=3 T[6]=5 T[7]=6 T[8]=1
T[9]=2 iar pentru nodul de plecare, T[1]=0.

Dacă nu aplicam criteriul de a vizita nodurile în ordinea crescătoare a


acestora, puteam avea şi alte variante, de exemplu:
1, 4, 7, 6, 5, 3, 2, 9, 8
sau
1, 8, 2, 9, 3, 5, 6, 7, 4

Traversarea în adâncime stă la baza rezolvării a numeroase probleme


referitoare la grafuri cum ar fi:
• sortarea topologică;
• determinarea muchiilor critice dintr-un graf (acele muchii pe care
dacă le eliminăm, graful devine neconex);
• determinarea componentelor biconexe dintr-un graf (o
componentă biconexă a unui graf este un subgraf conex
maximal al acestuia care are proprietatea că oricare muchie o
eliminăm respectivul subgraf rămâne conex;
• determinarea muchiilor de întoarcere dintr-un graf: o muchie de
întoarcere este o muchie pe care dacă o parcurgem în timp ce
traversăm un graf în adâncime, se închide un ciclu. Practic,
aceste muchii sunt cele care nu apar în arboprele de
parcurgere! Pentru graful din figura 2.20 considerând
traversarea în adâncime cu plecare din nodul 1 şi cu vizitarea
vecinilor în ordine crescătoare, muchiile [3,1] şi [4,1]sunt
muchii de întoarcere.

144
2.7.2 Traversarea în lăŃime (BFS)

1. Parcurgerea în lăŃime începe cu nodul


iniŃial, denumit şi nod de start. Se
vizitează mai întâi nodul de start şi se
marchează ca vizitat.
2. Se vizitează în ordine toŃi vecinii
nevizitaŃi ai nodului de start.
3. Apoi se vizitează în ordine toŃi vecinii
nevizitaŃi ai vecinilor nodului de start
şi aşa mai departe, până la epuizarea
tuturor nodurilor accesibile din nodul de
start.

Parcurgerea în lăŃime ne permite să determinăm cel mai scurt


lanŃ de la nodul de plecare la fiecare dintre celelalte noduri.
Implementarea acestui algoritm este simplă dacă utilzăm o
structură de date de tip coadă. De fapt remarcăm asemănarea dintre
acest algoritm şi algoritmul Lee.
Pentru graful din figura 2.20 cu nodul de start tot nodul 1 şi căutând
vecinii unui nod tot în ordine crescătoare, obŃinem următoarea ordine
de vizitare a nodurilor grafului:
1, 2, 3, 4, 8, 7, 9, 5, 6.

Complexitatea în timp a algoritmilor de parcurgere în adâncime


şi în lăŃime a unui graf depinde de modalitatea de reprezentare a
acestuia. În cazul reprezentării prin liste de adiacenŃă, complexitatea
este O(m+n). În cazul reprezentării prin matrice de adiacenŃă este
O(n2).
În cele ce urmează prezentăm un program care implementează
cele două modalităŃi de parcurgere. Graful pentru care urmează să
facem traversările vom presupune că este dat în fişierul graf.in în
formatul utilizat şi programul 2.1 (în care am exemplificat modalităŃile
de reprezentare pentru un graf), şi anume:
• pe prima linie n şi m
• pe următoarele m linii câte două valori reprezentând
capetele unei muchii.
Pentru implementarea parcurgerii DFS am utilizat reprezentarea
grafului prin matricea de adiacentă. Pentru BFS am reprezentat graful
prin listele de adiacenŃă. FuncŃiile prin care am construit şi afişat aceste
modalităŃi de reprezentare pentru un graf sunt cele din programul 2.1.
FuncŃiile dfs şi bfs implementează algoritmii descrişi mai înainte.
FuncŃia dfs1 determină muchiile de întoarcere. Ideea care stă la baza

145
determinării acestor muchii este următoarea: presupunem că prin
aplicarea algoritmului de parcurgere în adâncime am ajuns la un
moment dat în nodul x şi că în T[x] avem deja memorat predecesorul
nodului x. Atunci:
• pentru fiecare vecin i nevizitat a lui x apelăm parcurgerea în
adâncime;
• pentru fiecare vecin i care deja a fost vizitat, cu excepŃia
predecesorului lui x, muchia care leagă pe x de i închide un
ciclu, deci este de întoarcere (şi o reŃinem într-un vector de
muchii declarat ca în programul 2.1). Pentru a nu afişa de două
ori fiecare muchie de întoarcere, vom memora doar muchiile în
care i< x.

//programul 2.2 algoritmi de traversare a grafurilor


#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>
#include <deque>
#include <queue>
#define nmax 101
using namespace std;
typedef pair <int,int> muchie;

bool A[nmax][nmax]; //matricea de adiacenta

bool vizitat[nmax];

int T[nmax],T1[nmax]; //pentru memorarea


predecesorilor
int dist[nmax]; //pentru memorarea distantei

vector <muchie> mi; //muchiile de intoarcere


int n,m;
deque <int> LA[nmax];

void citirematricead//vezi programul 2.1


void afisarematricead//vezi programul 2.1
void citirelistead//vezi programul 2.1
void afisaredeq//vezi programul 2.1
void afisarelistead//vezi programul 2.1

void dfs(int x)
{ cout << x << ' ';
146
vizitat[x] = true;
for (int i=1;i<=n;i++)
if (A[x][i]==1 && !vizitat[i])
{ vizitat[i]=true;
T[i]=x;
dfs(i);
}
}

void dfs1(int x)
{ vizitat[x] = true;
for (int i=1;i<=n;i++)
if (A[x][i]==1)
if (!vizitat[i])
{ vizitat[i]=true;
T[i]=x;
dfs1(i);
}
else
{ if (T[x] != i)
if (i<x)
{ muchie aux;
aux.first = i;
aux.second = x;
mi.push_back(aux);
}
}
}

void bfs(int x)
{ queue <int> q;
T1[x]=0;
dist[x]=0;
vizitat[x]=1;
cout << x << ' ';
q.push(x);
while (!q.empty())
{ int v = q.front(); //suntem in nodul v
q.pop();
for (int i=0;i<LA[v].size();i++)
{ int y = LA[v][i];
if (vizitat[y] == false)
{ cout << y << ' ';
T1[y] = v;
dist[y] = dist[v]+1;
147
vizitat[y]=true;
q.push(y);
}
}
}
}
int main()
{ citirematricead(A,n,m,"graf.in");
//afisarematricead(A,n);
cout << "parcurgerea in adancime..."<<endl;
fill(vizitat, vizitat+n+1,false);
dfs(1);
cout << endl<<"vectorul de predecesori afisat in
formatul i-T[i]..."<<endl;
for (int i=1;i<=n;i++)
cout << i << '-'<<T[i]<<" ";
cout << endl << "situatia nodurilor
vizitat/nevizitat" << endl;
for (int i=1;i<=n;i++)
cout << i << '-' << vizitat[i]<<" ";
cout << endl;

citirelistead(LA,n,m,"graf.in");
//afisarelistead(LA,n);
cout << "parcurgerea in latime..."<<endl;
fill(vizitat, vizitat+n+1,false);
bfs(1);
cout << endl<<"vectorul de predecesori afisat in
formatul i-T[i]..."<<endl;
for (int i=1;i<=n;i++)
cout << i << '-'<<T1[i]<<" ";
cout << endl<<"vectorul de distante afisat in
formatul i-d[i]..."<<endl;
for (int i=1;i<=n;i++)
cout << i << '-'<<dist[i]<<" ";

fill(vizitat, vizitat+n+1,false);
dfs1(1);
cout << endl<<"muchiile de intoarcere:" <<endl;
for (int i=0;i<mi.size();i++)
cout << mi[i].first<<'-'<<mi[i].second<<" ";
return 0;
}

148
Programul va afişa în fereastra consolă următoarele (având ca date de
intrare graful din figura 2.22):
parcurgerea in adancime...
1 2 3 5 6 7 4 9 8
vectorul de predecesori afisat in formatul i-T[i]...
1-0 2-1 3-2 4-7 5-3 6-5 7-6 8-1 9-2
situatia nodurilor vizitat/nevizitat
1-1 2-1 3-1 4-1 5-1 6-1 7-1 8-1 9-1
parcurgerea in latime...
1 2 3 4 8 9 5 7 6
vectorul de predecesori afisat in formatul i-T[i]...
1-0 2-1 3-1 4-1 5-3 6-5 7-4 8-1 9-2
vectorul de distante afisat in formatul i-d[i]...
1-0 2-1 3-1 4-1 5-2 6-3 7-2 8-1 9-2
muchiile de intoarcere:
1-3 1-4

În numeroase aplicaŃii se consideră că analizarea unui nod are o


anumită durată. În acest caz, este util să considerăm că pe parcursul
derulării algoritmului de traversare (indiferent că aceata este în
adâncime sau în lăŃime) nodurile grafului se pot afla în una din
următoarele trei stări:
- nod neprelucrat (sau nedescoperit) încă sau nod alb (colorare
propusă de exemplu în [2]);
- nod descoperit (în cursul algoritmului de explorare) sau nod gri (nod
în care s-a ajuns şi pentru care a început căutarea nodurilor adiacente
lui);
- nod închis (sau terminat) sau nod negru (nod pentru care au fost
exploraŃi toŃi vecinii).
Pentru fiecare nod x se pot calcula:
- momentul de timp tg[x] la care a fost colorat în gri (a fost
atins);
- momentul de timp tn[x] la care a fost colorat în negru (a fost
terminat).
Evident că pentru orice nod x, tg[x]< tn[x].
DiferenŃa tn[x]-tg[x] reprezintă intervalul de timp în care un nod
este deschis (este „în curs de prelucrare”).
Programul 2.3 determină momentele de timp când are loc colorarea în
gri şi negru a nodurilor grafului din figura 2.24 la o parcurgere în
adâncime, în ipoteza că durata explorării unui nod care nu are
succesori în algoritmul de traversare este 1.

149
3
6
1
4
7
2 5
fig.2.24 graf pentru exemplificarea algoritmului de determinare
a timpilor de atingere şi terminare a nodurilor unui graf

//program 2.3: traversare dfs cu determinarea


//timpilor de descoperire şi inchidere a nodurilor
#include <iostream>
#include <fstream>
#include <algorithm>
#include <queue>
#define nmax 101
#define alb 0
#define gri 1
#define negru 2
using namespace std;

int culoare[nmax];
int T[nmax]; //pentru memorarea predecesorilor
int tg[nmax], tn[nmax], timp=0;
int n,m;
deque <int> LA[nmax];

void citirelistead//vezi programul 2.1


void afisaredeq//vezi programul 2.1
void afisarelistead//vezi programul 2.1

void dfs(int x)
{ cout << x << ' ';
culoare[x] = gri;
timp++;
tg[x] = timp;
for (int i=0;i<LA[x].size();i++)
{
int vecin = LA[x][i];
if (culoare[vecin]==alb)
{
T[vecin]=x;
dfs(vecin);
150
}
}
timp++;
tn[x] = timp;
culoare[x] = negru;
}

int main()
{citirelistead(LA,n,m,"graf.in");
//afisarelistead(LA,n);
fill(culoare, culoare+n+1, alb);
timp = 0;
cout << "ordinea de vizitare a nodurilor:"<<endl;
for (int x=1;x<=n;x++)
if (culoare[x] == alb)
{
dfs(x);
cout << endl;
}
cout<<"vectorul de predecesori afisat in formatul";
cout<<" i-T[i]:"<<endl;
for (int i=1;i<=n;i++)
cout << i << '-'<<T[i]<<" ";
cout << endl << "timpii de colorare gri/negru";
cout<<" a nodurilor:" << endl;
for (int i=1;i<=n;i++)
cout<<i<<": "<<tg[i]<<"/"<<tn[i]<<endl;
return 0;
}
Pentru graful din figura 2.24 programul va afişa în fereastra consolă
următoarele:
ordinea de vizitare a nodurilor:
1 2 4 3 5
6 7
vectorul de predecesori afisat in formatul i-T[i]:
1-0 2-1 3-4 4-2 5-4 6-0 7-6
timpii de colorare gri/negru a nodurilor:
1: 1/10
2: 2/9
3: 4/5
4: 3/8
5: 6/7
6: 11/14
7: 12/13

151
2.7.3 AplicaŃii ale algoritmilor de traversare

A. Determinarea muchiilor critice dintr-un graf conex

Muchiile critice ale unui graf conex sunt acele muchii pe care
dacă le eliminăm, graful nu mai este conex. Este simplu să observăm
că muchiile critice sunt cele care nu fac parte din niciun ciclu.
Pentru a determina aceste muchii am putea proceda astfel:
• să eliminăm fiecare muchie şi apoi să verificăm dacă
graful parŃial rezultat este conex. Algoritmul este corect,
dar complexitatea lui este mare: O(m×(m+n));
• să reŃinem muchiile care formează arborele de
parcurgere şi apoi să determinăm muchiile care nu apar
printre acestea. Dacă sortăm structura în care reŃinem
muchiile din arborele de parcurgere, atunci complexitatea
algoritmului va fi O(m+n) pentru parcurgere şi
O(m×log2n) pentru căutarea fiecărei muchii din graful
iniŃial în structura cu muchiile arborelui de parcurgere
(acesta va avea n-1 muchii). Cum m+n « m×log2n,
complexitatea algoritmului va fi O(m×log2n);
• să modificăm algoritmul de traversare în adâncime după
cum urmează: reŃinem într-un tablou pentru fiecare nod
„nivelul” acestuia. Acest nivel este 1 pentru nodul de
plecare iar pentru fiecare dintre celelalte noduri se
calculează în funcŃia recursivă care implementează
traversarea în adâncime şi este minimul dintre:
- 1+nivelul nodului predecesor;
- nivelul nodurilor adiacente de care respectivul nod
este legat prin muchii de întoarcere.
Evident muchiile critice pot fi numai cele parcurse prin algoritmul
de traversare. O astfel de muchie este critică dacă nivelul nodului
curent este strict mai mare decât nivelul nodului predecesor.
De exemplu, pentru graful din figura 2.22, dacă pornim
traversarea din nodul 1, atunci:
- nodul 1 are nivelul 1;
- nodul 2 are în urma apelului recursiv nivelul 2 ;
- nodurile 3, 4 şi 5 au iniŃial în urma apelurilor recursive nivelurile
3, 4 respectiv 5, dar când din nodul 5 vom „vedea” nodul 2, atunci
nivelul aceluia devine 2, valoare care va fi atribuită şi nivelului nodurilor
4 şi 3.
- nodul 6 are în urma apelului recursiv nivelul 3.

152
2

1
5 6

3 4
fig.2.25 graf pentru exemplificarea algoritmului de
determinare a muchiile critice

Pentru a putea determina muchiile critice printr-o singură


traversare, funcŃia care implementează traversarea va avea doi
parametri: nodul curent şi nodul predecesor.

//programul 2.4 determinarea muchiilor critice


#include <iostream>
#include <fstream>
#define nmax 101
using namespace std;
bool A[nmax][nmax]; //matricea de adiacenta
bool vizitat[nmax];
int T[nmax]; //pentru memorarea predecesorilor
int NIVEL[nmax];//memoreazy nivelului fiecarui nod
int n,m;
void citirematricead //vezi programul 2.1
void afisarematricead //vezi programul 2.1

int dfs(int x, int tata)


{ NIVEL[x] = NIVEL[tata]+1; //niveltata+1;
vizitat[x] = true;
for (int i=1;i<=n;i++)
if (A[x][i]==1)
if (!vizitat[i])
{ vizitat[i]=true;
T[i]=x;
int nivelfiu = dfs(i,x);
if (nivelfiu < NIVEL[x])
NIVEL[x]= nivelfiu;
}
else
if (T[x] != i) //nodul i a mai fost vizitat
if (NIVEL[i]<NIVEL[x])
153
//deci [i,x] este intoarcere
NIVEL[x] = NIVEL[i];
if (NIVEL[tata]<NIVEL[x])
if (tata != 0)
//pentru a nu afisa muchia fictiva [0,1]
cout << x << ' ' << tata << endl;
return NIVEL[x];
}
int main()
{ citirematricead(A,n,m,"graf.in");
cout << "Muchiile critice din graf..."<<endl;
fill(vizitat, vizitat+n+1,false);
fill(NIVEL,NIVEL+n+1,0);
dfs(1,0);
cout << endl<<"vectorul de predecesori afisat";
cout<< " in formatul i-T[i]..."<<endl;
for (int i=1;i<=n;i++)
cout << i << '-'<<T[i]<<" ";
cout << endl<<"nivelul fiecarui nod in formatul";
cout << nod-nivel" << endl;
for (int i=1;i<=n;i++)
cout << i << '-' << NIVEL[i]<<" ";
return 0;
}

Pentru graful din figura 2.25 programul va afişa în fereastra consolă


următoarele:
Muchiile critice din graf...
6 4
2 1
vectorul de predecesori afisat in formatul i-T[i]...
1-0 2-1 3-2 4-3 5-4 6-4
nivelul fiecarui nod in formatul nod-nivel
1-1 2-2 3-2 4-2 5-2 6-3

B. Determinarea unui ciclu eulerian într-un graf eulerian

Pentru a putea determina un ciclu eulerian într-un graf, în primul


rând trebuie să verificăm dacă graful este eulerian. Pentru aceasta
este foarte util ca atunci când citim datele de intrare (pentru
reprezenta graful) să construim şi un tablou în care în elementul din
poziŃia i să reŃinem gradul nodului i. Apoi:

154
• trebuie să verificăm dacă graful are toate muchiile într-o
singură componentă conexă; aceasta se face printr-o
parcurgere urmată de verificarea dacă există noduri
nevizitate care au grad nenul. Dacă există astfel de noduri
rezultă că graful are muchii în cel puŃin două componente
conexe, deci nu este eulerian!
• Dacă am trecut de această primă verificare să verificăm dacă
toate gradele nodurilor din graf sunt valori pare. Având
tabloul de grade, acest lucru se poate face foarte uşor.
În cazul în care graful este eulerian, determinarea ciclului
eulerian se poate face după următorul algoritm în care utilizăm,
între altele, două liste: una denumită ciclueuler şi cealaltă
ciclucurent.

1. IniŃializează ciclueuler ca listă vidă


2. Determină un nod cu grad nenul
3. Dacă acesta există atunci
a. nodul de start  nodul cu grad nenul
b. ciclucurent  listă vidă
c. Nodul curent  nodul de start
d. Adaugă nodul de start în ciclucurent
e. Repetă
i. Caută i = un vecin al nodului
curent cu gradul nenul
ii. Adaugă nodul i în ciclucurent
iii. Decrementează gradul nodului i cu 2
iv. Nodul curent  i
Cât timp nodul curent ≠ nodul de start
f. Dacă ciclueuler este vidă atunci
i. ciclueuler  ciclucurent
Altfel
ii. Inserează ciclucurent în ciclueuler
în locul nodului de start
g. Reia pasul 3
4. Altfel // nu mai există noduri cu grad nenul
a. ciclueuler conŃine ciclul eulerian
Programul 2.4 implementează acest algoritm. Cele două liste sunt
implementate prin variabile de tip list cu denumirile cceuler şi
respectiv cccrt. Graful este reprezentat prin matricea de adiacenŃă,
ceea ce permite anularea foarte lejeră a muchiilor care au fost deja
parcurse şi introduse în ciclul curent (cccrt). Pentru o mai bună
înŃelegere a modului în care se desfăşoară algoritmul, programul
afişează conŃinutul acestor două liste la fiecare iteraŃie a pasului 3.
Se poate astfel observa modul în care algoritmul inserează ciclul curent
155
în ciclul eulerian. Tabloul viz este utilizat atât în algoritmul de
traversare, cât şi în cel care determină ciclul eulerian. În acest al doilea
caz, în el marcăm vârfurile care apar cel puŃin odată în ciclul în
construcŃie. Aceasta este necesar pentru ca un nou ciclu curent să fie
inserat în ciclul eulerian după un nod care este deja în lista în care
construim ciclul eulerian.
//program 2.5 ciclu eulerian
#include <iostream>
#include <fstream>
#include <list>
#include <vector>
#define nmax 101
using namespace std;
ifstream fin("graf.in");
bool A[nmax][nmax];
bool *viz;
void dfs(int nod, int n)
{ viz[nod]=1;
for (int i=1;i<=n;i++)
if (A[nod][i]==1 && viz[i]==0)
{
viz[i] = 1;
dfs(i,n);
}
}
int main()
{ int n,m;
int nodstart,nodcrt,poz;
list <int> cceuler, cccrt;
list <int> :: iterator it;
fin >> n >> m;
vector <int> grade(n+2,0);
viz = new bool[n+2];
fill(viz,viz+n+2,0);
for(int i=0;i<m;i++)
{ int x,y;
fin >> x >> y;
grade[x]++; grade[y]++;
A[x][y]=A[y][x]=1;
}
dfs(1,n);
for (int i=1;i<=n;i++)
if (viz[i]==0 && grade[i]>0)
{ cout << "Graful nu este eulerian";
return 0;
156
}
bool ok = true,gasit;
for (int i=1;i<=n;i++)
{ if (grade[i]%2 == 1)
ok = false;
}
if (!ok)
{ cout << "graful nu e eulerian";
return 0;
}
fill(viz,viz+n+2,0);
viz[1]=1;
do
{
gasit = 0;
//cautam un nod cu grad nenul
for (nodcrt=1;nodcrt<=n;nodcrt++)
{ if (grade[nodcrt]!=0 && viz[nodcrt]==1)
{ gasit = 1;
nodstart = nodcrt;
cccrt.clear();
cccrt.push_back(nodstart);
do
{
for (int i=1;i<=n;i++)
if (A[nodcrt][i]==1)
{ cccrt.push_back(i);
viz[i]=1;
A[nodcrt][i]=A[i][nodcrt]=0;
grade[nodcrt]=grade[nodcrt]-2;
nodcrt = i;
break;
}
}
while (nodcrt!=nodstart);
if (cceuler.size()==0)
{
cceuler = cccrt;
}
else
{
for (it=cceuler.begin();it!=cceuler.end();it++)
if (*it == nodstart)
{
break;
157
}
cccrt.pop_back();
cceuler.insert(it,cccrt.begin(),cccrt.end());
}
cout << "ciclu curent: ";
for (it=cccrt.begin();it!=cccrt.end();it++)
cout << *it<<' ';
cout << nodstart << endl;
cout << "ciclu euler: ";
for (it=cceuler.begin();it!=cceuler.end();it++)
cout << *it<<' ';
cout << endl<< endl;
break;//reluam cautarea unui nod cu grad nenul
//incepand de la primul
}
}
}
while (gasit);
cout << "un ciclu eulerian in graful dat:\n";
for (it=cceuler.begin();it!=cceuler.end();it++)
cout << *it<<' ';
return 0;
}

Pentru graful din figura 2.17 programul va afişa în fereastra consolă


următoarele:
ciclu curenta: 1 2 3 4 10 1
ciclu euler: 1 2 3 4 10 1

ciclu curenta: 10 6 7 5 11 7 8 9 7 10
ciclu euler: 1 2 3 4 10 6 7 5 11 7 8 9 7 10 1

un ciclu eulerian in graful dat:


1 2 3 4 10 6 7 5 11 7 8 9 7 10 1

158
2.8 Probleme propuse

Problema 1. DesenaŃi un graf neorientat cu 5 vârfuri care să fie


hamiltonian dar să nu fie eulerian.

Problema 2. DesenaŃi un graf neorientat cu 5 vârfuri care să fie


eulerian dar să nu fie hamiltonian.

Problema 3. Lista de muchii pentru un graf neorientat cu 13 noduri


este următoarea:
[1,2], [2,3], [3,4], [4,5], [5,7], [10,7], [7,12],
[12,10], [10,9], [9,3], [9,13], [5,13], [4,11],
[4,8], [8,6] , [4,12].
PrecizaŃi ordinea de vizitare a nodurilor din graf la o traversare în lăŃime
plecând din nodul 12. Vecinii unui nod vor fi vizitaŃi în ordine
crescătoare!

Problema 4. Fie graful complet K5 cu nodurile numerotate începând de


la 1. PrecizaŃi muchiile de întoarcere din acest graf când se face o
parcurgere în adâncime plecând din nodul 5. Se ştie că în parcurgere,
vecinii unui nod vor fi vizitaŃi în ordine crescătoare (de exemplu, dacă
vreŃi să scrieŃi muchia aflată între nodurile 2 şi 4, atunci în răspuns veŃi
scrie [2,4] şi NU [4,2])!

Problema 5. Se considera graful complet cu 10 noduri. Din acesta se


elimina toate muchiile de forma [i,i+1] cu i=1,2,3,...,9 (adică
muchiile [1,2], [2,3],...,[9,10]). PrecizaŃi ce se afişează la o
traversare (parcurgere) în adâncime a subgrafului astfel rezultat
plecând din nodul 4. Vecinii unui nod vor fi vizitaŃi în ordine
crescătoare!

Problema 6. Care dintre următoarele şiruri de numere pot reprezenta


gradele nodurilor unui graf neorientat conex cu 5 noduri.
a. 4 4 4 4 4 d. 1 1 3 3 3
b. 2 2 2 2 0 e. 0 1 2 3 4
c. 3 3 3 3 3 f. 0 1 2 3 4

159
Problema 7. Într-un graf neorientat cu 10 noduri, fiecare nod cu număr
par este adiacent cu toate nodurile cu număr impar. Ce sa va afişa la o
parcurgere în adâncime a acestui graf plecând din nodul 7. Vecinii unui
nod sunt vizitaŃi în ordinea lor crescătoare.

Problema 8. Un graf neorientat cu 6 noduri are următoarele muchii:


[1,2], [2,5], [5,6], [1,3], [3,6], [2,6], [1,6].
Care este matricea de adiacenŃă a grafului complementar grafului cu
muchiile de mai sus?

Problema 9. ScrieŃi o funcŃie cu doi parametri: n număr natural


(2≤n≤100) şi A = matrice pătratică de 100 de linii şi 100 coloane.
FuncŃia va da matricei A valori astfel încât aceasta să reprezinte
matricea de adiacenŃă a unui graf bipartid complet în care una dintre
submulŃimi este formată din nodurile impare iar cealaltă din nodurile
pare.

Problema 10. ScrieŃi o funcŃie cu doi parametri: n număr natural


(2≤n≤100) şi LA = listele de adiacenŃă ale unui graf.
FuncŃia va da parametrului LA valori astfel încât aceasta să reprezinte
matricea de adiacenŃă a unui graf bipartid complet în care una dintre
submulŃimi este formată din nodurile impare iar cealaltă din nodurile
pare. ReprezentaŃi listele de adiacenŃă într-un format la alegere.

Problema 11. ScrieŃi o funcŃie cu patru parametri:


- na număr natural, (2 ≤ na ≤ 100);
- A matricea de adiacenta a unui graf neorientat cu na noduri;
- nb număr natural, (2 ≤ nb ≤ 100);
- B matricea de adiacenta a unui graf neorientat cu nb noduri.
FuncŃia va returna valoarea 1 sau true dacă graful reprezentat prin
matricea de adiacenta A este graf parŃial al grafului reprezentat prin
matricea de adiacenta B şi 0 sau false în caz contrar.

Problema 12. ScrieŃi o funcŃie cu patru parametri:


- ma număr natural, (2 ≤ ma ≤ 100);
- LMA lista de muchii graf neorientat cu ma muchii;
- mb număr natural, (2 ≤ mb ≤ 100);
- LMB lista de muchii graf neorientat cu mb muchii.
160
FuncŃia va returna valoarea 1 sau true dacă graful reprezentat prin
lista de muchii LMA este graf parŃial al grafului reprezentat lista de
muchii LMB şi 0 sau false în caz contrar. ReprezentaŃi lista de muchii
într-un format la alegere.

Problema 13. Fişierul graf.in conŃine:


- pe prima linie trei numere natural n, m şi k, 3≤n≤100, 2≤m≤10000,
2≤k≤100. n şi m reprezintă numărul de noduri şi respectiv numărul de
muchii dintr-un graf conex;
- pe următoarele m linii câte două numere v1 şi v2 1≤v1,v2≤n,
reprezentând câte o muchie a grafului;
- pe ultima linie k numere naturale.
DeterminaŃi dacă cele k numere de pe ultima linie formează un lanŃ în
graful dat.

Problema 14. Fişierul graf.in conŃine:


- pe prima linie două numere natural n şi m, 3≤n≤100, 2≤m≤10000. n
şi m reprezintă numărul de noduri şi respectiv numărul de muchii
dintr-un graf;
- pe următoarele m linii câte două numere v1 şi v2 1≤v1,v2≤n,
reprezentând câte o muchie a grafului;
DeterminaŃi numărul de muchii din fiecare componentă conexă a
grafului dat în fişier.

Problema 15. Fişierul graf.in conŃine:


- pe prima linie două numere natural n şi m, 3≤n≤100, 2≤m≤10000. n
şi m reprezintă numărul de noduri şi respectiv numărul de muchii
dintr-un graf;
- pe următoarele m linii câte două numere v1 şi v2 1≤v1,v2≤n,
reprezentând câte o muchie a grafului;
- pe ultima linie două numere x şi y 1≤x,y≤n, reprezentând două
noduri ale grafului dat.
a. DeterminaŃi toate nodurile aflate pe cel puŃin un drum de lungime
minimă dintre nodurile x şi y.
b. determinaŃi toate nodurile prin care trec toate drumurile de lungime
minimă dintre nodurile x şi y (cerinŃa b. este o problemă propusă de
Victor Manz la Olimpiada JudeŃeană de Informatică 2006 (subiecte
naŃionale)).
De exemplu, dacă fişierul graf.in are următorul conŃinut:

161
9 11
1 2
2 3
2 7
2 9
3 5
9 8
5 4
5 8
4 6
5 7
5 6
1 6
programul va afişa:
- la cerinŃa a. valorile: 1 2 3 5 6 7
- la cerinŃa b. valorile: 1 2 5 6
ExplicaŃii: graful din exemplu este următorul:

3 4
2
1 7 5 6

9 8
fig.2.26 graful din problema 15.

162
CAPITOLUL 3

GRAFURI ORIENTATE

3.1 Introducere

Se nume te graf orientat sau digraf un ansamblul format din


două mulŃimi notate V şi E, unde V este o mulŃime finită şi nevidă de
elemente, iar E o mulŃime de perechi ordonate cu elemente distincte
din mulŃimea V. Elementele mulŃimii V se numesc vârfuri iar mulŃimea V
se mai numeşte şi mulŃimea nodurilor sau vârfurilor. Elementele
mulŃimii E se numesc arce iar mulŃimea E se numeşte mulŃimea arcelor
grafului.
Formal, definiŃia unui graf orientat poate fi scrisă astfel:

V = {x1 , x2 ,..., xn }
G (V , E ) = 
 E = {(a, b) / a, b ∈ V , (a, b) ≠ (b, a )}

O definiŃie intuitivă este următoarea:

Un graf este o mulŃime de obiecte (numite vârfuri) legate între


ele printr-o mulŃime de arce cărora le sunt atribuite direcŃii. În general
se notează cu n numărul de vârfuri şi cu m numărul de arce. Un digraf
poate fi reprezentat geometric ca o mulŃime de puncte legate între ele
prin linii care au sensuri ilustrate prin săgeŃi:

fig.3.1: graf orientat

163
3.2 NoŃiuni de bază

• vârf este un element al mulŃimii de obiecte:


• arc (xi,xj) este o legătură dintre două vârfuri;
• vârfuri adiacente sunt două vârfuri între care există un
arc;
• arce incidente sunt două arce care au un capăt (vârf)
comun;
• arc incident la un vârf este un arc care are unul dintre
capete în vârful respectiv;
• gradul intern al unui vârf x notat d-(x) este egal cu
numărul de arce care „intră” în vârful x;
• gradul extern al unui vârf x notat d+(x) este egal cu
numărul de arce care „pleacă” din vârful x;
• vârf izolat este un vârf cu gradul intern şi gradul extern 0;

ProprietăŃi ale gradelor vârfurilor unui graf orientat cu n vârfuri


• 0 ≤ d-(x) ≤ n-1
• 0 ≤ d+(x) ≤ n-1
• d-(1) + d-(2) +.. + d-(n) = d+(1) + d+(2) +...+
d+(n) = m

164
3.3. Drum, circuit, tare-conexitate

• drum într-un graf orientat este o succesiune de vârfuri vi1,


vi2, vi3, ..., vik cu proprietatea că între oricare două
vârfuri alăturate din respectiva succesiune există un arc, deci
există arcele (vi1,vi2), (vi2,vi3),..., (vik-1,vik).
• drum simplu este un drum în care arcele care-l formează sunt
diferite între ele.
• drum multiplu este un drum care nu este simplu;
• drum elementar este un drum în care oricare vârf care îl
formează apare în acel drum o singură dată.
• drum neelementar este un drum în care cel puŃin un vârf apare
de cel puŃin două ori.
• lungimea unui drum este egală cu numărul de arce care îl
alcătuiesc. Dacă un arc este parcurs de mai multe ori (în cazul
drumurilor multiple, el se numără de fiecare dată când e
parcurs).

• circuit într-un graf orientat este un drum în care primul vârf şi


ultimul vârf sunt identice. Într-un circuit arcele care îl formează
sunt distincte.
• circuit elementar este un circuit în care oricare vârf care îl
formează apare o singură dată (cu excepŃia primului);
• circuit neelementar este un circuit în care cel puŃin un vârf
apare de cel puŃin două ori.
• lungimea unui circuit este egală cu numărul de arce care îl
alcătuiesc.

Un graf tare conex este un graf în care există cel puŃin un drum
între oricare două vârfuri.
Dacă un graf nu este tare conex, el este format din componente
tare conexe. O componentă tare conexă este un subgraf maximal tare
conex al unui graf dat. Graful din figura 3.2 are trei componente conexe
formate din următoarele submulŃimi de vârfuri: {1}, {2,5}, {3,4,6}

165
3
1
2
4

5 6
fig.3.2: Graf orientat cu 3 componente tare conexe

Se poate determina dacă un graf orientat este tare conex utilizând


următorul algoritm:
1. se face o traversare a grafului plecând dintr-
un vârf oarecare x, parcurgând arcele în sens
direct; fie M1 submulŃimea vârfurilor vizitate
prin această traversare;
2. se face o traversare a grafului plecând dintr-
un vârf oarecare x, parcurgând arcele în sens
invers; fie M2 submulŃimea vârfurilor vizitate
prin această traversare;
3. dacă M1∩M2 = {1,2,...,n} atunci
graful este tare conex
altfel
M1∩M2 reprezintă o componentă tare conexă.

166
3.4 Metode de reprezentare a digrafurilor

Sunt similare cu metodele de reprezentare a grafurilor


neorientate. Vom prezenta doar diferenŃele care apar. Formulele pentru
complexitatea operaŃiilor uzuale date la grafurile neorientate sunt
valabile şi în cazul digrafurilor.

3.4.1 Matricea de adiacenŃă

Se notează de regulă cu A. Pentru un graf cu n vârfuri, această matrice


are n linii şi n coloane. Elementele ei primesc valori după formula:
 0 pentru i = j
a i , j = 1 daca exista arcul (i, j)
 0 daca nu exista arcul (i, j)

De exemplu, pentru digraful din figura 2, matricea de adiacenŃă este

0 0 0 0 0 0
1 0 0 1 1 0
A= 0 0 0 1 0 0
0 0 0 0 0 1
0 1 0 1 0 1
0 0 1 0 0 0

ProprietăŃile matricelor de adiacenŃă:


• diagonala principală conŃine numai valoarea 0;
• nu este simetrică fata de diagonala principala
• gradul extern al vârfului x este egal cu numărul de valori egale
cu 1 din linia
• gradul intern al vârfului x este egal cu numărul de valori egale
cu 1 din coloana x
• pentru un vârf izolat, linia şi coloana corespunzătoare din
matricea de adiacenŃă conŃin numai valoarea 0
• numărul de valori egale cu 1 din matrice este egal cu m;

167
3.4.2 Liste de adiacenŃă

Pentru fiecare vârf se construieşte o listă cu vârfurile spre care


„pleacă” arce din respectivul vârf.
De exemplu, pentru graful orientat din figura 3.2, listele de adiacenŃă
sunt următoarele:
1:
2: 1,5,4
3: 4
4: 6
5: 6,4,2
6: 3
ProprietăŃile listelor de adiacenŃă:
• gradul extern al unui vârf este egal cu numărul de valori din lista
lui;
• gradul intern al unui vârf este egal cu numărul de lise în care
apare acel vârf;
• numărul total de valori care apar în toate listele este egal cu m.
Pentru reducerea complexităŃii diverselor operaŃii referitoare la un graf
este indicat să sortăm aceste liste.
Tot pentru reducerea complexităŃii operaŃiilor în programe este utilă
crearea unui al doilea grup de liste de adiacenŃă în care pentru vârful i
vom avea o listă cu vârfurile de la care „vin” arce la i. Pentru graful
orientat din figura 3.2, aceste liste de adiacenŃă sunt următoarele:
1: 2
2: 5
3: 6
4: 2,3,5
5: 2
6: 4,5

168
3.4.3 Lista de arce

Se construieşte o listă cu arcele grafului.


De exemplu, pentru graful din figura 3.2, lista de arce este următoarea:

(2,1), (2,5), (2,4), (6,3), (3,4), (5,4), (5,2),


(5,6), (4,6)

ProprietăŃile listei de arce:


• numărul total de perechi care apare este egal cu m;
• gradul extern al unui vârf este egal cu numărul de apariŃii ale
vârfului respectiv în prima poziŃie din perechile din listă;
• gradul intern al unui vârf este egal cu numărul de apariŃii ale
vârfului respectiv în a doua poziŃie din perechile din listă.

Programul următor aplică cele trei modalităŃi de reprezentare şi


determină componentele tare conexe. Implementarea celor trei
modalităŃi de reprezentare se face astfel:
- matricea de adiacenŃă printr-o matrice cu elemente de tip bool;
- listele de adiacenŃă printr-un tablou de vectori; pentru reducerea
complexităŃii operaŃiei de căutare în aceste liste ele au fost
sortate;
- lista de arce printr-un vector de perechi a câte două valori;
Am presupus că numărul maxim de vârfuri este 100. Pentru alte valori,
se modifică declaraŃia #define. Câteva observaŃii referitoare la acest
program:
- am scris două funcŃii care calculează gradul intern pentru un vârf
pe baza matricei de adiacenŃă (gradintern) şi a listelor de
adiacenŃă (gradinternla); programul le apelează pe ambele
pentru a afişa gradele interne pentru toate vârfurile din digraf;
- funcŃia dfsd implementează traversarea digrafului parcurgând
arcele în sens direct iar funcŃia dfsi implementează traversarea
digrafului parcurgând arcele în sens invers;
- tabloul ctcx este un tablou de marcaje: ctcx[vf] va reŃine
numărul componentei conexe căreia îi aparŃine vârful vf.
- graful este dat în fişierul „graf.in” cu următorul conŃinut:
o pe prima linie n şi m cu semnificaŃiile utilizate şi până
acum;
o pe următoarele m linii câte două valori i j cu
semnificaŃia: există arc de la vârful i la vârful j;

169
//program 3.1 metode de reprezentare a digrafurilor
//determinare componente tare conexe
#include <iostream>
#include <fstream>
#include <deque>
#include <algorithm>
#include <vector>
#define nmax 101
using namespace std;
int n,m;
bool A[nmax][nmax]; //matricea de adiacenta
vector <pair <int,int> > LARCE; //lista de arce
deque <int> LA[nmax]; //listele de adiacenta
int ctcx[nmax];
void citiremat(bool A[nmax][nmax], int &n, int &m,
char numefisier[])
{ ifstream fin (numefisier);
fin >> n >> m;
for (int i=1;i<=n;i++)
for (int j=1;j<=n;j++)
A[i][j]=0;
for (int i=1;i<=m;i++)
{
int x,y;
fin >> x >> y;
A[x][y] = 1;
}
fin.close();
}
void afisaremat(bool A[nmax][nmax], int n)
{ for (int i=1;i<=n;i++)
for (int j=1;j<=n;j++)
{ cout << A[i][j]<<' ';
if (j==n)
cout << endl;
}
}
int gradinternma(bool A[nmax][nmax], int n, int
varf)
{ int s=0,i;
if (varf<1 || varf > n)
{ cout << "varf inexistent" << endl;
return -1;
}
for (i=1;i<=n;i++)
170
s+=A[i][varf];
return s;
}
void citirela(deque <int> LA[], int &n, int &m, char
numefisier[])
{ ifstream fin (numefisier);
fin >> n >> m;
for (int i=1;i<=m;i++)
{ int x,y;
fin >> x >> y;
LA[x].push_back(y);
}
for (int i=1;i<=n;i++)
sort(LA[i].begin(), LA[i].end());
fin.close();
}
void afisaredeq(deque <int> q)
{ for (int i=0;i<q.size();i++)
cout << q[i] << ' ';
cout << endl;
}
int gradinternla(deque <int> LA[], int n, int varf)
{ if (varf<1 || varf > n)
{ cout << "varf inexistent" << endl;
return -1;
}
int s=0;
for (int i=1;i<=n;i++)
s +=
binary_search(LA[i].begin(),LA[i].end(),varf);
return s;
}
bool existaarcla(deque <int> LA[], int &n, int v1,
int v2)
{ if (v1<1 ||v1>n ) return false;
if (v2<1 ||v2>n ) return false;
return
binary_search(LA[v1].begin(),LA[v1].end(),v2);
}
bool existaarcma(bool A[nmax][nmax], int n, int v1,
int v2)
{ if (v1<1 ||v1>n ) return false;
if (v2<1 ||v2>n ) return false;
return A[v1][v2];
}
171
void citirelarce(vector <pair <int,int> > &LM, int
&n, int &m, char numefisier[])
{ ifstream fin (numefisier);
fin >> n >> m;
for (int i=1;i<=m;i++)
{ pair <int,int> aux;
fin >> aux.first >> aux.second;
LM.push_back(aux);
}
fin.close();
}
void afisarelarce(vector <pair <int,int> > LM)
{ int i;
for (i=0;i<LM.size()-1; i++)
cout<< LM[i].first << '-' << LM[i].second<<" ; ";
cout << LM[i].first << '-' << LM[i].second<< endl;
}
bool viz1[nmax], viz2[nmax];
void dfsd(int x)
{ viz1[x] = true;
for (int i=1;i<=n;i++)
if (A[x][i]==1 && viz1[i] == false)
{ viz1[i]=true;
dfsd(i);
}
}
void dfsi(int x)
{ viz2[x] = true;
for (int i=1;i<=n;i++)
if (A[i][x]==1 && viz2[i] == false)
{ viz2[i]=true;
dfsi(i);
}
}
int main()
{ citiremat(A,n,m,"graf.in");
cout << "Matricea de adiacenta\n";
afisaremat(A,n);
cout<< lista gradelor interne din matrice:\n"<<;
for (int k=1;k<=n;k++)
cout<<k<<":"<<gradinternma(A,n,k)<<" ";
cout<<endl;
cout << "\nListe de adiacenta\n";
citirela(LA,n,m,"graf.in");
172
for (int i=1;i<=n;i++)
{ cout << i << " : ";
afisaredeq(LA[i]);
}
cout << endl;
cout << "lista gradelor interne: " << endl;
for (int k=1;k<=n;k++)
cout<<k<<":"<<gradinternla(LA,n,k)<<" ";
cout << endl;
cout << "\nLista arcelor:\n";
citirelarce(LARCE,n,m,"graf.in");
sort(LARCE.begin(), LARCE.end());
afisarelarce(LARCE);
int v1,v2;
cout<<"dati 2 noduri (intre 1 si "<<n<<"): ";
cin >> v1 >> v2;
bool rez = existaarcma(A,n,v1,v2);
if (rez)
cout<<"exista arcul intre cele doua varfuri\n";
else
cout << "nu exista arc intre aceste varfuri\n";
bool rez1 = existaarcla(LA,n,v1,v2);
if (rez1)
cout<<"exista arcul intre cele doua varfuri\n";
else
cout << "nu exista arcul intre aceste varfuri\n";
int k=0;
for (int i=1;i<=n;i++)
{ if (ctcx[i]==0)
{ k++;
cout<<"componenta tare conexa "<<k<<endl;
fill(viz1+1,viz1+n+1,false);
fill(viz2+1,viz2+n+1,false);
dfsd(i);
dfsi(i);
for (int j=1;j<=n;j++)
if (viz1[j] == true && viz2[j]==true)
{ ctcx[j]=k;
cout << j << ' ';
}
cout << endl;
}
}
return 0;
}
173
Pentru digraful din figura 3.2 programul va afişa în fereastra consolă
următoarele:
Matricea de adiacenta
0 0 0 0 0 0
1 0 0 1 1 0
0 0 0 1 0 0
0 0 0 0 0 1
0 1 0 1 0 1
0 0 1 0 0 0
lista gradelor interne din matrice:
1:1 2:1 3:1 4:3 5:1 6:2

Liste de adiacenta
1 :
2 : 1 4 5
3 : 4
4 : 6
5 : 2 4 6
6 : 3
lista gradelor interne:
1:1 2:1 3:1 4:3 5:1 6:2

Lista arcelor:
2-1 ; 2-4 ; 2-5 ; 3-4 ; 4-6 ; 5-2 ; 5-4 ; 5-6 ; 6-3

dati 2 noduri (intre 1 si 6): 4 6


exista arcul intre cele doua varfuri date
exista arcul intre cele doua varfuri date

componenta tare conexa 1


1
componenta tare conexa 2
2 5
componenta tare conexa 3
3 4 6

174
3.5 Drumuri de cost minim în grafuri orientate

În acest capitol vom presupune că se dă un graf orientat în care


fiecare arc are ataşat un cost pozitiv (vezi figura 3.2). Astfel de grafuri
mai sunt denumite grafuri ponderate. Prin drum de cost minim între
două vârfuri x şi y vom înŃelege un drum de la x la y care are
proprietatea că suma costurilor arcelor care îl compun este minimă.
Evident pot exista mai multe drumuri de cost minim.
Problema determinării drumurilor de cost minim se poate pune în mai
multe moduri:
1. Determinarea drumurilor de cost minim de la un vârf (denumit
vârf sursă) la toate celelalte vârfuri; Această variantă a
problemei se numeşte „problema drumurilor de cost minim de
sursă unică”.
2. Determinarea drumurilor de cost minim de la toate vârfurile
grafului la un vârf (denumit vârf destinaŃie). Această problemă se
rezolvă la fel ca prima problemă inversând în prealabil sensurile
arcelor şi păstrând costurile!
3. Determinarea drumurilor de cost minim de la un vârf (denumit
vârf sursă) la un alt vârf denumit vârf destinaŃie. Această
problemă se rezolvă prin varianta 1. Nu se cunosc algoritmi cu
complexitate mai mică decât cei care rezolvă problema
drumurilor de cost minim de sursă unică 1;
4. Determinarea drumurilor de cost minim între oricare pereche de
vârfuri. Evident că această variantă se poate rezolva aplicând
varianta 1 pentru fiecare vârf al grafului. Vom vedea că există un
algoritm dedicat pentru rezolvarea acestei probleme.

4 2
1
9 8 6 9 9
4 2
3 3
1 6
3 1 7
7
5 2
fig.3.3: graf ponderat

MenŃionăm că există algoritmi care determină drumurile de cost minim


şi pentru grafuri orientate în care arcele au costuri negative, cu condiŃia
ca în acele grafuri să nu existe cicluri de cost negativ.

Majoritatea algoritmilor care determină drumuri de cost minim


(între care şi cei doi pe care îi vom prezenta in cele ce urmează) se
175
bazează pe o operaŃie specifică denumită „operaŃia de relaxare a unui
cost”. Aceasta urmăreşte să optimizeze costul drumului determinat la
un moment dat între două vârfuri, i şi j, comparându-l cu costul
drumului de la i la un vârf intermediar k şi de la acel vârf k la vârful j.
Evident dacă suma celor două costuri (de la i la k şi de la k la j este
mai mică decât costul „drumului direct” de la i la j, atunci vom
micşora costul drumului de la i la j „mergând” prin vârful k. Pentru
figura 3.4, notând cu cij costul drumului de la vârful i la vârful j,
operaŃia de relaxare:
dacă cij > cik + ckj atunci
cij  cik + ckj
predecesorul vârfului j  k

j
cij
i ckj
cik k

fig.3.4: relaxarea costului drumului


de la vâtful i la vârful j

3.5.1 Reprezentarea grafurilor ponderate

Reprezentarea grafurilor ponderate constituie „punctul de plecare”


pentru algoritmii pe care îi vom prezenta în continuare. ModalităŃile de
reprezentare sunt asemănătoare cu cele a grafurilor care nu au ataşate
costuri la muchii sau arce.

A) Matricea ponderilor

Vom nota această matrice cu H Dacă notăm cu C(x,y) costul arcului


(x,y), matricea ponderilor are următoarea definiŃie:

0 pentru i = j

hi , j = ∞ daca nu exista arcul (i, j)
C(i, j) daca exista arcul (i, j)

În programe vom folosi pentru ∞ o valoare suficient de mare, dar nu


INT_MAX deoarece variabile având această valoare vor fi implicate în
operaŃii de adunare ceea ce ar determina erori de rulare sau rezultate
176
eronate dacă respectivele variabile ar avea această valoare maximă a
tipului int.
Pentru graful din figura 3.3, matricea ponderilor are următoarea
configuraŃie:

0 469 ∞ ∞
∞ 09 ∞ ∞ 9
∞ ∞ 011 7
H=
8 ∞ 20 3 ∞
∞ ∞ 7 ∞ 0 2
∞ ∞ 3 ∞ ∞ 0

B) Liste de adiacenŃă ponderate


Pentru fiecare vârf vf din graf vom reŃine un şir de perechi de valori;
valorile din fiecare pereche vor reprezenta un vârf spre care există arc
de la vf şi costul ataşat acelui arc. De exemplu, pentru graful din
figura 3.3, aceste liste de adiacenŃă au următoarea configuraŃie:
1: (2,4),(3,6),(4,9)
2: (3,9),(4,9)
3: (4,1),(5,1),(6,7)
4: (1,8),(3,2),(5,3)
5: (3,7),(6,2)
6: (3,3)

3.5.2 Algoritmul Dijkstra

A fost descoperit de Edsger W. Dijkstra în 1956 şi publicat trei ani mai


târziu. [4]
El se aplică pentru rezolvarea problemei drumurilor de cost minim de
sursă unică: determină drumurile de cost minim de la un vârf dat, pe
care îl vom nota s la celelalte vârfuri ale grafului. Algoritmul
construieşte două tablouri d şi T având următoarea semnificaŃie:
• d[i] = valoarea drumului de cost minim de la
sursă (vârful s) la vârful i; d[s]=0.
• T[i] = predecesorul vârfului i pe drumul de
cost minim de la sursă (vârful s) la vârful i;
T[s]=0.

177
Tabloul d ne dă valoarea drumului de cost minim de la vârful sursă la
fiecare dintre celelalte vârfuri ale grafului iar tabloul T ne permite să
determinăm arcele din care sunt formate aceste drumuri.
Algoritmul mai utilizează un tablou v cu semnificaŃia:
• v[i] = true dacă vârful i a fost analizat şi
false în caz contrar.

Vom prezenta o formă a algoritmului care se bazează pe matricea


ponderilor. În algoritm am notat cu ∞ o valoare suficient de mare (vezi
explicaŃiile de la subcapitolul 3.3.1).

1. construieşte H (matricea ponderilor)


2. iniŃializează tabloul d cu ∞
3. iniŃializează tabloul v cu false
4. T[s]  0
5. d[s]  0
6. pentru k  1,n-1 execută
a. determină p astfel încât
d[p] = min (d[i], i=1,2,...,n) şi
v[i]=false
b. v[p]  true
c. pentru i  1,n execută
i. dacă d[i] > d[p] + H[p][i] atunci
d[i]  d[p] + H[p][i]
T[i]  p
sfarşitdacă
sfarşitpentru
sfarşitpentru

Complexitatea algoritmului este O(n2).


Nu este dificil să rescriem algoritmul utilizând listele de adiacenŃă
ponderate. Utilizând minheap-uri Fibonacci, complexitatea algoritmului
poate fi redusă la O(m+n×log n) [6].
Figura 3.5 ilustrează evoluŃia algoritmului pentru graful din figura 3.3.
Câteva precizări:
- pentru tabloul v am notat cu f valoarea false şi cu t valoarea
true;
- notaŃia p este cea din pasul 6.a al algoritmului de mai sus
(vârful care furnizează minimul din tabloul d)

178
p=1
2 4 2
1 1
9 6
4 4
3 6 3 6

5 5
d: 0 oo oo oo oo oo d: 0 4 6 9 oo oo
T: 0 -1 -1 -1 -1 -1 T: 0 1 1 1 -1 -1
v: f f f f f f v: t f f f f f
p=2 p=3
4 2 4 2
1 1
9 6 9 6 9
4 4
3 6 1 3 6
1
5 5
d: 0 4 6 9 oo 13 d: 0 4 6 7 7 13
T: 0 1 1 1 -1 2 T: 0 1 1 3 3 2
v: t t f f f f v: t t t f f f
p=5 p=4
4 4 2
1 1
6 6
4 4
1 3 6 1 3 6
1 1
5 2 5 2

d: 0 4 6 7 7 9 d: 0 4 6 7 7 9
T: 0 1 1 3 3 5 T: 0 1 1 3 3 5
v: t t t f t f v: t t t t t f
Fig. 3.5 Desfăşurarea algoritmului Dijsktra

Programul 3.2 implementează algoritmul Dijsktra.


Graful este dat prin fişierul denumit în program „grafponderat.in”
cu structura:
- pe prima linie n şi m cu semnificaŃiile utilizate şi până acum;
- pe următoarele m linii câte trei valori i j c cu semnificaŃia: arc
de la i la j cu costul c;
//programu 3.2 algoritmul Dijsktra
#include <iostream>
#include <fstream>
#define nmax 101

179
using namespace std;
int h[nmax][nmax],T[nmax],d[nmax];
bool v[nmax];
int s,i,j,k,p,n,m;
int oo = 1000000;
ifstream fin("grafponderat.in");
void afisaretab(int a[], int n)
{for (i=1;i<=n;i++)
{ cout.width(6); cout << a[i];}
cout << endl;
}
int main()
{ fin >> n >> m;
for (i=1;i<=n;i++)
for (j=1;j<=n;j++)
if (i==j)h[i][j] = 0;
else h[i][j] =oo;
for (i=1;i<=m;i++)
{ int x,y,cost;
fin >> x >> y >> cost;
h[x][y] = cost;
}
for (i=1;i<=n;i++)
{ for (j=1;j<=n;j++)
{cout.width(4);
if (h[i][j]==oo) cout <<"oo";
else cout << h[i][j];
}
cout << endl;
}
cout << endl;
fill(v,v+n+1,false);
fill(d,d+n+1,oo);
fill(T,T+n+1,-1);
cout << "sursa = "; cin >> s;
d[s]=0; T[s]=0;
for (k=1;k<=n-1;k++)
{ int minim = oo;
for (i=1;i<=n;i++)
if (d[i] <= minim && v[i]==false)
minim=d[i],p=i;
v[p] = true;
for (i=1;i<=n;i++)
if (d[i] > d[p] + h[p][i])
{ d[i] = d[p] + h[p][i];
180
T[i] = p;
}
}
cout << "tabloul d:"<<endl;
afisaretab(d,n);
cout << "tabloul T:"<<endl;
afisaretab(T,n);
return 0;
}
Pentru digraful din figura 3.3 programul va afişa în fereastra consolă
următoarele:
0 4 6 9 oo oo
oo 0 9 oo oo 9
oo oo 0 1 1 7
8 oo 2 0 3 oo
oo oo 7 oo 0 2
oo oo 3 oo oo 0
sursa = 1
tabloul d:
0 4 6 7 7 9
tabloul T:
0 1 1 3 3 5

3.5.3 Algoritmul Roy-Floyd

Cunoscut şi ca algoritmul Floyd – Warshall, a apărut la sfârşitul


anilor 50 şi începutul anilor 60 ca rezultat al studiilor efectuate de
Robert Floyd, Bernard Roy şi Stephen Warshall.
El se aplică pentru a determina drumurile de cost minim între
oricare două vârfuri ale unui graf. Algoritmul determină două matrice D
şi T în care elementele au următoarea semnificaŃie:
Di,j = costul minim al unui drum de la vârful i la
vârful j
Ti,j = predecesorul vârfului j pe drumul de cost
minim de la vârful i la vârful j

181
Algoritmul este următorul:

1. construieşte matricea ponderilor H


2. D  H
3. pentru i  1,n execută
pentru j  1,n execută
dacă exista arcul(i,j) atunci
T[i][j]  i
sfârşitdacă
sfârşitpentru
sfârşitpentru
4. pentru k  1,n execută
pentru i  1,n execută
pentru j  1,n execută
dacă D[i][j] > D[i][k]+ D[k][j] atunci
//*
D[i][j]  D[i][k]+ D[k][j]
T[i][j]  T[k][j]
sfârşitdacă
sfârşitpentru
sfârşitpentru
sfârşitpentru

Se poate observa că testul din instrucŃiunea dacă


marcată cu * are şanse să fie îndeplinit numai dacă cele trei valori i,j
şi k sunt diferite. Totuşi introducerea acestui test suplimentar nu
micşorează complexitatea algoritmului. Aceasta este O(n3).
Implementarea algoritmului este dată în programul 3.3. FuncŃia
retinedrum determină vârfurile care alcătuiesc drumul de cost minim
între vârfurile date ca parametri: v1 sursa şi v2 destinaŃia. Principiul ei
este unul „clasic”:

1 iniŃializează vârful curent cu vârful


destinaŃie
2 cât timp vârful curent ≠ 0
a. pune vârful curent pe stivă
b. vârful curent  predecesorul vârfului
curent
Utilizăm o stivă pentru a reŃine vârfurile; astfel la ieşirea din funcŃie
putem afişa vârfurile în ordinea inversă faŃă de cum le-a parcurs
algoritmul, deci în ordinea lor firească (de la sursă la destinaŃie).

182
//program 3.3 Algoritmul Roy-Floyd
#include <iostream>
#include <fstream>
#include <stack>
#define nmax 101
using namespace std;
int h[nmax][nmax],T[nmax][nmax],D[nmax][nmax];
int oo = 100000;
void afisardrum(int v1, int v2, stack<int>&st)
{ while (v2 != 0)
{ st.push(v2);
v2 = T[v1][v2];
}
}
void afisarematrice(int a[nmax][nmax], int n)
{for (int i=1;i<=n;i++)
{ for (int j=1;j<=n;j++)
{cout.width(4);
if (a[i][j]==oo) cout <<"oo";
else cout << a[i][j];
}
cout << endl;
}
}
int main()
{ int s,i,j,k,p,n,m;
ifstream fin("ponderi.in");
stack <int> st;
fin >> n >> m;
for (i=1;i<=n;i++)
for (j=1;j<=n;j++)
if (i==j)h[i][j] = 0;
else h[i][j] =oo;
for (i=1;i<=m;i++)
{ int x,y,cost;
fin >> x >> y >> cost;
h[x][y] = cost;
}
//afisarematrice(h,n);
cout << endl;
for (i=1;i<=n;i++)
for (j=1;j<=n;j++)
{ D[i][j] = h[i][j];
if (D[i][j] != oo && i!=j)
T[i][j] = i;
183
}
for (k=1;k<=n;k++)
for (i=1;i<=n;i++)
for (j=1;j<=n;j++)
if (D[i][j] > D[i][k] + D[k][j])
{ D[i][j] = D[i][k] + D[k][j];
T[i][j] = T[k][j];
}
cout << "Matricea D"<<endl;
afisarematrice(D,n);
cout << "Matricea T"<<endl;
afisarematrice(T,n);
cout << "\nDrumul de la varful 5 la varful 1\n";
afisardrum(5,1,st);
while (!st.empty())
{ cout << st.top() << ' ';
st.pop();
}
return 0;
}

Pentru digraful din figura 3.3 programul va afişa în fereastra consolă


următoarele:
Matricea D
0 4 6 7 7 9
17 0 9 9 10 12
9 13 0 1 1 3
8 12 2 0 3 5
14 18 5 6 0 2
12 16 3 4 4 0
Matricea T
0 1 1 3 3 5
4 0 2 2 3 5
4 1 0 3 3 5
4 1 4 0 4 5
4 1 6 3 0 5
4 1 6 3 3 0
Drumul de la varful 5 la varful 1
5 6 3 4 1

Un algoritm asemănător permite determinarea închiderii


tranzitive a unui graf orientat. Închiderea tranzitivă a unui graf G =
(V,E) este un graf G*=(V,E*), unde mulŃimea arcelor E* va conŃine
arcul (i,j) numai dacă în graful G există drum de la vârful i la vârful

184
j. Pentru aceasta se atribuie fiecărui arc din G costul 1 şi în urma
aplicării algoritmului Roy-Floyd se foloseşte matricea D astfel:
dacă D[i][j]<n atunci
există drum de la vârful i la vârful j.

185
3.6 Flux maxim într-o reŃea de transport

3.6.1 DefiniŃii

Fie un graf orientat G = (V,E).


O reŃea de transport este un graf orientat care îndeplineşte următoarele
condiŃii:
• există un singur vârf denumit vârf sursă pe care îl vom cu srs
care are d+(srs)>0 şi d-(srs)=0
• există un singur vârf denumit vârf destinaŃie pe care îl vom cu
dst care are d+(dst)=0 şi d-(dst)>0
• fiecărui arc (i,j) îi este asociată o valoare nenegativă
denumită capacitate pe care o vom nota c(i,j)
• fiecare vârf al grafului diferit de srs şi dst se găseşte pe un
drum care leagă vârful srs de vârful dst
Figura 3.6 reprezintă un graf orientat de tip reŃea de transport.

2 7 3
5 4
3 3 2
1 4 5 9
3 5 9
6 4 6
7 8

fig.3.6: reŃea de transport

Un astfel de graf este adecvat pentru a modela situaŃii din lumea


reală: sursa poate fi o unitate de producŃie iar destinaŃia un loc de
desfacere a produselor fabricate în acea unitate de producŃie.
Drumurile care apar între sursă şi destinaŃie corespund posibilităŃii de
transport a produselor. Vârfurile intermediare ale grafului pot
reprezenta depozite temporare pentru produse iar capacitatea asociată
unui arc poate reprezenta valoarea maximă care poate fi transportată
pe porŃiunea respectivă de drum. Printr-un astfel de graf se pot modela
şi siutaŃii în care avem mai multe unităŃi de producŃie şi mai multe
puncte de desfacere. În acest caz, se adaugă un vârf sursă fictiv de la
care pleacă câte un arc spre fiecare vârf sursă propriu-zis; capacitatea
fiecăruia dintre aceste arce va fi egală cu suma capacităŃilor arcelor
care pleacă din sursa la care este incident. Analog, dacă sunt mai
186
multe destinaŃii, se poate adăuga o un vârf destinaŃie fictiv iar de la
fiecare destinaŃie propriu-zisă se adaugă un arc spre această destinaŃie
fictivă. Capacitatea unui astfel de arc care leagă una dintre destinaŃiile
iniŃiale cu destinaŃia fictivă va fi egală cu suma capacităŃilor de la arcele
care intră în destinaŃie iniŃială.

Un flux într-o reŃea de transport este o funcŃie φ:V×V  R care


îndeplineşte următoarele condiŃii:
• 0 ≤ φ(i,j) ≤c(i,j) (fluxul asociat unui arc nu poate depăşi
capacitatea arcului respectiv;
• ∑ϕ (i, j ) = ∑ϕ ( j, i ) (această condiŃie arată că suma fluxurilor
j∈V j∈V

asociate arcelor care „intră” într-un vârf oarecare i este egală


cu suma fluxurilor asociate arcelor care „ies” din vârful i);

Se defineşte valoarea fluxului dintr-o reŃea de transport ca fiind

Γ = ∑ϕ ( srs, j )
j∈V

adică suma fluxurilor asociate arcelor care pleacă din vârful sursă.
Evident această valoare va fi egală cu suma fluxurilor asociate arcelor
care intră în vârful destinaŃie.
Figura 3.7 ilustrează un flux pentru reŃeaua de transport din fig. 3.6.
Valorile fluxului ataşat fiecărui arc sunt scrise în chenar. Valoarea
fluxului din reŃeaua ilustrată în această figură este 6.

2 7 3
5 1
1 4 1
2 3 2
1 4 5 9
4 1 2
3 1 5 3
4 6 4
6 7 8
1 4
fig.3.7: reŃea de transport

3.6.2 Determinarea fluxului maxim într-o reŃea de trasport

În cele ce urmează vom prezenta algoritmul Ford-Fulkerson [5] pentru


a determina fluxul maxim care poate fi asociat unei reŃele de transport.
Pentru descrierea acestui algoritm vom defini câteva noŃiuni ajutătoare:
187
• drum într-o reŃea de transport este un lanŃ elementar între nodul
sursă şi cel destinaŃie (adică arcele pot fi parcurse atât în sens
direct cât şi în sens invers);
• valoarea reziduală a unui arc este o funcŃie care asociază
fiecărui arc din graf o valoare calculată după formula:

c(i, j ) − ϕ (i, j ) daca arcul (i, j) e parcurs in sens direct


vr (i, j ) = 
ϕ (i, j) daca arcul (i, j) e parcurs in sens invers

• drum în creştere este un drum în reŃeaua de transport în care


valoarea reziduală a fiecărui arc este strict pozitivă.
• capacitatea reziduală a unui drum în creştere notată ε, este
minimul valorilor reziduale ale arcelor care alcătuiesc acel drum.

Cu aceste definiŃii, algoritmul Ford-Fulkerson are următoarea descriere:

1. se asociază reŃelei de transport un flux de


valoare nulă
2. se caută un drum în creştere
3. dacă acesta există şi are capacitatea
reziduală ε atunci
a) se modifică fluxul pe arcele care
alcătuiesc drumul în creştere determinat
astfel:
i. dacă arcul (i,j) este parcurs în sens
direct atunci
φ(i,j) = φ(i,j) + ε
ii. dacă arcul (i,j) este parcurs în sens
invers atunci
φ(i,j) = φ(i,j) – ε
b) se reia pasul 2

 altfel (când nu mai există drum în


creştere) algoritmul se opreşte iar
fluxum maxim este valoarea fluxului
din acest moment (egal cu suma
fluxurilor asociate arcelor care
pleacă din vârful sursă)

Complexitatea acestui algoritm este O(m×F), unde prin F am notat


valoarea maximă a fluxului. Edmonds–Karp [3] au demonstrat că timpul
de rulare a algoritmului este mai scurt şi nu mai depinde de valoarea

188
fluxului dacă se caută la fiecare iteraŃie drumul de lungime minimă.
Pentru aceasta, drumurile în creştere se vor determina printr-un
algoritm foarte asemănător cu cel de traversare în lăŃime. Deosebirea
faŃă de cel prezentat în capitolele anterioare este că în acest caz se va
putea parcurge un arc şi în sens invers dacă fluxul asociat lui la
momentul efectuării parcurgerii este strict pozitiv. Complexitatea
algoritmului în acest caz este O(n×m2). O complexitate și mai bună
este dată de algoritmul de preflux [8] care a condus la algoritmul de flux
maxim prezentat de Goldberg şi Tarjan cu complexitatea O(n2×m).
Să ilustrăm aplicarea algoritmului Edmonds–Karp pentru reŃeaua
de transport din figura 3.6:

2 7 3
5 0
0 4 0
2 3 2
1 4 5 9
0 0 0
3 0 5 0
9 0
6 4 6
7 8
0 0

fig.3.8.a: flux iniŃial nul


Drumul în creştere de lungime minimă pentru această configuraŃie este
1,4,3,9. Valorile reziduale ale arcelor care îl compun sunt: (1,4):2, (4,5):3,
(5,9):2 => valoarea reziduală a drumului în creştere este 2.

2 7 3
5 0
0 4 0
2 3 2
1 4 5 9
2 2 2
3 0 5 0
9 0
6 4 6
7 8
0 0
fig.3.8.b: flux actualizat după determinarea primului drum în creştere.
Drumul în creştere de lungime minimă pentru această configuraŃie este
1,6,7,8,9. (ObservaŃie: Drumul 1,2,3,5,9 nu este în creştere deoarece arcul
(5,9) este „saturat”: valoarea fluxului este egală cu valoarea capacităŃii).
Valorile reziduale ale arcelor care îl compun sunt: (1,6):3, (6,7):4, (7,8):6,
(8,9):9 => valoarea reziduală a drumului în creştere este 3.

189
2 7 3
5 0
0 4 0
2 3 2
1 4 5 9
2 2 2
3 3 5 0
9 3
6 2 6
7 8
3 3
fig.3.8.c: fluxul după determinarea celui de-al doilea drum în creştere.
Drumul în creştere de lungime minimă pentru această configuraŃie este
1,2,3,5,4,7,8,9. (ObservaŃie: arcul 5,4 este parcurs în sens invers!). Valorile
reziduale ale arcelor care îl compun sunt: (1,2):5, (2,3):7, (3,5):4, (5,4):2,
(4,7):5, (7,8):3, (8,9):6 => valoarea reziduală a drumului în creştere este 2.

2 7 3
5 2
2 4 2
2 3 2
1 4 5 9
2 0 2
3 3 5 2
9 5
6 2 6
7 8
3 5
fig.3.8.d: fluxul după determinarea celui de-al treilea drum în creştere. Nu
mai există drumuri în creştere, deci fluxul maxim din reŃea este 7.

Programul 3.4 implementează algoritmul Ford-Folkerson.

//program 3.4 Flux maxim


#include <fstream>
#include <iostream>
#include <queue>
using namespace std;
int a[101][101], c[101][101], f[101][101];
int viz[101],T[101];
int n,m,lung,minim;
ifstream fin("retea.in");
struct arc{int n1,n2;};
arc drum[101];
int bf()
{
int i,j;
queue <int> q;
fill(viz,viz+101,0);fill(T,T+101,0);
q.push(1);
190
viz[1] = 1;
while (q.empty() != 1)
{ i = q.front();
if (i == n)
return 1;
q.pop();
for (j = 1;j<=n;j++)
{ if (viz[j] == 0 && c[i][j] - f[i][j] > 0)
{//arc parcurs in sens direct
viz[j] = 1;
T[j] = i;
q.push(j);
}
if (viz[j] == 0 && f[j][i] > 0)
{//arc parcurs in sens invers
viz[j] = 1;
T[j] = i;
q.push(j);
}
}
}
return 0;
}
void calcdrum()
{
int i=0,b=n,a;
minim = 10000000;
a = T[b];
while (a!=0)
{
drum[i].n1 = a;
drum[i].n2 = b;
if (c[a][b] > f[a][b])
{if (minim > c[a][b] - f[a][b])
minim = c[a][b] - f[a][b];
}
else
if (minim > f[b][a])
minim = f[b][a];
i++;
b = a;
a = T[b];
}
lung = i;
}
191
int main()
{
int i,j,k,ok=1;
fin >> n >> m;
for (i=0;i<m;i++)
{fin >> i >> j;
fin >> c[i][j];
}
while (ok == 1)
{
ok=bf(); //returneaza 1 daca gaseste un drum
if (ok == 1)
{ calcdrum(); //calculeaza si minimul
cout<<"\n\ngasit un drum de lungime "<< lung;
cout<<" capacitate reziduala "<< minim << endl;
cout << "format din arcele: ";
for (k=0;k<lung;k++)
{ i = drum[k].n1;
j = drum[k].n2;
cout<< i << '-' << j << " , ";
if (c[i][j] - f[i][j] > 0)
f[i][j] += minim;
else
f[j][i] -= minim;
}
}
}
int fluxmax = 0;
for (i=1;i<=n;i++)
fluxmax += f[1][i];
cout<<"\n\nflux maxim= "<<fluxmax<<endl;
cout<<"starea fiecarui arc:capacitate/flux\n";
for (i=1;i<=n;i++)
for (j=1;j<=n;j++)
if (c[i][j]>0)
{ cout<<'('<<i<<'-'<<j<<"):";
cout<<c[i][j]<<'/'<<f[i][j]<<endl;
}
return 0;
}

Pentru reŃeaua de transport din figura 3.6 programul va afişa în


fereastra consolă următoarele:

192
gasit un drum de lungime 3 capacitate reziduala 2
format din arcele: 5-9 , 4-5 , 1-4 ,

gasit un drum de lungime 4 capacitate reziduala 3


format din arcele: 8-9 , 7-8 , 6-7 , 1-6 ,

gasit un drum de lungime 7 capacitate reziduala 2


format din arcele: 8-9 , 7-8 , 4-7 , 5-4 , 3-5 , 2-3
, 1-2 ,
flux maxim= 7
starea fiecarui arc:capacitate/flux
(1-2):5/2
(1-4):2/2
(1-6):3/3
(2-3):7/2
(3-5):4/2
(4-5):3/0
(4-7):3/2
(5-9):2/2
(6-7):4/3
(7-8):6/5
(8-9):9/5

193
3.7 Sortarea topologică

Sortarea topologică a vârfurilor unui digraf reprezintă o ordonare


liniară acestor vârfuri astfel încât dacă există arcul (i,j), atunci în
structura de date care rezultă prin această sortare, vârful i va apare
înaintea vârfului j. Sortarea topologică se poate face numai dacă
digraful este acilcic. Această sortare poate fi văzută ca o aranjare a
vârfurilor digrafului pe o axă orizontală astfel încât toate arcele din
respectivul digraf sunt orientate de la stânga la dreapta. Ordinea în
care vor apare vârfurile digrafului în structura liniară nu este unică.
Digrafurile aciclice sunt utilizate în numeroase aplicații pentru a
preciza o ordine de desfășurare a unor evenimente. Pentru a ilustra
această afirmație vom folosi exemplul dat de Cormen et al în [2]: este
vorba de ordinea în care cineva se îmbracă dimineața. În figura 3.9
diverselor articole de îmbrăcăminte implicate în această acțiune le
corespund vârfurile digrafului iar arcele care apar în digraf indică
ordinea firească în care sunt îmbrăcate aceste articole (de exemplu
pantalonii înaintea curelei!). O sortare topologică a acestui digraf ne va
da o ordine pentru îmbrăcare. Algoritmul de sortare topologică are la
bază traversarea în adâncime a digrafului cu determinarea timpilor la
care fiecare vârf a fost atins şi ulterior terminat (algoritmul implementat
în programul 2.3, adaptat pentru digrafuri). Ceea ce trebuie făcut
suplimentar faŃă de algoritmul 2.3 este ca atunci când un vârf este
marcat ca terminat (şi deci colorat în negru) el să fie inserat în prima
poziŃie a structurii liniare care va rezulta prin acest tip de sortare.
Complexitatea algoritmului este cea a traversării grafului în adâncime.
Programul 3.5 implementează algoritmul de sortare topologică.
Structura liniară care rezultă din acest algoritm este o listă dublu
înlănŃuită implementată pe baza containerul list. Pentru o afişare
corespunzătoare exemplului de mai sus şi ilustrată prin figura 3.9 am
declarat un tablou de string-uri în care numărul de ordine al fiecărui
vârf (ales într-un mod arbitrar) este pus în corespondenŃă cu numele
articolului din respectivul vârf. Aşa cum scriam şi în paragraful anterior,
programul va afişa o ordine pentru îmbrăcare.

194
lenjerie 7 ciorapi 5
15/16 9/12
ceas 8
17/18
pantaloni 6 pantofi 9
13/14 10/11

curea 3 cămaşă 2
6/7 5/8

cravată 1
1/4

sacou 4
2/3

fig.3.9: digraf aciclic ilustrând ordinea în care trebuie desfăşurate anumite


activităŃi (i.e. îmbrăcate anumite articole). Numerele din chenare
reprezintă o numerotare arbitrară a vârfurilor din digraf. Cele două
numere de sub denumirea fiecărui articol reprezintă timpii când vârful a
fost atins şi respectiv terminat în algoritmul de parcurgere în adâncime.

//program 3.5: sortare topologica


#include <iostream>
#include <fstream>
#include <algorithm>
#include <queue>
#include <list>
#include <string>
#define nmax 101
#define alb 0
#define gri 1
#define negru 2
using namespace std;

int culoare[nmax];
int T[nmax]; //pentru memorarea predecesorilor
int tg[nmax], tn[nmax], timp;
int n,m;
deque <int> LA[nmax];
list <string> listatopo;
195
string numeactiune[nmax] =
{"","cravata","camasa","curea","sacou","ciorapi",
"pantaloni", "lenjerie","ceas","pantofi"
};
void citirelistead(deque <int> LA[], int &n, int &m,
char numefisier[])
{ ifstream fin (numefisier);
fin >> n >> m;
for (int i=1;i<=m;i++)
{
int x,y;
fin >> x >> y;
LA[x].push_back(y);
}
for (int i=1;i<=n;i++)
sort(LA[i].begin(), LA[i].end());
fin.close();
}

void dfs(int x, list<string> &lista)


{ culoare[x] = gri;
timp++;
tg[x] = timp;
for (int i=0;i<LA[x].size();i++)
{
int vecin = LA[x][i];
if (culoare[vecin]==alb)
{
T[vecin]=x;
dfs(vecin,lista);
}
}
timp++;
tn[x] = timp;
culoare[x] = negru;
listatopo.push_front(numeactiune[x]);
}

void afisarelist(list <string> lst)


{
list<string>::iterator it;
for (it=lst.begin();it != lst.end();it++)
cout << *it << " ";
}

196
int main()
{
citirelistead(LA,n,m,"digraf.in");
fill(culoare, culoare+n+1, alb);
timp = 0;
for (int x=1;x<=n;x++) /*1*/
if (culoare[x] == alb)
{
dfs(x,listatopo);
}
cout << "nodurile sortate topologic:\n";
afisarelist(listatopo);
return 0;
}

Pentru digraful aciclic din figura 3.9 programul va afişa în fereastra


consolă următoarele:
nodurile sortate topologic:
ceas lenjerie pantaloni ciorapi pantofi camasa curea
cravata sacou

Dacă inversăm ordinea de parcurgere a vârfurilor din funcŃia main(),


adică scriem instrucŃiunea notată /*1*/ în formatul
for (int x=n;x>=1;x--)

rezultatul obŃinut în fereastra consolă va fi:

nodurile sortate topologic:


camasa cravata ciorapi lenjerie pantaloni pantofi
curea sacou ceas

Aceasta reprezintă de asemenea o sortare topologică corectă.

197
3.8 Probleme propuse

Problema 1. Un graf orientat are următoarea matrice de adiacenŃă:


0 1 1 1 0 1
1 0 0 0 1 1
1 1 0 1 1 1
1 1 0 0 0 0
1 1 0 1 0 1
0 0 0 0 0 0
DeterminaŃi componentele tare conexe ale acestui graf.

Problema 2. Matricea ponderilor asociata unui graf orientat cu 9


vârfuri este următoarea:

0 ∞ ∞ ∞ ∞ 1 5 12 ∞
11 0 ∞ 13 7 ∞ 4 2 ∞
1 ∞ 0 9 8 3 ∞ ∞ ∞
6 8 ∞ 0 ∞ ∞ ∞ 8 2
13 2 ∞ ∞ 0 8 ∞ 13 ∞
13 12 ∞ 10 ∞ 0 ∞ ∞ 3
∞ 8 ∞ ∞ ∞ 8 0 ∞ 3
9 4 1 ∞ ∞ 6 ∞ 0 9
∞ 8 ∞ ∞ 3 ∞ ∞ ∞ 0

AplicaŃi algoritmul Dijsktra de la curs având ca vârf sursă vârful 5 din


acest graf şi precizaŃi conŃinutul tablourilor notate cu d şi T în algoritmul
descris la subcapitolul 3.3.2.

Problema 3. Se presupune că avem un graf orientat cu n vârfuri în


care muchiile au costuri strict pozitive şi că acestui graf i s-a aplicat
algoritmul Dijsktra în forma subcapitolul 3.3.2. rezultând tablourile d şi
T. ScrieŃi o funcŃie parametri T şi x unde T are semnificaŃia de mai sus
iar x este un număr natural, 1≤x≤n care returnează lungimea
drumului de cost minim de la vârful sursa la vârful x. Se reaminteşte că
lungimea unui drum este egală cu numărul de vârfuri prin care acesta
trece.

198
Problema 4. ConcepeŃi un algoritm pentru determinarea drumurilor de
cost minim care leagă un unic vârf sursă cu toate celelalte vârfuri din
digraf mai eficient decât algoritmul Dijsktra.
(IndicaŃie: sortaŃi topologic vârfurile grafului, apoi parcurgeŃi structura
liniară rezultată şi aplicaŃi operaŃiunea de relaxare fiecărui vârf spre
care pleacă câte un arc din vârful curent.)

Problema 5. În urma aplicării algoritmului Roy-Floyd asupra unui graf


orientat cu 9 vârfuri a fost obŃinută următoarea matrice a predecesorilor
(matricea T din algoritmul de la subcapitolul 3.3.3):

0 5 8 6 9 1 1 2 6
3 0 8 2 2 1 2 2 7
3 5 0 6 3 1 1 2 6
4 5 8 0 9 1 1 4 4
3 5 8 2 0 1 2 2 7
3 5 8 6 9 0 2 2 6
3 5 8 6 9 7 0 2 7
3 8 8 6 3 1 1 0 6
3 5 8 2 9 1 2 2 0

PrecizaŃi arcele care alcătuiesc următoarele drumuri de cost minim:


• de la varful 3 la varful 5
• de la varful 2 la varful 1
• de la varful 1 la varful 8

Problema 6. Cuplaj maxim într-un graf bipartit


Presupunem că este dat un graf bipartit, cu mulŃimea nodurilor V
şi mulŃimea muchiilor notată E. Se numeşte cuplaj într-un graf bipartit o
mulŃime C inclusă în E, care are proprietatea că oricare două muchii
din această mulŃime NU sunt incidente. Un cuplaj este maxim dacă are
un număr maximal de muchii (orice muchie am încerca să adăugăm în
acea mulŃime, această nouă muchie va fi incidentă cu cel puŃin una
dintre muchiile deja existente).
Fişierul graf.in conŃine:
- pe prima linie două numere natural n şi m, 3≤n≤100, 2≤m≤10000. n
şi m reprezintă numărul de noduri şi respectiv numărul de muchii
dintr-un graf bipartit;

199
- pe următoarele m linii câte două numere v1 şi v2 1≤v1,v2≤n,
reprezentând câte o muchie a grafului;
DeterminaŃi un cuplaj maxim în acest graf.
(IndicaŃie: transformaŃi graful într-o reŃea de transport în care muchiile
vor fi orientate de la una dintre partiŃii la cealaltă partiŃie – vezi
observaŃiile finale de la subcapitolul 3.4.1. Fiecare muchie
(transformată deci într-un arc) va avea capacitate egală cu 1. CalculaŃi
fluxul maxim din această reŃea iar valoarea acestuia va reprezenta
numărul de muchii din cuplajul maxim).

200

S-ar putea să vă placă și