Sunteți pe pagina 1din 10

STRUCTURI DE DATE SI ALGORITMI - LABORATOR 2

---- Liste simplu înlănţuite. Implementarea listelor simplu înlănţuite cu alocare dinamică ---

I. Liste simplu înlănţuite

În programare listele sunt secvenţe de înregistrări (adesea denumite noduri) între care există o serie de
relaţii de referire reprezentate prin adrese de legătură. Listele simplu înlănţuite sunt particularizate prin
faptul că orice înregistrare face referire doar la o singură altă înregistrare din listă: următoarea. Listele
simplu înlănţuite sunt printre cele mai simple şi în acelaşi timp printre cele mai utilizate structuri de
date. Cu ajutorul acestora se pot reprezenta structuri precum stivele, cozile, tablourile asociative, tabele
hash, etc.

Principial, arhitectura unei liste simplu înlănţuite este redată mai jos în Fig. 1. În acest caz discutăm
despre noduri cu o construcţie minimală ce au în componenţă o variabilă etichetă şi un element de
legătură către următorul nod din listă însă pot exista cazuri când înregistrările au mai multe elemente
componente cu diferite roluri pornind de la acela de a stoca o informaţie utilă dar ajungând şi până la
acela de a eticheta anumite proprietăţi ale înregistrării curente. Se pot extrage două concluzii privind
figura: 1) lista este încheiată cu un identificator terminal (cel mai adesea NULL), 2) nodurile listei nu pot
fi reprezentate cu ajutorul unui singur tip de date fundamental existent.

Figura 1 - Reprezentarea unei liste simplu înlănţuite.


Fiecare nod conţine o variabilă de tip int şi un pointer către următorul element din listă.

Pentru reprezentarea nodurilor de informaţie pentru listele simplu înlănţuite, în limbajul C / C++
se utilizează un struct care trebuie să conţină în componenţa sa cel puţin un pointer reprezentând
legătura către următorul nod precum şi o serie de alte intrări reprezentând informaţia utilă, pointeri
către alte structuri unde se stochează informaţia utilă sau etichete de identificare / marcare asociate cu
nodul curent. Spre exemplu structura de mai jos reprezintă tipul pentru noduri dintr-o presupusă listă
care conţine o singura informaţie utilă: un index de identificare:

struct My_Nod
{
int index_nod; // reprezinta informatia utila
My_Nod * next; // va stoca legatura catre elemente urmatoare
};

Un exemplu mai complicat în care se presupune definirea tipurilor de date pentru o listă simplu
înlănţuită care conţine înregistrări legate de informaţiile private şi publice ale unor persoane ar putea fi
scris în felul următor: se defineşte un struct adiţional care reprezintă şablonul pentru informaţiile
publice respectiv unul pentru informaţiile private iar struct-ul pentru nod va conţine un pointer către
două astfel de struct 'specializate'. Se procedează în acest fel deoarece în anumite cazuri se poate face
economie de memorie ocupată: un pointer în struct-ul unui nod ocupă mai puţină memorie decât dacă
acesta ar conţine variabile pentru toate înregistrările de interes care la momentul respectiv se consideră
că nu ar fi disponibile. Codul este cel de mai jos:

//structura care contine informatia publica


struct Public_Info
{
char * user_name; // pointer catre sir de caractere - alias
int varsta; // varsta
};

//structura care contine informatia privata - este posibil ca aceste date sa nu fie completate de utilizator
struct Private_Info
{
bool i_am; // boy / girl

char * prenume; // pointer catre sir de caractere - prenume


char * nume; // pointer catre sir de caractere - nume
char * e_mail_add; // adresa e-mail
char * fb_id; // id retea socializare
};

//structura care defineste sablonul pentru noduri


struct My_Nod
{
int index_nod; // un index de identificare a nodului, poate fi un id unic
Public_Info * pu_data; // pointer catre o structura care contine informatia publica
Private_Info * pr_data; // pointer catre o structura care contine informatia privata
My_Nod * next; // pointer catre urmatorul nod din lista
};
II. Implementarea listelor simplu înlănţuite

Implementarea efectivă a listelor simplu înlănţuite, pe lângă definirea modului de reprezentare în


memorie (vezi mai sus) presupune şi definirea unor secvenţe de cod care să se ocupe cu operaţiile
principale ce se pot face asupra acestora: inserare, extragere, căutare a unui nod, ştergere a unui nod,
etc. Un cititor atent a observat până în acest moment că modul de definire a structurilor face referire
directă la o implementare bazată pe alocare dinamică. Această variantă este cea abordată în laborator şi
deşi este mai lentă la nivel de acces decât o implementare bazată pe alocarea fixă (statică) are totuşi
avantajul unei flexibilităţi crescute atunci când vine vorba de manipularea unei cantităţi informaţionale
foarte ridicate.

Vom parcurge mai jos fiecare operaţie principală de manipulare: inserare, extragere, căutare,
ştergere, pentru un exemplu în care discutăm despre o listă trivială conţinând în struct-ul nodului o
variabilă de tip întreg reprezentând un index dat de utilizator. În cazul în care se doreşte utilizarea unei
liste cu noduri cu mai multe înregistrări trebuie specificată o funcţie suplimentară de încărcare a datelor
într-un struct nod. Aceasta, în funcţie de cum a fost construită, poate fi apelată fie în cadrul funcţiei
'AddNod', fie în exterior, caz în care 'AddNod' va primi ca parametru un pointer către nodul creat pentru
a-l putea lega la listă.

//sablon noduri
struct My_Nod
{
int index;
My_Nod * next;
};

// AddNod() adauga un nod in lista - consideram ca se adauga la sfarsitul listei


void AddNod(My_Nod ** pLista, int _index)
{
My_Nod * tmp = *pLista; // folosesc un pointer local pentru a parcurge lista.
// *pLista (este tot o adresa, pentru ca tipul sau este pointer la
// pointer), evident, indica primul nod din lista.

//pas 0: daca lista nu exista; se va introduce primul nod


if(!tmp)
{
*pLista = new My_Nod; //rezerv memorie pentru primul element
(*pLista) ->next = NULL; // marchez sfarsitul listei (primul element este si ultimul)
(*pLista) ->index = _index; // adaug indexul venit ca parametru. Remarcati cum accesul
// spre datele membre din nod (struct) se face folosind
// operatorul -> deoarece structul este indicat printr-un
// pointer.
return;
}
//pas 1: daca lista exista deja, parcurg pana la ultimul element ...
while(tmp->next)
tmp = tmp->next;

// pas 2: .... si adaug un nou nod


tmp->next = new My_Nod; //rezerv memorie pentru nu nou nod si fac legatura cu lista
tmp = tmp->next; // fac deplasarea pe acest nod

tmp->index = _index; // inserez indexul


tmp->next = NULL; // marchez sfarsitul listei

Figura 2 - Inserarea unui nou nod la sfârşitul listei curente


În mod evident, varianta prezentata mai sus nu este singura modalitate de a adăuga un element
într-o listă. Inserarea se poate face şi după un element impus, sau la începutul structurii înlănţuite.
Totuşi, trebuie remarcate următoarele lucruri: A) pentru manipularea listelor se folosesc pointerii.
Putem parcurge fiecare element al acesteia utilizând adresele la care sunt stocate struct-urile
reprezentând nodurile. B) pentru adăugarea unui nou nod listei se face alocare de memorie (de
dimensiunea unui struct reprezentând tipul listei) şi legarea acestuia la structura deja înlănţuită. Şi
această operaţiune este strâns legată de utilizarea pointerilor.
Extragerea unui element în listă se poate face după codul de mai jos:

My_Nod * GetNod(My_Nod ** pLista, int _index)


{
My_Nod * pTmp = *pLista; // stochez referinta catre inceputul listei in pointerul local
My_Nod * pExtras = NULL; // aici se va retine nodul extras (daca se gaseste)

//caz particular 1 - nu exista nici un element in lista


if(!pTmp)
return NULL;

//caz particular 2 - nodul este primul element din lista sau unicul element din lista
if(pTmp->index == _index)
{
*pLista = (*pLista)->next; //modific varful listei
return pTmp; //pTmp indica deja spre varful listei asa ca este intors
}
// pas 1: parcurg lista in cautarea nodului cu indexul '_index';
// Totusi sunt constrans sa ma pozitionez pe nodul anterior nodului cautat pentru a putea
// realiza extragerea din lista.
while(pTmp->next)
{
//daca gasesc nodul, intrerup parcurgerea
if(pTmp->next->index == _index)
break;

//altfel parcurgea continua prin modificarea adresei continuta de pointerul curent cu


// adresa urmatorului element din lista.
pTmp = pTmp->next;
}
//verific daca parcurgerea de mai sus s-a oprit pe un nod valid sau a ajuns la sfarsitul listei fara
// a gasi elementul cautat.
if(!pTmp->next)
return NULL; //nu am gasit nici un nod cu indexul '_index'
//pas 2: daca se ajunge aici, inseamna ca am gasit nodul si trebuie extras.
pExtras = pTmp -> next; //nu uitati ca nodul cautat este cel ulterior nodului pe care s-a oprit
// parcurgerea

//pas 3: modific legaturile in lista astfel incat sa 'sar' peste nodul extras.
// Aici intervine constrangerea din parcurgere. // Totodata 'desprind' nodul extras din lista.
pTmp -> next = pTmp ->next->next;
pExtras->next = NULL;

//intorc pointerul catre locatia de memorie unde se afla nodul extras


return pExtras;
}

Figura 3 - Exemplificare extragere nod cu indexul I din lista


-

Figura 4 - Căutarea unui nod cu indexul I în listă

Căutarea nu diferă mult de codul de mai sus, fiind simplificată în sensul că se păstrează legăturile în listă
(nodul nu trebuie extras).

My_Nod * SearchNod(My_Nod * pList, int _index)


{
My_Nod * pTmp = pLista; // stochez referinta catre inceputul listei in pointerul local

if(!pTmp) //nu exista nici un element in lista


return NULL;

//parcurg lista in cautarea nodului cu indexul '_index';


while(pTmp)
{
//daca gasesc nodul, intrerup parcurgerea
if(pTmp-> index == _index)
break;

//altfel parcurgea continua prin modificarea adresei continuta de pointerul curent cu


// adresa urmatorului element din lista. Aceasta din urma se gaseste in interiorul
// nodului curent.
pTmp = pTmp->next;
}
//intorc nodul. Daca s-a ajuns la capatul listei fara sa fi gasit elmentul cautat, pTmp va fi NULL.
return pTmp;
}

Figura 5 - Ştergerea unui nod din listă

Ştergerea unui nod se poate face utilizând codul de mai jos. Funcţia primeşte drept parametrii vârful
listei şi nodul de şters prin intermediul cheii sale.

bool Delete(My_Nod ** pLista, int _index)


{
My_Nod * tmp = *pLista;
My_Nod * pSters ;

//caz particular 1 - lista nu exista


if(!*pLista)
return 0;

//caz particular 2 - nodul de sters este primul element din lista sau unicul elem din lista
if((*pLista)->index == _index)
{
*pLista = (*pLista)->next; // deplasez varful listei

delete tmp; // tmp a fost apriori setat cu primul elem din lista
return 1;
}

//caz general - pas 1: caut nodul de sters astfel incat sa ma pozitionez inaintea acestuia
while(tmp->next)
{
if(tmp->next->index == _index)
break;

tmp = tmp->next;
}

//pas 2: sterg daca sunt pe un nod valid (nu ultimul din lista)
if(tmp->next)
{
pSters = tmp->next; // selectez nodul
tmp->next = tmp->next->next; // se sunteaza nodul de sters
delete pSters; // stergerea efectiva
return 1;
}

return 0;
}

Exerciţii:

1) Să se scrie toate funcţiile necesare care implementează o listă simplu înlănţuită conţinând drept
informaţie un câmp de tipul int.
OBS: Funcţiile trebuie să îndeplinească următoarele cerinţe:
- să introducă un element în listă după ultimul nod. Elementul introdus este un nod de tipul
listei.
- să caute un nod în listă după informaţia stocată şi să întoarcă un pointer la nodul găsit
- să şteargă un nod din listă care are informaţia egală cu parametrul primit.
- să şteargă toate componentele listei

2) Utilizând pachetul de funcţii scrise la problema 1), să se scrie un program care citeşte de la tastatură o
serie de valori şi le inserează succesiv în listă.

OBS: Se va implementa o funcţie care este apelată înainte de adăugarea unui nou nod. Aceasta citeşte
informaţia de la tastatură şi crează un nod după care întoarce un pointer către acesta. Nodul va fi
introdus în listă folosind funcţia de adăugare. Să se scrie în plus o funcţie care inserează un nou nod în
listă după un nod identificat prin informaţia stocată şi să se verifice funcţionarea acesteia. Funcţia are
următorul prototip:

//intoarce 1 daca am reusit, 0 altfel


bool InsertNod(My_Nod * _startSearch, int _dupa_Eticheta, My_Nod * _in_Nod);

3) Utilizând funcţiile de mai sus să se implementeze o listă simplu înlănţuită conţinând informaţii unice şi
să se scrie o funcţie void MarkData(My_Nod * _inLista) care inserează eticheta '0' după fiecare nod cu o
valoare impară de informaţie din intervalul *0, 9+.

4) Utilizând funcţiile de mai sus să se scrie un program care citeşte de la tastatură un număr de K noduri,
le introduce în listă după care şterge fiecare duplicat din listă. Exemplu: dacă se vor citi de la tastatură
valorile 1, 1, 2, 3, 1, 4, 7, 9, 7, 3 , 2, rezultatul trebuie să fie: 1, 2, 3, 4, 7, 9.

5) Să se se implementeze un program care preia o secvenţă de valori de 0 şi 1 citite de la tastatură


(acestea vor fi reţinute în variabila membră de tip int) şi le inserează într-o listă. Aceştia se vor considera
a fi biţii unui stream de date binar.
Să se scrie o funcţie care primeşte drept parametru varful listei şi care după fiecare al cincilea bit
consecutiv de 0, insereze o valoare de 1. Nodurile listei finale vor fi afişate la consolă. Înainte de
încheierea execuţie se cere ca toate nodurile listei să fie şterse prin apelul succesiv al funcţiei de
ştergere a unui nod.

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