Sunteți pe pagina 1din 48

Proiectul privind Învăţământul Secundar (ROSE)

 SCHEMA DE GRANTURI PENTRU UNIVERSITĂŢI – NECOMPETITIVE
Beneficiar: Universitatea „Vasile Alecsandri” din Bacău
Titlul subproiectului:
Matematică, Informatică și Biologie - educaţie şi profesii pentru viitor.
Rămâi Integrat şi Total Motivat (Mate-Info-Bio-RITM)
Acord de grant nr. 191/SGU/NC/II din 12.09.2019

Material didactic pentru anul I, programul de studii INFORMATICĂ


pentru disciplina

STRUCTURI DE DATE
Lect. univ. dr. Iulian FURDU

2020
STRUCTURI DE DATE

Obiectivul principal al acestui curs este de a forma acele idei de bază și


abilități de care studenții (și nu numai) au nevoie în activitatea de programare, pe
orice platformă, indiferent de limbaj. Altfel spus obiectivele disciplinei în termeni
generali se referă la ridicarea nivelului de cultură informatică prin proiectarea
algoritmilor de rezolvare a unor probleme, precum și posibilitatea de implementare în
limbaje de programare şi de generalizare a lor precum și la rearea unei baze
algoritmice pentru studiul altor discipline, la dezvoltarea gândirii logice, a capacităţii
de înţelegere și generalizare, de tratare riguroasă a algoritmilor, la crearea
posibilităţilor de aplicare la alte discipline a noţiunilor predategeneral. Totodată
obiectivele specifice vizează:

1. Elaborarea codurilor sursă adecvate și testarea unitară a unor componente într-un


limbaj de programare cunoscut, pe baza unor specificaţii de proiectare date.

2. Însuşirea de către studenţi a structurilor de date fundamentale folosite în


construirea sistemelor software. 3. Cunoaşterea unor algoritmi fundamentali specifici
unor structuri de date. 4. Însuşirea metodelor de verificare a algoritmilor

Cum la Fundamentele programării sunt discutate bazele a ceea ce înseamnă


procesul de transformarea unei probleme în algoritm și apoi în cod executabil
punându-se accentul pe algoritmică și pe cunoașterea unui limbaj de programare, în
cadrul Structurilor de date accentul se pune pe recunoașterea și adaptarea celei mai
potrivite structuri de date în gestiunea rezolvării unei anumite probleme. De prisos să
afirmăm că nu există în general algoritmi care să nu conțină structuri de date.

Prezentul material ar trebui parcurs secvențial pentru o mai bună înțelegere,


resursele indicate fiind în ordinea apariției, materialul având rolul unui auxiliar atât
pentru partea teoretică cât șipentru partea practică: conține un breviar teoretic
aferent fiecărei teme, cu eventuale trimiteri la resurse adiționale, dublat de un set de
exemple relevante temei. Nivelul științific e de popularizare, fără matematizarea și
formalizarea în exces a noțiunilor, nu sunt demostrații tehnice sau algoritmi dificil de
înțeles. Limbajul folosit va fi C, excepție intrucțiunile de citire/afișare ce vor fi preluate
din C++; majoritatea explicațiilor de algoritmică sunt în pseudocod.

2
CUPRINS

I. Recapitulare structuri de date cunoscute. Clasificare


structuri de date. Liste

II. Stive (Implementări, interfeţe, demonstraţii)

III. Cozi (Implementări, interfeţe, demonstraţii)

IV. Liste înlănţuite (dublu, circulare: implementări, operaţii,


demonstraţii).

V. Grafuri (conexe, orientate/neorientate, reprezentari,


parcurgeri)

VI. Tabele de dispersie (dictionare, hashing)

VII. Arbori (parcurgeri, aplicaţii). Tipuri de arbori și aplicațiile


lor (AVL, BST, etc)

VIII. Recapitulare

Exemple de subiecte de examen

Resurse disponibile online

Bibliografie

3
I. Recapitulare structuri de date cunoscute.
Clasificare structuri de date. Liste

“O structură de date este o descriere abstractă a modului de organizare și


implicit de stocare a datelor pentru a permite efectuarea eficientă a anumitor
operațiuni pe acestea. Totodată o structură de date presupune o colecție de date
între care s-a stabilit o serie de relații care conduc la un anumit mecanism de selecție
și identificare a componentelor; acestea sunt identificate prin nume sau prin poziția
pe care o ocupă.”

De exemplu, un arbore binar este o structură de date, din care derivă heap-ul,
arborii de căutare binară BST, diagramele de decizie binară BDD etc. Unele structuri
de date sunt special create având anumite proprietăți care le fac potrivite pentru a
rezolva în mod eficient anumite probleme. (ex. BST: căutare după chei care pot avea
asociate valori, similar cu căutarea numerelor de telefon (valori) după numele
persoanei (chei))

Fig 1.1.BST(https://en.wikipedia.org/wiki/Binary_search_tree)

Fig 1.2. Diagramă de decizie binară redusă ordonată [1]

4
Un tip de date este o clasă (potențial infinită) de obiecte concrete care au
aceleași proprietăți. De exemplu, "integer" este un tip de date care conține toate
numerele întregi, "string" conține toate șirurile de caractere etc. Nu e necesar ca un
tip de date să fie neapărat fundamental (de bază, primitiv) într-un limbaj. De
exemplu:

struct MyComplex {
    double Preala, Pimaginara;
};

definește un tip de date care reprezintă toate obiectele posibile etichetate


MyComplex care conțin două numere reale etichetate Preala și Pimaginara, prin
analogie cu numerele complexe.

Pe scurt, o structură de date este un obiect matematic, cu un set de proprietăți care


pot fi realizate în mai multe moduri ca tipuri de date. Un tip de date în sens
general este doar o clasă de valori care pot fi construite și reprezentate în mod
concret, împreună cu operațiile permise pe aceste valori.

5. O structură de date este o colecție de elemente pentru care s-a precizat:


             -tipul elementelor
             -proprietățile de organizare a elementelor
             -regulile de acces la elemente
6. Componentele unei structuri de date pot fi:
             -date elementare(v. Tabelul 1)
             -structuri de date
7. O structură de date este o entitate de sine stătătoare. Ea este identificată printr-
un nume, iar componentele ei își mențin atributele. Fiecărei structuri de date îi
este specific un anumit mecanism de identificare și de selecție a componentelor
colecției de date.

Datele pot fi clasificate folosind mai multe criterii:

Criteriul naturii (tipului) datelor


Date de tip întreg
În limbajul C++ sunt implementate mai multe subtipuri întregi de date (v.
Fundamentele programării).
– Tipuri de date întregi pozitive (unsigned): unsigned char (1 octet), unsigned int (2
octeţi), unsigned long (4 octeţi)
– Tipuri de date întregi cu semn (signed): signed char (1 octet), signed int (2 octeţi),
signed long (4 octeţi).

Date de tip caracter


În limbajul C/C++ este implementat tipul char.

Date de tip logic (boolean)


În limbajul C nu este implementat tipul logicbool; el poate fi totuși folosit dacă se
include <stdbool.h> la salvarea sursei cu .c; pentru evaluarea unei operaţii logice
se folosesc valorile numerice 1 sau diferit de 0 (true) şi 0 (false). În C++ bool este
nativ, deci nu e nevoie să se includă vreun header anume la salvarea sursei .cpp.

Date de tip real


În limbajul C++ sunt implementate mai multe subtipuri reale de date: float (4 octeţi),
double (8 octeţi), long double (10 octeţi).

5
Tipul datelor determină modul în care sunt reprezentate acestea în memoria internă
și operatorii permişi pentru prelucrarea lor.

Se poate testa lungimea în octeţi a fiecărui tip de dată folosind operatorul sizeof().
Acesta furnizează numărul de octeţi folosiţi pentru memorarea unei date,
precizată printr-o expresie sau prin tipul datei.

TypeBytes Range
--------------------------------------------------------------------
short int 2 -32,768 -> +32,767 (32kb)
unsigned short int 2 0 -> +65,535 (64Kb)
unsigned int 4 0 -> +4,294,967,295 ( 4Gb)
int 4 -2,147,483,648 -> +2,147,483,647 ( 2Gb)
long int 4 -2,147,483,648 -> +2,147,483,647 ( 2Gb)
signed char 1 -128 -> +127
unsigned char 1 0 -> +255
float 4
double 8
long double 12

Tabelul 1. Tipurile de date elementare și dimensiunea lor într-o arhitectură


pe 32 biți [2]

Criteriul compunerii
Date simple sau date elementare
Sunt date independente unele de altele, din punct de vedere al reprezentării lor în
memorie.Localizarea unei date pe suportul de memorare nu se face în funcţie de
locaţia unei alte date pe suport.
Date compuse sau structuri de date.
Sunt colecţii de date între care există anumite relaţii (numite relaţii
structurale).Includem aici tablourile (vectori, matrici, masive), înregistrările și uniunile,
fișierele stringurile, tipul enumerat [2], liste, arbori, grafuri, etc.

Fiecare componentă a structurii are o anumită poziţie în cadrul ei. Pentru fiecare
tip de structură de date, în limbajul de programare trebuie să fie definiţi algoritmi de
localizare a componentelor în cadrul structurii de date.

Între componentele structurii există și legături de conţinut, adică întregul ansamblu


de date din colecţie poate caracteriza un obiect, o persoană, un fenomen, un
proces.

Criteriul variabilităţii
Constante
Sunt date a căror valoare nu se modifică în timpul execuţiei programului.

Într-un program pot fi folosite constante, predefinite sau nu. Aceste constante au un
identificator, iar în program se va folosi, evident, identificatorul.

În limbajul C/C++, declararea constantelor se face cu declaraţia const, iar definirea


lor cu #define. O sumă de considerații asupra constantelor găsiți la [1].

Variabile
Sunt date a căror valoare se modifică în timpul execuţiei programului, când pot avea
o valoare iniţială, mai multe valori intermediare şi o valoare finală.

Funcție de tipul componentelor structurii:


6
            -structuri omogene (componentele sunt de același tip)
            -structuri neomogene(componentele sunt de tipuri diferite)
Funcție de modul de localizare a componentelor structurii:
-structuri cu acces direct (o componentă poate fi localizată fără să se țină
cont de celelalte componente ale structurii)
-structuri cu acces secvențial (o componentă poate fi localizată numai dacă
se parcurg componentele care o preced în structură)
Funcție de tipul de memorie în care sunt create:
-structuri interne (în memoria internă)
-structuri externe (în memoria externă)
Funcție de timpul de utilizare:
-structuri de date temporare
-structuri de date permanente
Funcție de stabilitatea structurii:
-structuri dinamice (în timpul existenței, în urma executării unor procese, își
modifică numărul de componente și relațiile dintre ele)
-structuri statice (nu își modifică în timpul existenței numărul de componente și
relațiile dintre ele)
După posibilitatea de descompunere în structuri de același tip: pot fi recursive
sau nu;
Asupra unei structuri de date se pot executa mai multe operații care pot afecta
valoarea componentelor structurii și/sau structura de date:
1.Crearea, prin care se realizează structura de date în forma initială, pe suportul de
memorare utilizat
2.Consultarea, i.e. accesul la componentele structurii în vederea prelucrării valorilor
acestora, a extragerii de informații
3.Actualizarea, prin care se schimbă starea structurii astfel încât ea să reflecte
corect valoarea componentelor la un moment dat. Actualizarea se face prin trei
operații: adăugarea unor componente noi, ștergerea unor componente și modificarea
valorii componentelor
4.Sortarea, prin care se (re)aranjează componentele structurii utilizând criterii de
ordonare aplicate valorilor componentelor
5.Copierea, prin care se realizează o dublură a structurii, pe același suport sau pe
suporturi diferite de memorare
6.Mutarea, prin care se transferă structura, pe același suport, la o adresă, (sau
suport de memorare) diferit
7.Redenumirea, prin care se schimbă identificatorul structurii
8.Divizarea, prin care se realizează două sau mai multe structuri dintr-o structură de
bază
9.Combinarea, prin care se realizeaza o singură structură de date, prin combinarea
a două sau mai multe structuri
10.Ștergerea, prin care se distruge structura de date
Tipul de structura de date definește apartenența structurii de date la o anumită
familie de structuri cărora le corespunde același mod de organizare logică, același
model de reprezentare fizică și care pot fi supuse acelorași operații.

7
Liste

Listele fac parte din cotidian, în fiecare zi construim liste ca o modalitate de a pune
lucrurile în ordine: lista cumpărăturilor, lista contactelor din telefon, lista examenelor
dintr-o sesiune, etc,
O listă liniară este un set finit de elemente ordonate liniar, adică există un
element considerat a fi primul în listă, un element considerat a fi ultimul, pentru
oricare alt element din listă existând un element precedent și un element următor.
Principalele operații cu liste sunt:
 accesarea sau modificarea unui element din listă
 inserarea unui element în listă
 eliminarea unui element din listă
Modul în care aceste operatii sunt efectuate depinde de modul de reprezentare a
listelor. Există două tipuri de reprezentări: secventială (statică) și înlănțuită
(dinamică).

1.1 Alocarea secvențială

În alocarea secvențială lista se reprezintă ca un șir în care elementele sunt


memorate în locații consecutive de memorie. În C, o astfel de listă este, de fapt, un
șir definit prin:
identificator_tip identificator_sir[nr_elemente];

unde identificator_tip este tipul elementelor sirului, identificator_sir este numele


șirului (listei), iar nr_elemente este o constantă ce reprezintă numărul maxim posibil
de elemente ale șirului. În C, se poate face referire la elementele șirului prin
intermediul indicelui, începând de la 0, notatidentificator_sir[0], ultimul
fiindidentificator_sir[nr_elemente –1], iar un element de indice k, 1  k  nr_elemente
- 2 este identificator_sir[k]și are ca precedent elementul identificator_sir[k-1]fiind
urmat de elementul identificator_sir[k+1]. Elementele șirului sunt memorate astfel
încât:
&identificator_sir[k] = &(identificator_sir[0] + k *sizeof (identificator_tip))

unde sizeof(identificator_tip) returnează numărul de octeți necesari memorării unei


singure variabile de tipul identificator_tip.

Fie un șir definit


T x[N];
unde N este o constantă suficient de mare, iar T este un tip de date cunoscut
anterior, iar n este numărul de elemente din listă,n  N.

x[0] x[1] x[2] x[N]


... ... ...

Operațiile cu liste, se pot descrie astfel:

8
 accesarea/ modificarea unui element se face prin intermediul indicelui
elementului (similar cu vectorii)
 inserarea unui element se poate face într-o poziție dată, după sau înaintea unui
element dat. În continuare vom inserara un element numit elem_nou într-o poziție
datăk, deoarece celelalte se reduc la aceasta.

Algoritm. Fie0  k  n. Dacă n = N atunci se produce OVERFLOW respectiv


spațiul alocat listei este ocupat în totalitate și nici o altă inserare nu este posibilă. În
caz contrar se mută elementele șirului x[k], ..., x[n-1], câte un bloc de memorie spre
dreapta, începand cu ultimul (în caz contrar se suprascriu valorile cu cele ale
elementului din stânga=primul – dacă începem să mutăm din stânga). Se introduce
elem_nou în poziția k. Se actualizează numărul de elemente al șirului:

if n = N then OVERFLOW
else for i = n-1, k, -1
x[i+1] = x[i]
endfor
endif
x[k] = elem_nou
n = n +1
 eliminarea elementului de indice k din listă.
Algoritm. Dacă n = 0 atunci se produce UNDERFLOW, respectiv lista este vidă
și deci eliminarea unui element din listă nu este posibilă. Presupunem 0  k  n-1. Se
salvează informația conținută de elementul de indice k pentru eventuale prelucrări
ulterioare. Se mută elementele șirului x[k+1], ..., x[n-1], câte un bloc de memorie spre
stânga, începând cu x[k+1]. Se actualizează numărul de elemente al șirului:

if n = 0 then UNDERFLOW
else elem_sters = x[k]
for i = k, n-2,1
x[i] = x[i+1]
endfor
endif
n = n –1

1.2 Alocarea înlănțuită

De multe ori, memoria nu este ocupată consecutiv, zone de memorie libere alternând
cu zone de memorie ocupate. Alocarea secvențială a unei liste se poate face dacă
există blocuri de memorie suficient de mari pentru a o memora. Dacă, de exemplu,
vrem să alocam spațiu pentru o listă cu M elemente, dar nu există nici un bloc de
memorie de mărime M, deși există trei blocuri de memorie de mărime M/2, ar trebui
să găsim o modalitate de reorganizare a memoriei, altfel lista nu poate fi creată sau
să găsim o metodă alternativă de reprezentare a listei care să elimine neajunsul ca
elementele să fie memorate în locații consecutive. Acesta din urmă este cazul
alocării înlănțuite.
În acest caz, pentru a păstra ordinea listei va trebui să cunoaștem care este primul
element al listei, al doilea etc. fapt pentru care vom folosi un pointer la primul element
al listei (numit pointerul listei notat HEAD, ori TOP, FIRST, VÂRF etc). Pentru a ști

9
care este următorul element în listă fiecare element va trebui să dețină pe lângă
informația corespunzătoare lui și un pointer la elementul următor. Elementele
nemaifiind memorate în locații consecutive, acești pointeri ajută la reconstituirea
listei. Ultimul element al listei va avea drept componentă pointerul NULL. Din acest
motiv, fiecare element va fi de tip structură, cu două câmpuri: info (folosit pentru
memorarea informației elementului) și link (un pointer la elementul următor). Datorită
reprezentării structurate, elementele unei astfel de liste se mai numesc noduri.

Tipul de date asociat unui nod, în C:


struct nod
{
T info;
struct nod *link;
};
typedef struct nod NOD;

unde T este presupus definit anterior (eventual printr-o definiție typedef).

HEAD
info link info info
...

În aceste condiții, operațiile cu liste se pot descrie astfel:


 accesarea/ modificarea unui element ce conține o valoare datăk, k de tip T.
Algoritm.Folosim variabilele gasit de tip boolean, gasit = false dacă elementul cu
valoarea k nu a fost găsit în listă șitrue în caz contrar, respectivi iter de tip pointer la
un nod;iter pointează la primul nod al listei și va parcurge lista până când nodul dorit
este găsit sau, în caz contrar până cand se ajunge la ultimul nod al listei.
gasit = false
iter = HEAD
while (not gasit and iter  NULL)
if iter -> info = k then gasit = true
else iter = iter -> link
endif
endwhile
if gasit then acceseaza nodul pointat de iter
else afiseaza mesajul "Nu exista nici un nod cu informatia data."
endif

 inserarea unui nou nod


i) la începutul listei:
Algoritm.Folosim variabilele info_nou ce conține valoarea informației nodului ce
trebuie introdus în listă șip=pointer la un nod.
Se alocă memorie pentru un nod nou și se returnează p - pointer la noul nod.
if p  NULL then
p -> link = HEAD
p -> info = info_nou
HEAD = p
else OVERFLOW
endif

10
HEAD
NU
...

p
info_nou

Obs. Algoritmul functionează chiar dacă lista este vidă și se poate folosi
pentru crearea unei liste prin introducerea pe rând, a elementelor listei.
ii) la sfârșitul listei:
Algoritm. Folosim variabilele info_nou ce conține valoarea informației nodului ce
trebuie introdus în listă, p pointer la un nod șiiter care inițial pointează la primul nod al
listei pe care o va parcurge până când acesta va pointa la ultimul nod.
Se alocă memorie pentru un nod nou și se returnează p - pointer la noul nod.
if p  NULL then
p -> link = NULL
p -> info = info_nou
iter = HEAD
while (iter  NULL and iter -> link  NULL)
iter = iter -> link
endwhile
if iter = NULL then HEAD = p //lista vida, adaug primul elem
else iter ->link =p
endif
else OVERFLOW
endif

HEAD

NULL

iter

info_nou NULL
p

iii) după un nod dat:


Algoritm. Folosim variabilele info_nou pentru valoarea informației nodului ce trebuie
introdus în listă șip pointer la un nod cât șiinainte- pointer la nodul după care se
dorește introducerea noului nod.
Se alocă memorie pentru un nod nou și se returnează p - pointer la noul nod.
if p  NULL then
p -> link = inainte ->link
inainte -> link =p
p -> info = info_nou

11
else OVERFLOW
endif

...

inainte

p info_nou

iv) eliminarea unui nod dat din listă:


Algoritm. Folosim variabilele sterge și iter de tip pointer, sterge este pointer la nodul
care trebuie eliminat din listă șiiter care inițial pointează la primul nod al listei va
parcurge lista până când acesta va pointa la nodul dinaintea nodului de șters din
listă.

iter = HEAD
while (iter  NULL and iter -> link  sterge)
iter = iter -> link
endwhile
if iter = NULL then tipareste mesajul "Eroare. Lista este vida.
UNDERFLOW."
else recupereaza informatia nodului de sters sterge->info
iter ->link = sterge -> link
endif

... ...

iter sterge

Lab 1.
1. Scrieți un program care implementează algoritmii de mai sus pentru o listă liniară
alocată secvențial. 2. Scrieți un program care implementează algoritmii de mai sus
pentru o listă liniară alocată înlănțuit.

12
II. Stive (Implementări, interfeţe, demonstraţii)

Stiva este o listă liniarăîn care inserările și stergerile din listă se pot face numai pe la
un capat al listei, numit vârful stivei. Singurul element care poate fi accesat la un
moment dat este cel din vârful stivei. Se poate face o analogie între o stivă folosita în
programare și o stivă de caiete. Adăugarea unui caiet se poate face numai în vârful
stivei de caiete, peste cele existente și singurul caiet ce poate fi accesat, eventual
eliminat din stivă este cel din vârf.

stergere
inserare

...

Fig.2.1 Stiva

Stivele se mai numesc și liste push-down sau LIFO (last în/first out) deoarece primul
element care este extras din stivă este ultimul introdus în aceasta.
Cele două operații inserarea și ștergerea se mai numesc șipush respectiv
pop.

II.1 Alocarea secvențială

În alocarea secventială, presupunând că stiva este definităastfel:


T x[N];
unde N este o constantă suficient de mare, T este un tip de date definit anterior, iar n
este numărul de elemente din stivă,n  N, iar elementul din varful stivei este
considerat elementul x[n-1],
x[0] x[1] x[2] x[n -1]
... ... ...

atunci operația de inserare se poate descrie:


if n = N then OVERFLOW
else n = n + 1
x[n-1] = elem_nou
endif

iar operația de ștergere se poate descrie:


if n = 0 then UNDERFLOW
else elem_sters = x[n-1]
n = n – 1
endif

13
II.2 Alocarea înlănțuită

În alocarea înlănțuită, folosim aceeași structură ca șiîn cazul listei liniare


struct nod
{
T info;
struct nod *link;
};
typedef struct nod NOD;
unde T este presupus definit anterior și notăm cu vf pointerul stivei (adică pointerul
care pointează la elementul din vârful stivei).
NOD *vf;
Cu aceste notații, operațiile cu stive se pot descrie astfel:

 inserarea unui nod nou:  ștergerea/accesarea unui nod:


Algoritm. Folosim variabilele info_nou ce Algoritm.
conține valoarea informației nodului ce
trebuie introdus în listă șip pointer la un
nod.
Alocămemorie pentru un nod nou.
Returnează p, un pointer la noul nod.
if p  NULL then if vf = NULL then UNDERFLOW
p -> link = top else elem_sters = vf -> info
p -> info = info_nou vf = vf -> link
vf = p endif
else OVERFLOW
endif

info_nou top
p info

top

.............

NULL
.......

NULL
Lab 2. Scrieți un program care implementează algoritmii de mai sus pentru o stivă.

14
III. Cozi (Implementări, interfeţe, demonstraţii)

O coadă este o listă liniară în care ștergerea/accesarea unui element se pot face
numai pe la un capăt al său, numit front, iar inserarea se face la celălalt capăt al
cozii, numit rear. Se poate face o analogie între o coadă folosită în programare și, de
exemplu, o coadă pentru tipărirea mai multor fișiere text. Pentru tipărirea unui nou
fișier va trebui să asteptăm până când toate fișierele sunt tipărite în ordinea în care
comenzile de tipărire au fost efectuate, exemplu ce ilustrează conceptul de coadă.

stergere
front
...

rear
inserare

Fig. 3.1 Coada

Cozile se mai numesc și liste FIFO (first in/first out) deoarece primul element care
este extras din coadă este primul introdus.

3.1 Alocarea secvențială

În alocarea secvențială, presupunând că avem o coadă definită:


T x[N];
unde N este o constantă suficient de mare, T este un tip de date definit anterior
(eventual printr-o definiție de tipul typedef), iar elementele cozii sunt în ordine x[R],
x[R+1], ..., x[F], cu indicii 0  R  F  N – 1,
x[R] x[R+1] x[F]
... ... ...

atunci operația de inserarese poate descrie astfel:


if R = 0 and F = N - 1 then OVERFLOW
else if R > 0 then x[R – 1] = elem_nou
R = R –1
else for i = F, R, -1
x[i+1] = x[i]
endfor
x[0] = elem_nou
F = F +1
endif
endif
iar operația de ștergerese poate descrie astfel:
if R > F then UNDERFLOW
else elem_sters = x[F]
F = F -1
endif

15
3.2 Alocarea înlănțuită

Folosim aceeași structură ca și în cazul listei liniare

struct nod
{
T info;
struct nod *link;
};
typedef struct nod NOD;
unde T este presupus definit anterior; notăm cu front, respectiv rear, pointerul la
primul nod al cozii, respectiv la ultimul nod.
NOD *front, rear;
Cu aceste notații, operațiile cu cozi se pot descrie astfel:

 inserarea unui nod nou:  ștergerea/accesarea unui nod:


Algoritm. Folosim variabilele info_nou ce Algoritm.
conține valoarea informației nodului ce
trebuie introdus în listă și p pointer la un
nod. Se alocămemorie pentru un nod
nou. Return p - pointer la noul nod.
if p  NULL then
if front = NULL then UNDERFLOW
p -> link = NULL
else elem_sters = front -> info
p -> info = info_nou
front = front -> link
rear -> link = p
endif
rear = p
else OVERFLOW
endif
front
info
front

...........

.............
rear

rear NULL

info_nou
p NULL

III.3. Alte tipuri de cozi


16
Coada cu priorităţi - Priority Queues
Elementele au, pe lângă cheie şi o prioritate:
- cea mai înaltă prioritate este 1, urmată de 2, etc.
Ordinea liniară este dată de regulile:
- elementele cu aceeaşi prioritate sunt extrase (şi procesate) în ordinea intrării;
- toate elementele cu prioritate i se află înaintea celor cu prioritate i+1 (şi deci vor
fi extrase înaintea lor).
Extragerile se fac dintr-un singur capăt.
Ca să se poată aplica regulile de mai sus la extragere, inserarea unui nou element
cu prioritate i se va face la sfârşitul listei ce conţine toate elementele cu prioritate i.

Double Ended Queue sau DEQUE


- structură liniară în care inserările şi ştergerile se pot face la oricare din cele două
capete, dar în nici un alt loc din coadă.
Pot apărea restricţii pentru astfel de de cozi, funcție de modelarea anumitor
probleme, restricții de tipul:
- inserările se pot face la un singur capăt şi extragerile la amândouă.

Lab 3.
1. Scrieți un program care implementează algoritmii de mai sus pentru o coadă. 2.
Implementați o coadă cu priorități și una DEQUE, chei la alegere.

17
IV. Liste înlănţuite
(dublu, circulare: implementări, operaţii, demonstraţii)

4.1 Liste dublu înlănțuite

Pentru folosirea mai ușoară a listelor liniare putem avea, pentru fiecare nod al listei
nu numai un pointer la elementul următor ci și unul la nodul precedent. O astfel de
reprezentare a unei liste se numește listă dublu înlănțuită. Astfel, orice nod va avea
forma:

llink info rlink

Orice lista dublu înlănțuită va deține doi pointeri FIRST, către primul nod și LAST
care pointează la ultimul nod.

FIRST LAST

NULL llink info rlink llink info rlink


... NULL

Deasemenea, se fac următoarele setări FIRST->llink=NULL și LAST->rlink=


NULL.
Observați că pentru orice pointer p la un nod avem:
(p -> rlink) -> llink = p și (p -> llink) -> rlink = p.
Operațiile cu liste dublu înlănțuite se pot descrie astfel:
 accesarea/ modificarea unui element ce conține o valoare dată se face ca și în
cazul listei liniare simplu înlânțuite, parcurgându-se lista fie de la început la
sfârșit, fie de la sfârșit la început.
 inserarea unui nou nod
o la inceputul listei:
Algoritm. Folosim variabilele info_nou ce conține valoarea informației nodului ce
trebuie introdus în listă și p pointer la un nod.
Alocă memorie pentru un nod nou. Returnează p - pointer la noul nod.
if p  NULL then
p -> llink = NULL
p -> rlink = FIRST
p -> info = info_nou
FIRST -> llink = p
FIRST = p
else OVERFLOW
endif

18
FIRST LAST

llink info rlink llink info rlink


NULL
... NULL

llink info rlink

NULL
p

o la sfârșitul listei:

Algoritm. Folosim variabilele info_nou ce conține valoarea informației nodului ce


trebuie introdus in listă, p pointer la un nod
Alocă memorie pentru un nod nou. Returnează p, un pointer la noul nod.
if p  NULL then
p -> rlink = NULL
p -> info = info_nou
p -> llink = LAST
LAST -> rlink =p
LAST=p
else OVERFLOW
endif

FIRST LAST

NULL llink info rlink llink info rlink


... NULL

llink info rlink


p NULL

o după un nod dat:


Algoritm. Folosim variabilele info_nou ce conține valoarea informației nodului ce trebuie
introdus în listă și p pointer la un nod și inainte pointer la nodul după care se dorește
introducerea noului nod.
Alocă memorie pentru un nod nou. Returneaza p - pointer la noul nod.
if p  NULL then
p -> llink = inainte
p -> rlink = inainte -> rlink
inainte -> rlink = p
p -> rlink ) -> llink = p
p -> info = info_nou
else OVERFLOW

19
endif

inainte

llink info rlink llink info rlink


... ...

llink info rlink

p
 eliminarea unui nod
o eliminarea primului nod:
Algoritm.
elem_sters = FIRST -> info
FIRST = FIRST -> rlink
FIRST ->llink = NULL

FIRST

llink info rlink llink info rlink


NULL
...

NULL
o eliminarea ultimului nod:
Algoritm.
elem_sters = LAST -> info
LAST = LAST -> llink
LAST ->rlink = NULL

LAST

llink info rlink llink info rlink


... NULL

NULL
 eliminarea unui nod dat din listă:
20
Algoritm. Folosim variabilele sterge,un pointer la nodul care trebuie eliminat din listă,
inainte, pointer la nodul dinainte și dupa, pointer la nodul după nodul ce trebuie șters.
elem_sters = sterge -> info
inainte = sterge -> llink
dupa = sterge -> rlink
dupa ->llink = inainte
inainte ->rlink = dupa

inainte sterge dupa

... ... llink info rlink llink info rlink llink info rlink ...

4.2 Liste circulare

Într-o listă circulară ultimul element indică spre primul, atât în alocarea statică cât și
în cea înlănțuită. În alocarea statică va trebui sa folosim un tip struct pentru e reține
indexul începutului șirului de elemente.
struct Element
{ int info;
int link;
};
Element x[9];// similar cu inlantuirea, dar fara pointer catre
urmatorul nod
Primul nod din șir se va reține într-o variabilăfirst, iar parcurgerea poate fi:
int elem_crt;
first=2;
………………………
if (first!=null){
elem_crt = first;// alte prelucrari asupra lui x[]
while (x[elem_crt].link!=first) {
elem_crt = x[elem_crt].leg;//are ca val index nod urm
………………………… // alte prelucrari asupra lui x[]
}
}

Liste circulare alocate dinamic


Presupunem aceeași structură nod.
Operaţiile cu o listă circulară se descriu astfel:
-accesarea/ modificarea unui element ce contine o valoare dată k, k de tip T, se face
la fel ca in cazul listei liniare.
-inserarea unui nou nod
o la începutul listei (sau la sfârşitul listei, este acelaşi lucru):

21
Algoritm. Folosim variabilele info_nou ce conţine valoarea informaţiei nodului ce
trebuie introdus în listă, p pointer la un nod şi iter care iniţial pointează la primul nod
al listei şi va parcurge lista până când acesta va pointa la ultimul nod din listă.
Alocă memorie pentru un nod nou. Returneaza p, un pointer la noul nod.
if p !=NULL then
p -> link = HEAD
p -> info = info_nou
iter = HEAD
while (iter -> link != HEAD)
iter = iter -> link
endwhile
iter -> link =p
HEAD = p
else OVERFLOW
endif

o după un nod dat: se face la fel ca în cazul listei liniare


- eliminarea unui nod dat din listă:
o eliminarea primului nod
Algoritm. Folosim variabila iter care iniţial pointează la primul nod al listei şi va
parcurge lista până când acesta va pointa la ultimul nod din listă.
elem_sters = HEAD -> info
iter = HEAD
while (iter -> link != HEAD)
iter = iter -> link
endwhile
iter -> link = HEAD -> link
HEAD = HEAD -> link
HEAD

...
info link info link info

o eliminarea ultimului nod:


Algoritm. Folosim variabila iter care iniţial pointează la primul nod al listei şi va
parcurge lista până când acesta va pointa la penultimul nod din listă.

iter = HEAD
while (iter != NULL and iter -> link &&HEAD and (iter -> link)
->link != HEAD)
iter = iter -> link
endwhile
if iter = NULL then UNDERFLOW
else if iter -> link = HEAD then elem_sters = iter ->info

22
else elem_sters = (iter - >link) ->info
iter -> link = HEAD
endif
endif

HEAD iter
....

info link info link info link

o eliminarea altui nod din listă se face la fel ca în cazul listei liniare

Lab 4.
1. Scrieți un program care implementează algoritmii de mai sus pentru liste dublu
înlănțuite. 2. Analog pentru liste circulare.

23
V. Grafuri (conexe, orientate/neorientate, reprezentări, parcurgeri)

5.1 Noțiuni generale

Definiție: Se numeste graf neorientat o structură G = (N, M) în care N este o


mulțime finită, iar M o mulțime de perechi neordonate de elemente din V. Mai exact
M  {{a, b} | a, b V, a  b}. Mulțimea N se numește mulțimea nodurilor grafului G,
iar M mulțimea muchiilor. Notăm muchiile sub forma [a, b], unde a,b V.
În reprezentare grafică, se folosesc cercuri pentru noduri, și linii pnetru
muchii. De exemplu, G1 = (V1, M1) unde V1 = {a, b, c, d}, iar M1 = {[a, c], [b, c], [a,
d]} se reprezintă:

Fig.5.1 Graf neorientat G1

Definiție: Se numește graf orientat o structură G = (V, A) în care V este o


mulțime finită, iar A o mulțime de perechi ordonate de elemente din V. Mai exact A 
VV. Mulțimea V se numeste mulțimea vârfurilor grafului G, iar A mulțimea arcelor.
Dacă (a, b)  A atunci a se numește extremitatea inițială a arcului, iar b extremitatea
finală.
În reprezentare grafică, vârfurile se reprezintă prin cercuri, iar arcele prin
săgeți de la vârful sursă către cel destinație. De exemplu, următorul graf orientat G2
= (V2, A2) unde V2 = {a, b, c, d}, iar A2 = {(a, c), (c, b), (b, c), (a, a)} se reprezintă:

Fig.5.2 Graf orientat G2

Observăm că în cazul grafurilor orientate sunt permise arce cu vârful de start


identic cu cel de sfârșit.

Două vârfuri ale unui graf neorientat între care există o muchie se numesc
adiacente. Dacă a, b sunt două vârfuri ale unui graf orientat astfel încât există un arc
de la a la b atunci spunem că b este adiacent vârfului a.
Gradul unui vârf x al unui graf neorientat, notat d(x), este dat de numărul de
vârfuri adiacente cu el. Un vârf de grad zero se numeste izolat.

24
x = vârf d(x) = grad
a 2
b 1
c 2
d 1

Tabelul 5.1 Gradele nodurilor (graf G1 din Fig. 5.1)

Pentru grafuri orientate se definesc gradul de intrare al unui vârf, notat d-(x),
ca fiind numărul de arce care au ca extremitate finală vârful respectiv iar gradul de
ieșire, notat d+(x), se definește ca numărul de arce ce au ca extremitate inițială
vârful respectiv. Pentru exemplele de mai sus, avem:

x = vârf d-(x) = grad d+(x)= grad


intrare iesire
a 1 2
b 1 1
c 2 1
d 0 0

Tabelul 5.2. Gradele nodurilor într-un graf orientat (graf G2 din Fig. 5.2)

Dat un graf G = (V, E), se numește drum de la vârful x la un vârf y, o


succesiune de vârfuri, pe care o notăm [x, x1, x2, ..., xp , y] dacă G este neorientat și
(x, x1, x2, ..., xp , y) dacă G este orientat, astfel încât: [x, x1], [x1, x2], ..., [xp , y] sunt
muchii (dacă G este neorientat) sau (x, x1), (x1, x2), ..., (xp , y) sunt arce (dacă G
este orientat). Lungimea unui drum este dată de numărul muchiilor (arcelor)
drumului. Un drum se numește elementar dacă x, x1, x2, ..., xp sunt distincte și x1,
x2, ..., xp , y sunt de asemenea distincte. Un drum într-un graf neorientat [x, x1,
x2, ..., xp , y] pentru care x = y, drumul conține cel puțin o muchie și toate muchiile
sunt distincte se numește ciclu. Un drum într-un graf orientat (x, x1, x2, ..., xp , y)
pentru care x = y, drumul contine cel puțin un arc și toate arcele pe care le conține
sunt distincte se numește circuit. Un graf fără cicli se numește aciclic.
Exemple:
-[d, a, c, b] este un drum elementar de la d la b de lungime 3 în G1
-(a, a, c) este un drum de la a la c de lungime 2 în G2
-(a, c, b, c) este un drum de la a la c de lungime 3 în G2
-(a, c, b) este un drum elementar de la a la b de lungime 2 în G2
-nu există nici un drum de la d la orice alt nod în G2
-G1 este aciclic
-G2 are un singur ciclu: (b,c,b) de lungime 2.

Un graf neorientat se numeste conex dacă între orice două vârfuri ale sale
există un drum. Componentele conexe ale unui graf sunt clasele de echivalentță ale
vârfurilor aflate în relația "exista drum intre". De exemplu, G1 este conex în timp ce
G2 nu este conex, dar are 2 componente conexe.
Un graf neorientat se numeste complet dacă există muchie între oricare două
vârfuri.
Un graf G' = (V', E') este subgraf al grafului G = (V, E) daca V' V si E'  E.
Un graf G' = (V', E') este graf parțialal grafului G = (V, E) dacă V' = V și E'  E.
Se numește graf ponderat (sau cu ponderi) un graf G = (V, E) pentru care fiecărei
muchii i se asociază o pondere dată de obicei de o funcție f: E R.

25
5.2 Reprezentarea grafurilor

Reprezentarea grafurilor, orientate sau neorientate se poate face în două feluri:


1) Matricea de adiacență
Fie G = (V, E) un graf dat și n = numărul de vârfuri notate 1, 2, ..., n. Matricea de
adiacență este o matrice A= (aij), 1  i, j  n definită astfel:
aij = 1 dacă (i, j)  E și 0 în caz contrar
Exemple: Matricile de adiacență ale grafurilor de mai sus sunt:
(Pentru ambele grafuri vom nota vârfurile: a 1, b 2, c 3 si d 4.)

Matricea de adiacență a lui G1 Matricea de adiacență a lui G2


1 2 3 4 1 2 3 4
1 0 0 1 1 1 1 0 1 0
2 0 0 1 0 2 0 0 1 0
3 1 1 0 0 Folosirea 3 0 1 0 0 acestui mod de
4 1 0 0 0 4 0 0 0 0 reprezentare a
unui graf prezintă avantaje și
dezavantaje. Dezavantajele ar fi in primul rând folosirea unui spațiu mare de
memorie, în special dacă numărul de vârfuri este mare. De asemenea, de multe ori
aceasta matrice este rară. Matricea de adiacență a unui graf neorientat este
simetrică, cum relația de vecinătate este reciprocă la grafuri neorientate.
2) Liste de adiacență
Pentru fiecare vârf asociem lista vârfurilor adiacente lui, numită lista de adiacență. În
această reprezentare, pentru un graf dat G = (V, E) avem un șir Adj cu n elemente
unde n = numărul de vârfuri pe care le vom nota 1, 2, ..., n astfel încât Adj[i] conține
toate vârfurile j pentru care (i, j)  E. De obicei, vârfurile în fiecare listă de adiacență
sunt sortate într-o anumită ordine, dar acest lucru nu este obligatoriu.
Exemplu: Listele de adiacențăpentru primul graf: (Vom nota vârfurile: a 1, b 2, c
3 si d 4. )

1 3 4 NULL

2 3 NULL

3 1 2 NULL

1 NULL
4

Fig. 5.3 Reprezentarea grafului G1 prin liste de adiacență

5.3. Parcurgeri

Parcurgerea unui graf presupune examinarea tuturor vârfurilor grafului, cu


scopul prelucrării informaţiilor asociate acestora. Există două metode fundamentale

26
de parcurgere a grafurilor: 1. Parcurgerea în adâncime (DFS – Depth First Search) 2.
Parcurgerea în lăţime (BFS – Breadth First Search).
Fie graful (obs. graful e format din trei componente conexe, între care un nod
izolat [3]):

Fig. 5. 4 Graf format din trei componente conexe

1. Parcurgerea în adâncime se caracterizează prin faptul că se merge „în


adâncime” ori de câte ori este posibil. Parcurgerea începe cu vârful iniţial, denumit
vârful de start, care este şi vizitat. Se vizitează apoi primul vecin nevizitat al vârfului
de start. Vârful y este considerat vecin al vârfului x dacă există muchia (pentru graful
neorientat), respectiv arcul (pentru graful orientat). Se vizitează în continuare primul
vecin nevizitat al primului vecin al vârfului de start, şi aşa mai departe, mergând în
adâncime, până când ajungem într-un vârf care nu mai are vecini nevizitaţi. Situați
într-un astfel de vârf, revenim la vârful său părinte (din care acest nod a fost vizitat).
Dacă acest nod mai are vecini nevizitaţi, alegem primul vecin nevizitat al său şi
continuăm parcurgerea în acelaşi mod. Dacă nici acest vârf nu mai are vecini
nevizitaţi, revenim în vârful său părinte şi continuăm în acelaşi mod, până când toate
vârfurile accesibile din vârful de start sunt vizitate.
Exemple: din nodul 1 – 1, 2, 3, 5, 4, 6, 7, 8, 9
din nodul 5 – 5, 2, 1, 4, 6, 3, 7, 8, 9
din nodul 10 – 10, 11
Obs. În cazul reprezentării prin liste de adiacenţă, complexitatea este O(n+m). În
cazul reprezentării prin matrice de adiacenţă este O(n2).

2. Parcurgerea în lăţime începe cu vârful iniţial = vârful de start, care se


vizitează mai întâi. Se vizitează în ordine toţi vecinii nevizitaţi ai vârfului de start. Apoi
se vizitează în ordine toţi vecinii nevizitaţi ai vecinilor vârfului de start şi aşa mai
departe, până la epuizarea tuturor vârfurilor accesibile din vârful de start.
Exemple: din nodul 1 – 1, 2, 4, 3, 5, 6, 7, 8, 9
din nodul 5 – 5, 2, 4, 7, 1, 3, 6, 8, 9
din nodul 10 – 10, 11
Obs. În cadrul parcurgerii în lăţime fiecare vârf este vizitat pe cel mai scurt lanţ/drum,
începând din vârful de start.

Lab 5.
1. Parcurgeți graful figurat mai sus în lățime, indicând lista nodurilor, având ca nod de
start fiecare nod din cele rămase, pe rând (vezi exemplele de parcurgere). 2. Aceeași
cerință ca la 1, pentru parcurgerea în adâncime. 3. Scrieți un program care parcurge
în adâncime și în lățime un graf cu chei întregi (reprezentarea la alegere), indicând
parcursul. Pentru bonus: 4. Scrieți un program care adaugă și/sau șterge o muchie
dintr-un graf neorientat. 5. Scrieți un program care pentru un graf neorientat
determină dacă graful este conex.
27
VI. Tabele de dispersie (dicționare, hashing)

Considerații generale. Când discutăm despre căutare profităm de un lucru:


cunoaștem informații despre locurile în care sunt stocate articolele din colecție unul
față de celălalt [5]. De exemplu, știind că a fost creată o listă, am putea căuta în timp
logaritmic folosind o căutare binară. În această secțiune vom încerca să mergem cu
un pas mai departe construind o structură de date care poate fi căutată în O(1) ca
timp. Acest concept poartă numele de hashing. Pentru a face acest lucru, va trebui
să știm și mai multe despre locul în care ar putea fi articolele atunci când le vom
căuta în colecție. Dacă fiecare articol este unde estimăm că ar trebui să fie, căutarea
poate utiliza o singură comparație pentru a descoperi prezența articolului. Acest lucru
însă, nu apare în mod obișnuit.
Un tabel hash este o colecție de articole care sunt stocate astfel încât să fie ușor de
găsit mai târziu. Fiecare poziție a tabelului hash - numităslot, poate reține un element
și este uzual numit printr-o valoare întreagă începând de la 0. De exemplu, vom avea
un slot numit 0, un slot numit 1, un slot numit 2 ș.a.m.d. Inițial, tabela hash nu conține
elemente, astfel încât fiecare slot este gol.

Fig.6.1 Tabela hash vidă [5]

Pentru a adăuga elemente în tabelă astfel încât să le putem găsi ulterior rapid se
folosește o funcție numită funcție hash care face legătura între slot și elementul de
adăugat în slot. Funcția hash aplicată unui element întoarce poziția pe care acesta o
va ocupa în tabela hash, în intervalul 0..m-1 unde m este dimensiunea tabelei, uzual
un număr prim. Rațiunea pentru care este indicat ca mărimea tabelei hash să fie
număr prim o găsiți de exemplu la resursa [6]. Fie setul de elemente întregi 54, 26,
93, 17, 77 și 31 de adăugat în tabela de mai sus. O primă funcție hash, creată după
tehnica “metoda restului” ia un element și îl împarte la dimensiunea tabelului,
întorcându-se restul ca valoare hash (h (item) = item% 11). Astfel h(54)=54%11=10
(Fig.6.2 )

Fig.6.2 Tabela hash pentru funcția hash(item)=item%11 [5]

Se observă acum că putem căuta în O(1) un element; după ce am calculat funcția


hash(element)=poziție, căutăm în tabela hash la poziția rezultată, elementul.
Inserarea elementelor este o altă acțiune, anterioară sau nu căutarii acestora.
Problema care apare este cea a coliziunilor, de exemplu dacă vrem să introducem
55, h(55)=0 iar poziția 0 este ocupată de 77. O funcție hash perfectă nu permite
existența coliziunilor. Din păcate, având în vedere o colecție arbitrară de articole, nu

28
există o modalitate sistematică de a construi o funcție perfectă de hash. Din fericire,
nu avem nevoie ca funcția hash sa fie perfectă pentru a obtine eficiența.
O modalitate de a avea întotdeauna o funcție hash perfectă este de a mări
dimensiunea tabelei hash, astfel încât fiecare valoare posibilă din colecția de
elemente să poată fi inserată. Acest lucru garantează că fiecare articol va avea un
slot unic și este practic pentru un număr mic de articole, dar nu este eficient atunci
când numărul de articole este mare.
Scopul este crearea de funcții hash care să reducă la minimum numărul de
coliziuni, să fie ușor de calculat și să distribuie uniform articolele în tabelul hash.
Există o serie de moduri comune de a extinde metoda restului.
Metoda împachetării (sau plierii,indoirii, segmentării) constă în divizarea
itemului în bucăți de dimensiuni egale (asumând că e numeric, ultima parte poate să
nu fie de dimensiuni egale). Aceste bucăți sunt apoi sumate pentru a da valoarea
hash rezultată.
Exemplu: dacă articolul nostru este numărul de telefon 436-555-4601, am lua
cifrele și le-am împărți în grupuri de 2 (sau de mai mult de 2) : 43,65,55,46,01.
Sumând obținem 210. Dacă presupunem că tabelul nostru de hash are 11 sloturi,
atunci trebuie să efectuăm pasul suplimentar de divizare cu 11 și de păstrare a
restului. În acest caz, 210%11 este 1, astfel încât numărul de telefon 436-555-4601
este postat la slotul 1. Unele metode de pliere merg cu un pas mai departe și
inversează fiecare a doua bucată înainte de adăugare. Pentru exemplul de mai sus,
obținem 43 + 56 + 55 + 64 + 01 = 219 ceea ce dă 219% 11 = 10.
O altă tehnică numerică pentru construirea unei funcții hash se numește
metoda pătratului " din mijloc" (mid-square method). Mai întâi ridicăm elementul la
pătrat, apoi extragem o parte din cifrele rezultate, de regulă din mijloc.
Exemplu: dacă itemul ar fi 44, am calcula mai întâi 44^2 = 1.936,extragem
cifrele in mijloc (poate fi una, sau mai multe, depinde). Aici vom extrage 2, deci 93,
iar iar 93%11 dă 5. În final 44 va fi inserat la poziția 5. Procedând astfel, sunt șanse
mai mari ca distribuția elementelor să fie mai uniformă (v. Tabelul 6.1)

Item Rest Mid-Square


54 10 3
26 4 7
93 5 9
17 6 8
77 0 4
31 9 6

Tabelul 6.1. Distribuția elementelor pentru hash prin metoda restului vs mid-square

În situația în care eleemntele nu sunt numerice (caractere, stringuri etc) se adoptă


alte funcții hash. De exemplu, pentru cuvântul cat putem folosi suma codurilor ascii
ale caracterelor ce compun cuvântul combinată cu metoda restului.
Astfel: ord('c')=99, ord('a')=97, ord('t')=116, sumăm și calculăm restul să zicem tot
modulo 11, obținem 312%11=4, poziția ocupată de cat.
Funcția hash pentru un string va fi (Python, [5]):

def hash(astring, tablesize):


sum = 0

29
for pos in range(len(astring)):
sum = sum + ord(astring[pos])
return sum%tablesize

Deficiența acestei metode apare în cazul în care avem anagrame ale unui cuvintelor
deja hash-uite, respectiv aceleași litere dar în altă ordine. În aceste cazuri, apar
coliziuni cum, de exemplu h(cat)=h(tac)=h(atc) etc = 4. [5].
Pentru a evita acest lucru, putem lua în calcul ponderile elementelor, date de poziția
lor în șir, astfel: (Fig.6.3)

Fig. 6.3. Considerarea ponderilor în funcții hash pentru stringuri

Tratarea coliziunilor în tabele hash


Reamintim că o coliziune apare atunci când slotul e ocupat și trebuie să introducem
un nou element la aceeași poziție. O metoda constă în: ne uităm un tabela de hash și
încercăm să găsim un slot liber (deschis) pentru a reține în el acel item care a
provocat coliziunea. Se începe de la poziția inițială a valorii hash și apoi se trece într-
o manieră secvențială prin sloturi până când se găsește primul slot care este gol.
Rețineți că este posibil să fie nevoie să revenim la primul slot (circular) pentru a
acoperi întreaga tabelă de hash. Acest proces de căutare este denumit adresare
deschisă prin faptul că încearcă să găsească următorul slot liber sau adresă din
tabelul hash. Vizitând în mod sistematic fiecare slot, efectuăm o tehnică de adresare
deschisă numită sondare liniară.
Exemplu [5]. Considerăm funcția hash inițială h(item)=item%11, pentru itemii
54,26,93,17,77,31,44,55,20. Completarea primilor 6 poziții conduce la (Fig. 6.2).
Când ajungem la 44, h(44)=0, avem coliziune. Conform sondării liniare, căutăm
sistematic primul slot liber și plasăm acolo pe 44, care va ajunge pe slotul 1 (al
doilea). Similar procedăm cu 55, următorul în șirul de elemente, ceea ce înseamnă
că primele 3 poziții vor conține pe 77, 44, 55. Similar h(20)=9, coliziune și nu mai
avem sloturi libere către dreapta, caz în care reparcurgem tabela și inserăm 20 în
prima poziție liberă, slotul 3. (Fig.6.4)

Fig.6.4 Distribuția elementelor folosind adresarea deschisă

Un prim dezavantaj este faptul că în cazul căutării, dacă nu găsim elementul din
prima, va trebui să îl căutăm secvențial, începând cu poziția care îi succede și
30
reluând circular o dată (în cazul în care am ajuns la sfărșit). Astfel, pe 93 îl găsim pe
poziția 5, dar pentru 20, va trebui să parcurgem tabela începând cu poziția 9 pâna la
final și să reparcurgem de la început tabela. Un alt dezavantaj al adresării deschise
este clusterizarea, itemii au tendința de a se aglomera în zona valorii lor de hash,
așa cum se observă pentru 77, 44 și 55, care sunt toți lângă slotul 0.
O modalitate de a reduce clusteringulconstă în extinderea tehnicii de sondare liniară,
astfel încât în loc să căutăm secvențial următorul slot deschis, "sărim" sloturile,
distribuind astfel mai uniform elementele care au provocat coliziuni.
Exemplu: salt(skip) de 3 poziții. Aceasta înseamnă că, odată ce se produce o
coliziune, vom analiza fiecare al treilea slot până vom găsi unul care este gol (Fig.
6.5).

Fig. 6.5 Distribuția elementelor folosind salt de 3 poziții

Se observă că 44, inițial în slotul 1, este acum în slotul 3, 44 fiind primul din lista
inițială pentru care obțineam coliziune. Această tehnică de ocupare prin salt se
numește rehashing, respectiv reevaluez hash-ul inițial prin alt hash [5], conform
newhashvalue=rehash(oldhashvalue)
Saltul peste 3 poziții, se poate defini ca rehash(pos)=(pos+3)%sizeoftable, iar în
general: rehash(pos)=(pos+skip)%sizeoftable, unde skip e pasul [5].
Pasul trebuie ales astfel încât să asigurăm vizitarea tuturor sloturilor, motiv în plus
pentru a alege dimensiunea tabelei hash număr prim.
O altă metodă constă în a folosi un salt pătratic [5] în loc de unul constant, în sensul
că în loc săavem salt de 3 (constant), vom face salt de 1, 4, 9, 16, respectiv dacă
prima valoare hash este h, următoarele vor fi h+1, h+4, h+9, h+16. Tabela finală,
folosind salt pătratic va arăta ca în Fig. 6.6

Fig. 6.6. Tabela hash obținută prin salt pătratic

O altă metodă de a trata coliziunile constă în folosirea înlănțuirii, respectiv a utilizării


listelor, înlănțuirea permițând mai multor itemi să se afle la aceeasi locație în tabela
hash. Pentru exemplu considerat, tabele hash și listele aferente sunt figurate în Fig.
6.7. [5]

31
Fig. 6.7 Tabela hash obținută prin înlănțuire

Lab. 6.
1. Fie lista de numere:33, 112, 17, 29, 41, 120, 222, 101, 65. a) Figurati o tabela
hash de mărime 11 în care le introduceti folosind adresarea deschisă, funcția hash
fiind h(item)=item%11. b) Refigurați tabela redistribuind elementele prin folosirea unui
salt de 3. c) Refigurați tabela redistribuind elementele prin adresare pătratică. d)
Refigurați tabela utilizând liste înlănțuite în cazul coliziunilor. 2. Implementați pentru
lista de mai sus cerințele a)-d) cât și căutarea unei valori. 3. Implemenați și populați o
tabelă hash, pentru un set de 9 cuvinte la alegere, folosind funcția hash bazată pe
suma codurilor ascii ale caracterelor. Algoritmul trebuie să permită vizualizarea
tabelei hash și căutarea unei valori.

32
VII. Arbori (parcurgeri, aplicaţii).
Tipuri de arbori și aplicațiile lor (AVL, BST, etc.)

7.1 Noțiuni generale

Definiție: Un graf neorientat conex fără cicluri se numește arbore.


Exemple: Următoarele grafuri sunt arbori:

(1) (2)
Fig. 7.1. Arbori

Definiția de mai sus a noțiunii de arbore este cea folosită în literatura de


specialitate, în teoria grafurilor. Un tip special de arbore îl constituie acel arbore în
care se pune în evidenta un nod, numit rădăcină. Acesta este tipul de arbore folosit
în algoritmii computaționali și în continuare ne vom referi la arbori ca fiind arbori cu
rădăcină. Putem spune că un arbore este o mulțime de noduri legate între ele prin
muchii ce indică relațiile dintre noduri, relații de tip tată-fiu, similare arborilor
genealogici. În informatică arborii sunt vizualizați cu rădăcina în sus și frunzele în jos.
Nodurile sunt arajate pe nivele. Pe nivelul 0 se află un singur nod, rădăcina. Nodurile
fiecărui nivel al aborelui sunt fii nodurilor nivelului precedent. Un nod care are fii se
numește tată. Fiii cu același tată se numesc frați. De exemplu, in figura de mai sus,
in cazul aborelui 1, dacă alegem 2 ca fiind rădăcină, reprezentarea arborelui pe
nivele este:

Fig. 7.3 Arborele din Fig 7.1, pe nivele, de rădăcină 2

iar nodul 2 este tatăl nodurilor 6, 1, 3, și 7 (care sunt urmașii lui 2); 5 este fiul lui 6; 4
este fiul lui 3; iar 8 este fiul lui 7. Nodurile 5, 4, 8, și 1 nu au nici un fiu. Nodurile care
nu au fii se mai numesc frunze sau noduri terminale, iar muchiile dintre noduri,
ramuri. Nodurile 6, 1, 3 și 7 sunt frați. De asemenea, nodurile 5, 4 și 8 sunt urmasii lui
2, iar rădăcina este singurul nod care nu are tată. În general, un nod al unui arbore
poate avea un numar arbitrar de fii.

33
Dacă orice nod al unui arbore nu are mai mult de n fii atunci arborele se
numeste arbore n-ar. Un arbore în care orice nod nu are mai mult de 2 fii se numeste
arbore binar.Se numește înălțime a unui arbore lungimea celui mai lung drum de la
rădăcinăla un nod terminal. Pentru arborele de mai sus înălțimea este 2. Observați
că între orice nod și rădăcinăexistă exact un singur drum.

7.2 Arbori binari de căutare


În algoritmul de căutare binară pentru a sorta elementele unei liste, după
fiecare iterație algoritmul reduce numărul de elemente în care se face căutarea la
jumătate. Algoritmul este eficient dar structura de date folosita este o listă liniară, în
care inserările și ștergerile nu sunt eficiente (mai exact sunt de ordin n). În arborii
binari de căutare – Binary Search Trees (BST) operațiile de bază sunt proporționale
cu înălțimea arborelui, care pentru un arbore binar complet cu n noduri este log n.
Definiție: Un arbore binar de căutare este un arbore binar în care, dacă se
presupune că fiecărui nod îi asociem un element al colecției de obiecte ce dorim să o
sortăm, toate nodurile ce se află în subarborele stâng al oricărui nod dat au o valoare
mai micăsau cel mult egală cu valoarea nodului dat, iar toate nodurile ce se află în
subarborele drept au o valoare mai mare sau cel mult egală cu valoarea nodului.
Formal, proprietatea arborelui binar de căutare se descrie astfel: fie x un nod dintr-un
arbore binar de căutare. Dacă y este un nod din subarborele stâng al lui x, atunci
cheie[y] cheie[x]. Dacă y este un nod din subarborele drept al lui x, atunci cheie[x]
cheie[y].
Exemple:
1. Arbore care este arbore binar de căutare

Fig. 7.4 Arbore binar de căutare

2. Arbore care nu este arbore binar de căutare (9>4, iar 9 este nod în
subarborele stâng a lui 4).

34
Fig. 7.5 Contraexemplu - arbore binar de căutare

7.3 Operaţii specifice într-un arbore binar de căutare


Arborii de căutare sunt structuri de date ce posedă multe operaţii specifice
structurilor dinamice, precum: CAUTĂ, MINIM, MAXIM, PREDECESOR,
SUCCESOR, INSEREAZĂ şi ŞTERGE.

Traversări ale arborilor


Proprietatea arborelui binar de căutare ne permite să tipărim toate cheile în
ordine crescătoare cu ajutorul unui algoritm recursiv simplu, numit traversarea
arborelui îninordine: se tipăreşte cheia rădăcinii unui subarbore între valorile din
subarborele său stâng şi cele din subarborele său drept. Similar, o traversare a
arborelui în preordine va tipări cheia rădăcinii înaintea cheilor din subarbori, iar o
traversare a arborelui în postordine va tipări cheia rădăcinii după cheile din subarbori.
Procedura de tipărire a elementelor unui arbore binar de căutare (apelul cu
nodul rădăcină):
ARBORE-TRAVERSARE-INORDINE(x)
dacă x  NIL atunci
ARBORE-TRAVERSARE-INORDINE (stânga[x])
afişează cheie[x]
ARBORE-TRAVERSARE-INORDINE (dreapta[x])
Exemplu, traversarea în inordine a arborelui din Fig.7.4 afişează cheile: 1, 4, 9, 10,
21, 15, 23, 28.
Căutare în arbori binari de căutare
Pentru a căuta o valoare dată într-un arbore binar de căutare începem
comparând valoarea dată cu rădăcina arborelui și mergem apoi în jos la stânga sau
la dreapta, depinde cum este valoarea căutată față de valoarea rădăcinii. În exemplul
1 de mai sus, pentru a căuta valoarea 9, comparăm 9 cu 10 - valoarea rădăcinii, cum
9 < 10 comparăm valoarea căutată cu rădăcina subarborelui stâng, vom compara
deci 9 și 4 și cum 9>4 se merge și mai jos un nivel și se compară valoarea căutată cu
valoarea rădăcinii subarborelui drept, observăm că am găsit valoarea căutată.
Fiecare comparație reduce numărul de elemente comparate la jumatate, deci
algoritmul este similar căutării binare într-o listă liniară, cum s-a precizat anterior.
Acest lucru se întâmplădoar când arborele este echilibrat. De exemplu, pentru un
arbore ca cel din figura de mai jos timpul de căutare este proporțional cu numărul de
elemente ale întregii liste.

35
Fig. 7.6 Arbore binar de căutare. Căutare liniară

Procedura de căutare a unui nod de cheie k cunoscută (un pointer x la


rădăcina arborelui) returnează un pointer la nodul având cheia k (dacă există un
asemenea nod în arbore) sau NIL în caz contrar
ARBORE-CAUTĂ (x, k)
dacă x = NIL sau k = cheie[x] atunci
returnează x
dacă k < cheie[x] atunci
returnează ARBORE-CAUTĂ (stânga[x], k)
altfel
returnează ARBORE-CAUTĂ (dreapta[x], k)

Inserare și ștergere în arbori binari de căutare


Operațiile de inserare și ștergere trebuie efectuate astfel încât proprietatea de
arbore binar de căutare să se mențină.
Algoritm de inserare (varianta recursivă): Fie arborele binar de căutare R =
pointer la rădăcina arborelui și v noua valoare ce trebuie inserată în arbore.
Insert (T, v)
if R = NULL then Aloca memorie pentru un nod nou. Returneaza
p, un pointer la noul nod
p-> info = v
p->stang = NULL
p-> drept = NULL
R = p
else if v > R-> info then
call Insert (R->drept, v)
else if v < R-> info then
call Insert (R-> stang, v)

else write "valoare deja in arbore"


stop
endif
endif
endif
Algoritm de ștergere: Se dau arbore binar de căutare R = pointer la rădăcina
arborelui și nodul N ce trebuie eliminat din arbore.
36
Există trei cazuri:
i) N este nod terminal. Dacă se notează cu P un pointer la tatălnodului N, atunci
nodul N este înlocuit cu NULL, adicăP->stang = NULL dacă N este fiu stâng
sauP->drept = NULLdacă N este fiu drept.
ii) N are un singur fiu și fie X un pointer la fiul lui N atunci fiul lui N devine fiul tatălui
lui N, adică dacă se notează cu P un pointer la tatăl nodului N, atunci P->stang =
X dacă N este fiu stâng sau P->drept = X dacă N este fiu drept.
iii) N are 2 fii. Algoritmul este:
Găsește T cel mai din dreapta nod al subarborelui stâng al lui N.
Înlocuiește informația nodului N cu informația din nodul T.
Șterge nodul T.

7.2 Arbori AVL


Arborii AVL sau arborii echilibrați sunt arbori binari ordonați, care au în plus o
proprietate de echilibru, valabilă pentru orice nod al arborelui: înălțimea subarborelui
stâng al nodului diferă de înălțimea subarborelui drept al nodului prin cel mult o
unitate.
Structura elementelor unui arbore echilibrat poate fi reprezentată astfel[4]:
struct nod
{
int key;
int ech;
nod *left, *right;
};
nod * rad;
unde key reprezinta eticheta nodului (un număr întreg), ech- reprezintă factorul de
echilibrare, iar left și right sunt pointeri către fiul din stânga, respectiv din dreapta[4].
Reamintim că înălțimea unui arbore este un număr natural egal cu lungimea celui
mai lung drum de la nodul rădăcină la unul din nodurile terminale.
Definiție1: Se numește factor de echilibrare diferența dintre înălțimea
subarborelui drept și înălțimea subarborelui stâng.
Definiție2: Atașând fiecărui nod un câmp care reprezintă factorul său de
echilibrare, se spune că arborele binar este echilibrat când toți factorii de echilibrare
ai nodurilor sunt-1,0,+1.
Formal, acest lucru se traduce astfel [4]:

Fig. 7.7 AVL echilibrare [4]

| hs – hd | <= 1, oricare ar fi nodul N din arbore, unde hs și hd reprezintăînălțimea


subarborelui stâng, respectiv înălțimea subarborelui drept.
De exemplu, fie arborele din figura de mai jos [4].

37
Fig. 7.8 Arbore binar dezechilibrat [4]

Exemple de calcul al înălțimii unui subarbore și a factorilor deechilibru [4]


(Fig. 7.8)
-înălțimea întregului arbore este 5, egală cu lungimea celui mai lung drum de la nodul
rădăcină la unul din nodurile terminale.
- înălțimea subarborelui stâng al rădăcinii este 4, adică lungimea celui mai lung drum
dela nodul cu eticheta 7 la unul din nodurile terminale, cu etichetele 2, respectiv 1.
-pentru a afla factorul de echilibru al rădăcinii scădem înălțimea subarborelui stâng
din
înălțimea subarborelui drept: 1-4=-3.
-factorul de echilibru al nodului cu eticheta 6. Observăm că nodul nu are niciun copil,
caz în care factorul de echilibru este 0.
Arborii AVL se comportăla fel ca arborii binari ordonați, mai puțin în cazul operațiilor
de inserție și ștergere de chei.

Inserarea în arbori AVL [4]


O inserție într-un arbore binar ordonat poate duce la dezechilibrarea anumitor
noduri, manifestată prin nerespectarea formulei | hs – hd | <=1 pentru respectivele
noduri.
În principiu, o cheie se inserează într-o primă fază, ca intr-un arbore binar
ordonat obișnuit, se pornește de la rădăcină și se urmează fiul stâng sau fiul drept, în
funcție de relația dintre cheia de inserat și cheia nodurilor prin care se trece, până se
ajunge la un fiu nul, unde se realizează inserția propriu-zisă.
În acest moment se parcurge drumul invers (care este unic) și se caută pe acest
drum primul nod care nu este echilibrat, adică primul nod ai cărui subarbori diferă
ca înălțime prin 2 unități.
Acest nod trebuie echilibrat. El se va afla întotdeauna într-unul din următoarele 4
cazuride echilibrare

Cazul 1-rotație simplă dreapta (Fig. 7.9)

Cazul 2 - rotație simplă stânga – este simetric în oglindă față de cazul 1- rotație
simplă dreapta (Fig. 7.10)

38
Fig. 7.9 Rotație simplă dreapta [4]

Fig. 7.10 Rotație simplă stânga [4]

Analog avem rotație dublă dreapta și rotație dublă stânga prin care un nod urcă două
nivele în rădăcină:

39
Fig. 7.11 Rotație dublă dreapta [4]

Fig. 7.12 Rotație dublă stânga [4]

40
Exemplu: fie secvența de chei 4,5,7,2,1,3,6 care se inserează într-un
arboreAVL inițial vid. Evoluția arborelui și echilibrările sunt [4]:
-nodurile cu cheile 4,5 se vor insera ca și la arborii binari de căutare
-nodul cu cheia 7 ar fi de inserat în dreapta nodului cu eticheta 5, caz în care
arborele este dezechilibrat.

Fig. 7.13 Menținerea echilibrării la inserția valorii 7 [4]

-nodul cu cheia 2 se va insera în stânga nodului cu cheia 4 fără ca arborele să se


dezechilibreze
-nodul cu cheia 1 se va insera în stânga nodului cu cheia 2, dar în acest caz arborele
este dezechilibrat:

Fig. 7.14 Menținerea echilibrării la inserția valorii 1 [4]

-după inserarea nodului 3 arborele se va dezechilibra din nou:

41
Fig. 7.15 Menținerea echilibrării la inserția valorii 3 [4]

-nodul cu cheia 6 s-ar insera în stânga nodului cu cheia 7, dar în acest caz arborele
ar fidezechilibrat:

Fig. 7.16 Menținerea echilibrării la inserția valorii 6 [4]

Inserarea se face recursiv.

Ștergerea nodurilor în arborii AVL


Cel mai simplu de șters sunt frunzele și nodurile care au un singur fiu. Dacă
nodul care trebuie să fie șters are doi fii, îl inlocuim prin nodul aflat în extrema
dreaptă a subarborelui său stang. Echilibrarea este necesară numai dacă în urma
ștergerii s-a redus înălțimea subarborelui.[4]
În cazul cel mai defavorabil ștergerea unui nod se realizează în O(log n)
operații.
Exemplu: Se consideră arborele din figura 7.17 [4]. În urma ștergerii nodului
cu cheia 6 apare necesitatea echilibrării. Arborele echilibrat este prezentat in figură:
Nodul cu eticheta 7 se numeste nod critic, întrucât după ștergerea nodului cu
eticehta 6 factorul lui de echilibru este 2.

42
Fig. 7.17 Menținerea echilibrării în cazul ștergerii [4]

Lab 7.
1. Scrieți o procedură iterativă pentru căutarea unei valori într-un BST. 2.
Implementați operațiile: STERGE, MINIM, MAXIM, PREDECESOR, SUCCESOR
pentru BST. 3. Parcurgeți arborele binar (exemplul 1. secțiunea 7.2) în preordine și în
postordine indicând lista cheilor nodurilor.

43
VIII. Recapitulare

Indicații. Parcurgeți întâi materialul rapid. Apoi parcurgeți din nou partea I și
încercați să răspundeți la întrebările din Test marcate cu 1. Ar fi bine ca, de fiecare
dată când nu reușiți să răspundeți la o întrebare să reparcurgeți întreg capitolul.
Treceți apoi la II și tot așa, până la final, răspunzând la intrebările ce încep cu un
număr identic cu al capitolului. Apoi faceți Exemple de subiecte, unul din numere
(timp de lucru 2 ore) pentru a vă verifica.
Orice alte surse bibliografice sunt binevenite.

Test de (auto)verificare - nivel mediu


1.1 Definiți tipul de date
1.2 Definiți structura de date
1.3 Indicați minim 3 modalități de clasificare a datelor
1.4 Accesul la componentele structurii în vederea prelucrării valorilor acestora se
numește....?
1.5 Definiți lista liniară
1.6 Câte modalități de alocare există pentru liste?
1.7 În interiorul șirului de valori ale unei liste alocate secvențial pot exista valori lipsă?
2.1 Definiți stiva
2.2 Comentați asupra corectitudinii identității LIFO=FIFO=FILO
2.3 Scrieți pseudocodul aferent inserării într-o stivă în alocare dinamică
2.4 Figurați ștergerea unui nod dintr-o stivă, știind valoarea informației nodului k și
scrieți pseudocodul aferent
5.1 Definiți coada
5.2 Comentați asupra afirmației LILO=FIFO?
5.3 Scrieți pseudocodul aferent inserării într-o coadă în alocare secvențială
5.4 Ce alte tipuri de cozi cunoașteți și prin ce diferă între ele?
4.1 Ce diferență este între listele dublu înlănțuite și cele simplu înlănțuite?
4.2 Scrieți pseudocodul aferent ștergerii într-o listă dublu înlănțuită (poziție la
alegere)
4.3 Justificați denumirea de listă circulară
4.4 Există vreun neajuns dacă șterg pointerul ce marchează intrarea într-o listă
circulară alocată dinamic? Justificați.
4.5 Pot parcurge o listă circulară în ambele sensuri?
5.1 Cum aș putea transforma un graf orientat în unul neorientat? Dar invers?
5.2 Descrieți modalitățile de reprezentare ale grafurilor
5.3 Descrieți modalitățile de parcurgere ale unui graf
6.1 Comentați asupra utilității hashingului
6.2 Enumerați modalitățilede minimizare a coliziunilor în tabele hash
6.3 Figurati o tabela hash de mărime 11 în care introduceți următoarele numere
folosind adresarea deschisă, funcția hash fiind h(item)=item%11: a) 33, 112, 17, 29,
41, 120, 222, 101, 65. b) Refigurați tabela redistribuind elementele prin folosirea unui
salt (skip) de 3 c) Refigurați tabela redistribuind elementele prin adresare pătratică d)
Refigurați tabela utilizând liste înlănțuite în cazul coliziunilor
7.1 Definiți arborii binari
7.2 Câte modalități de reprezentare cunoașteți pentru arborii binari?
7.3. Câte modalități de parcurgere cunoașteți pentru arborii binari?

44
7.4. Parcurgeți un arbore binar complet de înălțime 3 (chei naturale, la alegere, fără
duplicate) în inordine și în postordine. Indicați secvența de chei pentru fiecare
parcurgere după ce figurați arborele inițial
7.5 Ce caracterizează un BST?
7.6 Care sunt operațiile uzuale într-un BST?
7.7 Câte tipuri de rotiri există pentru arborii AVL? Care sunt acestea și cum
funcționează
7.8Se consideră secvența de chei: 5, 7, 4, 1, 3, 2, 6 care se inserează într-un arbore
AVL. Se cere: a) Evoluția arborelui și echilibrările (figurați pe pași, ce fel de
rotație s-a facut, acolo unde este cazul)b) Ștergeti un nod care să aibă minim 2 fii.
Figurați pe pași dezechilibrele, dacă apar și menționați prin ce tip de rotații le
corectați.

45
Exemple de subiecte de examen

Nr. 1.
I. Scrieți secvența relevantă de cod pentru parcurgerea grafurilor neorientate în
lățime
II. Cozi (alocări, operații)
III. Parcurgeți un arbore (Fig.7.4) în preordine
IV. Se consideră secvența de chei: 5, 7, 4, 1, 3, 2, 6 care se inserează într-un arbore
AVL. Se cere: a) Evoluția arborelui și echilibrările (figurați pe pași, ce fel de
rotație s-a facut, acolo unde este cazul) b) Ștergeti un nod care să aibă minim 2 fii.
Figurați pe pași dezechilibrele, dacă apar și menționați prin ce tip de rotații le
corectați.

Nr. 2
I. Scrieți secvența relevantă de cod pentru parcurgerea grafurilor neorientate în
adâncime
II. Stive (alocări, operații)
III. Parcurgeți un arbore (Fig.7.4) în postordine
IV. Se consideră secvența de chei: 1, 3, 2, 6, 4, 5, 7 care se inserează într-un arbore
AVL. Se cere: a) Evoluția arborelui și echilibrările (figurați pe pași, ce fel de
rotație s-a facut, acolo unde este cazul) b) Ștergeti un nod care să aibă minim 2 fii.
Figurați pe pași dezechilibrele, dacă apar și menționați prin ce tip de rotații le
corectați

46
Resurse disponibile online

[1].https://en.wikipedia.org/wiki/Binary_decision_diagram
[2]. http://www.tutorialspoint.com/ansi_c/c_basic_datatypes.htm
[3].http://webserv.lgrcat.ro/2010-
2011/Catedre/Informatica/11/Parcurgere_Conexitate.pdf
[4]. http://software.ucv.ro/~cmihaescu/ro/laboratoare/SDA/docs/avl.pdf
[5].https://runestone.academy/runestone/books/published/pythonds/SortSearch/Hash
ing.html#tbl-hashvalues1
[6]. https://cs.stackexchange.com/questions/11029/why-is-it-best-to-use-a-prime-
number-as-a-mod-in-a-hashing-function

47
Bibliografie

1. Tudor S. – Informatica. Curs pentru clasele a IX-a și a X-a, Editura L&S INFO-
MAT, 2008.
2. Andone R., Gârbacea I. - Algoritmi fundamentali o perspectivă C++, Editura
Libris, Cluj Napoca, 1995.
8. Calude C. - Complexitatea calcului, aspecte calitative, Editura Ştiinţifică și
Enciclopedică, Bucureşti, 1982.
9. Cormen T.H., Leiserson E.C., Rivest R.R. - Introducere în algoritmi, Editura Libris
Agora, 2000 (traducere în limba română).
10. Dahl O.J., Dijkstra E.W., Hoare C.A.R. - Structured Programing, Academic Press,
1972.
11. Knuth E. Donald - Arta programării calculatoarelor, orice ediție.
12. Livovschi L., Georgescu H. - Sinteza şi analiza algoritmilor, Editura Ştiinţifică și
Enciclopedică, Bucureşti, 1986.
13. E. Horowitz, S. Sahni, Fundamentals of Computer Algorithms – 1985
(Fundamentals of Data Structures).
14. R. Vişinescu, V. Vişinescu - Algoritmi și structuri de date - Teorie şi aplicaţii.
Probleme de concurs, Editura Edusoft, 2006.

48

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