Sunteți pe pagina 1din 38

C3: Tipuri de structuri de date.

Liste vs. Vector


Structuri de date

O structura de date (SD)

• tip de date caracterizat printr-o anumita dispunere a datelor in memorie.

• modul de organizare / stocare faciliteaza manipularea acestora (eg. accesul,


modificarea, stergerea elementelor).

OBS:

 Nu exista stuctura de date care sa fie buna in orice situatie.

 Alegerea unei structuri se face in functie de cerinta problemei.

 Modul in care datele sunt organizate poate sa rezolve o buna parte din problema.

 In functie de modul de organizare => SD liniare si neliniare


Structuri de date

 SD liniare - o structura de date e liniara daca elementele sale sunt organizate secvential (un
element poate sa aiba maxim 2 vecini: elementul urmator si elementul anterior). Usor de
implementat in memorie. Traversarea structurii se face liniar.

 Tablouri - elemente stocate in locatii succesive de memorie


- am acces usor la elemente prin indice (se stie adresa primului element)
- dar e costisitor sa scot/adaug un element in vector (dinamic)
- cautarea e costisitoare (daca e ordonat - e ceva mai “ieftina”)

 Liste - elemente stocate in locatii nesuccesive de memorie


- nu am acces usor la elemente, dar pot sa adaug/sterg necostisitor elemente
- operatia de cautare a unui element e costisitoare

 Stive - elemente in locatii nesuccesive de memorie


- LIFO – Last In First Out – ultimul element adaugat e primul procesat
- nu am acces la alt element in afara de ultimul - teoretic -

 Cozi - elemente in locatii nesuccesive de memorie


- FIFO – First In First Out - primul element adaugat e primul procesat
- am acces doar la primul si ultimul element
Structuri de date

 SD neliniare - o structura de date e neliniara daca un element poate fi


conectat la mai mult de 2 elemente adiacente.

 Arbori - de interes cand se doreste, de exemplu, tinerea unei colectii de


elemente in stare mereu sortata =>
cautarile nu sunt costisitoare ca in
cazul SD liniare. Adaugarea si stergerea
de elemente e simpla.

 Grafuri - de interes cand relatia intre elemente


este importanta; care cu care nod e conectat;
care e cea mai scurta cale de la A la E; modeleaza
bine situatii din lumea reala. Unii algoritmi sunt
lenti si complecsi.
Structuri de date

SD neprimitive SD primitive
Structuri de date

Liniare
integer
Neliniare Liniare

float
Arbori Liste inlantuite Liste neinlantuite
char
Grafuri Vector
Dublu Simplu Circulara pointer
Stiva

Coada

Simplu
Dublu Dublu
Simplu Circulara
Structuri de date
Observatie: Orice tip de date are un anumit domeniu (set de valori) si un anumit set de operatii
posibile. (Ex: pentru tipul boolean – valorile sunt true si false si setul de operatii include AND,
OR, NOT; in timp ce, pentru int valorile sunt in: -32,768 -> 32,767, iar operatiile includ +,-, *,/).

Operatii posibile pentru o structura de date

Creare – operatia de declarare are ca efect alocarea de spatiu pentru o variabila/un obiect de
tipul SD

Distrugere – elibereaza spatiul ocupat de acea entitate

Inserare element – adaugare de date in structura; se face diferit in functie de SD

Selectare element – accesarea unui element din structura in functie de tipul SD ( ex: elementul
cel mai din varf al unei stive, un element dintr-un arbore folosind o cheie, elementul cu indexul x
dintr-un tablou)

Modificare/stergere element – modificarea/stergerea datelor din SD dupa selectarea


elementului
Cum credeti sa se implementeaza aceste operatii?
Liste
• Le folosim in multe contexte.
Ex: - intr-un restaurant - meniul e o lista de produse oferite cu preturile aferente
- lista de cumparaturi (ce si cat cumparam)
De interes –> sa adaug/scot usor elemente din lista, nu ma intereseaza pozitia lor

Lista = structura de date abstracta constituita dintr-o succesiune de elemente. Fiecare


element din lista are un succesor si un predecesor (mai putin ultimul/primul element).
*tip de date abstract (abstract data type - ADT) - un tip de date/clasa ce poate sa contina
informatie de orice tip si are asociate o serie de functionalitati. (Ex: vector – poate stoca
date de orice tip – pot sa adaug/scot/sterg elemente din el, pot sa ii aflu/modific lungimea)

Cel mai simplu mod – dar nu neaparat cel mai eficient – este memorarea listei intr-un
vector. Succesiunea elementelor este astfel implicita (ordinea indicilor vectorului).

In cazul in care stuctura de date e dinamica (suporta frecvent operatii de inserare, stergere
a elementelor, redimensionare vector) aceasta implementare nu e eficienta.

In functie de numarul de legaturi intre elementele consecutive ale listei, ea poate sa fie
simplu sau dublu inlantuita (vom discuta doar primul caz, pentru moment).
Liste
• Listele simplu inlantuite sunt structuri de date dinamice, omogene.

• Spre deosebire de tablouri, listele nu sunt alocate ca blocuri continuue (consecutive)


de memorie , ci ca elemente separate de memorie.

• Fiecare element/nod al listei contine, in afara de informatia utila, adresa urmatorului


element (mai putin ultimul element care contine NULL).

• Aceasta organizare permite numai acces secvential la elementele listei.

• Adica, pentru accesarea listei trebuie cunoscuta adresa primului element (numit capul
listei/ inceputul listei); elementele urmatoare sunt accesate parcurgand lista (Ex: ca un
lift; ca sa ajung de la parter la etajul 3 trec prin P->1->2->3).

• Lista simplu inlantuita poate fi reprezentata grafic astfel:

struct Elem
{
Data val; // info memorata
struct Elem* next;
//adresa elementului urmator
};
Crearea unei liste simplu inlantuite
struct Elem
Pentru a asigura un grad mai mare de generalitate listei, {
tipul datelor utile (in cazul de mai jos - int), va fi definit astfel: int
Data val; // info utila
struct Elem* next;
typedef int Data; //adresa elem. urmator
};
In cazul in care se doreste memorarea unui alt tip de date, trebuie schimbata doar
declaratia aliasului Data si verificat daca operatiile realizate pe un int in
implementarea functiilor (ex: =, ==) sunt valabile pentru noul tip de date stocat.

typedef char Data[14]; //informatia utila e un sir de 14 caractere


//in acest caz - in loc de = folosesc strcpy si in loc de == folosesc strcmp

struct Student {char*nume; int varsta;} //informatia utila e de tip Student


typedef struct Student Data; //dar in acest caz?

Daca nu se doreste definirea tipului de informatie stocat prin ceva de tip Data, se
foloseste direct numele tipului de date stocat.
Crearea unei liste simplu inlantuite

Pentru memorarea listei se foloseste o structura autoreferita.


Structura unui element dintr-o lista simplu inlantuita este:

struct Elem
{
Data val; // datele efective memorate
struct Elem* next; // legatura catre nodul urmator (o adresa)
};

In cazul in care elementul este ultimul din lista, pointerul next va avea
valoarea NULL.

Ca sa nu scriu struct Elem mereu cand declar un nou element(in C), pot sa
numesc acest tip Node.

typedef struct Elem Node;


Crearea unei liste simplu inlantuite si
Adresari de baza in liste
Declararea listei se face sub forma:
//main - la inceputul programului am definit Data, Elem si Node
Node* head; //acesta e capul listei – va contine adresa primului element din lista
head = NULL; // lista nu contine inca elemente - e vida ; am creat lista

Node n1,n2; //declar 2 elemente de tip Node


Data d1=2, d2=3; // consideram ca Data e alias pentru int si in d1 si d2 stochez valori
n1.val=d1; //informatia utila a nodului n1 ia valoarea d1
n2.val=d2; //informatia utila a nodului n2 ia valoarea d2

head=&n1; //capul listei este n1

n1.next = &n2; //adresa urmatorul element dupa n1 este adresa lui n2


n2.next=NULL; 1. Cum arata in memorie? Cum ar arata o structura
printf(“%d “, head->val); //2 de tip tablou care stocheaza aceeasi info in acelasi fel.
printf(“%d “, (head->next)->val); //3 2. head, n1 si n2 sunt pe stack, as vrea ca nodurile
listei sa se gaseasca in heap
Crearea unei liste simplu inlantuite si
//main
Adresari de baza in liste
Node* head=NULL; //head e pe stack
head = (Node*) malloc (sizeof(Node)); //adaug un element – e in heap
head->next=NULL; //adresa urmatorului element -> NULL
head->val = 0; //valoarea stocata -> 0

Node*n;
for (int i=0; i<5;i++){ //adaug alte noduri - la inceput
n= (Node*) malloc (sizeof(Node));
n->next=head; n->val=i+1;
head=n; //adresa de inceput a listei se tot modifica
} //ce ar trebui sa fac ca sa adaug la final?

Node*iter = head; //iter contine adresa de inceput a listei


if (iter!=NULL) //daca lista nu e vida
while( iter->next != NULL) //daca nu am ajuns la sfarsitul listei
{ printf(“%d ”, iter->val);
iter=iter->next; //trec la elementul urmator
} Ce se afisaza?
Cum ar arata in memorie o structura de tip tablou care stocheaza aceeasi informatie in cel mai
apropiat fel?

#include <stdlib.h>
#include <stdio.h>

void aloc(int** v, int n){


*v=(int*)malloc(sizeof(int)*n);
}

int main(){
int *p=NULL;
int n=6;
aloc(&p, n); // modific ce se gaseste in p (NULL)
for (int i=0; i<n;i++)
p[i]=n-i-1;
1. Care era complexitatea procedurii
for (int i=0; i<n;i++) de adaugare elemement pe pozitia
printf(“%d ”,p*i+); 0/ k /n-1/n?
return 0; 2. Ce se intampla daca nu avem destul
} spatiu in p?
Operatii cu liste
I. Inserare element - Inserarea unui element se poate face la inceputul, sfarsitul sau pe o
anumita pozitie a listei.

1. Inserare la inceput
Acesta este cazul cel mai simplu: trebuie doar alocat spatiul pentru element, legat de primul
element din lista si repozitionat capul listei:

// Inserare element la inceputul unei liste simplu inlantuite


void addAtBeginning(Node**head,Data v) //head- inceputul listei –se modifica
// v- informatia pe care vreau sa o stochez
{
Node* newNode=(Node*)malloc(sizeof(Node));
newNode->val=v;
newNode->next=*head;
*head=newNode;
}
// am transmis variabila head de tip pointer prin referinta, deoarece modific adresa stocata
//in pointerul head
//complexitate O(1) – nu fac nicio parcurgere
Operatii cu liste

Inserare element la inceputul listei cu modificarea capului listei

//main

Node* head = NULL; //creez lista – e initial goala


for (int i=0;i<4;i++)
addAtBeginning(&head,i);
Operatii cu liste
2. Inserare la sfarsit – presupune crearea unui nou element, –
parcurgerea listei incepand cu primul element si adaugarea
noului element la final.
void addAtEnd(Node**head, Data v)
{ Node*aux=*head, *headcopy=*head;
Node* newNode=(Node*)malloc(sizeof(Node)); //creez un nou nod
newNode->val=v; //stochez in el informatia data

while (headcopy!=NULL) {// cat timp nu am ajuns la finalul listei


aux=headcopy; // stochez potentialul ultim element in aux
headcopy=headcopy->next; // si trec la urmatorul element
}

if(aux==NULL) addAtBeginning(&*head,v); //daca lista era goala se


//modifica adresa continuta de capul listei
else{
aux->next=newNode; //pun noul nod pe ultima pozitie
newNode->next=NULL; //si urmatorul element e NULL
}
} //complexitate O(n) – trebuie parcursa secvential lista
Operatii cu liste
Inserare element la finalul listei

//main

Node* head = NULL; //aceasta e lista – e initial goala


for (int i=0;i<4;i++)
addAtEnd(&head,i);

//i=0; //i>0;
Operatii cu liste
3. Inserare pe o anumita pozitie (cazul general)
void addAtPos(Node**head,Data v, int pos) //pos=pozitia dupa care vreau sa adaug
{ if (*head!=NULL){ //daca lista nu e goala
Node*aux, *headcopy=*head;
Node* newNode=(Node*)malloc(sizeof(Node));//creez un nod nou
newNode->val=v; //stochez informatia data v
int p=0; //p e folosit la cautarea pozitiei pos in lista

while (headcopy!=NULL && p<pos) { //daca nu am ajuns la finalul


aux=headcopy; //listei sau la pozitia dorita
headcopy=headcopy->next; //trec la urmatorul element
p++; //tin minte cate elemente am parcurs
}
Operatii cu liste
if (pos==0) addAtBeginning(&*head,v); // daca trebuia adaugat pe pozitia 0
else{
aux->next=newNode;
newNode->next=headcopy;
}

} else addAtBeginning(&*head,v);
//daca lista era goala
}

//complexitate O(n);
int main(){
Node* head = NULL; //creez lista – e initial goala
for (int i=0;i<4;i++) addAtBeginning(&head,i); //adaug 4 elemente la inceputul ei
addAtPos(&head, 2, 100); //adauga pe pozitia 2 – elementul cu informatia 100
return 0;
//Tema: Implementati o functie care adauga nodul nou dupa un nod ce contine
}
Lista vs. Vector – inserare element
• Considerati un vector alocat dinamic (initial de dimensiune n).

Data *v = (Data*) malloc( sizeof(Data) * n);

• Adaugati un element la inceputul, mijlocul si finalul vectorului. Care e complexitatea?

La inceput: - toate elementele trebuie mutate cu o pozitie: v[i+1] <- v[i]


Pe pozitia k: - toate elementele incepand cu v[k] trebuie mutate cu o pozitie:
v[i+1] <- v[i], i>=k
La final: - se adauga elementul

*operatiile sunt posibile daca exista destul spatiu, altfel trebuie facuta o realocare (ceea ce
inseamna de cele mai multe ori copierea valorilor deja stocate = > O(n)).

• Complexitatea daca se adauga la inceput/mijloc/sfarsit: O(n)/O(n)/O(1) - daca mai e loc


– altfel se realoca spatiu -> O(n)

Tema – Implementati!
Cat spatiu realoc? +1 sau *2 ?
Operatii cu liste
II. Parcurgerea listei
1. - cu afisarea datelor stocate in ordine

void print(Node*head){ //nu modific lista - > nu mai transmit head prin referinta
while (head!=NULL) //daca nu am ajuns la sfarsitul listei
{
printf( “ %d “ , head->val ); //afisaza informatia din acel nod
head=head->next; //si treci la urmatorul
}
}

//
int main(){
Node* head = NULL; //aceasta e lista – e initial goala
for (int i=0;i<4;i++) addAtBeginning(&head,i); //adaug 4 elemente la inceputul ei
print(head); //o parcurg si afisez continutul ei
return 0;
}
//Tema: Implementati o functie de parcurgere a listei din doua in doua elemente
Operatii cu liste
II. Parcurgerea listei
2. – recursiv de la final la inceput

void printR(Node*head){
if (head!=NULL) {
printR(head->next);
printf( “ %d “ , head->val );
}

//
int main(){
Node* head = NULL; //aceasta e lista – e initial goala
for (int i=0;i<4;i++) addAtBeginning(&head,i); //adaug 4 elemente la inceputul ei
printR(head); //o parcurg si afisez continutul ei de la final la inceput
return 0;
}
//Tema: Cum as putea sa afisez elementele in ordine- de la inceput la final folosind printR
//si ce am implementat pana acum?
//Mai folosesc o lista.
Operatii cu liste
III. Modificarea valorii elementului de pe o anumita pozitie
void modify(Node*head, Data newdata, int pos) //newdata – valoarea pe care vreau sa o
{ Node*aux=head; //stochez in nodul de pe pozita pos
int p=0;
while (head!=NULL && p<pos) {
aux=head;
head=head->next;
p++;
}
if (aux!=NULL) aux->val=newdata; //modific continutul elementului de pe pos
}
//complexitate O(n) – din cauza parcurgerii secventiale

Node* head = NULL; //creez lista – e initial goala


for (int i=0;i<4;i++) addAtBeginning(&head,i); //adaug 4 elemente la inceputul ei
modify(head,100,1); //modific continutul elementului de pe pozitia 1 din 1 in 100

//Cum se modifica elementul de pe pozitia pos dintr-un vector? Care e complexitatea?


Operatii cu liste

IV. Stergerea unui element in functie de valoarea sa

void delete(Node**head, Data val)


{
if (*head==NULL) return; //daca lista e goala nu am ce element sa sterg

//stergere cap lista


Node *headcopy=*head;
if (headcopy->val==val) { //daca primul element contine informatia val
*head=(*head)->next; // modific adresa capului listei
free(headcopy); //si sterg primul element
return;
}

//…->
Operatii cu liste
//…->
//stergerea unui element oarecare
Node *aux=*head;
while (headcopy!=NULL) {
if (headcopy->val!=val){ //trec prin lista pana gasesc elementul de sters
aux=headcopy;
headcopy=headcopy->next;
}else{
aux->next=headcopy->next;
free(headcopy); //sterg elementul cautat
return;
}

}
}

// Tema: Implementati o functie de stergere a unui element in functie de pozitia /


//adresa sa.
Operatii cu liste
V. Stergerea intregii liste

void deleteList(Node **head){


Node* headcopy;

while (*head!=NULL){
//cat timp nu am ajuns la final sau lista nu e goala
headcopy=(*head)->next;
free(*head);
*head=headcopy;
}
*head=NULL;
}

//main
Node* head = NULL;
for (int i=0;i<4;i++) addAtBeginning(&head,i);
delete(&head,0); //sterg primul element
delete(&head,1); //sterg al doilea element din ce mai ramasase
deleteList(&head); //sterg toata lista
Concluzii
Lista vs. Vector

Lista inlantuita Vector dinamic Vector static


Accesare element (indexing) O(n) O(1) O(1)
Inserare/stergere element la O(1) O(n) -
inceput
Inserare/stergere element la O(n) O(1), daca nu e plin -
final O(n) altfel
Inserare/stergere element la O(n) O(n) -
mijloc
Spatiu folosit in plus O(n) O(n) 0
Concluzii Lista vs Vector
In mod normal, vectorii ar trebui folositi rar in aplicatii.

Folositi vectori daca:


 datele au dimensiune fixa sau dimensiunea e putin probabil sa varieze;
 dimensiunea nu e foarte mare;
 indexarea este operatia cea mai folosita.

Folositi lista daca:


 dimensiunea datelor variaza (doriti sa adaugati/stergeti elemente des)
 indexarea nu e cea mai importanta / necesara operatie;

Vectorii raman utili in cazul bi si n dimensional, dar si in aceste contexte, informatia se


poate reprezenta altfel.

Concluzie: "arrays are out of fashion"

Hashmap (tabela de dispersie) – e o alternativa buna pentru stocarea de date (vector –>
indexat – de liste; indexarea in mare a datelor rezolva mare parte din problema accesarii
rapide a elementelor) – vom vedea in alt curs
Tema – Liste simplu inlantuite
1. Creati o lista direct in forma ordonata, datele (valori intregi) sunt citite de la tastatura.
Afisati-o!

2. Interclasati doua liste ce contin valori intregi ordonate (fara a pastra listele originale) si
afisati rezultatul.

3. Creati o functie pentru eliminarea elementelor care se repeta intr-o lista si afisati lista
rezultata.

4.* Polinoamele cu multi coeficienti nuli se numesc rare. Stocati informatiile utile pentru
un astfel de polinom sub forma de lista (ex: x^16+2x^5+8x). Calculati suma a doua polinoame
rare si afisati polinomul rezultat.

5. Scrieti o functie care determina cate elemente are o lista – se da adresa primului elem.

6. Scrieti o functie care determina mijlocul unei liste (optim).

7. ** Pentru exemplul dat in curs - inlocuiti tipul Date din int cu:

struct Student {char*nume; int varsta;}

Mai functioneaza corect afisarile si atribuirile? Ce modificari trebuiesc facute? Implementati!


Lista cu noduri santinela

Ce probleme/dificultati de implementare ati observat pentru lista simplu inlantuita?


Cazul in care trebuie adaugat/sters un element de la inceputul/ finalul listei trebuie
tratat diferit fata de restul cazurilor.
Solutie: Se folosesc noduri dummy – santinele pe post de prim/ultim element al listei sau
pentru ambele capete:

Nodurile santinela nu contin informatie utila si cresc dimensiunea listei (nesemnificativ).


Ce modificari apar?
 Nu se mai modifica niciodata adresa de inceput a listei => nu mai trebuie transmisa
variabila prin referinta in functiile de adauga, sterge element.
 Trebuie sa stiu adresa pentru nodul santinela de la final - z, iar testul de final de lista
nu mai e el->next ==NULL, ci el->next==z.
Lista cu santinela la inceput

Creare lista:
Node *head= (Node*)malloc(sizeof(Node));
head->next=NULL;

Inserare nod t dupa nod x:


t->next=x->next; x->next = t; //fara test de prim element

Test lista vida: if (head->next == NULL)

Parcurgere: for (t = head->next; t!=NULL; t = t->next)


// cum scriu parcurgerea cu while?
Lista cu santinela la final

Initializare:
Node *head, *z= malloc(sizeof(Node));
head=z;
z->next=z;

Eliminare nod t dupa nod x:


x->next=x->next->next; //fara test de ultim element

Test lista vida: if (head == z)

Parcurgere: for ( t = head; t!=z; t = t->next)

Tema:
Implementati aceste
versiuni de liste,
inclusiv functiile de
creare /distrugere
lista.
Lista simplu inlantuita circulara
• Ultimul element din lista are ca nod urmator – primul element al listei
• Niciun element nu are nodul urmator NULL
• Pentru a identifica o lista circulara simplu inlantuita putem sa retinem adresa
oricarui element din lista

typedef int Data;


typedef struct Elem Node;

struct Elem
{
Data val; // datele efective memorate
struct Elem* next; // legatura catre nodul urmator
};
//definirea tipului se face la fel ca in cazul listei necirculare
Lista simplu inlantuita circulara
1. Inserare element dupa un anumit nod

void addAtPos(Node**head, int val, Node* pos) //pos - adresa nodului din lista dupa
//care se face inserarea; daca lista goala; pos este head
{
Node *headcopy=*head;
Node* newNode=(Node*)malloc(sizeof(Node));
newNode->val=val;

if (*head==NULL){ // daca lista e goala


newNode->next=newNode;
*head=newNode;
}else{
newNode->next=pos->next;
//in main
pos->next=newNode; int i;
} Node* head = NULL;
}
for (i=0;i<4;i++)
addAtPos(&head,i,head);
Lista simplu inlantuita circulara
2. Stergere element
void delete(Node**head, Data val){
if (*head==NULL) return;
if (*head==(*head)->next && (*head)->val==val){ // acesta e singurul nod din lista
free(*head);
*head=NULL; //lista e goala
return;
}
Node *headcopy=(*head)->next;
Node *aux=*head;
while (headcopy!=*head) {
if (headcopy->val!=val){
aux=headcopy;
headcopy=headcopy->next;
}else{
aux->next=headcopy->next;
free(headcopy);
return;
}
}
Lista simplu inlantuita circulara

if (headcopy->val==val) { //daca trebuia sa sterg chiar primul element.


aux->next=headcopy->next;
*head=headcopy->next;
free(headcopy);
return;
}
}
Lista simplu inlantuita circulara
3. Stergerea listei

void deleteList(Node **head){

Node* headcopy=(*head)->next;

while (*head!=headcopy){

Node *aux=headcopy;

headcopy=headcopy->next;

free(aux);

free(*head);

*head=NULL;

//Tema: reprezentati grafic ce se intampla.


Teme – lista simplu inlantuita circulara

1. Fie un poligon convex. Se citeste numarul de varfuri si apoi coordonatele


lor, in ordine trigonometrica intr-o lista. Sa se determine perimetrul
poligonului.

2. Jocul lui Joseph. N copii numerotati de la 1 la N sunt asezati in cerc.


Copilul 1 alege un numar p si incepand cu el se numara pana la p, copilul
p e eliminat. Incapand cu p+1 se reia procedura, pana sunt eliminati toti
copii. Afisati numele copiilor in ordinea in care sunt dati afara.

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