Sunteți pe pagina 1din 76

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

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

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;
}
- 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ă */
}
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 */
}
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.
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.
Fig.2.7.1. Model de coadă

Se introduce
un element
nou
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.

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 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 +, -, *, /.

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.).
Ca şi la lista simplu înlănţuită, principalele operaţii sunt:

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


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

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

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ă.
Î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);

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

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 continuare, este prezentat în

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

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;
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;
}
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:
a) î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;
}
b) 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;
}
c) î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;
d) 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;
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;

b) ş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 */
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.

Fig. 3.2. Modelele de liste pentru problema 3.2.


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.

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

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


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.
/* 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);
}
}
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ă;
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);
}
}
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);
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;
 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.

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.).
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.
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;
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 */
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.
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)
{
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 (c 1 < 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:
cu condiţia:
n n


i 1
L p n

p
i
  q n 1
hj i0 
j q
h
'
i
i j
i 1 j 0
Se numeşte arbore optimal, un arbore binar de căutare care pentru anumite valori p i, 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 p i ş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
k i 1
k
 q
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:
c q
ii
pentru 0  i  n
ii

c  q min (c  c )
ij ij
i k  j
i , k 1 kj

Fie rij valoarea lui k pentru care se obţine minimul din relaţia lui c ij. 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(n 3). 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]];
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);
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) {
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)


{
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;
}
/* 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);
}
}

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

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


/* 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);
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ă.

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.
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
2.1. 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.
2.2. 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 :


Matricea de adiacenţe este simetrică pentru grafuri neorientate şi nesimetrică pentru cele orientate.

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


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

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

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.
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;
- 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]))
{
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*/
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.
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 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.
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:
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;
}
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];
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;
}
î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ă.

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.
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.
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.
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.
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;
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 */
}
}
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 φ(x 1, x2, … ,xn)
între elementele sale.
Relaţia φ(x1, x2, … ,xn) se numeşte relaţie internă, mulţimea S=S 1xS2x…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 x k+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 x k Є Sk şi se reia verificarea condiţiei φ `. Dacă mulţimea de
valori xk s-a epuizat, se revine la alegerea altei valori pentru x k-1 ş.a.m.d. Această micşorare a lui k dă numele
metodei, ilustrând faptul că atunci când nu se 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++)
{
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;
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;
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++)
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 n 1+n2+...+nm elemente, ordonate crescător.
Se ştie că interclasarea a doi vectori care conţin n 1, 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 tp i şi un timp de terminare tfi (tpi < tfi).
Dacă este selectată activitatea i, ea se desfăşoară pe durata [tp i, 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.

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 s 0 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”.

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 )    dacă x este vârf terminal

min c(y) y este vârf terminal

   subarborelui de rădăcină x

 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ă.
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, 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 =(a 1, 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
a , a
prelucrarea secvenţei p p 1
,..., a q  se poate face prelucrând secvenţele a , a
p p 1

,..., a m şi

a , a ,..., a 
m 1 m2 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);

Variabilele şi funcţiile din funcţia DIVIDE_IMPERA au următoarele semnificaţii:


eps – este lungimea maximă a unei secvenţe a p, ap+1, ...,aq pentru care prelucrarea se poate
face direct;
m – este indicele intermediar în care secvenţa a p, 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)
{
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);
}
}
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 10 11 9 10 11 12
13 14 15 12 13 14 15

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 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).
Dacă di, di+1, ..., dj (1 i < j  n) este un şir optim de decizii care transformă starea s i-1 în starea sj,

Fig. 2.1.2 Stări

trecând prin stările intermediare s i, si+1, ..., sj-1 şi dacă pentru oricare k [i, j-1] rezultă că d i, di+1, ..., dk şi dk+1
dk+2, ..., dj sunt ambele şiruri optime de decizii de trecere din starea s i-1 în starea sk, respectiv din starea sk în
starea si-1, atunci este satisfăcut principiul optimalităţii.
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 d i*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 A i*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 (A i* Ai+1*…* Ak) *
(Ak+1* Ak+2*…* Aj).
Se calculează valorile Ci, i+d pentru fiecare nivel d, până se ajunge la C 1,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{
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;
};
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]);
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ă v k de un vârf aparţinând lui x, dar
neinclus în cale.
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;
}
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]);
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 h i în cm de la pământ până la
poziţia în care se află şi prin greutatea sa g i î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
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.
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 =(a 0, 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(n 2). Î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 a j 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 a j-1, aj-2, ..., ai+1 au fost
deplasate spre dreapta cu o poziţie. Metoda care procedează întocmai se numeşte inserare directă.
Metoda inserării binare constă în căutarea binară a locului unde trebuie inserat elementul a j, 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(n 2), 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 a i  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 a i cu ai+1; dacă ordinea e bună se compară a i+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”.
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 a j dintre a0, a1, ..., an-1 şi se aduce
pe poziţia zero în vector prin interschimbarea a 0 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.
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];
}

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++)
{
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 */
{
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])


/* 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();
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 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ă).

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

S-ar putea să vă placă și