You are on page 1of 114

CUPRINS

PREFAŢĂ.................................................................................Pag. 2
LISTE SIMPLU ÎNLĂNŢUITE.............................................Pag. 5
LISTE CIRCULARE SIMPLU ÎNLĂNŢUITE....................Pag. 15
LISTE DUBLU ÎNLĂNŢUITE..............................................Pag. 22
ARBORI...................................................................................Pag. 30
ARBORI BINARI DE CĂUTARE..........................................Pag. 39
REPREZENTAREA ŞI TRAVERSAREA
GRAFURILOR........................................................................Pag. 58
ALGORITMI PENTRU PRELUCRAREA
GRAFURILOR........................................................................Pag. 63
TABELE DE DISPERSIE...................................................... Pag. 69
METODE GENERALE DE ELABORARE A
ALGORITMILOR (I).............................................................Pag. 75
METODE GENERALE DE ELABORARE A
ALGORITMILOR (II)............................................................Pag. 87
METODE GENERALE DE ELABORARE A
ALGORITMILOR (III)..........................................................Pag. 95
ALGORITMI FUNDAMENTALI DE SORTARE...............Pag. 104
BIBLIOGRAFIE......................................................................Pag. 115
PREFAŢĂ

3
4
Lucrarea de laborator nr. 1.

LISTE SIMPLU ÎNLĂNŢUITE

1. Conţinutul lucrării

În lucrare sunt prezentate operaţiile importante asupra listelor simplu


înlănţuite şi particularităţile stivelor şi cozilor.

2. Consideraţii teoretice

Lista este o mulţime finită şi ordonată de elemente de acelaşi tip.


Elementele listei se numesc noduri.
Listele pot fi organizate sub formă statică, de tablou, caz în care
ordinea este implicit dată de tipul tablou unidimensional, sau cel mai des,
sub formă de liste dinamice, în care ordinea nodurilor este stabilită prin
pointeri. Nodurile listelor dinamice sunt alocate în memoria heap. Listele
dinamice se numesc liste înlănţuite, putând fi simplu sau dublu înlănţuite.
În continuare se vor prezenta principalele operaţii asupra listelor
simplu înlănţuite.
Structura unui nod este următoarea:

typedef struct tip_nod {


int cheie; /* câmp neobligatoriu */
alte câmpuri de date utile;
struct tip_nod urm; /* legătura spre
următorul nod */
} TIP_NOD;

Modelul listei simplu înlănţuite este prezentat în fig. 2.1.

5
Fig. 2.1. Model de listă simplu înlănţuită
Pointerii prim şi ultim vor fi declaraţi astfel:
TIP_NOD *prim, *ultim;

2.1 Crearea unei liste simplu înlănţuite

Crearea unei liste simplu înlănţuite se va face astfel:


a) Iniţial lista este vidă:

prim = 0; ultim = 0;

b) Se generează nodul de introdus:

n=sizeof(TIP_NOD);
p=(TIP_NOD *)malloc(n); /* rezervare spaţiu de
memorie în heap*/
citire date în nodul de adresă p;

c) Se fac legăturile corespunzătoare:

p->urm = 0; /*nodul este ultimul în listă */


if(ultim != 0) ultim->urm = p; /* lista nu este vidă */
else prim = p;
/* nodul p este primul introdus în listă */
ultim=p;

6
2.2 Accesul la un nod al unei liste simplu înlănţuite

În funcţie de cerinţe, nodurile listei pot fi accesate secvenţial,


extrăgând informaţia utilă din ele. O problemă mai deosebită este găsirea
unui nod de o cheie dată şi apoi extragerea informaţiei din nodul respectiv.
Căutarea nodului după cheie se face liniar, el putând fi prezent sau nu în
listă.
O funcţie de căutare a unui nod de cheie “key” va conţine secvenţa de
program de mai jos; ea returnează adresa nodului respectiv în caz de găsire
sau pointerul NULL în caz contrar:
TIP_NOD *p;
p=prim;
while( p != 0 ) if (p->cheie == key)
{
/* s-a găsit nodul de cheie dată */
/* el are adresa p */
return p;
}
else p=p->urm;
return 0; /* nu există nod de cheie = key */

2.3 Inserarea unui nod într-o listă simplu înlănţuită

Nodul de inserat va fi generat ca la paragraful 2.1; se presupune că


are pointerul p.
Dacă lista este vidă, acest nod va fi singur în listă:

if (prim == 0) {
prim=p; ultim=p; p->urm=0;
}

Dacă lista nu este vidă, inserarea se poate face astfel:


a) înaintea primului nod
if(prim != 0) {
p->urm = prim; prim = p;
}

7
b) după ultimul nod:

if (ultim != 0) {
p -> urm = 0; ultim -> urm = p; ultim = p;
}
c) înaintea unui nod precizat printr-o cheie “key”:
- se caută nodul de cheie “key”:
TIP_NOD *q, *q1;
q1=0; q=prim;
while(q!=0)
{
if(q->cheie==key) break;
q1=q; q=q->urm;
}
- se inserează nodul de pointer p, făcând legăturile
corespunzătoare:

if(q!=0) { /*nodul de cheie “key” are adresa q */


if (q==prim) {
p->urm=prim; prim=p;
}
else {
q1->urm=p; p->urm=q;
}
}
d) după un nod precizat printr-o cheie “key”:
- se caută nodul având cheia “key”:

TIP_NOD *q;
q=prim;
while(q!=0) {
if(q->cheie==key) break;
q=q->urm;
}

8
- se inserează nodul de adresă p, făcând legăturile
corespunzătoare:

if (q !=)0) { /* nodul de cheie “key” are adresa q */


p -> urm = q -> urm;
q -> urm=p;
if (q == ultim) ultim = p;
}

2.4 Ştergerea unui nod dintr-o listă simplu înlănţuită


La ştergerea unui nod se vor avea în vedere următoarele probleme:
lista poate fi vidă, lista poate conţine un singur nod sau lista poate conţine
mai multe noduri.
De asemenea se poate cere ştergerea primului nod, a ultimului nod
sau a unui nod dat printr-o cheie “key”.
a) Ştergerea primului nod
TIP_NOD *p;
if(prim!=0) { /* lista nu este vidă */
p=prim; prim=prim->urm;
elib_nod(p);
/*eliberarea spaţiului de memorie */
if(prim==0) ultim=0;
/* lista a devenit vidă */
}

9
b) Ştergerea ultimului nod
TIP_NOD *q, *q1;
q1=0; q=prim;
if(q!=0) { /* lista nu este vidă */
while(q!=ultim)
{
q1=q; q=q->urm;
}
if(q==prim) {
prim=0; ultim=0;
}
else {
q1->urm=0; ultim=q1;
}
elib_nod(q);
}
c) Ştergerea unui nod de cheie “key”
TIP_NOD *q, *q1;
/* căutare nod */
q1=0; q=prim;
while (q!=0)
{
if(q->cheie == key) break; /* s-a găsit nodul */
q1=q; q=q->urm;
}
if(q != 0) { /* există un nod de cheie “key” */
if (q == prim) {
prim=prim_>urm;
elib_nod(q);
/*eliberare spaţiu */
if( prim==0) ultim=0;
}
else {
q1->urm=q->urm;
if(q==ultim) ultim=q1;
elib_nod(q); /* eliberare spaţiu */
}

10
2.5 Ştergerea unei liste simplu înlănţuite
În acest caz, se şterge în mod secvenţial fiecare nod:
TIP_NOD *p;
while( prim != 0) {
p=prim; prim=prim->ultim;
elib_nod(p);
/*eliberare spaţiu de memorie */
}
ultim=0;

2.6 Stive
Stiva este o listă simplu înlănţuită bazată pe algoritmul LIFO (Last In
First Out), adică ultimul nod introdus este primul scos. Modelul stivei, care
va fi avut în vedere în continuare, este prezentat în fig.2.6.1.

Fig. 2.6.1. Model de stivă

Fiind o structură particulară a unei liste simplu înlănţuite, operaţiile


principale asupra unei stive sunt:
- push - pune un element pe stivă; funcţia se realizează conform
paragrafului 2.3.a., adică prin inserarea unui nod înaintea primului;
- pop - scoate elementul din vârful stivei; funcţia se
realizează conform paragrafului 2.4.a., adică prin ştergerea primului nod;
- clear - ştergerea stivei; funcţia se realizează conform
paragrafului 2.5.
În concluzie, accesul la o stivă se face numai pe la un capăt al său.

11
2.7. Cozi
Coada este o listă simplu înlănţuită, bazată pe algoritmul FIFO (First In
First Out), adică primul element introdus este primul scos. Modelul cozii
care va fi avut în vedere în consideraţiile
următoare, este prezentat în fig.2.7.1.

Se introduce
un element
nou

Fig.2.7.1. Model de coadă


Deci coada are două capete, pe la unul se introduce un element, iar de la
celalalt capăt se scoate un element.
Operaţiile importante sunt:
- introducerea unui element în coadă - funcţia se realizează prin
inserarea după ultimul nod, conform celor prezentate la paragraful
2.3.b.;
- scoaterea unui element din coadă – funcţia se realizează prin
ştergerea primului nod, conform celor prezentate la paragraful
2.4.a.;
- ştergerea cozii – funcţia se realizează conform paragrafului 2.5.

3. Mersul lucrării
3.1.Să se definească şi să se implementeze funcţiile pentru structura de
date
typedef stuct {
int lungime;
struct TIP_NOD *inceput, *curent, *sfarşit;
} LISTA;
având modelul din fig.3.1.

12
Fig.3.1.Modelul listei pentru problema 3.1

3.2.Să se implementeze o listă ca un tablou static ce conţine


pointeri la nodurile de informaţie din heap, conform modelului din fig.3.2.

Fig.3.2. Model de listă pentru problema 3.2.


3.3. De la tastatură se citesc cuvinte. Să se creeze o listă simplu
înlănţuită ordonată alfabetic, care conţine în noduri cuvintele distincte şi
frecvenţa lor de apariţie. Se va afişa conţinutul listei în ordine alfabetică .

3.4. Se consideră un depou de locomotive cu o singură intrare şi cu


o singură linie de cale ferată, care poate cuprinde oricâte locomotive. Să se
scrie programul care realizează dispecerizarea locomotivelor din depou.
Programul prelucrează comenzi de intrare în depou a unei
locomotive, de ieşire din depou a unei locomotive şi de afişare a
locomotivelor din depou.

3.5. Aceeaşi problemă 3.4, cu deosebirea că depoul are intrarea la


un capăt şi ieşirea la capătul opus.

3.6. Sortarea topologică.


Elementele unei mulţimi M sunt notate cu litere mici din alfabet.
Se citesc perechi de elemente x, y (x, y aparţin mulţimii M) cu semnificaţia

13
că elementul x precede elementul y. Să se afişeze elementele mulţimii M
într-o anumită ordine, încât pentru orice elemente x, y cu proprietatea că x
precede pe y, elementul x să fie afişat înaintea lui y.

3.7.Să se scrie programul care creează două liste ordonate


crescător după o cheie numerica şi apoi le interclasează.

3.8.Să se conceapă o stuctură dinamică eficientă pentru


reprezentarea matricelor rare. Să se scrie operaţii de calcul a sumei şi
produsului a două matrice rare. Afisarea se va face in forma naturală.

3.9.Să se conceapă o structură dinamică eficientă pentru


reprezentarea în memorie a polinoamelor. Se vor scrie funcţii de calcul a
sumei, diferenţei, produsului şi împărţirii a două polinoame.

3.10. Se citeşte de la tastatură o expresie postfixată corectă


sintactic. Să se scrie programul de evaluare a sa. Expresia conţine variabile
formate dintr-o literă şi operatorii binari +, -, *, /.

14
Lucrarea de laborator nr. 2.

LISTE CIRCULARE SIMPLU ÎNLĂNŢUITE

1. Conţinutul lucrării
În lucrare sunt prezentate principalele operaţii asupra listelor circulare
simplu înlănţuite: crearea, inserarea unui nod, ştergerea unui nod şi
ştergerea listei.

2. Consideraţii teoretice
Lista circulară simplu înlănţuită este lista simplu înlănţuită a cărei
ultim element este legat de primul element; adică ultim -> urm = prim.
În cadrul listei circulare simplu înlănţuite nu există capete. Pentru
gestionarea ei se va folosi un pointer ptr_nod, care adresează un nod
oarecare al listei, mai precis ultimul introdus(fig.2.1.).

Fig. 2.1 Modelul listei circulare simplu înlănţuite

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


• crearea;
• accesul la un nod;
• inserarea unui nod;
• ştergerea unui nod,
• ştergerea listei.

15
Structura unui nod este următoarea:

typedef struct tip_nod {


int cheie;
/* nu este obligatoriu acest câmp */
câmpuri;
struct tip_nod *urm;
} TIP_NOD;

2.1. Crearea listei circulare simplu înlănţuite

Iniţial lista este vidă:

ptr_nod = 0;

Introducerea în listă a câte unui nod se va face astfel:

/* crearea nodului */
n = sizeof(TIP_NOD); /* dimensiunea nodului */
p = (TIP_NOD *)malloc(n); /* rezervarea memorie în
heap */
citire date în nod la adresa p;
if (ptr_nod = = 0) { /* lista este vidă */
ptr_nod = p;
ptr_nod -> urm = p;
}
else { /* lista nu este vidă */
p -> urm = ptr_nod -> urm;
ptr_nod -> urm = p;
ptr_nod=p; /* ptr_nod pointează la
ultimul nod inserat */
}

16
2.2. Accesul la un nod
Nodurile pot fi accesate secvenţial plecând de la nodul de pointer
ptr_nod:

p=ptr_nod;
if(p! = 0) /* lista nu este vidă */
do {
acceseaază nodul şi preia informaţia;
p = p -> urm;
}
while (p! = ptr_nod);

sau căutând un nod de cheie dată key; în acest caz o funcţie care va returna
pointerul la nodul găsit va conţine următoarea secvenţă de program:

p = ptr_nod;
if (p! = 0) /* lista nu este vidă */
do {
if ( p -> cheie == key)
{
/* s-a găsit nodul */
/* nodul are adresa p */
return p;
}
p = p -> urm;
}
while (p! = ptr_nod);
return 0;

2.3. Inserarea unui nod


Se pun următoarele probleme:
 inserarea înaintea unui nod de cheie
dată;
 inserarea după un nod de cheie dată.

17
În ambele cazuri se caută nodul de cheie dată având adresa q; dacă
există un astfel de nod ,se creează nodul de inserat de adresă p şi se fac
legăturile corespunzătoare.
a) Inserarea înaintea unui nod de cheie dată
 se caută nodul de cheie dată (adresa sa va fi q):
TIP_NOD *p,*q,*q1;
q = ptr_nod;
do {
q1 = q; q = q -> urm;
if (q -> cheie = =key ) break; /* s-a găsit nodul */
}
while (q! = ptr_nod);

 se inserează nodul de adresă p;


if (q -> cheie == key) {
q1 -> urm = p; p -> urm = q;
}
b) Inserarea după un nod de cheie dată
 se caută nodul de cheie dată:

TIP_NOD *p,*q;
q = ptr_nod;
do {
if (q -> cheie == key ) break;
q = q -> urm;
}
while(q!=ptr_nod);

18
 se inserează nodul de adresă p :
if (q -> cheie == key) {
p -> urm =q -> urm;
q -> urm = p;
}
2.4. Ştergerea unui nod de cheie dată
Ştergerea unui nod de cheie dată key se va face astfel:
 se caută nodul de cheie dată:
q = ptr_nod;
do {
q1 = q; q = q -> urm;
if (q -> cheie == key ) break;
/* s-a găsit nodul */
}
while (q! = ptr_nod);
 se şterge nodul, cu menţiunea că dacă se
şterge nodul de pointer ptr_nod, atunci ptr_nod va pointa spre nodul
precedent q1:
if (q-> cheie == key)
{
if (q==q -> urm) ptr_nod==0;
/* lista a devenit vidă */
else {
q1 -> urm = q -> urm;
if (q == ptr_nod) ptr_nod = q1;
}
elib_nod(q);
}

19
2.5. Ştergerea listei
Ştergerea listei circulare simplu înlănţuite se va face astfel:
p = ptr_nod;
do {
p1 =p; p = p -> urm;
elib_nod(p1);
}
while (p! = ptr_nod);
ptr_nod = 0;

3. Mersul lucrării
3.1. Să se definească şi să se implementeze funcţiile pentru structura de
date:

struct LISTA_CIRC {
int lungime;
struct TIP_NOD *început; *curent;
}
având modelul din fig. 3.1.

Fig.3.1. Modelul listei pentru problema 3.1.

3.2. De la tastatură se citeşte numărul n şi numele a n copii. Să se


simuleze următorul joc: cei n copii stau într-un cerc. Începând cu un anumit

20
copil, se numără copiii în sensul acelor de ceasornic. Fiecare al n-lea copil
iese din cerc .Câştigă ultimul copil rămas în joc.
3.3. Să se implementeze un buffer circular, care conţine înregistrări cu
datele unui student şi asupra căruia acţionează principiul de sincronizare
producător-consumator, care constă în următoarele:
a) înregistrările sunt preluate în ordinea producerii lor;
b) dacă bufferul nu conţine înregistrări, consumatorul este
întârziat până când producătorul depune o înregistrare;
c) dacă bufferul este plin, producătorul este întârziat până
când consumatorul a preluat o înregistrare.

21
Lucrarea de laborator nr. 3.

LISTE DUBLU ÎNLĂNŢUITE

1. Conţinutul lucrării

În lucrare sunt prezentate principalele operaţii asupra listelor dublu


înlănţuite: crearea, inserarea unui nod, ştergerea unui nod, ştergerea listei.

2. Consideraţii teoretice

Lista dublu înlănţuită este lista dinamică între nodurile căreia s-a
definit o dublă relaţie: de succesor si de predecesor.
Modelul listei dublu înlănţuite, pentru care se vor da explicaţiile în

Fig. 2.1 Modelul listei circulare simplu înlănţuite

continuare, este prezentat în figura 2.1.


Tipul unui nod dintr-o listă dublu înlănţuită este definit astfel:

typedef struct tip_nod {


cheie; /* nu este obligatoriu */
date;
struct tip_nod *urm;
/* adresa următorului nod */
struct tip_nod * prec;
/* adresa precedentului nod */
} TIP_NOD;

22
Ca şi la lista simplu înlănţuită, principalele operaţii sunt:
• crearea;
• accesul la un nod;
• inserarea unui nod;
• ştergerea unui nod,
• ştergerea listei.
Lista dublu înlănţuită va fi gestionată prin pointerii prim şi ultim:
TIP_NOD *prim, *ultim;
prim -> prec = 0;
ultim -> urm = 0;

2.1. Crearea unei liste dublu înlănţuite


Iniţial lista este vidă:
prim = 0; ultim = 0;
După alocarea de memorie şi citirea datelor în nod, introducerea
nodului de pointer în listă se va face astfel:

if(prim= =0){ /* este primul nod în listă */


prim = p; ultim = p;
p -> urm = 0; p -> prec = 0;
}
else { /* lista nu este vidă */
ultim -> urm = p; p -> prec = ultim;
p -> urm = 0; p -> prec = ultim;
ultim = p;
}

23
2.2. Accesul la un nod
Accesul la un nod se poate face:
secvenţial înainte (de la „prim” spre „ultim”):
p = prim;
while (p != 0) {
vizitare nod de pointer p;
p = p -> urm;
}
secvenţial înapoi ( de la „ultim” spre „prim”):
p = ultim;
while (p != 0) {
vizitare nod de pointer p;
p = p -> prec;
}
• pe baza unei chei. Căutarea unui nod de cheie dată
key se va face identic ca la lista simplu înlănţuită (lucrarea 1, par. 2.2.).

2.3. Inserarea unui nod


Inserarea unui nod într-o listă dublu înlănţuită se poate face astfel:
) înaintea primului nod:
if (prim == 0) { /* lista este vidă */
prim = p; ultim = p;
p -> urm = 0; p -> prec = 0;
}
else { /* lista nu este vidă /*
p -> urm =prim; p -> prec = 0;
prim -> prec = p; prim = p;
}

24
) după ultimul nod:
if (prim == 0) { /* lista este vidă */
prim = p; ultim = p;
p -> urm = 0; p -> prec = 0;

}
else { /* lista nu este vidă /*
p -> urm =0; p -> prec = ultim; utim -> urm =
p;
ultim = p;
}
) înaintea unui nod de cheie dată key:
După căutarea nodului de cheie key, presupunând că acesta
există şi are adresa q, nodul de adresă p va fi inserat astfel:

p -> prec = q -> prec; p -> urm = q;


if (q -> prec != 0) q -> prec -> urm = p;
q -> prec = p;
if (q == prim) prim = p;
) după un nod de cheie dată key:
După căutarea nodului de cheie key, presupunând că acesta
există şi are adresa q, nodul de adresă p va fi inserat astfel:

p -> prec = q; p -> urm = q -> urm;


if (q -> urm != 0) q -> urm -> prec = p;
q -> urm = p;
if (ultim == q) ultim = p;

25
2.4. Ştergerea unui nod
Există următoarele cazuri de ştergere a unui nod din listă:
a) ştergerea primului nod; acest lucru se poate face cu
secvenţa de program:
p = prim;
prim = prim -> urm; /* se consideră listă nevidă */
elib_nod(p); /* eliberarea nodului */
if (prim == 0) ultim = 0; /* lista a devenit vidă */
else prim -> prec = 0;

) ştergerea ultimului nod:


p = ultim;
ultim = ultim -> prec; /* se consideră că lista nu este vidă */
if (ultim == 0) prim = 0; /* lista a devenit vidă */
else ultim -> urm = 0;
elib_nod(p); /* ştergerea nodului */

c) ştergerea unui nod precizat printr-o cheie key.


Presupunem că nodul de cheie key există şi are adresa p (rezultă din
căutarea sa):

if ((prim == p) && (ultim = =p))


{ /* lista are un singur nod */
prim = 0;ultim = 0;/*lista devine vidă*/
elib_nod(p);/*ştergere nod*

}
else if(p == prim) { /* se şterge primul nod */
prim = prim -> urm; prim -> prec =0;
elib_nod(p);
}
else if (p == ultim) { /* se şterge ultimul nod */
ultim = ultim -> prec;
ultim -> urm = 0;
elib_nod(p)
}
else { /* nodul de şters este diferit de capete */

26
p -> urm -> prec = p -> prec;
p -> prec -> urm = p -> urm;
elib_nod(p);
}

2.5. Ştergerea listei


Ştergerea întregii liste se realizează ştergând nod cu nod astfel:
TIP_NOD *p;
while (prim != 0) {
p = prim; prim = prim -> urm;
elib_nod(p);
}
ultim = 0;

3. Mersul lucrării
3.1. Să se definească şi să se implementeze funcţiile pentru
structura de date:
struct LISTA {
int lungime;
struct TIP_NOD *început, *curent, *sfârşit;
}
având modelul din fig.3.1.

Fig.3.1. Modelul listei pentru problema 3.1.

27
3.2. Să se scrie funcţiile pentru realizarea operaţiilor de creare,
inserare, ştergere pentru o listă dublu înlănţuită circulară având modelele din
fig.3.2.

Fig. 3.2. Modelele de liste pentru problema 3.2.

3.3. De la tastatură se citesc n cuvinte ;să se creeze o listă dublu


înlănţuită, care să conţină în noduri cuvintele distincte şi frecvenţa lor de
apariţie. Lista va fi ordonată alfabetic. Se vor afişa cuvintele şi frecvenţa lor
de apariţie a) în ordine alfabetică crescătoare şi b) în ordine alfabetică
descrescătoare.

3.4. Folosind o listă circulară dublu înlănţuită să se simuleze


următorul joc: n copii, ale căror nume se citesc de la tastatură, stau în cerc.
Începând cu un anumit copil (numele său se citeşte), se numără copiii în
sensul acelor de ceasornic. Fiecare al m-lea copil (m se citeşte) iese din joc.
Numărătoarea continuă începând cu următorul copil din cerc. Câştigă jocul
ultimul copil rămas în cerc.

3.5. Aceeaşi problemă ca la 3.4., dar numărătoarea se face în sens


contrar cu cel al acelor de ceasornic.

28
Lucrarea de laborator nr. 4.

ARBORI

1. Conţinutul lucrării
În lucrare sunt prezentate operaţiile de bază asupra arborilor binari,
binari total echilibraţi şi arborilor oarecare.

2. Consideraţii teoretice
Arborele binar, foarte des întâlnit în aplicaţii, este arborele în care
orice nod are cel mult doi descendenţi: fiul stâng şi fiul drept.

Construirea, traversarea şi ştergerea unui arbore binar.


Construirea unui arbore binar se face citind în preordine din fişierul de
intrare informaţiile din nodurile arborelui. Subarborii vizi trebuie să fie
notaţi cu un semn distinctiv. De exemplu pentru arborele din figura 2.1.1,
notând identificatorul pentru arborele vid cu * , introducerea
identificatorilor nodurilor se va face astfel:

ABD*G***CE**F*H**
Fig. 2.1.1. Arbore binar.

Tipul unui nod se declară astfel:

29
typedef struct tip_nod {
char ch; /* identificatorul nodului */
informaţie;
struct tip_nod *stg, *dr;
} TIP_NOD;

Construirea unui arbore binar se face conform funcţiei de construire,


având următoarea structură:

TIP_NOD *construire( )
{
TIP_NOD *p;
int n;
char c;
n=sizeof(TIP_NOD);
/* citire caracter de identificare nod */
scanf(“%c”, c);
if(c==’*’) return 0;
else {
/* construire nod de adresă p */
p=(TIP_NOD *)malloc(n);
/* introducere de informaţie în nod */
p->ch=c;
p->stg=construire( );
p->dr=construire( );
}
return p;
}
Apelul funcţiei se va face astfel:
rădăcina = construire ( )
Traversarea unui arbore binar se poate face în cele 3 moduri
cunoscute: preordine, inordine, postordine.
În programul următor sunt implementate operaţiile de construcţie şi
traversare a unui arbore binar. Nodul conţine numai identificatorul său.
Afişarea nodurilor vizitate se face cu indentare.

30
/* Program de construire şi afişare a arborilor binari */
#include <stdio.h>
#include <conio.h>
#include <alloc.h>
typedef struct tip_nod{
int nr.; /*informaţie */
struct tip_nod *stg,*dr;
} TIP_NOD;
TIP_NOD *rad;
void preordine(TIP_NOD *p, int nivel)
{
int i;
if (p!=0){
for(i=0;i<=nivel;i++) printf(" ");
printf("%2d\n",p->nr);
preordine(p->stg,nivel+1);
preordine(p->dr,nivel+1);
}
}
void inordine(TIP_NOD *p, int nivel)
{
int i;
if (p!=0){
inordine(p->stg,nivel+1);
for(i=0;i<=nivel;i++) printf(" ");
printf("%2d\n",p->nr);
inordine(p->dr,nivel+1);
}
}
void postordine(TIP_NOD *p, int nivel)
{
int i;
if (p!=0){
postordine(p->stg,nivel+1);
postordine(p->dr,nivel+1);
for(i=0;i<=nivel;i++) printf(" ");
printf("%2d\n",p->nr);
}
}

31
TIP_NOD *constructie()
{
TIP_NOD *p;
int inf,n;
n=sizeof(TIP_NOD);
printf("\nIntroduceti Inf.din nod inf=");
scanf("%d",&inf);
if(inf==0) return 0;
else {
p=(TIP_NOD *)malloc(n);
p->nr=inf;
p->stg=constructie();
p->dr=constructie();
}
return p;
}

void main(void)
{
rad=constructie();
printf("\nVIZITAREA IN PREORDINE\n");
preordine(rad,0);
getch();
printf("\nVIZITAREA IN INORDINE\n");
inordine(rad,0);
getch();
printf("VIZITAREA IN POSTORDINE\n");
postordine(rad,0);
getch();
}
2.7 Arbori binari total echilibraţi
Un arbore binar total echilibrat este un arbore binar care îndeplineşte
următoarea condiţie: numărul nodurilor unui oricare subarbore stâng diferă
cu cel mult 1 în plus faţă de numărul nodurilor subarborelui corespunzător
drept. Rezultă că frunzele sale se află pe ultimele două niveluri.
Algoritmul de construire a unui arbore binar total echilibrat cu n
noduri, este următorul:
a) un nod este rădăcină;

32
b) se iau nstg = [n/2] noduri pentru arborele stâng şi se trece la
construirea lui (pasul a);
c) se iau cele ndr=n-nstg-1 noduri rămase pentru subarborele
drept şi se trece la construirea lui (pasul a).
Pentru oricare nod există relaţia:
ndr <= nstg <= ndr + 1
În programul următor este implementat acest algoritm pentru
construirea unui arbore binar total echilibrat, citirea informaţiei în
noduri făcându-se în preordine.

#include <stdio.h>
#include <conio.h>
#include <alloc.h>
/* ARBORI BINARI TOTAL ECHILIBRATI */

typedef struct tip_nod{


int nr;/*informaţie */
struct tip_nod *stg,*dr;
} TIP_NOD;
TIP_NOD *rad;
void preordine(TIP_NOD *p, int nivel)
{
int i;
if(p!=0) {
for(i=0;i<=nivel;i++) printf(" ");
printf("%2d\n",p->nr);
preordine(p->stg,nivel+1);
preordine(p->dr,nivel+1);
}
}
void inordine(TIP_NOD *p, int nivel)
{
int i;
if (p!=0){
inordine(p->stg,nivel+1);
for(i=0;i<=nivel;i++) printf(" ");
printf("%2d\n",p->nr);
inordine(p->dr,nivel+1);
}

33
}
void postordine(TIP_NOD *p, int nivel)
{
int i;
if (p!=0){
postordine(p->stg,nivel+1);
postordine(p->dr,nivel+1);
for(i=0;i<=nivel;i++) printf(" ");
printf("%2d\n",p->nr);
}
}
TIP_NOD *constructie(int nr_noduri)
{
TIP_NOD *p;
int n_stg,n_dr;
int inf,n;
n=sizeof(TIP_NOD);
if(nr_noduri==0) return 0;
else {
n_stg=nr_noduri/2;
/*nr_noduri din subarborele stang */
n_dr=nr_noduri-n_stg-1;
/*nr.noduri din subarborele drept */
p=(TIP_NOD *)malloc(n);
printf("\nIntroduceti informatia din nod in
preordine ");
scanf("%d",&(p->nr));
p->stg=constructie(n_stg);
p->dr=constructie(n_dr);
}
return p;
}

void main(void)
{
int nr_total_noduri;
printf("\nNumarul total de noduri =");
scanf("%d",&nr_total_noduri);\
rad=constructie(nr_total_noduri);

34
printf("\nVIZITAREA IN PREORDINE\n");
preordine(rad,0);
getch();
printf("\nVIZITAREA IN INORDINE\n");
inordine(rad,0);
getch();
printf("VIZITAREA IN POSTORDINE\n");
postordine(rad,0);
getch();
}
2.8 Arbori oarecare

Arborele oarecare este un arbore a cărui noduri au mai mult de doi


descendenţi.
Un nod are următoarea structură:

typedef struct tip_nod {


informaţie;
int nr_fii; /*număr de fii */
struct tip_nod *adr_fii [maxfii];
/* adresele nodurilor fiu */
} TIP_NOD;
Construirea arborelui se realizează astfel:
− pentru fiecare nod se citeşte informaţia utilă şi numărul de fii;
− nodurile citite în postordine şi adresele sunt păstrate într-o stivă până
când apare nodul al cărui fii sunt. În acest moment adresele fiilor sunt
scoase din stivă, iar adresele lor sunt trecute în nodul tată, după care adresa
nodului tată este pusă în stivă. În final singura adresă în stivă va fi cea a
rădăcinii, arborele fiind construit.
Traversarea arborelui pe orizontală (nivel după nivel) se va face
astfel:
− se utilizează o coadă pentru păstrarea adreselor nodurilor ce urmează
să fie prelucrate;
− iniţial coada este vidă;
− se introduce în coada adresa rădăcinii;

35
− se scoate pe rând din coadă adresa a câte unui nod, se prelucrează
informaţia din nod, iar apoi se introduc adresele fiilor nodului respectiv. Se
repetă acest pas până când coada devine vidă.

3. Mersul lucrării

3.1 Se citeşte de la tastatură o expresie matematică în formă postfixată, sub


forma unui şir de caractere. Să se construiască arborele corespunzător
acestei expresii, fiecare nod conţinând un operator sau un operand.

3.2 Să se tipărească expresia de la punctul 3.1. în formă postfixată şi


infixată.

3.3 Să se evalueze expresia matematică de la punctul 3.1.

3.4 Să se evalueze un arbore care conţine în noduri constantele 0 şi 1 şi


operatorii AND, OR, NOT.

3.5 Să se scrie funcţii de pretty-print (tipărire frumoasă) a arborilor.

3.6 Să se scrie funcţii nerecursive pentru traversarea arborilor.

3.7 Arborele genealogic al unei persoane se reprezintă astfel: numele


persoanei este cheia nodului rădăcină şi pentru fiecare nod cheia
descendentului stâng este numele tatălui, iar a descendentului drept este
numele mamei. Se citesc două nume de la tastatură. Ce relaţie de rudenie
există între cele două persoane? Se presupune că o familie are doar un
singur fiu.

3.8 Să se scrie un program care transformă un arbore binar într-o listă


dublu înlănţuită.

3.9 Să se scrie un program care să interschimbe subarborele drept cu cel


stâng pentru un nod dat.

3.10 Să se scrie o funcţie care determină înălţimea unui arbore binar.


3.11 Să se scrie o funcţie care determină numărul de frunze ale unui arbore
binar.

36
3.12 Să se scrie o funcţie care determină dacă doi arbori binari sunt
echivalenţi (arborii binari sunt echivalenţi dacă sunt structural echivalenţi
şi datele corespunzătoare nodurilor sunt aceleaşi).

3.13 Să se scrie un program de construire şi traversare a unui arbore


oarecare conform indicaţiilor din lucrare (paragraful 2.3.).

37
Lucrarea de laborator nr. 5.

ARBORI BINARI DE CĂUTARE

1. Conţinutul lucrării
În lucrare sunt prezentate principalele operaţii asupra arborilor binari
de căutare: inserare, căutare, ştergere, traversare. De asemenea sunt
prezentaţi arborii binari de căutare optimali.

2. Consideraţii teoretice
Arborii binari de căutare sunt des folosiţi pentru memorarea şi
regăsirea rapidă a unor informaţii, pe baza unei chei. Fiecare nod al
arborelui trebuie să conţină o cheie distinctă.
Structura unui nod al unui arbore binar de căutare este următoarea:

typedef struct tip_nod {


tip cheie;
informaţii_utile;
struct tip_nod *stg, *dr;
} TIP_NOD;

În cele ce urmează, rădăcina arborelui se consideră ca o variabilă


globală:
TIP_NOD *rad;
Structura arborelui de căutare depinde de ordinea de inserare a
nodurilor.
2.1. Inserarea într-un arbore binar de căutare.
Construcţia unui arbore binar de căutare se face prin inserarea a câte
unui nod de cheie key. Algoritmul de inserare este următorul:

a) Dacă arborele este vid, se creează un nou nod care este


rădăcina, cheia având valoarea key, iar subarborii stâng şi drept fiind vizi.

38
b) Dacă cheia rădăcinii este egală cu key atunci inserarea nu se
poate face întrucât există deja un nod cu această cheie.
c) Dacă cheia key este mai mică decât cheia rădăcinii, se reia
algoritmul pentru subarborele stâng (pasul a).
d) Dacă cheia key este mai mare decât cheia rădăcinii, se reia
algoritmul pentru subarborele drept (pasul a).

Funcţia nerecursivă de inserare va avea următorul algoritm:

void inserare_nerecursivă (int key)


{
TIP_NOD *p, *q;
int n;
/* construcţie nod p*/
n=sizeof (TIP_NOD);
p=(TIP_NOD*)malloc(n);
p->cheie=key;
/* introducere informaţie utilă în nodul p */
p->stg=0; p->dr=0; /* este nod funză */
if (rad==0) { /* arborele este vid */
rad=p;
return;
}
/* arborele nefiind vid se caută nodul tată pentru nodul p */
q=rad; /* rad este rădăcina arborelui variabilă globală */
for ( ; ; )
{
if (key<q->cheie)
{ /* căutarea se face în subarborele stâng */
if (q->stg==0) { /* inserare */
q->stg=p;
return;
}
else q=q->stg;
}
else if (key>q->cheie)
{ /* căutarea se face în subarborele drept */
if (q->dr==0) { /* inserare */
q->dr=p;

39
return;
}
else q=q->dr;
}
else { /* cheie dublă */
/* scriere mesaj */
free (p);
return;
}
}
}

2.2 Căutarea unui nod de cheie dată key într-un arbore binar de
căutare.

Căutarea într-un arbore binar de căutare a unui nod de cheie dată se


face după un algoritm asemănător cu cel de inserare.
Numărul de căutări optim ar fi dacă arborele de căutare ar fi total
echilibrat (numărul de comparaţii maxim ar fi log 2 n – unde n este numărul
total de noduri).
Cazul cel mai defavorabil în ceea ce priveşte căutarea este atunci când
inserarea se face pentru nodurile având cheile ordonate crescător sau
descrescător. În acest caz, arborele degenerează într-o listă.
Algoritmul de căutare este redat prin funcţia următoare:

TIP_NOD *cautare (TIP_NOD *rad, int key)


/* funcţia returnează adresa nodului în caz de găsire sau 0
în caz că nu există un nod de cheia key */
{
TIP_NOD *p;
if (rad==0) return 0; /* arborele este vid */
/* dacă arborele nu este vid, căutarea începe din
rădăcina rad */
p=rad;
while (p!=0)
{
if (p->cheie==key) return p; /* s-a
găsit */

40
else if (key<p->cheie) p=p->stg; /*căutarea
se face în subarb.stâng */
else p=p->dr; /* căutarea se face în
subarborele drept */
}
return 0; /* nu există nod de cheie key */
}

Apelul de căutare este:

p=cautare (rad, key);

rad fiind pointerul spre rădăcina arborelui.

2.3 Ştergerea unui nod de cheie dată într-un arbore binar de


căutare
În cazul ştergerii unui nod, arborele trebuie să-şi păstreze structura de
arbore de căutare.
La ştergerea unui nod de cheie dată intervin următoarele cazuri:
a) Nodul de şters este un nod frunză. În acest caz, în nodul tată,
adresa nodului
fiu de şters (stâng sau drept) devine zero.
b) Nodul de şters este un nod cu un singur descendent. În acest
caz, în nodul tată,
adresa nodului fiu de şters se înlocuieşte cu adresa descendentului nodului
fiu de şters.
c) Nodul de şters este un nod cu doi descendenţi. În acest caz,
nodul de şters se înlocuieşte cu nodul cel mai din stânga al subarborelui
drept sau cu nodul cel mai din dreapta al subarborelui stâng.
Algoritmul de ştergere a unui nod conţine următoarele etape:
− căutarea nodului de cheie key şi a nodului tată corespunzător;
− determinarea cazului în care se situează nodul de şters.

41
2.4 Ştergerea unui arbore binar de căutare
Ştergerea unui arbore binar de căutare constă în parcurgerea în
postordine a arborelui şi ştergerea nod cu nod, conform funcţiei următoare:

void stergere_arbore(TIP_NOD *rad)


{
if (rad !=0) {
stergere_arbore (rad->stg);
stergere_arbore (rad->dr);
free (rad);
}
}

2.5 Traversarea unui arbore binar de căutare


Ca orice arbore binar, un arbore binar de căutare poate fi traversat în
cele trei moduri: în preordine, în inordine şi în postordine conform funcţiilor
de mai jos:

void preordine (TIP_NOD *p)


{
if (p!=0) {
extragere informaţie din nodul p;
preordine (p->stg);
preordine (p->dr);
}
}
void inordine (TIP_NOD *p)
{
if (p!=0) {
inordine (p->stg);
extragere informaţie din p;
inordine (p->dr);
}
}
void postordine (TIP_NOD *p)
{

42
if (p!=0) {
postordine (p->stg);
postordine (p->dr);
extragere informaţie din nodul p;
}
}

Apelul acestor funcţii se va face astfel:


preordine(rad);
inordine(rad);
postordine(rad);

2.6 Arbori binari de căutare optimali


Lungimea drumului de căutare a unui nod cu cheia x, într-un arbore
binar de căutare, este nivelul hi al nodului în care se află cheia căutată, în
caz de succes sau 1 plus nivelul ultimului nod întâlnit pe drumul căutării
fără succes.
Fie S ={ c1, c2, ... , cn } mulţimea cheilor ce conduc la căutarea cu
succes (c1 < c2 < ... < cn).
Fie pi probabilitatea căutării cheii ci (i=1,2,...,n).
Dacă notăm cu C, mulţimea cheilor posibile, atunci C S reprezintă
mulţimea cheilor ce conduce la căutarea fără succes. Această mulţime o
partiţionăm în submulţimile:
k0 – mulţimea cheilor mai mici ca c1;
kn – mulţimea cheilor mai mari ca cn;
ki (i=1,2,...,n) – mulţimea cheilor în intervalul (ci, ci+1).
Fie qi probabilitatea căutării unei chei din mulţimea ki.
Căutarea pentru orice cheie din ki se face pe acelaşi drum; lungimea
drumului de căutare va fi h’i.
Notăm cu L costul arborelui, care reprezintă lungimea medie de
căutare:
n n
L=∑ p h + ∑q h '
i i j i
i =1 j =0

cu condiţia:

43
n n

∑ p + ∑q
i =1
i
j =0
j
=1

Se numeşte arbore optimal, un arbore binar de căutare care pentru


anumite valori pi, qi date realizează un cost minim.
Arborii optimali de căutare nu sunt supuşi inserărilor şi eliminărilor.
Din punct de vedere al minimizării funcţiei L, în loc de pi şi qi se pot
folosi frecvenţele apariţiei căutărilor respective în cazul unor date de test.
Se notează cu Aij arborele optimal construit cu nodurile ci+1, ci+2, ..., cj.
Greutatea arborelui Aij este:
j j

q ij
= ∑ p + ∑q
k =i +1
k
k =i
k

care se poate calcula astfel:


q =q
ii i
pentru i = 1, 2, ..., n;
q =q
ij i , j −1
+ p +q j j
pentru 0 ≤ i ≤ j ≤ n
Rezultă că costul arborelui optimal Aij se va putea calcula astfel:
cii = qii pentru 0 ≤ i ≤ n
c = q min (c
ij ij
i< k ≤ j
i , k −1
+ c kj )

Fie rij valoarea lui k pentru care se obţine minimul din relaţia lui cij.
Nodul cu cheia c[rij] va fi rădăcina subarborelui optimal Aij, iar subarborii
săi vor fi Ai,k-1 şi Akj.
Calculul valorilor matricei C este de ordinul O(n3). S-a demonstrat că
se poate reduce ordinul timpului de calcul la O(n2).
Construirea se face cu ajutorul funcţiei următoare:

TIP_NOD *constr_arbore_optimal(int i, int j)


{
int n;
TIP_NOD *p;
if(i==j) p=0;
else {
n=sizeof (TIP_NOD);
p=(TIP_NOD*)malloc(n);
p->stg=constr_arbore_optimal(i, r[i][j]-1);
p->stg=constr_arbore_optimal[r[i][j]];

44
p->dr=constr_arbore_optimal(r[i][j], j);
}
return p;
}

2.7 Exemple
Primul program prezintă toate funcţiile descrise în lucrare asupra
unui arbore de căutare. Un nod conţine drept informaţie utilă numai cheia,
care este un număr întreg.
Al doilea program conţine funcţiile principale asupra unui arbore

binar de căutare optimal.

Exemplul nr.1 (arbori de căutare)

#include <stdio.h>
#include <conio.h>
#include <alloc.h>
typedef struct tip_nod{
int cheie;/*informatie */
struct tip_nod *stg,*dr;
} TIP_NOD;
TIP_NOD *rad;
void preordine(TIP_NOD *p, int nivel)
{
int i;
if (p!=0){
for(i=0;i<=nivel;i++) printf(" ");
printf("%2d\n",p->cheie);
preordine(p->stg,nivel+1);
preordine(p->dr,nivel+1);
}
}
void inordine(TIP_NOD *p, int nivel)
{
int i;
if (p!=0){
inordine(p->stg,nivel+1);

45
for(i=0;i<=nivel;i++) printf(" ");
printf("%2d\n",p->cheie);
inordine(p->dr,nivel+1);
}
}
void postordine(TIP_NOD *p, int nivel)
{
int i;
if (p!=0){
postordine(p->stg,nivel+1);
postordine(p->dr,nivel+1);
for(i=0;i<=nivel;i++) printf(" ");
printf("%2d\n",p->cheie);
}
}

void inserare(int key)


{
TIP_NOD *p,*q;
int n;
n=sizeof(TIP_NOD);
p=(TIP_NOD *)malloc(n);
p->cheie=key;
p->stg=0;p->dr=0;
if(rad==0){
rad=p;
return;
}
q=rad;
for(;;)
{
if (key < q->cheie){
if(q->stg==0){
q->stg=p;
return;
}
else q=q->stg;
}
else if (key > q->cheie) {

46
if(q->dr == 0) {
q->dr=p;
return;
}
else q=q->dr;
}
else { /* chei egale */
printf("\n Exista un nod de cheie = %d\n",key);
/* eventuala prelucrare a nodului */
free(p);
return;
}
}
}

TIP_NOD *inserare_rec(TIP_NOD *rad,int key)


{
TIP_NOD *p;
int n;
if (rad==0){
n=sizeof(TIP_NOD);
p=(TIP_NOD *)malloc(n);
p->cheie=key;p->stg=0;p->dr=0;
return p;
}
else {
if(key < rad->cheie) rad->stg=inserare_rec(rad->stg,key);
else {
if(key > rad->cheie) rad->dr=inserare_rec(rad->dr,key);
else { /* cheie dubla */
printf("\n Exista un nod de cheie=%d\n",key);
}
}
};
return rad;
}

TIP_NOD * cautare(TIP_NOD *rad, int key)


{

47
TIP_NOD *p;

if(rad==0) return 0;/*arborele este vid */


p=rad;
while(p != 0)
{
if(p->cheie == key) return p;/* s-a gasit nodul */
else if(key < p->cheie) p=p->stg;
else p=p->dr;
}
return 0; /* nu exista nod de cheie key */
}

TIP_NOD *stergere_nod(TIP_NOD *rad,int key)


{
TIP_NOD *p,*tata_p;/* p este nodul de sters, iar tata_p este tatal lui */
TIP_NOD *nod_inlocuire,*tata_nod_inlocuire;/*nodul care il va inlocui
pe p si tatal sau */
int directie; /*stg=1;dr=2*/
if(rad==0) return 0; /*arborele este vid */
p=rad; tata_p=0;
/* cautare nod cu cheia key */
while((p!=0)&&(p->cheie!=key))
{
if (key<p->cheie){ /*cautare in stanga */
tata_p=p;
p=p->stg;
directie=1;
}
else { /*cautare in dreapta */
tata_p=p;
p=p->dr;
directie=2;
}
}
if(p==0){
printf("\n NU EXISTA NOD CU CHEIA=%d\n",key);
return rad;
}

48
/* s-a gasit nodul p de cheie key */
if(p->stg==0) nod_inlocuire=p->dr; /* nodul de sters p nu are fiu
sting */
else if(p->dr==0) nod_inlocuire=p->stg; /*nodul de sters p nu are
fiu drept*/
else { /* nodul de sters p are fiu stang si fiu drept */
tata_nod_inlocuire=p;
nod_inlocuire=p->dr; /* se cauta in subarborele drept*/
while(nod_inlocuire->stg!=0)
{
tata_nod_inlocuire=nod_inlocuire;
nod_inlocuire=nod_inlocuire->stg;
}
if(tata_nod_inlocuire!=p)
{
tata_nod_inlocuire->stg=nod_inlocuire->dr;
nod_inlocuire->dr=p->dr;
}
nod_inlocuire->stg=p->stg;
}
free(p);
printf("\nNodul de cheie=%d a fost sters!\n",key);
if(tata_p==0) return nod_inlocuire; /*s-a sters chiar radacina
initiala */
else {
if (directie==1) tata_p->stg=nod_inlocuire;
else tata_p->dr=nod_inlocuire;
return rad;
}
}

void stergere_arbore(TIP_NOD *rad)


{
if(rad!=0) {
stergere_arbore(rad->stg);
stergere_arbore(rad->dr);
free(rad);
}
}

49
void main(void)
{
TIP_NOD *p;
int i, n,key;
char ch;
printf("ALEGETI Inserare recursiva r/R sau nerecursiva alt
caracter");
scanf("%c",&ch);
printf("\nNumarul total de noduri=");
scanf("%d",&n);
rad=0;
for(i=1;i<=n;i++)
{
printf("\nCheia nodului=");
scanf("%d",&key);
if((ch=='R')||(ch=='r')) rad=inserare_rec(rad,key);
else inserare(key);
}
printf("\nVIZITAREA IN PREORDINE\n");
preordine(rad,0);
getch();
printf("\nVIZITAREA IN INORDINE\n");
inordine(rad,0);
getch();
printf("VIZITAREA IN POSTORDINE\n");
postordine(rad,0);
getch();
fflush(stdin);
printf("\n Doriti sa cautati un nod DA=D/d Nu= alt caracter :");
scanf("%c",&ch);
while((ch=='D')||(ch=='d'))
{
printf("Cheia nodului cautat=");
scanf("%d",&key);
p=cautare(rad,key);
if(p!=0) printf("Nodul exista si are adresa p\n");
else printf("Nu exista un nod de cheie data\n");
fflush(stdin);

50
printf("\n Doriti sa cautati un nod DA=D/d Nu= alt
caracter : ");
scanf("%c",&ch);
}
fflush(stdin);
printf("\n Doriti sa sterget un nod DA=D/d Nu= alt caracter :");
scanf("%c",&ch);
while((ch=='D')||(ch=='d'))
{
printf("Cheia nodului de sters=");
scanf("%d",&key);
rad=stergere_nod(rad,key);
inordine(rad,0);
fflush(stdin);
printf("\n Doriti sa stergeti un nod DA=D/d Nu= alt
caracter : ");
scanf("%c",&ch);
}
printf("stergeti arborele creat ? da=d/D nu=alt caracter ");
fflush(stdin);
scanf("%c",&ch);
if((ch=='D')||(ch=='d')) {
stergere_arbore(rad);
rad=0;
printf("\nARBORELE ESTE STERS!!\n");
}
getch();
}

Exemplul nr.2 (arbori optimali)

#include <stdio.h>
#include <conio.h>
#include <alloc.h>

#define nmax 25

51
typedef struct tip_nod {
char cheie;
tip_nod *stg,*dr;
} TIP_NOD;

char chei[nmax]; /* cheile c1,c2,...,cn */


int p[nmax];/* frecventa de cautare a cheilor */
int q[nmax]; /* frecventa de cautare intre chei */
int r[nmax][nmax]; /* radacinile subarborilor optimali */

void calcul(int nr,float *dr_med)


{
/* determina structura arborelui */
int c[nmax][nmax];/* costul subarborilor optimali */
int g[nmax][nmax]; /* greutatea arborilor */
int i,j,k,m,l;
int x,min;
/* calculul matricei greutate */
for(i=0;i<=nr;i++)
{
g[i][i]=q[i];
for(j=i+1;j<=nr;j++)
g[i][j]=g[i][j-1]+p[j]+q[j];
}
/* calculul matricei c */
for(i=0;i<=nr;i++)
c[i][i]=g[i][i];
for(i=0;i<=nr-1;i++)
{
j=i+1;
c[i][j]=c[i][i]+c[j][j]+g[i][j];
r[i][j]=j;
}
/*calcul c[i][l+i] */
for(l=2;l<=nr;l++)
for(i=0;i<=nr-l;i++)
{
min=32000;

52
for(k=i+1;k<=l+i;k++)
{
x=c[i][k-1]+c[k][l+i];
if(x<min) {
min=x;
m=k;
}
}
c[i][l+i]=min+g[i][l+i];
r[i][l+i]=m;
}
printf("\nMATRICEA G\n");
for(i=0;i<=nr;i++)
{ for(j=i;j<=nr;j++)
printf("%d ",g[i][j]);
printf("\n");
}
getch();
printf("\nMATRICEA C\n");
for(i=0;i<=nr;i++)
{ for(j=i;j<=nr;j++)
printf("%d ",c[i][j]);
printf("\n");
}
getch();
printf("\nMATRICEA R\n");
for(i=0;i<=nr;i++)
{ for(j=i;j<=nr;j++)
printf("%d ",r[i][j]);
printf("\n");
}
getch();
printf("c[0][nr.]=%ld g[0][nr.]=%ld\n",c[0]
[nr.],g[0][nr.]);
getch();
*dr_med=c[0][nr.]/(float)g[0][nr.];
}

TIP_NOD* constr_arbore(int i,int j)

53
/* construirea arborelui optimal */
{
int n;
TIP_NOD *p;
if (i==j) p=0;
else {
n=sizeof(TIP_NOD);
p=(TIP_NOD*)malloc(n);
p->stg=constr_arbore(i,r[i][j]-1);
p->cheie=chei[r[i][j]];
p->dr=constr_arbore(r[i][j],j);
}
return p;
}

void inordine(TIP_NOD *p,int nivel)


{
/* Afisare in inordine a arborelui */
int i;
if(p!=0){
inordine(p->stg,nivel+1);
for(i=0;i<=nivel;i++)
printf(" ");
printf("%c\n",p->cheie);
inordine(p->dr,nivel+1);
}
}
void main(void)
{
TIP_NOD *radacina;
int i;
int n; /*n este numarul cheilor */
float drum_mediu;
printf("\nNumarul cheilor=");
scanf("%d",&n);
/*Citirea cheilor si a frecventelor de cautare a lor*/
for(i=1;i<=n;i++)
{
printf("Cheia[%d]=",i);

54
chei[i]=getche();
printf(" frecventa=");
scanf("%d",&p[i]);
}
/*Citirea frecventelor de cautare intre chei */
for(i=0;i<=n;i++)
{
printf("q[%d]=",i);
scanf("%d",&q[i]);
}
calcul(n,&drum_mediu);
printf("Drumul mediu=%6f\n",drum_mediu);
getch();
radacina=constr_arbore(0,n);
inordine(radacina,0);
getch();
}

4. Mersul lucrării
3.1 De la tastatură se citesc cuvinte ( şiruri de caractere ). Să se scrie un
program care creează un arbore de căutare, care conţine în noduri cuvintele
şi frecvenţa lor de apariţie. Să se afişeze apoi cuvintele în ordine
lexicografică crescătoare şi frecvenţa lor de apariţie.

3.2 Să se implementeze operaţia de interclasare a doi arbori de căutare.

3.3 Să se verifice dacă operaţia de ştergere a unui nod dintr-un arbore


de căutare este comutativă ( ştergerea nodurilor x şi y se poate face în orice
ordine).

3.4 Se consideră două liste liniare simplu înlănţuite cu câmpurile de


informaţie utilă conţinând numere întregi. Să se construiască o listă care
conţine reuniunea celor două liste şi în care elementele sunt ordonate
crescător. Se va folosi o structură intermediară de tip arbore de căutare.
Elementele comune vor apare a o singură dată.

55
3.5 Se consideră un arbore de căutare care conţine elemente cu
informaţia utilă de tip şir de caractere. Să se scrie o funcţie de căutare,
inserare şi ştergere a şirului de caractere permiţându-se folosirea
şabloanelor, spre exemplu * pentru orice subşir sau ? pentru orice caracter.

3.6 Informaţiile pentru medicamentele unei farmacii sunt: nume


medicament, preţ, cantitate, data primirii, data expirării.
Evidenţa medicamentelor se ţine cu un program care are drept
structură de date un arbore de căutare după nume medicament. Să se scrie
programul care execută următoarele operaţii:
− creează arborele de căutare;
− caută un nod după câmpul nume medicament şi
actualizează câmpurile de informaţie;
− tipăreşte medicamentele în ordine lexicografică;
− elimină un nod identificat prin nume medicament;
− creează un arbore de căutare cu medicamentele care au
data de expirare mai veche decât o dată specificată de la terminal.

3.7 Se va crea un arbore binar de căutare optimal care va avea în


noduri cuvintele cheie folosite în limbajul C. Frecvenţele pi şi qi se vor da în
funcţie de folosirea cuvintelor cheie în programele exemplu din lucrare.

56
Lucrarea de laborator nr. 6

REPREZENTAREA ŞI TRAVERSAREA
GRAFURILOR

1. Conţinutul lucrării
În lucrare sunt prezentate câteva noţiuni legate de grafuri, modurile de
reprezentare şi traversare a lor.

2. Consideraţii teoretice
Noţiuni de bază
Graful orientat sau digraful G =(V, E) este perechea formată din
mulţimea V de vârfuri şi mulţimea E ⊂ V V de arce. Un arc este o
pereche ordonată de vârfuri (v, w), unde v este baza arcului, iar w este
vârful arcului. In alţi termeni se spune că w este adiacent lui v.
O cale este o succesiune de vârfuri v[1],v[2],…,v[k], astfel că există
arcele (v[1],v[2]), (v[2],v[3]),…,(v[k-1],v[k]) în mulţimea arcelor E.
Lungimea căii este numărul de arce din cale. Prin convenţie, calea de la un
nod la el însuşi are lungimea 0.
O cale este simplă, dacă toate vârfurile, cu excepţia primului şi
ultimului sunt distincte între ele.
Un ciclu este o cale de la un vârf la el însuşi.
Un graf orientat etichetat este un graf orientat în care fiecare arc şi
/sau vârf are o etichetă asociată, care poate fi un nume, un cost sau o valoare
de un tip oarecare.
Un graf orientat este tare conex, daca oricare ar fi vârfurile v şi w
există o cale de la v la w şi una de la w la v.
Un graf G’ =(V’, E’) este subgraf al lui G daca V’⊂ V şi E’⊂ E. Se
spune că subgraful indus de V’⊂ V este G’ =(V’, E ∧(V’V’)).
Un graf neorientat sau prescurtat graf G =(N, R) este perechea
formată din mulţimea N de noduri şi mulţimea R de muchii. O muchie este
o pereche neordonată (v, w)=(w, v) de noduri.
Definiţiile prezentate anterior rămân valabile şi în cazul grafurilor
neorientate.

57
Moduri de reprezentare
Atât grafurile orientate, cât şi cele neorientate se reprezintă frecvent
sub două forme: matricea de adiacenţe şi listele de adiacenţe.
Astfel, pentru graful orientat G =(V, E), unde V este mulţimea
vârfurilor V ={1,2,…,n},matricea de adiacenţe A va fi definită astfel :

1 dacă (i, j)∈E


A[i][j]=
0 dacă (i, j)∉E

Matricea de adiacenţe etichetată A (sau matricea costurilor) va fi


definită astfel :

eticheta arcului (i, j) dacă (i, j)∈ E


A[i][j]=
un simbol dacă (i, j)∉E

Matricea de adiacenţe este simetrică pentru grafuri neorientate şi


nesimetrică pentru cele orientate.

Matricea de adiacenţe este utilă când se testează frecvent prezenţa sau


absenţa unui arc şi este dezavantajoasă când numărul de arce este mult mai
mic decât n x n.
Reprezentarea prin liste de adicenţe foloseşte mai bine memoria, dar
căutarea arcelor este mai greoaie. În această reprezentare, pentru fiecare nod
se păstrează lista arcelor către nodurile adiacente. Întregul graf poate fi
reprezentat printr-un tablou indexat după noduri, fiecare intrare în tablou
conţinând adresa listei nodurilor adiacente. Lista nodurilor adiacente poate
fi dinamică sau statică. Pentru graful din fig.2.2.1, sunt prezentate:
- matricea de adiacenţe în fig.2.2.2.;
- lista de adiacenţe dinamică în fig.2.2.3.;
- lista de adiacenţe statică în fig.2.2.4.

58
LISTA

Fig. 2.2.3.Lista de adiacenţe dinamică. Fig. 2.2.4.Lista de adiacenţe statică.

2.3.Explorarea în lărgime
Explorarea în lărgime constă în următoarele acţiuni:
− se trece într-o coadă vidă nodul de pornire;
− se trece extrage din coadă câte un nod care este prelucrat şi
se adaugă toate nodurile adiacente lui neprelucrate. Se repetă acest pas până
când coada devine vidă.

59
Algoritmul este următorul:

void explorare_largime(int s)
/* s este nodul de pornire */
{
int vizitate[NrMaxNoduri];
coada Q;
int i,NrNoduri,v,w;
for(i=0;i<NrNoduri;i++)
vizitate[i]=0; /*iniţializare vector cu zero*/
vizitate[s]=1;/*se vizitează nodul s */
prelucrează informaţia din s;
introducere nod s in coada Q;
while(coada Q nu este vidă)
{
extrage următorul nod v din coada Q;
for(fiecare nod w adiacent lui v)
if(nodul w nu a fost vizitat)
{
vizitate[w]=1;
prelucrează informaţia din w;
introducere nod w în coada Q;
}
}
}

2.4.Explorarea in adâncime
La explorarea în adâncime se marchează vizitarea nodului iniţial, după
care se vizitează în adâncime, recursiv, fiecare nod adiacent. După vizitarea
tuturor nodurilor ce pot fi atinse din nodul de start, parcurgerea se consideră
încheiată. Dacă rămân noduri nevizitate, se alege un nou nod şi se repetă
procedeul de mai sus.

60
Algoritmul este următorul:

void explorare_adancime(int s)
/* s este nodul de pornire* /
{
int vizitate[NrMaxNoduri];
stiva ST;
int NrNoduri,i,v,w;
for(i=0;i<NrNoduri;i++)
vizitate[i]=0; /*initializare vector cu zero*/
vizitate[s]=1; /*se incepe cu s */
prelucrare informatia din s;
introducere s in stiva ST;
while(stiva ST nu este vidă)
{
v=conţinutul nodului din vârful stivei;
w=următorul nod adiacent lui v nevizitat;
if(există w)
{
vizitate[w]=1;
prelucrează informaţia din w;
pune pe stiva ST nodul w;
}
else pop(ST); /* se şterge nodul v din vârful stivei ST */
}
}

3.Mersul lucrării
3.1. Pentru un graf orientat G =(V, E) şi V’⊂V să se
găsească subgraful indus G’ =(V’, E’).Elementele din V şi V’ se citesc.

3.2. Să se scrie câte o funcţie de construire pentru un graf G


=(V, E), conform celor 3 reprezentări posibile.

3.3. Pentru un graf reprezentat prin matricea de adiacenţe, să


se implementeze algoritmii de traversare prezentaţi în paragrafele 2.3. şi
2.4.

61
3.4. Să se scrie o funcţie care să verifice dacă există o cale
între două noduri date (v, w) ale unui graf orientat G =(V, E).

3.5. Pentru un graf neorientat dat G =(N, R), să se scrie o


funcţie care să verifice dacă graful este conex sau nu.

62
Lucrarea de laborator nr.7

ALGORITMI PENTRU PRELUCRAREA GRAFURILOR

1.Conţinutul lucrării

In lucrare sunt prezentaţi algoritmii lui Dijkstra şi Floyd pentru


găsirea căilor de cost minim între două noduri precizate, respectiv între
oricare două noduri ale unui graf şi algoritmii lui Kruskal şi Prim pentru
găsirea arborelui de cost minim de acoperire a unui graf.

2.Consideraţii teoretice
2.1.Căile de cost minim dintr-un vârf
Se consideră un graf orientat G =(V, E) etichetat, în care fiecare arc
are ca etichetă un număr nenegativ numit cost. Graful se reprezintă în
memorie prin matricea de adiacenţe etichetată, care se mai numeşte matricea
costurilor.
Fiind date două vârfuri, unul sursă şi unul destinaţie, se cere găsirea
drumului de cost minim de la sursă la destinaţie. Algoritmul lui Dijkstra de
rezolvare a acestei probleme constă in următoarele:
- se păstrează o mulţime S de vârfuri j∈V, pentru care există cel
puţin un drum de la sursă la j. Iniţial S ={sursa}.
- la fiecare pas, se adaugă la S un vârf a cărui distanţă faţă de un vârf
din S este minimă.
Pentru a înregistra căile minime de la sursă la fiecare vârf se
utilizează un tablou TATA, în care TATA[k] păstrează vârful anterior lui k
pe calea cea mai scurtă.
In descrierea algoritmului se fac următoarele notaţii:
- n numărul vârfurilor mulţimii V;
- mulţimea S se reprezintă prin vectorul caracteristic ( elementele sale sunt
S[i]=1, dacă i∈S şi S[i]=0, dacă i∉S;
- vectorul DISTANTE de n elemente al distanţelor minime de la sursă la
fiecare vârf;

63
- matricea de costuri COST de nxn elemente: COST[ i ][j]=c>0 dacă ( i
,j)∈E, COST[i][j]=0 dacă i =j si COST[i][j]=+∞ dacă (i, j)∉E;
- vectorul TATA.
Algoritmul lui Dijkstra este următorul:
#define NMAX …
#define INFINIT …
float DISTANTE[NMAX];
float COST[NMAX][NMAX];
int TATA[NMAX];
int S[NMAX];
void DIJKSTRA(int n,int sursa)
{
/* n este numărul de vârfuri;sursa este vârful sursă */
int i ,j,k,pas;
/*iniţializări*/
for (i=1;i<=n;i++)
{
S[i]=0;
DISTANTE[i]=COST[sursa][i];
if (DISTANTE[i]<INFINIT) TATA[i]=sursa;
else TATA[i]=0;
}
/*introducere sursa in S*/
S[sursa]=1;
TATA[sursa]=0;
DISTANTE[sursa]=0;
/*construire vectori DISTANTE si TATA */
for (pas=1;pas<=n-1;pas++)
{
găseşte vârful k neselectat cu DISTANTE[k] minim;
if (minimul anterior găsit==INFINIT) return;
S[k]=1; /* se adaugă k la mulţimea S */
for (j=1;j<=n;j++)
if ((S[j]=0) && (DISTANTE[k]+COST[k]
[j]<DISTANTE[j]))
{

64
DISTANTE[j]=DISTANTE[k]+COST[k]
[j];
TATA[j]=k;
}
}
}

Vectorul TATA conţine vârfurile accesibile din vârfurile sursa. El


permite reconstruirea drumurilor de la vârful sursă la oricare vârf accesibil.
Pentru vârfurile inaccesibile din vârful sursa vom avea S[i]=0 şi
DISTANTE[i]=INFINIT.

2.2.Căile de cost minim din oricare vârf


Algoritmul prezentat la 2.1. poate fi repetat din nodurile unui graf.
Acest lucru permite calculul unui tablou al drumurilor minime între toate
perechile de vârfuri ale grafului. In continuare se prezintă un algoritm mai
simplu, algoritmul lui Floyd.
Algoritmul lui Floyd constă în găsirea costurilor minime între
oricare două vârfuri i, j∈V. Aceste costuri minime se păstrează în matricea
A. Matricea A este iniţial egală cu matricea costurilor. Calculul distanţelor
minime se face în n iteraţii, n fiind numărul vârfurilor. La iteraţia k, A[i][j]
va avea ca valoare cea mai mică distanţă intre i si j pe căi care nu conţin
vârfuri peste k (exceptând capetele i si j). Se utilizează formula următoare:
Aij(k)= min (Aij(k-1),Aik(k-1)+Akj(k-1)).
Deoarece Aik(k)=Aik(k-1) şi Akj(k)=Akj(k-1) se poate utiliza o singură copie
a matricii A.

Algoritmul lui Floyd este următorul:

#define NMAX …
float C[NMAX][NMAX]; /*matricea costurilor*/
float A[NMAX][NMAX];
void FLOYD(int n)
{
int i,j,k;
for (i=1;i<=n;i++)
for (j=1;j<=n;j++)
A[i][j]=C[i][j];/*iniţializare A*/

65
for (i=1;i<=n;i++) A[i][i]=0;
/*iteraţiile*/
for (k=1;k<=n;k++)
for (i=1;i<=n;i++) //pentru toate liniile
for (j=1;j<=n;j++) //pentru toate coloanele
if (A[i][k]+A[k][j]<A[i][j])
A[i][j]=A[i][k]+A[k][j];
}

Pentru a păstra căile minime, se utilizează un tablou adiţional P,


unde P[i][j] ţine acel vârf k ce a condus la distanţa minimă A[i][j]. Dacă P[i]
[j]==0, atunci arcul (i, j) este calea minimă între i si j.
Pentru a afişa vârfurile intermediare aflate pe calea cea mai scurtă
între i si j se poate proceda conform algoritmului următor:

void CALE(int i, int j)


{
int k;
if (k!=0){
CALE(i,k);
scrie nodul k;
CALE(k,j);
}
}

2.3.Arborele de acoperire de cost minim

Fie G =(N, R) un graf neorientat conex. Fiecărei muchii (i, j)∈R i se


asociază un cost c[i][j]>0. Problema constă în a determina un graf parţial
conex A = (N, T), astfel încât suma costurilor muchiilor din T să fie minimă.
Se observă imediat că acest graf parţial este chiar arborele de acoperire.
Algoritmul lui Prim constă în următoarele:
- se porneşte cu o submulţime W, formată din nodul de plecare şi mulţimea
T vidă;
- la fiecare iteraţie, se selectează muchia (w, u) cu cel mai mic cost, w∈W şi
u∈N-W. Se adaugă u la W şi (w, u) la T. In final, W va conţine toate
nodurile din N, iar T va conţine muchiile arborelui de acoperire minimal.

66
void algoritm_PRIM(int n)
{
W={1}; //se pleacă din nodul 1
T={ }; //mulţimea vidă
while (W!=N)
{
selectează muchia (w,u) de cost minim cu w∈W şi
u∈N-W;
adaugă u la W;
adaugă (u,w) la T;
}
}

Un alt algoritm aparţine lui Kruskal. In acest caz, muchiile sunt


ordonate crescător după cost. Arborele de acoperire va conţine n-1 muchii.
La fiecare pas se alege muchia de cost minim care nu formează ciclu cu
muchiile aflate deja în T.
Acest algoritm are următoarea descriere:

void algoritm_Kruskal(int n)
{
T={};
while (T nu este arbore de acoperire)
{
selectează muchia (w,u) de cost minim din R;
şterge (w,u) din R;
if ( (w,u) nu creează un ciclu in T)
adaugă (w,u) la T;
}
}

Problema mai dificilă în algoritm constă în verificarea dacă o


muchie creează ciclu in T.

3.Mersul lucrării

3.1.Să se implemeteze algoritmul lui Dijkstra de găsire a căilor de


cost minim dintr-un vârf al unui graf orientat. Se va construi şi afişa

67
arborele având ca rădăcină vârful sursă. Care este performanta algoritmului
în ceea ce priveşte timpul de calcul?

3.2.Să se implementeze algoritmul lui Floyd de găsire a căilor de


cost minim din oricare vârf al unui graf neorientat. Se vor afişa căile de cost
minim între două vârfuri, date de la tastatură. Care este performanţa
algoritmului în ceea ce priveşte timpul de calcul?

3.3..Să se implementeze algoritmul lui Prim de găsire a arborelui


de acoperire a unui graf neorientat.

3.4.Să se implementeze algoritmul lui Kruskal de găsire a arborelui


de acoperire a unui graf neorientat. Să se facă o comparaţie în ceea ce
priveşte timpul de calcul între algoritmul lui Kruskal şi cel al lui Prim.

68
Lucrarea de laborator nr. 8

TABELE DE DISPERSIE

1. Conţinutul lucrării

În lucrare sunt prezentate principalele operaţii asupra unei tabele de


dispersie: construirea tabelei de dispersie, inserarea unei înregistrări,
căutarea unei înregistrări, afişarea înregistrărilor. De asemenea se fac câteva
consideraţii asupra alegerii funcţiei de dispersie.

2. Consideraţii teoretice

2.1.Tipuri de tabele

Tabelul este o colecţie de elemente de acelaşi tip, identificabile prin


chei. Elementele sale se mai numesc înregistrări.
Tabelele pot fi :
- fixe, cu un număr de înregistrări cunoscut dinainte şi ordonate;
- dinamice
Tabelele dinamice pot fi organizate sub formă de:
- listă dinamică simplu sau dublu înlănţuită;
- arbore de căutare;
- tabele de dispersie.
Din categoria tabelelor fixe face parte tabelul de cuvinte rezervate
dintr-un limbaj de programare. Acesta este organizat ca un tablou de
pointeri spre cuvintele rezervate, introduse în ordine alfabetică. Căutarea
utilizată este cea binară.
Tabelele dinamice organizate sub formă de liste au dezavantajul
căutării liniare. Arborele de căutare reduce timpul de căutare. În cazul în
care cheile sunt alfanumerice, comparaţiile sunt mari consumatoare de timp.
Pentru astfel de situaţii, cele mai potrivite sunt tabelele de dispersie.

2.2.Funcţia de dispersie ( hashing )

Funcţia de dispersie este funcţia care transformă o cheie într-un


număr natural numit cod de dispersie:

69
f: K -> H
unde K este mulţimea cheilor, iar H este o mulţime de numere naturale.
Funcţia f nu este injectivă .Două chei pentru care f(k1)=f(k2) se
spune că intră în coliziune, iar înregistrările respective se numesc sinonime.
Asupra lui f se pun două condiţii:
- valoarea ei pentru un kε K să rezulte cât mai simplu şi rapid;
- să minimizeze numărul de coliziuni.
Un exemplu de funcţie de dispersie este următoarea:
f(k)=γ (k) mod M
unde γ (k) este o funcţie care transformă cheia într-un număr natural, iar M
este un număr
natural recomandat a fi prim.
Funcţia γ (k) se alege în funcţie de natura cheilor. Dacă ele sunt
numerice, atunci γ (k)=k. În cazul cheilor alfanumerice, cea mai simplă
funcţie γ (k) este suma codurilor ASCII ale caracterelor din componenţa
lor; ca urmare funcţia f de calcul a dispersiei este următoarea:

#define M…
int f(char *key)
{
int i,suma;
suma=0;
for(i=0;i<length(key);i++)
suma=suma+*(key+i);
return suma%M;
}

70
2.3.Tabela de dispersie

Rezolvarea coliziunilor se face astfel: toate înregistrările pentru care


cheile intră în coliziune sunt inserate într-o listă simplu înlănţuită. Vor
exista astfel mai multe liste, fiecare conţinând înregistrări cu acelaşi cod de
dispersie. Pointerii spre primul element din fiecare listă se păstrează într-un
tablou, la indexul egal cu codul de dispersie .Ca urmare modelul unei tabele
de dispersie este următorul:

Un nod al listei are structura următoare:


typedef struct tip_nod {
char *cheie;
informaţie
struct tip_nod *urm;
}TIP_NOD;
Tabloul HT este declarat astfel:
TIP_NOD *HT[M];

71
Iniţial el conţine pointerii nuli:
for(i=0;i<M;i++)
HT[i]=0;
Căutarea într-o tabelă de dispersie a unei înregistrări având pointerul key la
cheia sa, se face astfel:
- se calculează codul de dispersie:
h=f(key);
- se caută înregistrarea având pointerul key la cheia sa, din lista având
pointerul spre primul nod HT[h].
Căutarea este liniară:

p=HT(h);
while(p!=0)
{
if(strcmp(key,p->cheie)==0) return p;
p=p->urm;
}
return 0;
Inserarea unei înregistrări într-o tabelă de dispersie se face astfel:
- se construieşte nodul de pointer p, care va conţine informaţia utilă şi
pointerul la cheia înregistrării:

p=(TIP_NOD*)malloc(sizeof(TIP_NOD));
citire_nod(p);
- se determină codul de dispersie al înregistrării:
h=f(p->cheie);

- dacă este prima înregistrare cu codul respectiv, adresa sa este depusă în


tabelul HT:

if(HT[h]==0){
HT[h]=p;
p->urm=0;
}

72
în caz contrar se verifică dacă nu cumva mai există o înregistrare cu cheia
respectivă. În caz afirmativ se face o prelucrare a înregistrării existente
( ştergere, actualizare) sau este o eroare (cheie dublă ). Dacă nu există o
înregistrare de cheia respectivă, se inserează în listă ca prim element nodul
de adresă p:

q=cautare(p->cheie);
if(q==o){ /* nu exista o înregistrare de cheia respectiva */
p->urm=HT[h];
HT[h]=p;
}
else prelucrare(p,q);/* cheie dubla */
Construirea tabelei de dispersie se face prin inserarea repetată a nodurilor.

2.4.Listarea tuturor înregistrărilor

Listarea tuturor înregistrărilor pe coduri se face simplu, conform


algoritmului următor:

for(i=0;i<M;i++)
{
if(HT[i]!=0)
{
printf(“\nInregistrări avand codul de dispersie=%d\n”,i);
p=HT[i];
while(p!=0){
afisare(p);
p=p->urm;
}
}

3.Mersul lucrării

3.1. Se va crea o tabelă fixă cu cuvintele rezervate din limbajul C. Se va


scrie apoi o funcţie de căutare binară a unui cuvânt în tabelă.

73
3.2. Să se implementeze algoritmii prezentaţi aferenţi unei tabele de
dispersie. Înregistrarea va conţine datele aferente unui student. Cheia va fi
numele şi prenumele studentului. Scrieţi în plus
faţă de cele prezentate o funcţie de ştergere a unei înregistrări de cheie dată.

3.3. Scrieţi un program care să tipărească identificatorii dintr-o tabelă de


dispersie în ordine alfabetică.

3.4. Să se afişeze frecvenţa de apariţie a literelor dintr-un text utilizând o


tabelă de dispersie.

74
Lucrarea de laborator nr. 9

METODE GENERALE DE ELABORARE A


ALGORITMILOR (I)

1.Conţinutul lucrării
În lucrare sunt prezentate principiile metodelor Greedy şi
backtracking, variantele lor de aplicare şi exemple.

2 2.Consideraţii teoretice
2.1.Metoda Greedy.
Metoda Greedy se aplică următoarelor tipuri de probleme:
Dintr-o mulţime A de n elemente se cere determinarea unei
submulţimi B care să îndeplinească anumite condiţii pentru a fi acceptată.
Numele metodei vine de la următorul fapt: se alege pe rând câte un
element din mulţimea A şi eventual se introduce în soluţie.
Se menţionează faptul că o dată ce un element a fost ales el rămâne
în soluţia finală, iar dacă un element a fost exclus, el nu va mai putea fi
reconsiderat pentru includere în soluţie.
Metoda determină o singură soluţie.
Există două variante de rezolvare a unei probleme cu ajutorul
metodei Greedy:

a) Varianta I
Se pleacă de la mulţimea B vidă. Se alege din mulţimea A un
element neales în paşii precedenţi. Se cercetează dacă adăugarea la soluţia
parţială B conduce la o soluţie posibilă. În caz afirmativ se adaugă
elementul respectiv la B.

75
Descrierea variantei este următoarea:
#define max ...
GREEDY1(int A[max], int n, int B[max], int *k)
/* A este mulţimea de n elemente date;
B este mulţimea extrasă de k elemente */
{
int x, v, i;
*k = 0; /* Mulţimea B este vidă */
for(i = 0; i<n; i++)
{
ALEGE (A, n, i, x);
/* se alege elementul x dintre elementele A[i], A[i+1], ...
A[n-1] şi se aduce pe poziţia i prin interschimbare */
POSIBIL (B, x, v);
/* v=1 dacă x prin adăugare la B conduce la soluţie posibilă
şi
v=0 în caz contrar */
if(v==1) ADAUGA(B, x, *k);
/* se adaugă x la B, k indicând numărul de elemente din B
*/
}
}

În varianta I a metodei, funcţia ALEGE stabileşte criteriul care


duce la soluţia finală.
b) Varianta II
Se stabileşte de la început ordinea în care trebuie considerate
elementele mulţimii A. Apoi se ia pe rând câte un element în ordinea
stabilită şi se verifică dacă prin adăugare la soluţia parţială B anterior
construită, se ajunge la o soluţie posibilă. În caz afirmativ se face adăugarea.

76
Descrierea variantei este următoarea:
#define max ...
GREEDY2(int A[max], int n, int B[max], int *k)
/* A este mulţimea de n elemente date;
B este mulţimea extrasă de k elemente */
{
int x, v, i;
*k = 0; /* soluţia vidă */
PRELUCRARE(A, n); /* rearanjare vector A */
for(i = 0; i<n; i++)
{
x=A[i];
POSIBIL (B, x, v);
/* v=1 dacă prin adăugarea lui x la B se ajunge la o soluţie
posibilă şi
v=0 în caz contrar */
if(v==1) then ADAUGA(B, x, *k);
/* se adaugă x la mulţimea B */
}
}

Dificultatea elaborării funcţiei PRELUCRARE este identică cu cea a


funcţiei ALEGE din varianta precedentă.
Exemplu: Determinarea arborelui de acoperire de cost minim prin
algoritmul lui Prim.
Problema a fost enunţată în cadrul lucrării nr.7 paragraful 2.3.
Algoritmul constă în următoarele:
a) Iniţial se ia arborele ce conţine un singur vârf. S-a
demonstrat că nu are importanţă cu care vârf se începe; ca urmare se ia
vârful 1. Mulţimea arcelor este vidă.
b) Se alege arcul de cost minim, care are un vârf în arborele
deja construit, iar celălalt vârf nu aparţine arborelui. Se repetă în total acest
pas de n-1 ori.

77
Pentru evitarea parcurgerii tuturor arcelor grafului la fiecare pas, se
ia vectorul v având n componente definit astfel:
0 dacă vârful i aparţine arborelui deja construit
U[i] = 
k dacă vârful i nu aparţine arborelui deja construit; k
este vârful arborelui deja construit a. î. muchia (i, k) este de
cost minim.
Iniţial v[1]=0 şi v[2]=v[3]=...=v[n]=1, adică iniţial arborele este A
=({1}, Ø).

/*Algoritmul lui Prim */


#include <stdio.h>
#include <conio.h>
#define nmax 10
#define MAX 0x7fff
void prim(int n,int c[nmax][nmax],
int muchii[nmax][2],int *cost)
/* n -numărul nodurilor;
c - matricea costurilor;
muchii-muchiile arborelui de acoperire de cost minim;
cost-costul arborelui de acoperire de cost minim */
{
int v[nmax]; /* v[i]=0 dacă i aparţine arborelui;
v[i]=j dacă i nu aparţine arborelui.
j este nodul din arbore a.i.
muchia (i,j) este de cost minim */
int i,j,k,minim;
*cost=0;
v[1]=0;
for(i=2;i<=n;i++) v[i]=1; /*arborele este ({1},{}) */
/* determinarea celor n-1 muchii */
for(i=1;i<=n-1;i++)
{
/*determinarea muchiei care se adaugă arborelui */
minim=0x7fff;
for(k=1;k<=n;k++)
if(v[k]!=0) if(c[k][v[k]] < minim)
{
j=k;

78
minim=c[k][v[k]];
}
muchii[i][0]=v[j];
muchii[i][1]=j;
*cost=*cost+c[j][v[j]];
/*reactualizare vector v */
v[j]=0;
for(k=1;k<=n;k++)
if(v[k]!=0)
if(c[k][v[k]]>c[k][j]) v[k]=j;
}
}
void main()
{
int n;/*nr. nodurilor */
int c[nmax][nmax]; /*matricea costurilor */
int muchii[nmax][2];/*muchiile arborelui*/
int i,j,k,cost;
printf("\nNr. nodurilor=");
scanf("%d",&n);
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
c[i][j]=MAX;
/*citirea costurilor(numere întregi) */
for(i=1;i<n;i++)
{
do
{
printf("\nIntroduceţi 0 pentru terminare sau nodul
adiacent\n");
printf(" cu %2d > ca el =",i);
scanf("%d",&j);
if(j>0)
{
printf("\nCostul c[%d][%d]=",i,j);
scanf("%d",&c[i][j]);
c[j][i]=c[i][j]; /*matricea este simetrica */
}
}

79
while(j>0);
};
prim(n,c,muchii,&cost);
printf("\nCOSTUL ARBORELUI = %d",cost);
printf("\nMUCHIILE ARBORELUI COSTUL
MUCHIEI\n");
for(i=1;i<=n-1;i++)
printf(" %2d - %2d %10d\n",muchii[i][0],muchii[i][1],
c[muchii[i][0]][muchii[i][1]]);
getch();
}

2.8 Metoda backtracking


Metoda backtracking se aplică algoritmilor pentru rezolvarea
următoarelor tipuri de probleme:
Fiind date n mulţimi S1, S2, ... Sn, fiecare având un număr nrsi de
elemente, se cere găsirea elementelor vectorului X =(x1, x2, ... xn) Є
S=S1xS2x…Sn, astfel încât să fie îndeplinită o anumită relaţie φ(x1, x2,
… ,xn) între elementele sale.
Relaţia φ(x1, x2, … ,xn) se numeşte relaţie internă, mulţimea
S=S1xS2x…Sn se numeşte spaţiul soluţiilor posibile, iar vectorul X se
numeşte soluţia rezultat.
Metoda backtracking determină toate soluţiile rezultat ale problemei.
Dintre acestea se poate alege una care îndeplineşte în plus o altă condiţie.
n
Metoda backtracking elimină generarea tuturor celor ∏nr s
i =1
i

posibilităţi din spaţiul soluţiilor posibile. În acest scop la generarea


vectorului X, se respectă următoarele condiţii:
a) xk primeşte valori numai dacă x1, x2, ... ,xk-1 au primit deja
valori;
b) după ce se atribuie o valoare lui xk, se verifică relaţia numită
de continuare φ`(x1, x2, … ,xk), care stabileşte situaţia în care are sens să se
treacă la calculul lui xk+1. Neîndeplinirea condiţiei φ` exprimă faptul că
oricum am alege xk+1, xk+2, ... ,xn nu se ajunge la soluţia rezultat. În caz de
neîndeplinire a condiţiei φ`(x1, x2, … ,xk), se alege o nouă valoare pentru xk
Є Sk şi se reia verificarea condiţiei φ`. Dacă mulţimea de valori xk s-a
epuizat, se revine la alegerea altei valori pentru xk-1 ş.a.m.d. Această
micşorare a lui k dă numele metodei, ilustrând faptul că atunci când nu se

80
poate avansa se urmăreşte înapoi secvenţa curentă din soluţia posibilă. Între
condiţia internă şi cea de continuare există o strânsă legătură. Stabilirea
optimă a condiţiilor de continuare reduce mult numărul de calcule.
Algoritmul backtracking este redat sub formă nerecursivă astfel:
#define nmax ...
backtracking_nerecursiv(int n)
/* se consideră globale mulţimile Si şi numărul lor de elemente
nrsi */
{
int x[nmax];
int k, v;
k=1;
while(k>0)
{
v=0;
while ((mai există o valoare α Є Sk netestată) & (v==0))
{
x[k]=α;
if (φ(x[1], x[2], ..., x[k]) este îndeplinită) v=1;
}
if (v==0) k=k-1;
else if (k==n) afişare (x, n); /* afişarea sau eventual
prelucrarea soluţiei */
else k=k+1;
}
}

Sub formă recursivă, algoritmul backtracking poate fi redat astfel:


#define nmax ...
int x[nmax];

/* se consideră globale n, mulţimile Si şi numărul lor de elemente


nrsi */
backtracking_recursiv(int k)
{
int j;
for (j=1;j<=nrsk;j++)
{

81
x[k]=Sk[j]; /* al j-lea element din mullţimea Sk */
if (φ(x[1], x[2], ..., x[k]) este îndeplinită)
if (k<n) backtracking_recursiv(k+1);
else afişare (x, n); /* afişarea sau eventual
prelucrarea soluţiei */
}
}

Apelul se face:
backtracking_recursiv(1);
Exemplu: Problema damelor de şah.
Se cere găsirea tuturor soluţiilor de aşezare pe tabla de şah de n linii
şi n coloane a n dame, astfel încât ele să nu se atace. Se consideră că ele se
atacă dacă sunt pe aceeaşi linie, coloană sau diagonală.
Întrucât pe o linie se găseşte o singură damă, soluţia se prezintă sub
formă de vector
x =(x1, x2, ... ,xn), unde xi reprezintă coloana pe care se află dama în linia i.

Condiţiile de continuare sunt:


a) două dame nu se pot afla pe aceeaşi coloană, adică:
X[ i ] < > X[ j] pentru i < > j ;
b) două dame nu se pot afla pe aceeaşi diagonală, adică:
k - i < > X[ k ] − X[ i ] pentru i = 1, 2, ... k - 1.

Varianta nerecursivă este următoarea:


#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#define nmax 10
void dame_nerecursiv(int n)
/* funcţia găseşte toate aşezările posibile pe o tablă
de şah de n*n pătrate pentru ca n dame să nu se atace */
{
int x[nmax];
int v;
int i,j,k,nr_solutie;

82
nr_solutie=0;
k=1;x[k]=0;
while(k>0)
{
/*găsirea unei aşezări corecte în linia k */
v=0;
while((v==0)&(x[k]<=n-1))
{
x[k]++;
v=1;i=1;
while((i<=k-1)&(v==1))
if((x[k]==x[i])|(abs(k-i)==abs(x[k]-x[i]))) v=0;
else i++;
}
if(v==0) k=k-1;
else {
if(k==n){
/*afişarea tablei */
nr_solutie++;
printf("\nSOLUTIA nr. %d\n",nr_solutie);
for(i=1;i<=n;i++)
{
for(j=1;j<=n;j++)
if (x[i]==j) printf("1");
else printf("0");
printf("\n");
};
getch();
}
else {
k++;
x[k]=0;
};
}
}
}
void main(void)
{
int n;

83
printf("\nOrdinul tablei de sah=");
scanf("%d",&n);
dame_nerecursiv(n);
printf("\nSFARSIT");
}

Varianta recursivă este următoarea:


#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#define nmax 10
/* Varianta recursivă a problemei damelor */
int n; /*dimensiunea (ordinul) tablei de şah */
int nr_solutie;
int x[nmax];
int FI(int k)
{
/*testează condiţiile de continuare */
int p;
for(p=1;p<=k-1;p++)
if((x[k]==x[p]) | (abs(k-p) == abs(x[k]-x[p]))) return 0;
return 1;
}
void back_recursiv(int k)
{
int i,j,p;
for(j=1;j<=n;j++)
{
x[k]=j;
if(FI(k)==1)
if(k<n) back_recursiv(k+1);
else {
/*tipărirea soluţiei */
nr_solutie++;
printf("\nSOLUTIA nr.%d\n",nr_solutie);
for(i=1;i<=n;i++)
{
for(p=1;p<=n;p++)

84
if(x[i]==p) printf("1");
else printf("0");
printf("\n");
};
getch();
}
}
}
void main(void)
{
printf("\nOrdinul tablei de sah n=");
scanf("%d",&n);
nr_solutie=0;
back_recursiv(1);
printf("\nSFARSIT\n");
}

3. Mersul lucrării
Utilizând metoda Greedy să se rezolve următoarele probleme:
3.1 Se dau m vectori V1, V2, ... Vm, care conţin n1, n2, ... nm elemente,
ordonate crescător după o cheie. Se interclasează vectorii daţi, obţinându-se
un vector de lungime n1+n2+...+nm elemente, ordonate crescător. Se ştie că
interclasarea a doi vectori care conţin n1, respectiv n2 elemente necesită un
timp proporţional cu suma lungimilor lor. Să se determine ordinea optimă în
care trebuie efectuată interclasarea tuturor vectorilor daţi.

3.2 Problema rucsacului. Greutatea maximă care poate fi transportată


cu un rucsac este G. Dându-se n materiale, fiecare având greutatea m şi
costul C pe unitatea de greutate, să se găsească ce cantitate din fiecare
material să fie introdus în rucsac pentru ca să se obţină câştigul maxim. Se
vor deosebi două cazuri:
a) un material poate fi luat numai în întregime;
b) se poate lua o fracţiune din material.

3.3 Problema activităţilor. Există o mulţime S=1, 2, 3, ..., n de n activităţi


care doresc să folosească o aceeaşi resursă (de exemplu o sală de clasă).
Această resursă poate fi folosită de o singură activitate la un anumit moment
de timp. Fiecare activitate i are un timp de pornire tpi şi un timp de

85
terminare tfi (tpi < tfi). Dacă este selectată activitatea i, ea se desfăşoară pe
durata [tpi, tfi). Spunem că activităţile i şi j sunt compatibile dacă duratele
lor nu se intersectează. Să se selecteze o mulţime maximală de activităţi
mutual compatibile.

Utilizând metoda backtracking să se rezolve următoarele probleme:

3.4.Colorarea grafurilor. Fiind dat un graf neorientat G =(X, Γ) unde X


este mulţimea formată din n noduri, iar Γ este mulţimea muchiilor şi un
număr de m culori, se cere să se determine toate colorările posibile ale
nodurilor grafului folosind cele m culori, astfel încât oricare două noduri
adiacente să fie colorate în mod diferit.

3.5.Problema ciclului hamiltonian. Se dă un graf conex neorientat G


=(X, Γ) prin matricea costurilor pozitive.
Se cere să se determine ciclul hamiltonian de cost minim (ciclul
hamiltonian este un ciclu care trece prin toate nodurile).

3.6. Fiind dată o matrice A de n × n elemente numere naturale, să se


determine cea mai mică sumă de n elemente luate din linii diferite şi coloane
diferite.

3.7.Fiind date o tablă de şah de dimensiune n × n pătrate şi un cal


plasat în pătratul din stânga sus al tablei, să se afişeze toate posibilităţile de
mutare a calului astfel încât să treacă o singură dată prin fiecare pătrat al
tablei.

3.8.Un labirint este codificat printr-o matrice de n × m elemente ale


cărui culoare sunt reprezentate prin elemente egale cu 1, situate în poziţii
consecutive pe o aceeaşi linie sau coloană, celelalte elemente fiind 0. O
persoană se găseşte în poziţia (i, j) din interiorul labirintului. Se cere
afişarea tuturor traseelor de ieşire din labirint care nu trec de mai multe ori
prin acelaşi loc.

3.9.Se consideră o mulţime formată din n elemente numere întregi. Să


se genereze toate submulţimile acestei mulţimi având proprietatea că suma
elementelor lor este egală cu S.

86
3.10.Se dau două mulţimi de numere întregi A şi B. Să se genereze
toate funcţiile f : A → B .
Lucrarea de laborator nr. 10.

METODE GENERALE DE ELABORARE A


ALGORITMILOR (II)

1. Conţinutul lucrării
În lucrare se prezintă esenţa metodelor “Branch and Bound”
(ramifică şi mărgineşte) şi a metodei “Divide et Impera”.

2 2. Consideraţii teoretice
2.1. Metoda “Branch and Bound”
Metoda “Branch and Bound” este înrudită cu metoda backtracking,
diferind ordinea de parcurgere a spaţiului stărilor şi a modului de eliminare
a subarborilor ce nu pot conduce la rezultat.
Metoda “Branch and Bound” se aplică următoarelor tipuri de
probleme: Se cunoaşte starea iniţială s0 şi starea finală sf (starea rezultat).
Din starea s0 se ajunge în starea sf printr-o serie de decizii. Ne interesează ca
numărul stărilor intermediare să fie minim.
Presupunem că stările reprezintă nodurile unui graf, iar arcele
indică faptul că o decizie a transformat starea si în starea si+1. Se introduc
restricţii ca să nu existe mai multe noduri în graf cu aceeaşi stare, deci graful
să nu fie infinit. Astfel graful se reduce la un arbore. Arborele se generează
până la prima apariţie a nodului final.
Există posibilitatea parcurgerii grafului în adâncime sau în lăţime,
însă timpul de ajungere la rezultat este mare. O strategie superioară din
punct de vedere al timpului de calcul se obţine alegând dintre descendenţii
vârfului curent pe cel mai aproape de starea finală. Pentru a putea aprecia
depărtarea faţă de starea finală, se va folosi o funcţie de cost c definită pe
mulţimea vârfurilor din arbore. Având valorile acestei funcţii, vom alege
dintre vârfurile descendente ale vârfului curent pe cel cu cost minim. O
astfel de parcurgere a grafului se numeşte “Least Cost” sau prescurtat “LC”.

87
Funcţia c ideală pentru a măsura distanţa de la vârf la vârful final
este:


 rezultat;
niv x − 1

c ( x ) = + ∞

min c(y) y este vârf terminal

  ∈ subarborelui de rădăcinăx}

dacă x este vârf terminal ≠ vârful


rezultat
dacă x nu este vârf rezultat

Funcţia c definită mai sus nu este aplicabilă, întrucât calculul ei


presupune parcurgerea tuturor vârfurilor arborelui, de fapt urmărindu-se
tocmai evitarea acestui lucru.
Se observă că dacă totuşi funcţia c este calculată, atunci coborârea
în arbore la vârful rezultat se face pe un drum deja format din vârfurile x ce
au ataşată o valoare c(x) egală cu c(rădăcină).
Neputând lucra cu funcţia c, atunci se defineşte o aproximare a sa
cu una din următoarele două variante:
a) cˆ( x ) =nivelul vârfului x + distanţa dintre starea curentă şi
starea finală;
b) cˆ( x ) =costul stării părinte + distanţa dintre starea curentă şi
starea finală.
Nivelul este numărul deciziilor prin care s-a ajuns la configuraţia
curentă.
Stabilirea funcţiei “distanţă” este specifică fiecărei probleme. De
exemplu, în cazul jocului PERSPICO (problema 3.1. din lucrare), distanţa
este numărul de plăcuţe care nu sunt la locul potrivit.
Funcţia care descrie metoda “Branch and Bound” este dată mai jos.
Lista L conţine vârfurile care memorează configuraţia stărilor. Pentru vârful
i luat în considerare se memorează tatăl său în vectorul TATA, permiţând ca
odată ajunşi în vârful rezultat să se poată reface drumul de la rădăcină la
vârful rezultat.
RAD este pointerul la vârful ce conţine starea iniţială.

88
TIP_NOD *RAD;
RAD=(TIP_NOD *)malloc(sizeof(TIP_NOD));
/* se depune configuraţia iniţială la adresa RAD */
void Branch_and_Bound()
{
TIP_NOD *i, *j;
LISTA L;
int iesire;
i=RAD;
LISTA_VIDA(L);
for(;;)
{
while(mai există vecini j ai lui i neprelucraţi)
if(j==vârf_rezultat)
{ afisare_drumul_de_la RAD la j;
return;
}
else {
ADAUG(j,L);
TATA[j]=i;
};
if(ESTE_VIDA(L))
{
printf("Nu exista solutie\n");
return;
}
else i=ELEMENT(L);
}
}
Funcţiile apelate au următoarea semnificaţie:
LISTAVIDĂ(L) – iniţializează lista L ca fiind vidă;
ESTEVIDĂ(L) –– returnează 1 dacă lista L este vidă sau 0 în
caz contrar;
ELEMENT(L) –– este funcţia ce returnează un element al
listei care are cel mai mic cost ĉ , pentru a ne afla în cazul pacurgerii LC a
arborelui stărilor;
ADAUG(j, L) ––– adaugă nodul j la lista L. Din descrierea
funcţiei se observă că atunci când un vârf i din lista L devine vârf curent,

89
sunt generaţi toţi descendenţii săi, aceştia fiind puşi în lista L. Unul din
aceşti descendenţi va deveni la rândul său pe baza costului ĉ vârf curent,
până când se ajunge la vârful rezultat (cel ce conţine starea finală).

2.9 Metoda “Divide et Impera”


Metoda “Divide et Impera” constă în împărţirea repetată a unei
probleme în două sau mai multe probleme de acelaşi tip şi apoi combinarea
subproblemelor rezolvate, în final obţinându-se soluţia problemei iniţiale.
Astfel, fie vectorul A =(a1, a2, ..., an), a cărui elemente se prelucrează.
Metoda “Divide et Impera” este aplicabilă dacă pentru orice p, q, naturali,
astfel încât 1 ≤ p < q ≤ n există un m ∈[ p +1, q −1] încât prelucrarea
secvenţei {a , a p p +1
,..., a q } se poate face prelucrând secvenţele

{a , a
p p +1
,..., a m } şi {a , a
m +1 m+2
,..., a q } şi apoi prin combinarea
rezultatelor se obţine prelucrarea dorită.
Metoda “Divide et Impera” poate fi descrisă astfel:
void DIVIDE_IMPERA(int p,int q,rezultat:alfa)
/* p, q reprezinta indicii secvenţei care se
prelucrează;
alfa reprezintă rezultatul */
{
int eps, m;
rezultat beta,gama;
if(abs(q-p)<=eps) PRELUCRARE(p,q,alfa);
else {
DIVIDE(p,q,m);
DIVIDE_IMPERA(p,m,beta);
DIVIDE_IMPERA(m+1,q,gama);
COMBINA(beta,gama,alfa);
}
}

Apelul funcţiei “Divide et Impera” se face astfel:

DIVIDE_IMPERA(1, n, alfa);

90
Variabilele şi funcţiile din funcţia DIVIDE_IMPERA au următoarele
semnificaţii:
eps – este lungimea maximă a unei secvenţe ap, ap+1, ...,aq
pentru care prelucrarea se poate face direct;
m – este indicele intermediar în care secvenţa ap, ap+1, ...,aq
este împărţită în două subsecvenţe de funcţia DIVIDE;
beta şi gama – reprezintă rezultatele intermediare obţinute în
urma prelucrării subsecvenţelor (ap, ap+1, ...,am) şi respectiv (am+1, am+2, ...,aq);
alfa – reprezintă rezultatul combinării rezultatelor
intermediare beta şi gama;
DIVIDE – împarte secvenţa (ap, ap+1, ...,aq) în două
subsecvenţe (ap, ap+1, ...,am) şi
(am+1, am+2, ...,aq);
COMBINĂ – combină rezultatele beta şi gama ale
prelucrării subsecvenţelor returnate de procedura DIVIDE, obţinând
rezultatul alfa a prelucrării secvenţei iniţiale.

Exemplu. Sortarea prin interclasare a unui vector de n elemente:


/*Program de sortare prin metoda divide et impera */
#include <stdio.h>
#include <conio.h>
#define nmax 100
int a[nmax]; /* vectorul de sortat */
void afisare(int n)
/* afisarea vectorului */
{
int i;
printf("\n");
for(i=0;i<n;i++)
{
printf("%5d",a[i]);
if(((i+1) % 10)==0)printf("\n");
}
printf("\n");
}
void comb(int inf,int med,int sup)
{

91
int i,j,k,l;
int b[nmax];
i=inf;j=med+1;k=inf;
while((i<=med)&(j<=sup))
{
if(a[i]<=a[j]) {
b[k]=a[i];
i++;
}
else{
b[k]=a[j];
j++;
}
k++;
}
for(l=i;l<=med;l++)
{
/* au ramas elemente in stanga */
b[k]=a[l];
k++;
}
for(l=j;l<=sup;l++)
{
/* au ramas elemente in dreapta */
b[k]=a[l];
k++;
}
/* secventa intre inf si sup este sortata */
for(l=inf;l<=sup;l++) a[l]=b[l];
}
void divide_impera(int inf,int sup)
{
int med;
if(inf<sup) {
med=(inf+sup) / 2;
divide_impera(inf,med);
divide_impera(med+1,sup);
comb(inf,med,sup);
}

92
}
void main(void)
{
int i,n;
printf("\nIntroduceti nr.elementelor n=");
scanf("%d",&n);
printf("\nIntroduceti elementele vectorului\n");
for(i=0;i<n;i++)
{
printf("a[%d]=",i);
scanf("%d",&a[i]);
}
printf("\nVECTORUL NESORTAT\n");
afisare(n);
divide_impera(0,n-1);
printf("\nVECTORUL SORTAT\n");
0 afisare(n);
getch();
}

3. Mersul lucrării
Se vor rezolva următoarele probleme prin metoda “Branch and Bound”:
3.1 Jocul PERSPICO. 15 plăcuţe pătrate sunt încadrate într-un cadru de
dimensiune 4x4, o poziţie fiind liberă. Orice plăcuţă vecină cu această
poziţie liberă poate fi mutată în locul ei. Cele 15 plăcuţe sunt numerotate de
la 1 la 15. Se începe dintr-o stare iniţială, care corespunde unei distribuţii
oarecare a celor 15 plăcuţe şi a locului liber în cele 16 poziţii posibile.
Problema constă în a trece, folosind mutări posibile, din starea iniţială în
starea finală (fig. 3.1.1).

1 3 4 1 2 3 4
5 2 7 8 5 6 7 8
9 6 1 11 9 1 1 12
0 0 1
1 1 1 12 1 1 1
3 4 5 3 4 5

93
Exemplu de configuraţie iniţială Configuraţie finală
Figura 3.1.1

3.2 Există următorul joc: pe o linie de cale ferată se află n vagoane


unul lângă altul, numerotate cu valori distincte din mulţimea 1...n. O macara
poate lua k vagoane de pe linie şi le poate aşeza în partea dreaptă, la
sfârşitul şirului de vagoane, care apoi prin împingere ajung din nou unul
lângă altul, în noua ordine creată după operaţia respectivă.
Dându-se ordinea iniţială a vagoanelor, se cere să se determine (dacă
este posibil) numărul minim de operaţii pe care trebuie să le efectueze
macaraua pentru ca în final vagoanele să se afle în ordine crescătoare 1,
2, ..., n .

3.3 Pe malul unui râu se află 2n băştinaşi din care n sunt canibali.
Aceştia doresc să traverseze râul utilizând o barcă care poate transporta cel
mult k persoane. Dacă pe un mal sau în barcă sunt mai mulţi canibali decât
ceilalţi, atunci canibalii îi vor mânca. Cum vor reuşi să treacă toţi pe malul
opus fără să se mănânce şi fără a apela la alte persoane.

3.4 Pe un pod se află n capre care vin dintr-un sens, cu n capre care vin
din sens opus. Acestea nu se pot ocoli, însă fiecare capră poate sări peste o
singură capră din grupul opus şi desigur poate avansa dacă în faţa sa este un
spaţiu liber.
Cum reuşesc aceste capre să traverseze podul doar prin cele două
mişcări posibile (avans şi săritură).

Se vor rezolva următoarele probleme prin metoda “Divide et


Impera”:

3.5 Fiind dat un vector ce conţine elemente de tip întreg ordonate


crescător, să se scrie o funcţie de căutare a unui element dat în vector,
returnându-se poziţia sa.

3.6 Problema turnurilor din Hanoi. Se dau trei tije. Pe una dintre ele
sunt aşezate n discuri de mărimi diferite, discurile de diametre mai mici
fiind aşezate peste discurile cu diametre mai mari. Se cere să se mute aceste
discuri pe o altă tijă, utilizând tija a treia ca intermediar, cu condiţia mutării

94
a câte unui singur disc şi fără a pune un disc de diametru mai mare peste
unul cu diametru mai mic.

Lucrarea de laborator nr. 11.

METODE GENERALE DE ELABORARE A


ALGORITMILOR (III)

1. Conţinutul lucrării
În lucrare sunt prezentate metoda programării dinamice şi metodele

euristice.

2 2. Consideraţii teoretice

2.1. Metoda programării dinamice


Metoda programării dinamice se aplică pentru rezolvarea
problemelor de optim, pentru care soluţia este rezultatul unui şir de decizii
secvenţiale, dependente de cele luate anterior şi care îndeplinesc principiul
optimalităţii.
Principiul optimalităţii constă în următoarele:
Fie stările s0, s1, ..., sn, în care s0 este starea iniţială şi sn este starea
finală, obţinute prin deciziile d1, d2, ..., dn (fig. 2.1.1).

Fig. 2.1.2 Stări

Dacă di, di+1, ..., dj (1 ≤ i < j ≤ n) este un şir optim de decizii care
transformă starea si-1 în starea sj, trecând prin stările intermediare si, si+1, ...,
sj-1 şi dacă pentru oricare k ∈[i, j-1] rezultă că di, di+1, ..., dk şi dk+1 dk+2, ..., dj
sunt ambele şiruri optime de decizii de trecere din starea si-1 în starea sk,
respectiv din starea sk în starea si-1, atunci este satisfăcut principiul
optimalităţii.

95
Aplicarea metodei programării dinamice se face astfel:
 se verifică principiul optimalităţii;
 se scriu relaţiile de recurenţă obţinute din regulile de
trecere dintr-o stare în alta şi se rezolvă.
Drept exemplu se ia înmulţirea optimă a unui şir de matrici.
R=A1*A2*...*An
în care Ai (i=1,n) este de dimensiunile di*di+1. Matricea rezultat R va fi de
dimensiunile di*dn+1.
Se ştie că la înmulţirea matricelor Ai şi Ai+1 se efectuează
di*di+1*di+2 operaţii de înmulţire. Dacă matricele au dimensiuni diferite,
numărul operaţiilor de înmulţire necesare obţinerii matricei rezultat R
depinde de ordinea efectuării produselor a câte două matrice. Se cere găsirea
ordinii de asociere pentru care numărul înmulţirilor să fie minim.
Rezolvarea acestei probleme se va face astfel:
Fie Cij numărul minim de înmulţiri de elemente pentru calculul
produsului Ai*Ai+1*...*Aj pentru 1 ≤ i < j ≤ n.
Se observă că:
a) Cii=0
b) Ci,i+1= di*di+1*di+2
c) C1n este valoarea minimă căutată.
d) este verificat principiul optimalităţii.
Cij = min {Ci,k+Ck+1,j+di*dk+1*dj+1 1 ≤ k ≤ j } asocierile
fiind de forma (Ai* Ai+1*…* Ak) * (Ak+1* Ak+2*…* Aj).
Se calculează valorile Ci, i+d pentru fiecare nivel d, până se
ajunge la C1,n. Pentru a construi arborele binar care va descrie ordinea
efectuării operaţiilor, se va reţine la fiecare pas indicele k care realizează
minimul, adică modul de asociere a matricelor. Vârfurile arborelui vor
conţine limitele subşirului de matrice care se asociază; rădăcina va conţine
(1,n), iar un subarbore care conţine în rădăcină (i, j) va avea descendenţi pe
(i, k) şi (k+1, j), unde k este valoarea pentru care se realizează optimul cerut.
În continuare este prezentat programul comentat de
rezolvare a acestei probleme.

#include <stdio.h>
#include <conio.h>
#include <alloc.h>
#define nmax 10
typedef struct tip_nod{

96
long ind1,ind2;
struct tip_nod *stg,*dr;
} TIP_NOD;
/*Inmultirea optima a unui sir de matrici A1*A2*...*An */
/* de dimensiuni d1*d2,d2*d3,...,dn*d(n+1) */
void prod_matr(int n,long c[nmax][nmax],int d[nmax+1])
{
int i,j,k,l,poz;
long min,val;
for(i=1;i<=n;i++)
c[i][i]=0;
for(l=1;l<=n-1;l++)
for(i=1;i<=n-l;i++)
{
j=i+l;
min=0x7fffffff;
for(k=i;k<=j-1;k++)
{
val=c[i][k]+c[k+1][j]+
(long)d[i]*d[k+1]*d[j+1];
if(val<min) {
min=val;poz=k;
};
};
c[i][j]=min;
c[j][i]=poz;
}
}
TIP_NOD *constr_arbore(TIP_NOD *p,int i,int j,long c[nmax]
[nmax])
{
p=(TIP_NOD *)malloc(sizeof(TIP_NOD));
p->ind1=i;p->ind2=j;
if(i<j) {
p->stg=constr_arbore(p->stg,i,c[j][i],c);
p->dr=constr_arbore(p->dr,c[j][i]+1,j,c);
}
else {
p->stg=0; p->dr=0;

97
};
return p;
}
void postordine(TIP_NOD *p,int nivel)
{
int i;
if(p!=0) {
postordine(p->stg,nivel+1);
postordine(p->dr,nivel+1);
for(i=0;i<=nivel;i++) printf(" ");
printf("(%ld,%ld)\n",p->ind1,p->ind2);
}
}

void main(void)
{
int i,j,n;
long c[nmax][nmax];
int d[nmax+1]; /* dimensiunile matricelor */
TIP_NOD *rad;
printf("\nIntroduceti nr.matricelor n=");
scanf("%d",&n);
printf("\nIntroduceti dimensiunile matricelor\n");
for(i=1;i<=n+1;i++)
{
printf("\nDimensiunea d[%d]=",i);
scanf("%d",&d[i]);
};
prod_matr(n,c,d);
/* Matricea c rezultata in urma calculelor */
printf("\nMATRICEA C\n");
for(i=1;i<=n;i++)
{
for(j=1;j<=n;j++)
printf("%6ld",c[i][j]);
printf("\n");
}
printf("\nNR.MINIM DE INMULTIRI = %ld",c[1]
[n]);

98
getch();
rad=0;
rad=constr_arbore(rad,1,n,c);
printf("\nARBORELE IN POSTORDINE\n");
postordine(rad,0);
getch();
}

2.2. Metode euristice


Prin algoritm euristic se va înţelege un algoritm care furnizează o
soluţie aproximativă, nu neapărat optimală, dar care poate fi implementată
uşor şi dă rezultate în timp util, de ordin polinomial.
Metodele euristice se aplică pentru rezolvarea unor probleme, la care
nu se ştie dacă admit optim şi care acceptă drept rezultate nu tocmai
optimul.
Una din idei, este descompunerea procesului de determinare a
soluţiei în mai multe procese succesive, cărora li se caută soluţia optimală.
Idea nu conduce la obţinerea în final în mod sigur a unei soluţii optime,
întrucât optimizarea locală nu implică în general optimizarea globală.
În problemele practice se pot căuta mai multe soluţii aproximative,
din care să se aleagă cea mai bună.
Drept exemplu se prezintă problema comis – voiajorului: Se dă un
graf neorientat G =(X, Γ ) în care toate nodurile sunt unite între ele printr-o
muchie, căreia i se asociază un cost strict pozitiv. Se cere determinarea unui
ciclu, care să înceapă dintr-un nod i, să treacă exact o dată prin toate
nodurile şi să se întoarcă în nodul iniţial. Se cere ca ciclul găsit să aibă un
cost minim.
Soluţia optimală a problemei se găseşte într-un timp de ordin
exponenţial prin metoda backtracking.

Algoritmul euristic prezentat mai jos bazat pe metoda Greedy


necesită un timp polinomial. Rezolvarea constă în următoarele:
Dacă (v1, v2, ..., vk) este calea deja construită, atunci:
 dacă (v1, v2, ..., vk) = = X, se adaugă muchia (vk, v1) şi
ciclul este încheiat;
 dacă (v1, v2, ..., vk) ≠ X, se adaugă muchia care leagă vk
de un vârf aparţinând lui x, dar neinclus în cale.

99
Pentru că ciclul este o cale închisă, putem considera ca punct de
plecare oricare nod. De aceea se pot alege nişte noduri de start, se determină
pentru fiecare ciclul corespunzător după strategia descrisă şi se reţine ciclul
de cost minim dintre ele.
În continuare este prezentat programul corespunzător.
#include <stdio.h>
#include <conio.h>
#define nmax 10
/*Problema comis_voiajorului */
void comis_voiajor(int n,int c[nmax][nmax],
int i,int ciclu[nmax+1],int *cost)
/* n este nr.nodurilor;c este matricea costurilor;
i este nodul de start;ciclu contine nodurile din ciclu;
cost este costul ciclului */
{
int p[nmax];
int k,v,j,vmin;
int costmin;
for(k=1;k<=n;k++)
p[k]=0;
*cost=0;
p[i]=1;ciclu[1]=i;
v=i; /*nodul curent */
for(k=1;k<n;k++) /* se adauga pe rand n-1 muchii */
{
costmin=0x7fff;
/*gasirea muchiei de cost minim care are
nodul de origine v*/
for(j=1;j<=n;j++)
if((p[j]==0)&&(c[v][j]<costmin))
{
costmin=c[v][j];
vmin=j;
}
*cost=*cost+costmin;
ciclu[k+1]=vmin;
p[vmin]=1;
v=vmin;
}

100
ciclu[n+1]=i;
*cost=*cost+c[v][i];
}
void main(void)
{
int i,j,n;
int cost_ciclu;
int ciclu[nmax+1];
int c[nmax][nmax];
printf("\nNr.nodurilor grafului = ");
scanf("%d",&n);
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
c[i][j]=0x7fff;
printf("\nIntroduceti costurile muchiilor care au ca origine");
printf("\n nodul i si celalalt nod mai mare ca i;daca nu mai
sunt");
printf("\n astfel de noduri se introduce 0 \n");
for(i=1;i<=n-1;i++)
{
while(1)
{
printf("Nodul adiacent lui %d =",i);
scanf("%d",&j);
if(j!=0){
printf("Costul muchiei (%d,
%d)=",i,j);
scanf("%d",&c[i][j]);
c[j][i]=c[i][j];
}
else break;
}
}
i=1;
comis_voiajor(n,c,i,ciclu,&cost_ciclu);
printf("\nCOSTUL CICLULUI =%d\n",cost_ciclu);
printf("\nCICLUL=");
for(i=1;i<=n+1;i++)
printf("%3d",ciclu[i]);

101
getch();
}

4. Mersul lucrării
Se vor rezolva prin metoda programării dinamice următoarele probleme:

3.1 Determinarea căilor de cost minim între oricare două vârfuri ale
unui graf orientat prin algoritmul lui Floyd. (problema 3.2 din lucrarea 7).

3.2 La un concurs de tir, ţinta este alcătuită din cercuri concentrice,


numerotate din exterior spre interior. Fiecărui sector determinat de două
cercuri succesive îi este ataşată o valoare strict pozitivă, reprezentând
punctajul primit de concurent în cazul lovirii acestui sector.
Să se determine numărul minim de lovituri pe care trebuie să le
execute un concurent pentru a obţine exact k puncte.

3.3 Se dau două numere naturale A şi B şi un vector v care conţine n


numere naturale. Să se determine dacă se poate trece din A în B, ştiind că
singurele operaţii permise sunt:
a) Adunarea la A a oricâte numere din vectorul v;
b) Scăderea din A a oricâte numere din vectorul v;
Fiecare număr poate fi adunat, respectiv scăzut de mai multe
ori.
Dacă răspunsul la întrebare este afirmativ, se cere numărul
minim de operaţii prin care se poate trece din A în B.

3.4 Se dau n segmente aflate pe o dreaptă. Să se determine cardinalul


maxim al unei submulţimi de segmente care are proprietăţile:
a) oricare două numere din submulţime nu se intersectează;
b) submulţimea conţine primul element.

3.5 Pe o creangă de măr, se află n mere, fiecare caracterizat prin


distanţa hi în cm de la pământ până la poziţia în care se află şi prin
greutatea sa gi în grame. Un culegător doreşte să culeagă o cantitate
exprimată în grame cât mai mare de mere. După ce este cules un măr
întreaga creangă devine mai uşoară şi se ridică în sus cu x cm. Culegătorul
ajunge doar la merele aflate la o înălţime mai mică sau egală cu d cm. Să se

102
determine greutatea maximă de mere care poate fi culeasă şi ordinea în care
sunt culese merele.
Se citesc: n, x, d şi (gi, hi) i=1...n.
Să se rezolve prin metode euristice următoarele probleme:
3.6 Să se găsească maximul unei funcţii f(x) în intervalul [a, b].

3.7 Se dă un graf neorientat cu n noduri. Se cere să se determine


numărul minim de culori necesare pentru a colora nodurile grafului dat,
astfel încât două vârfuri legate printr-o muchie să fie colorate cu culori
diferite.

3.8 Se dă un graf neorientat cu n noduri. Se cere să se determine o


submulţime maximă de noduri cu proprietatea că oricare două noduri din ea
nu sunt legate printr-o muchie.

103
Lucrarea de laborator nr. 12.

ALGORITMI FUNDAMENTALI DE
SORTARE

1. Conţinutul lucrării
În lucrare sunt prezentaţi algoritmii de sortare prin numărare, prin
inserare (directă şi shellsort), prin interschimbare (metoda bulelor şi
quicksort), prin selecţie şi interclasare.

2. Consideraţii teoretice
Sortarea constă în ordonarea crescătoare sau descrescătoare a
elementelor unui vector A =(a0, a1, ..., an-1). În practică, problema se
întâlneşte sub forma sortării unor articole după cheie, cheia fiind un câmp
din articol.

2.1. Sortarea prin numărare


Metoda sortării prin numărare constă în găsirea pentru fiecare
element a[i], a numărului de elemente din vector, mai mici ca el. Numerele
obţinute sunt memorate într-un vector c; elementele vectorului de sortat a,
sunt iniţial atribuite vectorului b. Pe baza vectorului c, elementele lui b vor
fi aranjate în vectorul a.
Dezavantajul metodei constă în utilizarea a doi vectori de lucru.
Timpul de prelucrare este de ordinul O(n2). În programul prezentat la
paragraful 2.6., funcţia sort_numărare redă algoritmul de mai sus.

2.2. Sortarea prin inserare


Sortarea prin inserare constă în următoarele:
Fie secvenţa a0 < a1 < a2 ... < aj-1.
Inserarea elementului aj în această secvenţă constă în compararea lui aj
cu aj-1, aj-2 ... până când se ajunge la ai < aj; se inserează aj după ai, cu
menţiunea că în prealabil elementele aj-1, aj-2, ..., ai+1 au fost deplasate spre
dreapta cu o poziţie. Metoda care procedează întocmai se numeşte inserare
directă.

104
Metoda inserării binare constă în căutarea binară a locului unde
trebuie inserat elementul aj, având în vedere că secvenţa a0, a1, ..., aj-1 este
ordonată crescător.
Tot din categoria inserării face parte şi metoda shell. Pentru a explica
metoda se introduce noţiunea de h-sortare. Numim h-sortare, sortarea prin
inserare directă a următoarelor secvenţe:
a0, ah, a2h, ....
a1, a1+h, a1+2h, ....
.
.
.
ah, a2h, a3h, ....
h este numit increment.
Metoda shell constă în alegerea unui număr de k incremenţi
h1 > h2 > h3 > ... > hk = 1
şi de a face o h1 – sortare, urmată de o h2 – sortare ş. a. m. d., în final o 1 –
sortare.
Performaţele metodei sunt strâns legate de alegerea incremenţilor.
În exemplul din paragraful 2.6., funcţia sort_inserare_directă redă
algoritmul de inserare directă, iar funcţia shell_sort redă algoritmul metodei
shell.
Timpul de prelucrare în cadrul metodei de inserare directă este de
ordinul O(n2), iar al metodei shell de ordinul O(n *lnn). De asemenea
timpul de prelucrare în cadrul metodei de inserare prin căutare binară este
de ordinul O(n *lnn).

2.3. Sortarea prin interschimbare


Sortarea prin interschimbare constă în modificări succesive de forma
ai ↔ aj, până când elementele vectorului apar în ordine crescătoare.
Din această categorie fac parte metoda bulelor şi metoda quicksort.
Metoda bulelor constă în compararea ai cu ai+1; dacă ordinea e bună se
compară ai+1 cu ai+2; dacă ordinea nu e bună se interschimbă ai cu ai+1 şi apoi
se compară ai+1 cu ai+2. După prima parcurgere a vectorului, pe ultima poziţie
ajunge elementul având valoarea cea mai mare, după a doua parcurgere
ajunge următorul element ş. a. m. d.
Timpul de prelucrare este de ordinul O(n2).
Sortarea rapidă quicksort a fost creată de Hoare şi foloseşte metoda
„Divide Et Impera”.

105
Principiul metodei este următorul: se selectează un element din tablou
numit pivot şi se rearanjează vectorul în doi subvectori, astfel încât cel din
stânga are toate elementele mai mici decât pivotul, iar cel din dreapta mai
mare ca pivotul. Procedeul se reia în subtabloul din stânga şi apoi în cel din
dreapta ş. a. m. d. Procedeul se termină când se ajunge cu pivotul în
extremitatea stângă şi respectiv dreaptă a tabloului iniţial.
Timpul de prelucrare a metodei quicksort este de ordinul O(n* lnn).
În exemplul de la paragraful 2.6., funcţia sort_metoda_bulelor redă
algoritmul de sortări prin metoda bulelor, iar funcţiile quicksort şi quick
redau algoritmul sortării prin metoda quicksort.

2.4. Sortarea prin selecţie


Sortarea prin selecţie directă constă în următoarele: se află minimul aj
dintre a0, a1, ..., an-1 şi se aduce pe poziţia zero în vector prin interschimbarea
a0 cu aj; apoi procedeul se repetă pentru a1, a2, ..., an-1 ş. a. m. d.
Timpul de prelucrare este de ordinul O(n2).
In exemplul de la paragraful 2.6., funcţia sort_selecţie redă algoritmul
de sortare prin metoda selecţiei directe.

106
2.5. Sortarea prin interclasare
Metoda sortării prin interclasare a fost prezentată în cadrul lucrării nr.
10, drept exemplificare a metodei de elaborare a algoritmilor „Divide et
Impera”

2.6. Exemplu
În programul de mai jos sunt implementaţi algoritmii de sortare
prezentaţi în paragrafele 2.1. –2.5.

#include <stdio.h>
#include <conio.h>
#define nmax 100
/* ALGORITMI DE SORTARE */

void citire_vector(int n,float a[nmax],float b[nmax])


/* CITIRE ELEMENTE VECTOR */
{
int i;
printf("\nIntroduceti elementele vectorului de sortat\n");
for(i=0;i<n;i++)
{
printf("a[%d]=",i);
scanf("%f",&a[i]);
b[i]=a[i];
}
}

void reconstituire(int n,float a[nmax],float b[nmax])


/* RECONSTITUIREA INITIALA A VECTORULUI a */
{
int i;
for(i=0;i<n;i++)
a[i]=b[i];
}

107
void afisare(int n,float a[nmax])
/* AFISARE VECTOR */
{
int i;
for(i=0;i<n;i++)
{
printf("%8.2f",a[i]);
if(((i+1) % 10)==0) printf("\n");
}
}

void sort_numarare(int n,float a[nmax])


/* SORTAREA PRIN NUMARARE */
{
int i,j;
float b[nmax];
int c[nmax];
for(i=0;i<n;i++)
{
c[i]=0; /* initializare vector de numarare */
b[i]=a[i]; /* copiere vector a in b */
}
/* numarare */
for(j=1;j<n;j++)
for(i=0;i<=j-1;i++)
if(a[i]<a[j]) c[j]++;
else c[i]++;
/* rearanjare vector a */
for(i=0;i<n;i++)
a[c[i]]=b[i];
}

void sort_inserare_directa(int n,float a[nmax])


/* SORTARE PRIN INSERARE DIRECTA */
{
int i,j;
float x;
for(j=1;j<n;j++)
{

108
x=a[j]; i=j-1;
while((i>=0) && (x<a[i]))
{
a[i+1]=a[i];
i=i-1;
}
a[i+1]=x;
}
}

void shell_sort(int n,float a[nmax])


/* SORTAREA PRIN METODA SHELL */
{
int i,j,incr;
float x;
incr=1;
while(incr<n)
incr=incr*3 + 1;
while(incr>=1)
{
incr=incr / 3;
for(i=incr;i<n;i++)
{
x=a[i];j=i;
while(a[j-incr]>x)
{
a[j]=a[j-incr];
j=j-incr;
if(j<incr) break;
};
a[j]=x;
}
}
}

void sort_metoda_bulelor(int n,float a[nmax])


/* SORTAREA PRIN INTERSCHIMBARE-METODA
BULELOR */
{

109
int i,j,gata;
float x;
j=0;
do{
gata=1;
j=j+1;
for(i=0;i<n-j;i++)
if(a[i]>a[i+1]){
gata=0;
x=a[i];a[i]=a[i+1];a[i+1]=x;
};
}while(gata==0);
}

void quick(int prim,int ultim,float a[nmax])


{
int i,j;
float pivot,x;
i=prim;j=ultim;
pivot=a[(prim + ultim) / 2];
do{
while(a[i]<pivot) i++;
while(a[j]>pivot) j--;
if(i<=j) {
x=a[i];a[i]=a[j];a[j]=x;
i++;j--;
};
}while(i<=j);
if(prim<j) quick(prim,j,a);
if(i<ultim) quick(i,ultim,a);
}

void quicksort(int n,float a[nmax])


/* SORTAREA RAPIDA QUICKSORT */
{
quick(0,n-1,a);
}

void sort_selectie(int n,float a[nmax])

110
/* SORTAREA PRIN SELECTIE */
{
int i,j,poz;
float x;
for(i=0;i<n-1;i++)
{
x=a[i];poz=i;
for(j=i+1;j<n;j++)
if(a[j]<x) {
x=a[j];
poz=j;
}
a[poz]=a[i];a[i]=x;
}
}

void main(void)
{
int i,n;
float a[nmax],b[nmax];
clrscr();
printf("\nIntroduceti nr.elementelor n=");
scanf("%d",&n);
citire_vector(n,a,b);
printf("\nVECTORUL NESORTAT\n");
afisare(n,a);
printf("\nVECTORUL SORTAT PRIN METODA
NUMARARII\n");
sort_numarare(n,a);
afisare(n,a);
getch();
printf("\nVECTORUL SORTAT PRIN METODA
INSERARII DIRECTE\n");
reconstituire(n,a,b); /* a devine vectorul nesortat
initial */
sort_inserare_directa(n,a);
afisare(n,a);
getch();

111
printf("\nVECTORUL SORTAT PRIN METODA
SHELL\n");
reconstituire(n,a,b); /* a devine vectorul nesortat
initial */

shell_sort(n,a);
afisare(n,a);
getch();
printf("\nVECTORUL SORTAT PRIN METODA
BULELOR\n");
reconstituire(n,a,b); /* a devine vectorul nesortat
initial */
sort_metoda_bulelor(n,a);
afisare(n,a);
getch();
printf("\nVECTORUL SORTAT PRIN METODA
QUICKSORT\n");
reconstituire(n,a,b); /* a devine vectorul nesortat
initial */
quicksort(n,a);
afisare(n,a);
getch();
printf("\nVECTORUL SORTAT PRIN METODA
SELECTIEI DIRECTE\n");
reconstituire(n,a,b); /* a devine vectorul nesortat
initial */
sort_selectie(n,a);
afisare(n,a);
getch();
}

3. Mersul lucrării
3.1. Fie tabloul de întregi:
59174320
Ordonaţi, fără program, acest şir în ordinea crescătoare a
elementelor, folosind cele trei metode ‘elementare’ de sortare: inserţia,
metoda bulelor şi selecţia, arătând la fiecare pas al metodei care este noua
configuraţie a tabloului. Câte comparaţii şi câte mutări de elemente au avut

112
loc pentru fiecare metodă? Care metodă este cea mai potrivită pentru acest
tablou de întregi? Care aranjare a tabloului ar fi fost cea mai defavorabilă?

3.2. Descrieţi algoritmul de sortare prin inserare, la care modificaţi


căutarea liniară cu o căutare binară (în cadrul subtabloului din stânga
elementului curent). Calculaţi şi pentru acest nou algoritm (numit sortare
prin inserţie cu căutare binară) numărul de paşi, numărul de comparaţii şi
mutări ale elementelor din listă pentru exemplul de la problema 3.1. Devine
acest algoritm mai performant?

3.3. Pentru tabloul de elemente reale:


-3.1, 0.1, 1.2, –5.7, -0.3, 6,
aplicaţi metodele de ordonare shell-sort şi quick-sort, pentru fiecare pas
reprezentând noua configuraţie a tabloului. Număraţi comparaţiile şi
mutările de elemente pe care le-aţi efectuat. Care algoritm este mai eficient
pentru acest tablou?

3.4. Analizaţi algoritmul de sortare quick-sort şi înlocuiţi varianta


prezentată ce foloseşte recursivitatea, cu o variantă nerecursivă. Ce variabile
trebuiesc iniţializate, cu ce valori şi unde are loc saltul în program pentru a
se elimina apelul recursiv?

3.5. Care algoritm dintre cei prezentaţi are nevoie de spaţiu de


memorie cel mai mare?

3.6. Scrieţi un algoritm care combină algoritmii de sortare quick-


sort pentru obţinerea de partiţii nesortate de lungime m, apoi utilizaţi
inserţia directă pentru terminarea sortării.

3.7. Adaptaţi algoritmul quick-sort pentru a determina într-un şir de


lungime n cu elemente întregi, al m-lea mai mic element .

3.8. Să se sorteze un număr de n elemente numerotate de la 1 la n


caracterizate prin anumite relaţii de precedenţă notate (j, k), având
semnificaţia că elementul j precede ca ordine elementul k. Sortarea trebuie
să aibă ca rezultat o listă în care oricare element k este devansat de
predecesorul său (sortarea topologică).

113
3.9. Să se descrie algoritmul care realizează următoarele acţiuni
specifice unui examen de admitere:
 efectuarea calculului mediilor candidaţilor la examenul de
admitere
 repartizarea celor admişi după opţiuni (se consideră că există m
opţiuni, fiecare cu un număr dat de candidaţi admişi) şi afişarea listei.
 afişarea în ordinea descrescătoare a mediilor a tuturor
candidaţilor neadmişi.
Se consideră că examenul constă din două probe, la medii egale
(calculate cu două zecimale, prin trunchiere), departajarea se face după nota
la prima probă (dacă egalitatea se menţine, se ia în considerare a doua),
admiţându-se depăşirea numărului de locuri în caz de egalitate şi după acest
criteriu.

114
Bibliografie:

115

You might also like