Laborator 3 - Liste înlănțuite (continuare)

Obiective

În urma parcugerii acestui laborator, studentul va:

  • putea implementa propria listă dublu înlănțuită
  • putea implementa propria listă circulară

LinkedList (conținut din laboratorul precedent)

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;
    struct Node* tail;
    int size;
};

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

  • void add_nth_node(struct LinkedList* list, int n, void* new_data); adaugă pe poziția n în listă elementul new_data. Adăugarea presupune modificarea câmpului next al nodului în urma căruia se va adăuga noul nod, cât și a câmpului next al nodului adăugat pentru a face legăturile necesare ca lista sa funcționeze corect. Dacă nodul este adăugat pe prima poziție, atunci el va deveni head-ul listei, iar dacă este adăugat pe ultima poziție, el va deveni tail-ul listei. Complexitate: O(n). Dacă se adaugă elementul în capul sau coada listei, se obține o complexitate mai buna: O(1).
  • struct Node* remove_nth_node(struct LinkedList* list, int n); șterge și întoarce al n-lea element al listei. Operația presupune modificarea listei astfel încât între nodurile vecine celui eliminat să se refacă legaturile pentru a permite listei sa funcționeze în continuare. Daca nu există un nod următor, head va deveni NULL, iar lista va fi goală. Dacă nodul eliminat era head-ul listei, atunci succesorul său îi va lua locul. Analog pentru ultimul nod, însă în această situație, nodul precedent devine tail. Complexitate: O(n). Dacă se șterge primul nod, se obține o complexitate mai buna: O(1). Aceeași complexitate se obține și dacă lista este dublu înlănțuită și se șterge ultimul nod.
  • int get_size(struct LinkedList* list); întoarce numărul curent de elemente stocate în listă. Complexitate: O(1)

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ă

Prezentată în laboratorul precedent.

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

Spre deosebire de o listă simplu înlănțuită, un nod al unei liste dublu înlănțuite conține informația utilă și două legături: una către nodul anterior și alta către nodul următor.

Putem reprezenta în cod acest nod astfel:

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

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

Liste circulare

Listele circulare se numesc astfel datorită caracterului lor circular: primul și ultimul nod al listei sunt conectate. Așadar, într-o listă circulară, câmpul next al ultimului element va indica spre primul element. Dacă nodurile au legătură și spre elementul anterior, atunci câmpul prev al primului element va indica spre ultimul element.

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

Pentru acest tip de listă, un nod este reprezentat la fel ca într-o listă liniară simplu înlănțuită:

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

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

Listă circulară dublu înlănțuită

Pentru acest tip de listă, un nod este reprezentat la fel ca într-o listă liniară dublu înlănțuită:

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

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

Schelet

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.

[7p] Implementaţi, plecând de la scheletul de cod, lista circulară dublu înlănțuită.

311CAb

[2p] Implementați o funcție care primește ca parametru un pointer la începutul unei liste dublu î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.

312CAb

[2p] Fiind date două liste dublu înlănţuite, A şi B, ale căror noduri stochează valori integer, construiţi o nouă listă dublu î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.

313CAa

[2p] Implementați o funcție care primește ca parametri doi pointeri la începuturile a două liste dublu înlănțuite sortate și întoarce o listă dublu î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.

314CAb

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

Exemple:

  • pentru lista vidă, rezultă lista vidă;
  • pentru lista 1, rezultă lista vidă;
  • pentru lista 1 ⇔ 2, rezultă lista 2;
  • pentru lista 1 ⇔ 2 ⇔ 3, rezultă lista 1 ⇔ 3;
  • pentru lista 1 ⇔ 2 ⇔ 3 ⇔ 4, rezultă lista 1 ⇔ 3 ⇔ 4;
  • pentru lista 1 ⇔ 2 ⇔ 3 ⇔ 4 ⇔ 5, rezultă lista 1 ⇔ 2 ⇔ 4 ⇔ 5.

315CAa

[2p] Implementați o funcție care primește ca parametri un pointer la începutul unei liste dublu î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):

  • pentru lista vidă, rezultă lista X;
  • pentru lista 1, rezultă lista X ⇔ 1;
  • pentru lista 1 ⇔ 2, rezultă lista 1 ⇔ X ⇔ 2;
  • pentru lista 1 ⇔ 2 ⇔ 3, rezultă lista 1 ⇔ X ⇔ 2 ⇔ 3;
  • pentru lista 1 ⇔ 2 ⇔ 3 ⇔ 4, rezultă lista 1 ⇔ 2 ⇔ X ⇔ 3 ⇔ 4
  • pentru lista 1 ⇔ 2 ⇔ 3 ⇔ 4 ⇔ 5, rezultă lista 1 ⇔ 2 ⇔ X ⇔ 3 ⇔ 4 ⇔ 5.

315CAb

[2p] Implementați o funcție care primește ca parametru un pointer la începutul unei liste dublu înlănțuite și întoarce o listă dublu înlănțuită obținută prin următoarea procesare a listei inițiale: între fiecare nod și succesorul său (din lista inițială) se introduce un nou nod, având ca valoare suma valorilor celor două noduri învecinate.

Exemplu: pentru lista 1 ⇔ 2 ⇔ 5 ⇔ 6 ⇔ 7 ⇔ 9 rezultă lista 1 ⇔ 3 ⇔ 2 ⇔ 7 ⇔ 5 ⇔ 11 ⇔ 6 ⇔ 13 ⇔ 7 ⇔ 16 ⇔ 9.

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ă dublu înlănțuită sortată.
  2. Implementați o funcție de detectare (+ înlăturare) a unui ciclu într-o listă dublu înlănțuită.

Și multe altele…

Bibliografie

  1. CLRS - Introduction to Algorithms, 3rd edition, capitol 10.2 - Linked lists
sd-ca/laboratoare/lab-03.txt · Last modified: 2020/03/12 14:43 by serban_ioan.ciofu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0