Table of Contents

Laborator 2 - Liste înlănțuite

Responsabili

Obiective

În urma parcugerii acestui laborator, studentul va:

Despre vectori

Cum ați învățat la PC și în laboratorul anterior, un vector este o colecție liniară și omogenă de date. În cazul alocării dinamice (care va fi și modul predominant în care vom lucra cu structurile de date), această zonă are o capacitate care se poate modifica (mări sau micșora) în funcție de diferite criterii de redimensionare (de exemplu: putem dubla capacitatea vectorului dacă ajungem la 75% din capacitatea sa curentă și putem înjumătăți capacitatea dacă vectorul esti plin în proporție mai mică de 25%). Obținem astfel mai multă memorie liberă, însă, trebuie să plătim prețul overhead-ului dat de realocarea array-ului.

Această implementare (folosind vectorul) are avantajul vitezei de acces sporite (elementele sunt în locaţii succesive de memorie), dar este limitată de cantitatea de memorie contiguă accesibilă programului.

LinkedList

O listă înlănțuită (LinkedList) reprezintă o structură de date liniară și omogenă. Spre deosebire de vector, lista înlănțuită nu își are elementele într-o zonă contiguă de memorie ci fiecare element (nod al listei) va conține pe langă informația utilă și legătură către nodul următor (listă simplu înlănțuită), sau legături către nodurile vecine (listă dublu înlănțuită). Alocând dinamic nodurile pe măsură ce este nevoie de ele, practic se pot obține liste de lungime limitată doar de cantitatea de memorie accesibilă programului.

Această implementare (folosind lista) are avantajul unei mai bune folosiri a memoriei, putând fi ocupată toată memoria liberă disponibilă, indiferent de dispunerea ei. Dezavantajul constă în timpul de acces la elementele listei, deoarece pentru a ajunge într-un element aleator, lista trebuie parcursă (în general) de la primul element pană la cel dorit.

O listă înlănțuită are întotdeauna cel puțin un pointer: head. După cum spune și numele, el reprezintă capul listei, începutul ei. head va indica mereu către primul element al listei. Un alt pointer ce poate fi folosit pentru a facilita lucrul cu lista este tail, care, după cum spune și numele, reprezintă coada listei, sfârșitul ei, el indicând către ultimul element al listei. Pe lângă cei doi pointeri precizați, este recomandat să avem salvată și lungimea listei.

În concluzie, putem defini o listă în cod astfel:

struct Node {
    void* data; // pointer void pentru a utiliza orice tip de date
    ...
};
 
struct LinkedList {
    struct Node* head;
    ...
};

Asupra unei liste înlănțuite ar trebui să putem executa urmatoarele operații:

Pentru metodele de adăugare sau ștergere vor trebui, în funcție de caz, efectuate verificări speciale pentru cazul unei liste vide, o listă cu un singur element, etc.

Tipuri de LinkedList

Există doua mari tipuri de liste înlănțuite: liniare și circulare.

Liste liniare

Listele liniare se numesc astfel deoarece ele au un caracter liniar: încep undeva și se termină altundeva, fără a fi o legatură înapoi la începutul listei. Așadar, într-o listă liniară, câmpul next al ultimului element va indica spre NULL. Dacă nodurile au legatură și spre elementul anterior, atunci câmpul prev al primului element va indica tot spre NULL.

Listă liniară simplu înlănțuită

În acest laborator vom discuta doar despre listele simplu înlănțuite. Cum am aflat anterior, un nod al unei liste simplu înlănțuite conține informația utilă și o legătură către nodul următor.

Putem reprezenta în cod acest nod astfel:

struct Node {
    void* data;
    struct Node* next;
};

Îmbinând informațiile despre listele liniare și particularitățile listei simplu înlănțuite, putem reprezenta grafic o lista simplu înlănțuită astfel:

Schelet

schelet_lab02_linkedlist.zip

Exerciții

Fiecare laborator va avea unul sau doua exerciții publice și un pool de subiecte ascunse, din care asistentul poate alege cum se formeaza celelalte puncte ale laboratorului.

[5p] Implementaţi, plecând de la scheletul de cod, lista liniară simplu înlănțuită.

311CAa

[2p] Implementați o funcție care primește ca parametru un pointer la începutul unei liste simplu înlănțuite și inversează ordinea nodurilor din listă (fără alocare de memorie auxiliară pentru o nouă listă).

Exemplu: pentru lista 1 → 2 → 3 rezultă lista 3 → 2 → 1.

311CAb

[2p] Implementați o funcție care primește ca parametri doi pointeri la începuturile a două liste simplu înlănțuite sortate și întoarce o listă simplu înlănțuită sortată ce conține toate elementele din cele două liste.

Exemplu: pentru listele 1 → 2 → 5 → 9 și 2 → 3 → 7 → 8 → 10 rezultă lista 1 → 2 → 2 → 3 → 5 → 7 → 8 → 9 → 10.

312CAa

[2p] Implementați o funcție care primește ca parametri un pointer la începutul unei liste simplu înlănțuite și șterge elementul aflat în mijlocul listei. Ștergerea trebuie să se facă printr-o singură parcurgere a listei.

Exemple:

312CAb

[2p] Implementați o funcție care primește ca parametri un pointer la începutul unei liste simplu înlănțuite și un element și adaugă elementul în mijlocul listei. Adăugarea trebuie să se facă printr-o singură parcurgere a listei.

Exemple (elementul adăugat este X):

313CAa

[2p] Implementați o funcție care primește ca parametru un pointer la începutul unei liste simplu înlănțuite și construiește două liste în care se vor afla toate elementele de pe poziții pare, respectiv impare, în aceeași ordine.

Exemplu: pentru lista 1 → 2 → 2 → 3 → 5 → 7 → 8 → 9, rezultă listele 1 → 2 → 5 → 8 și 2 → 3 → 7 → 9.

313CAb

[2p] Fiind date două liste simplu înlănțuite cu numar egal de elemente, contopiți-le astfel încât după fiecare nod provenit dintr-o listă să urmeze un nod din cealaltă listă. Nu alocați noduri noi!

Exemplu: pentru listele 1 → 3 → 5 și 2 → 4 → 6, rezultă lista 1 → 2 → 3 → 4 → 5 → 6.

314CAa

[2p] Se dă o listă simplu înlănțuită ale cărei noduri rețin valori de tip int și, se dă de asemenea, un integer X. Reordonați nodurile din listă astfel încât toate nodurile cu valori mai mici sau egale decât X să apară înaintea nodurilor cu valori mai mari decât X. Nu alocați noduri noi!

Exemplu: pentru lista 3 → 6 → 5 → 4 → 2 și integer-ul 3, o posibilă listă rezultat este 2 → 4 → 3 → 6 → 5.

314CAb

[2p] Fiind date două liste simplu înlănțuite, A și B, ale căror noduri stochează valori integer, construiți o nouă listă simplu înlănțuită, C, pentru care fiecare nod i este suma nodurilor asociate din A și B. Mai exact, nodul i din C reține suma dintre valoarea nodului i din A și valoarea nodului i din B. Dacă una dintre listele primite este mai lungă decât cealaltă, se consideră că nodurile asociate lipsă din cealaltă listă conțin valoarea 0, adică se păstrează valorile din lista mai lungă.

Exemplu: pentru listele A: 3 → 7 → 29 → 4 și B: 2 → 4 → 3, va rezulta lista C: 5 → 11 → 32 → 4.

315CAa

[2p] Se dau două liste simplu înlănțuite, A și B, ale căror noduri stochează în ordine inversă cifrele câte unui număr natural reprezentat în baza 10 (primul nod al unei liste stochează cea mai puțin semnificativă cifră). Creați o nouă listă simplu înlănțuită, C, care stochează suma celor două numere. Lista C trebuie construită în timp ce se parcurg listele A și B!

Exemplu: pentru listele A: 4 → 3 → 2 → 9 și B: 6 → 6 → 7, rezultă lista C: 0 → 0 → 0 → 0 → 1.

315CAb

[2p] Se dă o listă simplu înlănțuită ale cărei noduri stochează câte un caracter. Verificați dacă șirul de caractere stocat de lista simplu înlănțuită reprezintă un palindrom. Puteți folosi o nouă listă auxiliară sau puteți modifica lista primită, dar nu puteți să rețineți șirul în afara unei liste (de ex. nu aveți voie să parcurgeți lista și să stocați șirul la o adresă la care pointează un char*).

Exemplu: pentru lista l → u → p → u → l, rezultă ca este palindrom.

Interviu

Această secțiune nu este punctată și încearcă să vă facă o oarecare idee a tipurilor de întrebări pe care le puteți întâlni la un job interview (internship, part-time, full-time, etc.) din materia prezentată în cadrul laboratorului.

  1. Implementați o funcție de ștergere a duplicatelor dintr-o listă simplu înlănțuită sortată.
  2. Implementați o funcție de detectare (+ înlăturare) a unui ciclu într-o listă simplu înlănțuită.
  3. Găsirea celui de-al k-lea nod de la sfârșit spre început într-o listă simplu înlănțuită.

Și multe altele…

Bibliografie

  1. CLRS - Introduction to Algorithms, 3rd edition, capitol 10.2 - Linked lists