Documente Academic
Documente Profesional
Documente Cultură
1
4.4.Arbori de acoperire minimi………………………………………………………………………............
4.4.1.Algoritmul lui Kruskal de determinare a unui arbore de acoperire
minim .............................................................................................................
5. ÎNTREBĂRI RECAPITULATIVE………………………………………………………….............…............
Bibliografie....................................................................................................................
2
Algoritmi si structuri de date
Autori: Cristian Bologa, G.C. Silaghi
1. NOŢIUNI INTRODUCTIVE; ALGORITMI DE SORTARE
1.1. Definiţii
Un algoritm este o procedură de calcul bine definită care primeşte o valoare sau o
mulţime de valori ca date de intrare şi produce o valoare sau mulţime de valori ca date de
ieşire. Este deci un şir de paşi care transformă datele de intrare în date de ieşire.
Putem lua ca exemplu definirea problemei de sortare a unui şir de numere.
Datele de intrare (inputul) îl reprezintă o secvenţă de n numere (a1, a2, … an).
Datele de ieşire (outputul) îl reprezintă o permutare (sau o reordonare) (a1’, a2’,
…an ) a secvenţei de numere furnizată la intrare astfel încât a1’< a2’< …<an’.
’
Presupunem că procesorul poate lucra cu date de tip întreg sau flotant, fără ca
precizia să fie de larg interes (în demersul nostru de analiză a algoritmului)
3
Eficiența unui algoritm se referă la resursele de calcul și memorie necesare pentru
execuția algoritmului. Practic este un studiu teoretic al performanţei (în primul rând) şi al
resurselor utilizate. Progresele tehnologice din ultima perioadă fac ca importanţa mărimii
memoriei solicitate de un algoritm să scadă ca importanţă. Ceea ce trebuie să intereseze
programatorul este reducerea timpului de execuţie al programului. Există aplicaţii “real
time” la care constrângerile de timp sunt cruciale.
Un algoritm la care timpul de execuţie nu depinde de dimensiunea problemei este
un algoritm de timp constant a cărui complexitate se notează cu O(1). Timpul de execuţie al
unui algoritm de complexitate O(n) creşte liniar (direct proporţional) cu valoarea lui n. Un
algoritm liniar are complexitatea O(n), unul pătratic O(n2) iar unul cubic O(n3). Un algoritm
se numeşte polinomial dacă are o complexitate egală cu O(p(n)) unde p(n) este o funcţie
polinomială. Altă clasă de algoritmi sunt cei exponenţiali.
Un algoritm este mai eficient decât altul dacă are o rată de creştere mai mică,
pentru cazul cel mai defavorabil. Vorbim de eficienţa asimptotică a algoritmilor atunci când
rata de creştere a timpului de execuţie devine esenţială considerând mărimi mari ale setului
de date de intrare.
1.3. Sortarea
Sortarea este o operaţie des întâlnită în rezolvarea problemelor de natură
algoritmică. Problema sortării unei mulţimi de obiecte se poate reduce la problema sortării
cheilor asociate acestora. După ce cheile sunt sortate, folosind informaţia de asociere (care
leagă cheia de obiectul căreia îi aparţine), se pot rearanja obiectele în ordinea în care au
fost aranjate cheile lor.
Dându-se o secvenţă de elemente caracterizate de valorile e1, e2, ... en E, operaţia
de sortare este operaţia de găsire a unei permutări a secvenţei ei1, ei2, ... ein astfel încât eij
eik, oricare ar fi ij ik, unde “” este o relaţie de ordine definită peste mulţimea E.
O metodă de sortare se spune că este stabilă dacă după sortare, ordinea relativă a
elementelor cu chei egale coincide cu cea iniţială, element esenţial în special în cazul în care
se execută sortarea după mai multe chei.
O cerinţă fundamentală care se formulează faţă de metodele de sortare a
tablourilor se referă la utilizarea cât mai economică a zonei de memorie disponibile. Astfel,
algoritmii care utilizează doar zona de memorie alocată tabloului, fără a fi necesar un
tablou suplimentar se numesc sortări "în situ".
Luăm câte un număr din șirul inițial și îl plasăm în şirul sortat la poziția
corespunzătoare
4
Figura 1.1 Algoritmul de sortare prin inserţie
5
Divide –şirul A de n elemente se împarte în două subşiruri de n/2 elemente
(în cazul în care n este impar dimensiunea primului şir este cu 1 mai mare
decât dimensiunea celui de al doilea şir)
Combine –se combină prin interclasare cele două şiruri sortate obţinute la
pasul Conquer, rezultând un singur şir sortat
6
Un heap (o movilă) este un vector care poate fi vizualizat sub forma unui arbore
binar aproape complet cu proprietatea că cheia fiecărui nod din arbore este mai mare decât
cheile descendenţilor (deci fiecare nod are o cheie mai mică sau egală cu cea a tatălui său).
Ultimul rând al arborelui se completează de la stânga la dreapta.
Un heap poate fi reprezentat sub forma unui arbore binar sau sub forma unui
vector. Astfel, dacă v este un vector care conţine reprezentarea unui heap avem
următoarea proprietate:
Elementele v[left]..v[n] îndeplinesc condiţia de structură a heapului:
i>left avem: v[i]>v[2*i], dacă 2*in, respectiv
v[i]>v[2*i+1] dacă 2*i+1n.
Evident, pentru valori ale lui i mai mari decât n/2 nu se pune problema îndeplinirii
condiţiilor de mai sus.
Figura 1.4 prezintă un heap descris atât sub forma unui arbore cât şi sub formă de
şir de elemente.
Figura 1.4 Un exemplu de heap descris printr-un arbore respectiv printr-un şir
Ca să putem folosi Heap Sort va trebui să transformăm şirul de intrare într-un heap.
Vom crea iniţial procedura MAX-HEAPIFY(A,i) care primeşte ca şi parametri un vector A şi
un indice i din vector. Atunci când apelăm MAX-HEAPIFY se presupune că subarborii având
ca rădăcini nodurile Left(i) şi Right(i) sunt heapuri. Dacă elementul A(i) este mai mic decât
cel puţin unul din descendenţii Left(i) şi Right(i), înseamnă că acest element nu respectă
proprietatea de heap (deci nu avem un heap şi va trebui transformat ca să devină heap).
Sarcina procedurii MAX-HEAPIFY este să “scufunde” în heap valoarea A(i) astfel încât
subarborele care are în rădăcină valoarea elementului de indice i sa devină un heap.
Figura 1.5 prezintă algoritmul MAX-HEAPIFY:
7
Figura 1.6. Algoritmul de creare a unui heap
Prezentăm în figura 1.7 algoritmul de sortare utilizând un heap bazat pe algoritmii
BUILD-MAX-HEAP şi MAX-HEAPIFY.
Conquer: Cele două subşiruri A[p..q-1] şi A[q+1..r] sunt sortate prin apeluri
recursive ale algoritmului de sortare rapidă
Combine: Cele două subşiruri sunt sortate pe loc, nu este nevoie de nici o
combinare, şirul A[p..r] este ordonat.
8
Prezentăm în figura 1.9 algoritmul procedurii PARTITION care realizează
partiţionarea şirului:
9
2. TEHNICI DE PROGRAMARE: PROGRAMARE DINAMICᾸ; GREEDY; BACKTRACKING
Paşii 1->3 sunt baza unei abordări de tip programare dinamică. Pasul 4 poate fi
omis dacă se doreşte doar calculul unei singure soluţii optime.
10
2.1.1. Problema tăierii barei de oţel
Compania Sterling Enterprises cumpără bare de oţel lungi şi le taie în bucăţi mai
scurte pe care apoi le revinde. Se cunoaşte preţul pi cu care compania vinde o bară de
lungime i (i este o valoare întreagă). Se cere determinarea locurilor unde se vor face
tăieturile la o bară de lungime n, astfel încât să se obţină câştigul maxim rn.
Input: n ca fiind lungimea iniţială a barei precum şi preţurile pi pentru i=1..n. Deci p i
este preţul unei bare de lungime i.
Output: Soluţia optimă va însemna tăierea barei în k bucăţi, deci obţinerea partiţiei
i1, i2, … ik cu i1+i2+…ik=n astfel încât suma rn=pi1+p i2+…+pik să fie maximă
Observaţii: Tăierile nu costă nimic (sunt gratis) iar lungimile sunt întotdeauna numere
întregi.
Având în vedere că din 1 în 1 avem posibilitatea de a tăia sau nu bara, înseamnă că
pentru n-1 poziţii avem posibilitatea de a tăia sau nu. Numărul maxim de partiţii posibil
pentru o bară de lungime n este deci 2n-1.
În figura 2.1 prezentăm un exemplu de cum vom tăia bare de lungime 1, 2, ...10
pornind de la preţul unei bare de lungime 1, 2, … 10.
Figura 2.1 Un exemplu de tăiere optimă a unei bare de oţel de diverse lungimi (1->10)
Problema tăierii barei de oţel este o problemă cu structură optimă: soluţia optimă a
acestei probleme incorporează soluţii optime ale subproblemelor.
Rezolvarea problemei se va baza pe o soluţie recursivă. Descompunerea recursivă a
problemei tăierii barei de oţel se va face astfel:
Se taie o piesă de lungime i
rn= max1<=i<=n(pi+rn-i)
11
Va trebui să maximizăm profitul la tăierea bucăţii de lungime i respectiv bucăţii de
lungime n-i care a rămas. Recursivitatea continuă doar pentru a doua bucată care a rămas.
Abordarea în algoritmul descris în figura 2.2 este de tip top-down.
Linia 5 este echivalent cu a scrie:
rn =max(pn, r1+rn-1, r2+rn-2, …, r n-1+ r1)
unde pn este echivalentul de a nu tăia deloc bara.
Problema este că algoritmul CUT-ROD este foarte lent. Pentru n=40 rezolvarea va
ţine cel puţin câteva minute, poate chiar o oră. La fiecare creştere a lui n cu 1, timpul de
execuţie se va dubla. De ce este ineficient acest CUT-ROD? Pentru că el se autoapelează de
mai multe ori cu aceeaşi parametri, rezolvând astfel aceleaşi subprobleme în mod repetat
(adică acelaşi apel CUT-ROD(p,j) se face de mai multe ori ceea ce este ineficient).
Încercăm să elaborăm un algoritm astfel încât fiecare subproblemă să fie rezolvată
o singură dată. Vom utiliza o zonă de memorie suplimentară unde să se salveze rezultatele
fiecărei subprobleme. Timpul de execuţie exponențial se poate reduce astfel la un timp de
execuţie polinomial dacă numărul de subprobleme este polinomial și rezolvarea unei
subprobleme este polinomială.
Vom putea folosi 2 abordări:
Abordare top-down cu memo-izare: se scrie procedura recursivă în mod
obişnuit, dar o modificăm astfel încât să salvăm rezultatul fiecărei subprobleme
într-un şir. Înainte să se apeleze recursivitatea, se verifică dacă rezultatul dorit
este deja salvat în şir
Figura 2.3 Algoritmul top-down de tăiere a barei de oţel utilizând programare dinamică
Acelaşi algoritm dar utilizând o abordare BOTTOM-UP este prezentat în figura 2.4:
12
Figura 2.4 Algoritmul bottom-up de tăiere a barei de oţel utilizând programare dinamică
Algoritmul prezentat în anterior oferă valoarea optimă în cazul tăierii barei de oţel
dar nu oferă şi soluţia adică lista mărimilor bucăţilor tăiate. Pentru a reconstrui soluţia, vom
extinde abordarea anterioară prin memorarea în fiecare pas nu doar a valorii soluţiei
optime ci şi a alegerii făcute pentru a se ajunge la soluţia optimă.
Prezentăm în figura 2.5 algoritmul de tăiere a barei de oţel cu memorarea în şirul s
a mărimilor bucăţilor ce vor fi tăiate.
Figura 2.5 Algoritmul de tăiere a barei de oţel cu memorarea mărimilor bucăţilor ce vor fi
tăiate
13
INMULTIRE-MATRICE(A,B)
1: If coloane[A]!=linii[B]
2: scrie “dimensiuni incompatibile”
3: else
4: for i=1 to linii[A]
5: for j=1 to coloane[B]
6: C[i,j]=0
7: for k=1 to coloane[A]
8: C[i,j]=C[i,j]+A[i,k]*B[k,j]
9: returneaza C
Figura 2.6 Algoritmul de înmulţire a 2 matrici
Dacă avem o matrice A(p,q) pe care dorim să o înmulţim cu o matrice B(q,r) pentru
a rezulta matricea produs C(p,r) vor fi necesare p*q*r înmulţiri scalare.
Exemplu: Considerăm A1(10,100), A2(100,5) şi A3(5,50). Se pune problema când vom avea
un număr minim de înmulţiri scalare pentru a obţine produsul celor 3 matrici: când vom
calcula (A1A2)A3 sau când vom calcula A1(A2A3)
A1A2 –vom avea 10*100*5=5000 de înmulţiri scalare pt a calcula A1A2 de dimensiune 10x5.
Apoi vom avea 10*5*50=2500 înmulţiri scalare pentru a calcula (A1A2)A3 deci în total 7500
de înmulţiri scalare.
A2A3 -vom avea 100*5*50=25000 de înmulţiri scalare pentru a calcula A2A3 de dimensiune
100x50. Apoi vom avea 10*100*50=50000 înmulţiri scalare pentru a calcula A1(A2A3) deci în
total 75000 de înmulţiri scalare
În concluzie prima variantă de parantezare ne oferă un algoritm de 10 ori mai rapid.
Problema pe care urmează să o rezolvăm are următorul enunţ: Se dă un şir de
matrici (A1A2…An) care trebuie înmulţite. Matricea Ai are dimensiunile pi-1 x pi. Să se pună
parantezele pentru a rezolva produsul celor n matrici astfel încât numărul de înmulţiri
scalare să fie minim.
P(n) va fi numărul de parantezări distincte ale unui şir de matrici (altfel spus va fi
numărul de posibile soluţii pentru un şir de n matrici). Vom demonstra că verificarea
exhaustivă a tuturor parantezărilor nu conduce la un algoritm eficient.
Formula recursivă de calcul a lui P(n) este prezentată în figura 2.7:
Dacă costurile lui Ai..k şi Ak+1..j sunt optime, atunci şi costul lui A i..j este optim
14
Pentru a determina subproblemele necesare abordării prin prisma programării
dinamice, observăm că avem câte o subproblemă pentru fiecare alegere a lui i şi j, cu
1<=i<=j<=n. În total vom avea Cn2+n=ϴ(n) subprobleme.
Un algoritm recursiv poate întâlni fiecare subproblemă de mai multe ori pe ramuri
diferite ale arborelui său de recurenţă. Această proprietate de suprapunere a
subproblemelor este una din caracteristicile programării dinamice. Astfel, rezolvarea
fiecărei subprobleme Ai..j o păstrăm într-un tabel de dimensiuni nxn cu liniile i şi coloanele j.
Figura 2.8 prezintă algoritmul bottom-up de determinare a costurilor înmulţirii
şirului de matrici precum şi a indicilor pentru care s-a obţinut costul optim.
15
2. La o problemă, presupunem că ni se furnizează alegerea care să conducă la soluţia
optimală
Identificăm următoarele:
Primul pas al rezolvării unei probleme de optimizare utilizând programarea
dinamică constă în caracterizarea structurii unei soluţii optime. Spunem că problema
prezintă o substructură optimă dacă o soluţie optimă a problemei include soluţii optime ale
subproblemelor. În consecinţă,
Avem două elemente cheie pe care o problemă de optimizare trebuie să le aibă
pentru ca să putem aplica programarea dinamică:
Când o problemă prezintă o sub-structură optimală, este un candidat pentru
programarea dinamică
Se pune problema planificării unor activităţi care necesită utilizarea unei resurse
comune.
Datele de intrare (inputul) este reprezentat de o mulţime S={a1, a2, …an} de activităţi
care necesită o resursă ce poate fi utilizată doar de o singură activitate la un moment dat.
16
Fiecare activitate ai are un moment de start si şi un moment de terminare fi cu 0<=si<fi<∞.
O activitate ai are loc în intervalul [si,fi). Două activităţi ai şi aj sunt compatibile dacă si>=f j
sau sj>=fi (intervalele lor nu se suprapun).
Datele de ieşire (outputul) reprezintă subsetul maximal de activităţi compatibile.
Prezentăm în figura 2.10 un exemplu format din 11 activităţi (i=1,2, ..11) fiecare cu
momentul său de start si şi momentul de terminare fi.
În exemplul prezentat în figura 2.10, {a3, a9, a11} reprezintă un subset de activităţi
compatibile; acesta nu este un subset maximal deoarece {a1, a4, a8, a11} este mai mare.
Acesta din urmă este cel mai mare subset de activităţi compatibile; în acelaşi timp un alt
subset maximal este şi {a2, a4, a9, a11}.
Substructura optimală a problemei selecţiei activităţilor –putem uşor constata că
problema selecţiei activităţilor expune substructura optimă. Vom considera Sij mulţimea
activităţilor care încep după finalizarea activităţii ai şi se termină înainte de începutul
activităţii aj (slide –corectaţi “se termină” în loc de încep).
Presupunem că subsetul maximal de activităţi compatibile din Sij este Aij şi acesta
include o activitate ak.
Incluzând ak în soluţia optimă pentru Sij, am împărţit problema Sij în două
subprobleme S ik şi Skj cărora trebuie să le găsim subsetul maximal de activităţi. Sik sunt
activităţi care încep după ce activitatea ai s-a terminat şi se termină înainte ca activitatea ak
să înceapă iar S kj sunt activităţi care încep după ce activitatea ak s-a terminat şi se termină
înainte ca activitatea aj să înceapă.
Dacă Aik = Aij∩Sik şi Akj=Aij∩Skj atunci Aij=Aik ᴜ{ak} ᴜAkj unde Aik conţine activităţile din
Aij care se termină înainte ca ak să înceapă iar Akj conţine activităţile din Aij care pornesc
după ce ak se termină.
Deci soluţia optimală pentru Sij include soluţiile optimale pentru Sik şi Skj. Acest mod
de a caracteriza substructura optimă ne sugerează că am putea rezolva problema selecţiei
activităţilor utilizând programarea dinamică.
Dacă c[i,j] este mărimea unei soluţii optimale pentru Sij atunci c[i,j]=c[i,k]+c[k,j]+1.
Pentru sij trebuie să determinăm acea activitate ak pentru care are loc recurenţa din
figura 2.11.
Figura 2.11 Formula recurentă pentru calculul mărimii soluţiei optimale la problema
selecţiei activităţilor
17
Intuitiv, alegerea greedy este acea activitate care lasă resursele disponibile pentru
cât de multe activităţi disponibile, deci vom alege activitatea cu cel mai devreme moment
de terminare (dacă mai mult de o activitate din S are cel mai devreme moment de
terminare atunci vom putea alege oricare din aceste activităţi). Altfel spus, dacă activităţile
sunt sortate în ordinea crescătoare a timpului de terminare, atunci alegerea greedy este
activitatea a1.
Făcând o alegere greedy, ne rămâne o singură subproblemă de rezolvat: găsirea
activităţilor care pornesc după ce a1 s-a terminat. De ce nu putem considera activităţile care
se termină înainte ca a1 să înceapă? Deoarece avem s1<f1 şi f1 este cel mai devreme moment
de terminare al oricărei activităţi şi deci nici o activitate nu poate avea timp de terminare
mai mic sau egal decât s1. Deci toate activităţile compatibile cu a1 trebuie să înceapă după
ce a1 se termină.
Condiţia structurii optimale ne spune că dacă alegerea greedy este optimală atunci
şi rezultatul optim al problemei se compune din alegerea greedy şi rezultatul optimal al
subproblemei rămase.
O soluţie greedy recursivă care rezolvă problema selecţiei activităţilor este
prezentată în figura 2.12. Procedura RECURSIVE-ACTIVITY-SELECTOR primeşte ca parametri
timpii de început şi de sfârşit al activităţilor (reprezentaţi de vectorii s şi f), indicele i care
defineşte subproblema care trebuie rezolvată şi mărimea j a problemei originale. Această
procedură va returna mulţimea maximă de activităţilor compatibile din Si. Presupunem că
cele j activităţi primite ca date de intrare sunt deja ordonate crescător după timpul de
terminare a lor. Dacă nu sunt sortate, am putea face sortarea lor într-un timp de
complexitate O(nlgn).
18
În concluzie, proprietatea alegerii greedy poate fi enunţată astfel: alegem
alternativa care apare ca şi cea mai promiţătoare la problema curentă, fără a rezolva
subproblemele. De fapt, aceasta diferenţiază tehnica greedy de programarea dinamică prin
faptul că aceasta din urmă rezolvă subproblemele (câte o singură dată) pentru a putea
decide care este cea mai bună alegere. Utilizând greedy, facem alegerea care ni se pare cea
mai bună la un anumit moment, după care rezolvăm subproblema rămasă. Programarea
dinamică rezolvă subproblemele înainte să facă alegerea, în timp ce greedy face alegerea
înainte să rezolve orice subproblemă. Programarea dinamică lucrează în general cu o
strategie bottom-up (sau top-down cu memo-izare) în timp ce greedy foloseşte în general o
strategie top-down, făcând alegeri greedy una după alta, reducând astfel instanţa
problemei la o instanţă mai mică.
19
2)se presupun generate elementele v1,v2,v3…vk-1 aparţinând mulţimilor S1 S2 S3…Sk-1 pentru
generarea lui vk se alege primul element din Sk disponibil şi pentru valoarea aleasă se
testează îndeplinirea condiţiilor de continuare.
a)nu s-a găsit un astfel de element, caz în care se reia căutarea considerând generate
elementele v1,v2,v3…vk-1 iar vk se reia de la urmatorul element al mulţimii Sk rămas netestat
b)a fost găsit, caz în care se testează dacă acesta îndeplineşte condiţiile de continuare,
aparând astfel alte două posibilităţi:
b1) vk îndeplineşte condiţiile de continuare. Dacă s-a ajuns la soluţia finală (k=n) atunci se
afişează soluţia obtinută. Dacă nu s-a ajuns la soluţia finală se trece la generarea
elementului următor vk+1;
Figura 2.14 Algoritmul general de rezolvare a unei probleme utilizând metoda backtracking
Backtracking este o metodă care poate furniza toate soluţiile unei probleme.
20
|xi-xj|≠|i-j| pentru orice j=1,..k-{i} (prin aceasta ne asigurăm că dama k pe care am
aşezat-o nu se atacă pe una din diagonale cu damele deja aşezate).
21
3. NOŢIUNI DESPRE STRUCTURI DE DATE
Un tip abstract de date (Abstract Data Type ADT) este un tip de date necesar unui
analist, dar care este posibil să nu existe în limbajul de programare ceea ce impune
necesitatea implementării lui.
De exemplu tipul abstract Boolean reprezentat prin cele două valori de adevăr true
şi false. În limbajul Basic acesta este implementat prin şiruri de caractere (“true” şi “false”)
dar în limbajul C nu există acest tip de date, programatorii putându-se folosi de faptul că
valoarea 0 înseamnă întotodeauna “false” iar orice valoare diferită de 0 (exemplu valoarea
1) înseamnă “true”. Dacă doreşte, programatorul poate să-şi implementeze un tip abstract
de date Boolean. Specificarea unui ADT presupune specificarea:
Mulţimii valorilor admisibile
De exemplu, avem cazul unei structuri abstracte de date de tip coadă (queue)
(structură de date de tip FIFO First In First Out –primul intrat primul ieşit). Cunoştinţele
necesare unui analist privesc doar comportamentul unui ADT. În cazul unei ADT queue,
analistul trebuie să cunoască cum se face:
Inserarea in coadă: la sfârşit
Acesta are implicaţii pentru programatorul ADT-ului care poate alege structuri de
date corespunzătoare pentru implementare în timp ce pentru utilizatorul ADT-ului,
comportamentul ADT-ului este observat prin efectul şi rezultatul realizării operaţiilor
specificate.
22
Figura 3.1 O implementare statică a unei liste cu ajutorul unui şir
Santinela reprezintă un element de listă gol care marchează începutul sau sfârşitul
listei. Avantajul santinelei este că permite inserarea uşoară de elemente la început /
sfârşitul listei.
23
Lista înlănţuită este o structură de date în care obiectele sunt aranjate într-o ordine
liniară. La implementarea prin şiruri, ordinea este determinată de indici în timp ce la
implementarea prin pointeri, ordinea este determinată de un pointer care arată către
obiectul următor.
Pentru o listă liniară este obligatoriu să existe o variabilă, declarată în timpul
compilării, denumită cap de listă (head) care să păstreze adresa primului element al listei.
Pierderea acestei valori va duce la imposibilitatea accesării elementelor listei liniare.
În cazul unei liste dublu înlănţuite, la un element x, x.next arată către elementul
succesor iar x.prev arată către elementul predecesor. Dacă x.prev == NULL, atunci x este
primul element, denumit head. Dacă x.next == NULL, atunci x este ultimul element,
denumit tail.
În cazul listelor simplu înlănţuite, fiecare element al listei conţine adresa
următorului element al listei. Ultimul element poate conţine ca adresă de legătură fie
constanta NULL sau NIL (sau constanta 0 care nu indică nici un alt element), fie adresa
primului element al listei, în cazul listelor liniare circulare. În cazul unei liste circulare dublu
înlănţuite, ultimul element din listă pointează către head iar primul element din listă
pointează către tail.
Dacă lista este sortată, cheile apar în ordine sortată.
24
Figura 3.5 Algoritmul de ştergere a unui element într-o listă înlănţuită
LIST-DELETE şterge un element într-un timp ϴ(1), dar dacă dorim să ştergem un
element cu o cheie dată atunci în cel mai defavorabil caz vom avea nevoie de un timp ϴ(n)
datorită necesităţii apelului procedurii LIST-SEARCH care să ne întoarcă un pointer către
elementul care conţine cheia k.
Dacă ignorăm condiţiile de head şi tail ale listei, operaţia de delete se transformă
într-un algoritm mult mai scurt, prezentat în figura 3.6.
Figura 3.6 Algoritmul simplificat de ştergere a unui element într-o listă înlănţuită
Figura 3.7 Algoritmul de căutare respectiv inserare într-o listă dublu înlănţuită circulară cu
santinelă
3.3. Stiva
Stiva este o structură de tip LIFO (Last In First Out) –elementul ce ar urma să fie
şters este ultimul element adăugat într-o astfel de structură. Există două modalităţi de a
implementa o stivă: folosind un şir (un array) sau folosind o structură înlănţuită cu pointeri.
25
Operaţia de inserare într-o stivă este cel mai adesea denumită PUSH iar operaţia de
extragere este denumită POP. Figura 3.8 prezintă un exemplu de stivă cu cel mult n
elemente implementată folosind un array S[1..n].
Şirul are un atribut S.top care indexează ultimul element adăugat în stivă. Stiva
conţine elementele S[1..S.top], unde S[1] este elementul de la baza stivei iar S[S.top] este
elementul din vârf. Când S.top=0 stiva nu conţine nici un element şi este deci goală. Putem
testa dacă stiva este goală cu ajutorul operaţiei STACK-EMPTY din figura 3.9. Într-o stivă
goală (vidă) nu se poate face operaţia POP de extragere a unui element. Dacă S.top este
mai mare decât n atunci stiva este plină. În pseudocodul prezentat în figura 3.9 nu ne facem
griji cu privire la această posibilitate.
Fiecare din cele 3 operaţii pe stivă din figura 3.9 au un timp de execuţie O(1).
3.4. Coada
Coada este o structură de tip FIFO (First In First Out). Într-o elementul care va fi
şters este întotdeauna cel mai vechi element adăugat în coadă. Operaţia de adăugare a
unui element într-o coadă se numeşte ENQUEUE iar operaţia de extragere a unui element
dintr-o coadă se numeşte DEQUEUE. La fel ca operaţia POP de extragere a unui element
dintr-o stivă, şi operaţia DEQUEUE de extragere a unui element dintr-o coadă nu primeşte
nici un argument.
Figura 3.10 prezintă operaţiile de inserare respective extragere din coadă, fără a se
verifica dacă nu cumva încercăm să extragem un element dintr-o coadă goală sau dacă
încercăm să inserăm un element într-o coadă plină.
26
Figura 3.10 Operaţiile de inserare respectiv extragere dintr-o coadă
Fiecare din cele 2 operaţii pe coadă din figura 3.10 au un timp de execuţie O(1).
Multe aplicaţii necesită o structură de date care să suporte doar operaţii esenţiale
pentru un dicţionar: INSERT, SEARCH şi DELETE. O tabelă de dispersie (hash table) este o
structură de date care implementează dicţionare. Deşi căutarea unui element într-o tabelă
de dispersie poate ţine (în anumite condiţii) la fel de mult ca şi căutarea unui element într-o
listă înlănţuită (ϴ(n)), în practică această structură se comportă foarte bine. În anumite
circumstanţe, timpul mediu de căutare a unui element într-o astfel de structură este de
complexitate O(1).
Un hash table generalizează noţiunea unui şir (array) care foloseşte un indice
pentru adresarea directă a elementelor lui. Astfel, orice poziţie poate fi examinată într-un
timp O(1). Când numărul de chei stocate într-o astfel de structură este relative scăzut faţă
de numărul total de posibile chei, tabelele de dispersie sunt o alternativă foarte bună la
adresarea directă pe care o are un array. În loc de a utiliza cheia direct ca şi index pentru a
adresa un element al şirului, într-o tabelă de dispersie indexul este calculat pe baza cheii
(conceptul de hashing).
Adresarea directă este o tehnică care funcţionează bine dacă universul U al cheilor
este rezonabil de mic. Presupunem că o aplicaţie necesită elemente dintr-un dicţionar în
care fiecare element are o cheie asociată din mulţimea U={0,1,…m-1}. Vom presupune că
nu există două elemente cu aceeaşi cheie. Pentru reprezentarea mulţimii dinamice se
foloseşte un şir (o tabelă direct adresabilă) T[0..m-1] în care fiecare poziţie (sau slot)
corespunde unei chei din U. Figura 3.11 ilustrează acest concept: Fiecare element din
universul U={0,1,…9} corespunde unui index în tabela direct adresabilă T. Mulţimea
K={2,3,5,8} a elementelor mulţimii determină sloturi în tabelă care conţin pointeri către
elemente. Slotul k pointează spre un element din mulţime având cheia k. Dacă mulţimea K
nu conţine un element cu cheia k, atunci valoarea din tabela T este T[k]=NIL.
27
Figura 3.11 Exemplificarea conceptului de tabelă direct adresabilă
Uneori, pentru a salva spaţiu, în loc să păstrăm cheia şi datele adiţionale într-un
obiect extern tabelei direct adresabile, cu un pointer într-un slot al tabelei către obiect, am
putea stoca stoca obiectul chiar în slot. În acest ultim caz, un element cu cheia k este stocat
în slotul k.
Figura 3.12 prezintă operaţiile pe astfel de tabele direct adresabile.
Dacă universul de chei U este mare (adică m are o valoare mare), alocarea unui şir
de dimensiune m nu este fezabilă iar uneori este imposibilă. În general în aceste cazuri,
numărul de chei K care se stochează este mic în comparaţie cu mărimea lui U.
Când numărul de chei K stocat în dictionar este mult mai mic decât universul U al
tuturor posibilelor chei, o tabelă de dispersie va necesita mult mai puţin spaţiu decât o
tabelă direct adresabilă. Scopul de fapt este reducerea spaţiului de stocare cu menţinerea
în acelaşi timp a facilităţii de căutare a unui element într-un timp de complexitate O(1). In
cazul tabelelor de dispersie, timpul O(1) de căutare este valabil pentru cazul mediu de
utilizare, în timp ce la tabelele direct adresabile timpul O(1) are loc şi pentru cazul cel mai
defavorabil.
În cazul tabelelor de dispersie, pentru a determina locul în care va fi stocat
elementul cu cheia k, vom utiliza o funcţie de dispersie (hash function):
h:U->{0,1,…m-1}
Funcţia h mapează universul U al cheilor în sloturile tabelei hash T[0..m-1].
Mărimea m a tabelei T este în mod obişnuit mult mai mică decât universul U. Fiecare
element cu cheia k este salvat în locaţia T[h[k]]. Se spune că h[k] este valoarea hash a cheii
k. Figura 3.13 ilustrează aceste concepte.
28
Figura 3.13 Ilustrarea conceptului de tabelă de dispersie
În figura 3.13, cheile k2 şi k5 ocupă acelaşi slot –în acest caz spunem că cele 2 valori
intră în coliziune. Există tehnici de rezolvare a conflictelor create de coliziuni. Bineînţeles,
ideal ar fi să se evite coliziunile –aceasta se poate face printr-o alegere potrivită a funcţiei h.
Totuşi, datorită faptului că mărimea universului U este mai mare decât m, obligatoriu vor
exista cel puţin două valori care să aibe acelaşi cod hash. Alegerea funcţiei hash trebuie să
minimizeze numărul de coliziuni.
Cea mai folosită tehnică de rezolvare a coliziunilor este cea prin înlănţuire. Toate
elementele care au acelaşi cod hash vor fi plasate în aceeaşi listă înlănţuită. Slotul j conține
un pointer către lista care salvează elementele care au j ca şi valoare hash. Dacă nici un
element nu are valoare de hash pe j, atunci T[j] = NIL.
Figura 3.14 prezintă modalitatea de rezolvare a coliziunilor prin înlănţuire.
Figura 3.15 prezintă operaţiile care se fac pe o tabelă de dispersie atunci când
rezolvarea coliziunilor se face prin înlănţuire.
29
Figura 3.15 Operaţiile pe o tabelă de dispersie ce utilizează liste înlănţuite pentru valorile în
coliziune
Trei tehnici sunt folosite pentru a calcula secvenţa de chei necesară adresării directe:
1. Probarea liniară:
30
Dându-se cheia k, vom încerca slotul T[h’(k)] dacă este liber; apoi vom încerca slotul
’
T[h (k)+1] şi tot aşa până la slotul T[m-1]. Vom continua apoi cu sloturile T[0], T[1] până
la T[h’(k)-1].
2. Probarea pătratică
31
STRUCTURI DE DATE AVANSATE
Dacă x este un nod oarecare în arbore şi y este un nod în subarborele drept atunci
x.key<=y.key
32
Figura 4.2 prezintă algoritmul de afişare a elementelor unui arbore binar traversat
în inordine:
Figura 4.3 prezintă algoritmul de căutare într-un arbore binar de căutare. Procedura
primeşte un pointer către rădăcina arborelui şi o cheie dată k care urmează să fie căutată în
arbore. Procedura va returna un pointer către nodul cu cheia k (dacă acest nod există) sau
NIL în caz contrar.
Figura 4.4 prezintă acelaşi algoritm dar iterativ (deci fără a utiliza recursivitatea):
4.1.3. Minimul, maximul şi succesorul unui nod într-un arbore binar de căutare
Într-un arbore binar de căutare, întotdeauna vom putea găsi elementul minim
folosind un pointer care porneşte de la rădăcină şi urmează întotdeauna copilul din stânga.
Figura 4.5 ilustrează acest algoritm.
Analog, poate avea loc căutarea elementului maxim într-un arbore binar de căutare
urmărind de data aceasta întotdeauna copilul din dreapta. Figura 4.6 prezintă acest
algoritm.
33
Figura 4.6 Algoritmul de căutare a maximului într-un arbore binar de căutare
Figura 4.7 Algoritmul de găsire a succesorului unui element într-un arbore binar de căutare
34
4.1.5. Ştergerea unui nod într-un arbore binar de căutare
În cadrul operaţiei de ştergere a unui nod într-un arbore binar de căutare vom avea
nevoie de un algoritm care înlocuieşte un subarbore cu un alt subarbore.
Figura 4.10 prezintă algoritmul de ştergere a unui nod dintr-un arbore binar de
căutare.
Figura 4.10 Algoritmul de ştergere a unui nod într-un arbore binar de căutare
Operaţiile pe un arbore binar de căutare se fac într-un timp O(h) unde h este
înălţimea arborelui. Aceste operaţii sunt rapide în cazul în care înălţimea arborelui este
35
mică. Dacă înălţimea este mare (arbori creaţi dezechilibraţi sau dezechilibraţi în urma unor
inserări sau ştergeri succesive) atunci timpul acestor operaţii nu este mai bun decât în cazul
unei liste înlănţuite.
Arborii roşu şi negru garantează un timp O(lgn) în cazul cel mai defavorabil pentru
operaţiile de bază în acest arbore.
Un arbore roșu-şi-negru este un arbore binar de căutare la care fiecare nod are bit
suplimentar, reprezentând culoarea: roșu sau negru. Fiecare nod de arbore conține
următoarele campuri: cheia, culoarea, nodul stâng, nodul drept și părintele.
Un arbore roșu şi negru este un arbore binar de căutare cu următoarele proprietăţi:
1. Fiecare nod este sau roșu sau negru
2. Rădăcina este negru
3. Fiecare frunză (NIL) este neagră
4. Dacă un nod este roșu, atunci amândoi copii sunt negrii
5. Pentru fiecare nod, toate căile simple de la nodul respectiv la frunzele descendente
conțin același număr de noduri negre
Punând constrângeri asupra culorilor nodurilor oricărei căi simple de la rădăcină la
frunze, arborii roşu şi negru asigură că nici una din aceste chei nu este de două ori mai
lungă decât oricare alta; în consecinţă arborele este aproximativ balansat.
Într-un arbore roşu şi negru vom utiliza o singură santinelă pentru a reprezenta NIL.
Santinela T.nil este un obiect cu aceleaşi atribute ca un nod oarecare din arbore. Culoarea
santinelei este negru iar celelalte atribute p, left, right şi key pot avea valori arbitrare. Aşa
cum se vede în figura 4.11 punctul b, toţi pointerii spre NIL au fost înlocuiţi cu pointeri către
santinela T.nil. Vom utiliza santinela astfel încât vom putea trata orice copil NIL al unui nod
x ca un nod ordinar a cărui părinte este x.
36
Figura 4.11 Un exemplu de arbore roşu şi negru
Operaţiile TREE-INSERT şi TREE-DELETE într-un arbore cu n noduri se execută într-
un timp O(log(n)). Aceste operaţii schimbă arboreal, deci este posibil ca arborele rezultat să
nu mai îndeplinească condiţiile de arbore roşu şi negru. Vor fi necesare corecţii în arbore
pentru a păstra proprietăţile arborelui. Pentru a restaura aceste proprietăţi, vor trebui
schimbate culori ale unor noduri ale arborelui şi de asemenea schimbată structura de
pointeri.
Vom schimba structura de pointeri prin intermediul operaţiei de rotaţie care este o
operaţie locală într-un arbore de căutare care menţine proprietăţile arborelui binar de
căutare. Figura 4.12 prezintă cele două tipuri de rotaţii: rotaţii la stânga şi rotaţii la dreapta.
37
Figura 4.12 Operaţiile de rotaţie pe un arbore binar de căutare
Când facem o rotaţie la stânga a unui nod x, presupunem că copilul dreapta y nu
este T.nil; x poate fi orice nod în arbore a cărui copil dreapta nu este T.nil. Operaţia de
rotaţie la stânga (transformarea configuraţiei din dreapta figurii 4.14 în configuraţia din
stânga) îl face pe y noua rădăcină a subarborelui, cu x ca şi copilul stânga a lui y iar copilul
stânga a lui y (adică β) devine copilul dreapta a lui x.
Algoritmul LEFT-ROTATE din figura 4.13 presupune că x.right≠T.nil şi că părintele
rădăcinii este T.nil.
38
Figura 4.14 Algoritmul de inserare a unui nod într-un arbore roşu şi negru
Datorită faptului că colorarea în roşu a nodului proaspăt inserat ar putea încălca
proprietatea de colorare a arborilor roşu şi negru, vom apela procedura RB-INSERT-
FIXUP(T,z) (prezentată în figura 4.15) la linia 17 pentru a restaura această proprietate.
Figura 4.15 Algoritmul de restaurare a proprietăţilor unui arbore roşu şi negru după
inserarea unui nod
Ca şi alte operaţii de bază într-un arbore roşu şi negru, operaţia de ştergere a unui
nod se va face într-un timp O(lg n). În primul rând este necesară modificarea procedurii
TRANSPLANT (figura 4.9) făcută la arborii binar de căutare. Noua procedură RB-
TRANSPLANT este prezentată în figura 4.16.
39
Figura 4.16 Procedura de înlocuire a unui arbore cu un alt subarbore pentru un arbore roşu
şi negru
40
La un graf neorientat, (u,v) şi (v,u) reprezintă aceeaşi muchie, în consecinţă
matricea este simetrică faţă de diagonala principală (deci matricea de adiacenţă este
propria sa transpusă AT=A).
Pentru un graf ponderat, putem stoca costurile arcelor ca elemente ale matricii de
adiacenţă, iar acolo unde arcul nu există putem stoca NIL, ∞ sau 0.
Vom putea accesa un atribut d al unui unui nod v folosind notaţia v.d. De
asemenea, vom putea accesa un atribut f al unui arc (u,v) folosind notaţia (u,v).f.
41
Figura 4.18 Algoritmul de parcurgere în lăţime pentru un graf reprezentat cu liste de
adiacenţă
42
următorul nod care trebuie explorat după v. Această operaţie continuă până când au fost
descoperite toate nodurile accesibile din vârful sursă iniţial.
Vom folosi aceeaşi convenție de colorare şi anume alb, gri şi negru. Pentru fiecare
nod folosim 2 ştampile de timp:
v.d momentul în care nodul a fost descoperit pentru prima dată (când este
marcat gri)
Acest marcaj de timp este o valoare întreagă cuprinsă între 1 şi 2|V| deoarece
există un moment de descoperire şi un moment de terminare pentru fiecare din cele |V|
noduri ale grafului.
43
lungul unei linii orizontale. Muchiile orientate merg de la stânga la dreapta. Vârfurile sunt
aranjate de la stânga la dreapta în ordinea descrescătoare a timpului de terminare.
Figura 4.22 Algoritmul simplu de sortare topologică a unui graf orientat aciclic
44
4.3.5. Componentă tare conexă a unui graf
O componentă tare conexă a unui graf orientat G=(V,E) este o mulţime maximală de
vârfuri U V, astfel încât, pentru fiecare pereche de vârfuri u şi v din U există un drum de la
u la v precum şi de la v la u. Altfel spus, între oricare două noduri din componentă există cel
puţin un drum şi nu mai există nici un nod în afara componentei legat printr-un drum de un
nod al componentei.
Algoritmul din figura 4.23 determină componentele tare conexe ale unui graf
orientat G=(V,E) folosind două căutări în adâncime, una în G şi una în GT.
Figura 4.23 Algoritmul de determinare a componentelor tare conexe dintr-un graf orientat
La proiectarea circuitelor electronice, de multe ori este necesară legarea pinilor mai
multe componente, conectându-i cu fire, fiecare fir conectând doi pini. Pentru
interconectarea unei mulţimi de n pini vor fi necesare n-1 fire. Având în vedere că pot fi mai
multe aranjamente de interconectare, se cere să se determine cablarea care foloseşte cea
mai mică cantitate de cabluri.
Vom folosi un graf neorientat conex, G=(V,E) unde V este mulţimea pinilor şi E este
mulţimea interconectărilor posibile dintre perechile de pini; pentru fiecare pereche de pini
(u,v)ϵE, avem un cost w(u,v) ce reprezintă costul cablului de la pinul u la pinul v. Dorim să
determinăm submulţimea aciclică T E care conectează toate nodurile din V şi al cărei cost
total w(T)=∑(u,v)ϵT w(u,v) este minim.
Deoarece mulţimea T este aciclică şi conectează toate nodurile, ea trebuie să
formeze un arbore care va fi denumit arbore de acoperire deoarece “acoperă” graful G.
Problema determinării arborelui T va fi numită problema arborelui de acoperire minim.
Vom prezenta doi algoritmi de determinare a arborelui de acoperire minim: algoritmul lui
Kruskal şi algoritmul lui Prim.
Algoritmii pe care îi vom prezenta pentru determinarea arborelui de acoperire
minim sunt algoritmi greedy. Această strategie greedy este implementată în algoritmul
generic din figura 4.24, care dezvoltă arborele de acoperire minim adăugând o muchie o
dată. Algoritmul foloseşte o mulţime A care este întotdeauna o submulţime a unui arbore
de acoperire minim. La fiecare pas este determinată o muchie (u,v) care poate fi adăugată
la A, respectând proprietatea de mai sus, adică AU{(u,v)} este, de asemenea, o submulţime
a unui arbore de acoperire minim. Numim o astfel de muchie o muchie sigură (safe) pentru
A, deoarece poate fi adăugată, în siguranţă, mulţimii A, respectând proprietatea de mai sus.
45
Figura 4.24 Algoritmul generic de determinare a unui arbore de acoperire minim
Partea dificilă este găsirea unei muchii sigure în linia 3 a algoritmului. Vom defini o
tăietură (S, V-S) a unui graf neorientat G=(V,E) ca o partiţie a lui V. Spunem că o muchie
(u,v)ϵE traversează tăietura (S,V-S) dacă unul din punctele sale terminale este în S şi celălalt
este în S-V. Spunem că o tăietură respectă mulţimea de muchii A dacă nici o muchie din A
nu traversează tăietura. O muchie este o muchie uşoară care traversează o tăietură dacă
are costul minim dintre toate muchiile care traversează tăietura. Pot exista mai multe
muchii uşoare care traversează o tăietură dacă ele au costuri egale. Generalizând, spunem
că o muchie este o muchie uşoară care satisface o proprietate dată dacă are costul minim
dintre toate muchiile care satisfac acea proprietate.
Figura 4.25 Algoritmul lui Kruskal de determinare a unui arbore de acoperire minim
Precum algoritmul lui Kruskal, algoritmul lui Prim este un caz particular al
algoritmului generic pentru determinarea unui arbore de acoperire minim pentru un graf
conex. Algoritmul lui Prim are proprietatea că muchiile din mulţimea A formează
întotdeauna un singur arbore. Arborele se formează pornind de la un vârf arbitrar r şi creşte
46
până acoperă toate vârfurile din V. La fiecare pas, se adaugă arborelui o muchie uşoară care
uneşte mulţimea A cu un vârf izolat din GA=(V,A). La fel ca algoritmul lui Kruskal, algoritmul
lui Prim foloseşte o tehnică gredy deoarece arborelui ii este adăugată la fiecare pas o
muchie care adaugă cel mai mic cost la costul total al arborelui.
În procedura MST-PRIM din figura 4.26, graful G, rădăcina r a arborelui minim de
acoperire care urmează să fie dezvoltat şi costurile w sunt parametri de intrare. În timpul
execuţiei, toate vârfurile care nu sunt în arbore sunt într-o coadă de prioritate Q bazată pe
un câmp cheie. Pentru fiecare vârf v, atributul v.key este costul minim al oricărei muchii
care uneşte pe v cu un vârf din arbore.
Figura 4.26 Algoritmul lui Prim de determinare a unui arbore de acoperire minim
Figura 4.27 Costul unei căi exprimat ca suma costurilor muchiilor de pe acea cale
47
rezolvarea acestei probleme care să fie asimptotic mai rapizi decât cel mai bun algoritm
pentru determinarea drumurilor minime de sursă unică, în cazul cel mai defavorabil.
Problema drumurilor minime pentru surse şi destinaţii multiple: Să se determine drumul
minim de la u la v pentru fiecare pereche de vârfuri u şi v. Această problemă poate fi
rezolvată, de exemplu, prin aplicarea algoritmului pentru sursă unică pentru fiecare vârf al
grafului.
De regulă, algoritmii pentru determinarea drumurilor minime exploatează
proprietatea că un drum minim între două vârfuri conţine alte drumuri optimale. Această
proprietate de optimalitate substructurală este specifică atât programării dinamice cât şi
metodei greedy. Algoritmul Dijkstra aplică metoda greedy iar algoritmul Floyd-Warshall
pentru determinarea drumurilor minime pentru toate perechile de vârfuri este un algoritm
de programare dinamică.
Proprietatea de optimalitate substructurală a drumurilor minime poate fi enunţată
astfel:
Dacă se dă un graf orientat G=(V,E) şi o funcţie de cost w:E->R pe graful G, şi fie
p={v0, v1, …vk} calea de cost minim între 2 noduri v0 şi vk; atunci pentru orice 0<=i<=j<=k, p-
ij={vi,vi+1, …vj} este calea de cost minim între vi şi vj.
Considerând un graf G=(V,E), pentru fiecare nod vϵV vom memora predecesorul
acestuia v.π, care poate fi un alt nod sau NIL. Algoritmii de determinare a drumului minim
setează predecesorii astfel încât dacă se merge de la un nod v înapoi, vom găsi calea
minimă (printr-o traversare în ordine inversă) de la sursa s până la nodul v.
4.5.2. Relaxarea
48
valorii drumului minim. Estimările drumurilor minime şi predecesorii sunt iniţializaţi prin
procedura din figura 4.29.
Figura 4.30 Procedura de relaxare a unei muchii (u,v) într-un graf cu costuri
49
4.5.4. Drumuri minime de sursă unică în grafuri orientate aciclice
Prin relaxarea arcelor într-un graf orientat aciclic (DAG directed acyclic graph) cu
costuri conform unei ordonări topologice a vârfurilor sale putem calcula drumurile minine
de sursă unică într-un timp ϴ(V+E). Drumurile minime sunt întotdeauna bine definite într-
un DAG datorită faptului că acesta nu poate avea cicluri de cost negativ, chiar dacă graful
conţine arce de cost negativ.
Algoritmul începe prin sortarea topologică a grafului pentu impunerea unei ordini
liniare a vârfurilor. Dacă există un drum de la u la v atunci u precede v în ordinea
topologică. Algoritmul efectuează o singură trecere peste vârfurile sortate topologic şi pe
măsură ce fiecare vârf este procesat, sunt relaxate toate muchiile care pornesc din acel
vârf.
Algoritmul de căutare a drumurilor minime într-un graf orientat aciclic este
prezentat în figura 4.32.
Figura 4.32 Algoritmul de căutare a drumurilor minime într-un graf orientat aciclic
Algoritmul lui Dijkstra rezolvă problema drumurilor de lungime minimă cu sursă unică
într-un graf orientat G=(V,E) cu costuri nenegative. Deci în acdrul acestui algoritm vom avea
w(u,v)>=0 pentru orice arc (u,v)ϵE.
Algoritmul lui Dijkstra gestionează o mulţime S de noduri pentru a cărui elemente
algoritmul a calculat deja costurile finale ale drumurilor minime de la sursa s. Algoritmul
selectează câte un vârf u din mulţimea V-S pentru care estimarea drumului minim este
minimă, introduce acest vârf u în mulţimea S şi relaxează arcele divergente din din vârful u.
Figura 4.33 prezintă algoritmul lui Dijkstra.
50
5. ÎNTREBĂRI RECAPITULATIVE
1. Ce este un algoritm?
2. Ce este o instanţă a unei probleme?
3. Ce este pseudocodul?
4. Ce înseamnă studierea eficienţei unui algoritm?
5. Cum se defineşte operaţia de sortare?
6. Când spunem despre o metodă de sortare că este stabilă şi când spunem că ea este
“in situ”?
7. Prezentaţi principiul şi pseudocodul algoritmului de sortare prin inserţie.
8. Care sunt principiile şi în ce condiţii se poate aplica metoda Divide and Conquer?
9. Prezentaţi principiul şi pseudocodul algoritmului de sortare prin interclasare.
10. Ce este un heap şi ce proprietăţi îndeplinesc elementele lui?
11. Prezentaţi şi explicaţi algoritmul de transformare a unui şir de intrare într-un heap
şi algoritmul de creare a unui heap.
12. Prezentaţi şi explicaţi algoritmul de sortare utilizând un heap.
13. Prezentaţi principiul şi algoritmul sortării rapide.
14. Prezentaţi şi explicaţi algoritmul de partiţionare a unui şir în cazul sortării rapide
(ambele variante, inclusiv cel cu alegerea aleatoare a elementului pivot).
15. Ce sunt tehnicile de programare?
16. Principiile programării dinamice.
17. Prezentaţi şi explicaţi algoritmul recursiv al tăierii barei de oţel.
18. Prezentaţi şi explicaţi algoritmul tăierii barei de oţel utilizând metoda top-down din
programarea dinamică.
19. Prezentaţi şi explicaţi algoritmul tăierii barei de oţel utilizând metoda bottom-up
din programarea dinamică cu memorarea mărimilor bucăţilor ce vor fi tăiate
20. Prezentaţi şi explicaţi algoritmul bottom-up de determinare a costurilor înmulţirii
unui şir de matrici.
21. Prezentaţi şi explicaţi algoritmul recursiv de afişare a parantezării optime pentru un
produs de n matrici.
22. Principiile metodei greedy.
23. Prezentaţi cum se utilizează metoda greedy în cazul problemei selecţiei activităţilor.
24. Prezentaţi şi explicaţi algoritmul recursiv greedy de selecţie a activităţilor.
25. Prezentaţi şi explicaţi varianta greedy iterativă a algoritmului de selecţie a
activităţilor.
26. Care sunt paşii pe care îi vom utiliza într-o strategie greedy?
27. Principiile şi algoritmul metodei backtracking.
28. Prezentaţi şi explicaţi algoritmul nerecursiv de rezolvare a problemei damelor.
29. Ce este un tip de date şi ce este un tip abstract de date (ADT Abstract Data Type)?
30. Ce este o listă, cum se poate implementa o listă şi ce avantaje şi dezavantaje are
fiecare modalitate de implementare?
51
31. Prezentaţi şi explicaţi algoritmii de căutare, inserare în capul listei şi stergere într-o
listă înlănţuită.
32. Prezentaţi şi explicaţi algoritmul de căutare respectiv inserare într-o listă dublu
înlănţuită circulară cu santinelă
33. Ce este o stivă şi care sunt procedurile de depunere şi extragere dintr-o stivă?
34. Ce este o coadă şi care sunt procedurile de depunere şi extragere dintr-o coadă?
35. Ce este o tabelă direct adresabilă şi care sunt operaţiile într-o tabelă direct
adresabilă?
36. Ce este o tabelă de dispersie, ce este o funcţie de dispersie, ce sunt şi cum se
rezolvă coliziunile?
37. Prezentaţi şi explicaţi procedurile de inserare şi căutare într-o tabelă de dispersie.
38. Ce este un arbore binar de căutare?
39. Prezentaţi modalitătile de traversare a unui arbore binar şi algoritmii pentru fiecare
din această modalitate.
40. Prezentaţi şi explicaţi algoritmii iterativ respectiv recursiv de căutare într-un arbore
binar de căutare.
41. Prezentaţi şi explicaţi algoritmii de căutare a minimului, a maximului şi a
succesorului într-un arbore binar de căutare.
42. Prezentaţi şi explicaţi algoritmul de inserare într-un arbore binar de căutare.
43. Prezentaţi şi explicaţi algoritmul de ştergere într-un arbore binar de căutare.
44. Ce este un arbore roşu şi negru şi ce proprietăţi are?
45. Prezentaţi şi explicaţi algoritmul de rotaţie la stânga într-un arbore binar de
căutare.
46. Prezentaţi şi explicaţi algoritmul de inserare într-un arbore roşu şi negru.
47. Prezentaţi şi explicaţi algoritmul de restaurare a proprietăţilor unui arbore roşu şi
negru după inserarea unui nod.
48. Prezentaţi şi explicaţi algoritmul de restaurare a proprietăţilor unui arbore roşu şi
negru după ştergerea unui nod.
49. Ce este un graf şi care sunt cele două moduri de a reprezenta un graf?
50. Prezentaţi şi explicaţi algoritmul de parcurgere în lăţime pentru un graf reprezentat
cu liste de adiacenţă.
51. Ce este drumul de lungime minimă şi care este algoritmul de afişare a drumurilor
de lungime minimă?
52. Prezentaţi şi explicaţi algoritmul de parcurgere în adâncime pentru un graf
reprezentat cu liste de adiacenţă.
53. Ce este o sortare topologică şi care este algoritmul simplu de sortare topologică?
54. Ce este o componentă tare conexă şi care este algoritmul de determinare a
componentelor tare conexe dintr-un graf orientat?
55. Ce este un arbore de acoperire minim şi care este algoritmul generic de
determinare a unui arbore de acoperire minim?
56. Prezentaţi şi explicaţi algoritmul lui Kruskal de determinare a unui arbore de
acoperire minim.
52
57. Prezentaţi şi explicaţi algoritmul lui Prim de determinare a unui arbore de acoperire
minim.
58. Ce este un drum minim într-un graf şi care sunt variantele problemei drumului
minim?
59. Ce este relaxarea şi care este procedura de relaxare a unei muchii (u,v) într-un graf
cu costuri
60. Prezentaţi şi explicaţi algoritmul Bellman-Ford de rezolvare a problemei drumurilor
minime cu sursă unică în cazul general al unui graf care poate avea şi costuri
negative.
61. Prezentaţi şi explicaţi algoritmul de căutare a drumurilor minime într-un graf
orientat aciclic
62. Prezentaţi şi explicaţi algoritmul lui Dijkstra de determinare a drumurilor de
lungime minimă.
Bibliografie:
• T.H. Cormen; C.E. Leiserson; R.L. Rivest, C. Stein –Introduction to algorithms, ed. 3-
a, MIT Press, 2009, (ed. 1-a este tradusa în limba română).
• Aho A, Hopcroft J, Ullman J– Data Structures and Algorithms, Addison Wesley, 1983
• C. Bologa –Algoritmi şi structuri de date, Editura Risoprint, 2006.
• C. Giumale, I. Negreanu, S. Călinoiu, -Proiectarea şi analiza algoritmilor. Algoritmi de
Sortare, ed. ALL, Bucuresti, 1996
• D. Knuth - Arta programării calculatoarelor, vol, 1, 2, 3, ed. Teora, 1999 (traducere)
53