This is an old revision of the document!


Laborator 03 - Templates. ArrayList si LinkedList

Obiective

În urma parcurgerii acestui laborator studentul va:

  • înțelege conceptul de template
  • înțeleagă conceptul de ArrayList
  • înțeleagă conceptul de LinkedList
  • să poată compara cele două structuri de date

Templates

Motivul principal pentru care folosim C++ în cadrul SD este datorită funcționalității oferite de template-uri.

Acestea permit generalizarea tipurilor de date folosite în interiorul funcțiilor și claselor.

Sintaxa pentru acestea este:

template <class identifier> declaratie;
template <typename identifier> declaratie;

declaratie poate fi fie o funcție, fie o clasă. Nu există nicio diferență între keyword-ul class și typename - important este că ceea ce urmează după ele este un placeholder pentru un tip de date.

Function Template

În primul rând template-urile pot fi aplicate funcțiilor.

Un exemplu comun și simplu este următorul:

template<typename T>
T getMax(T a, T b) {
    return a > b ? a : b;
}

Funcția poate fi apelată astfel:

getMax<int>(2, 3);
getMax<double>(3.2, 4.6);

Class Template

Concret, să presupunem că avem o clasă numită KeyStorage care are:

  • o cheie (de tip int)
  • un membru de date generic (al cărui tip de date nu îl știm la momentul scrierii clasei).

Vrem să putem folosi codul clasei indiferent de tipul de date al membrului.

Iată cum putem face acest lucru:

KeyStorage.h
template<typename T>
class KeyStorage {
public:
    int key;
    T member;
};

În funcția main, să presupunem că vrem să folosim clasa cu membrul de tip long.

main.cpp
#include "KeyStorage.h"
 
int main() {
    KeyStorage<long> keyElement;
    return 0;
}

Practic, oriunde folosim tipul de date T în clasă, este înlocuit cu tipul pe care îl specificăm.

Where's the magic happening?

Sunt destul de multe lucruri de spus despre template-uri, dar ne vom concentra pe lucrurile care schimbă modul în care ați implementat până acum.

Template-urile sunt de fapt indicii pentru compilator pentru a genera cod la rândul lui! Practic, voi îi spuneți compilatorului un șablon generic pe care ați vrea să-l folosiți și el trebuie să fie pregătit să îl pună la dispoziția voastră când aveți nevoie.

Ce trebuie să rețineți din asta? Totul se întâmplă la compile time, nu la run time.

Compilatorul practic analizează modul în care voi folosiți clasa respectivă și generează pentru fiecare mod în care o folosiți șablonul corespunzător. Folosirea KeyStorage<int> și KeyStorage<float> determină compilatorul să genereze cod pentru ambele clase (înlocuind o dată T cu int și altă cu float).

Guideline-uri implementare

Pentru că totul se întâmplă la compile time, înseamnă că în momentul în care compilatorul întâlnește secvența de cod ce folosește template-uri trebuie să știe toate modurile în care aceasta este folosita.

Asta înseamnă că:

  • Trebuie să scrieți întreaga implementare în header! sau
  • Scrieți descrierea clasei generice în header, în fișierul de implementare fiecare metodă declarată este de fapt o funcție cu template și la sfârșitul implementării adăugat template class numeclasa<numetip>;

Ultimul rând de fapt forțează folosirea template-ului cu un anumit tip de date și deci compilatorul generează cod corespunzător (trebuie să scrieți asta pentru toate tipurile).

Clasa KeyStorage

Iată mai jos o structură mai dezvoltată pentru clasa KeyStorage, în care cheia este setată în constructor. .

KeyStorage.h
template<typename T>
class KeyStorage {
public:
    KeyStorage(int k);
    ~KeyStorage();
 
    T getMember();
    T setMember(T element);
 
private:
    T member;
    int key;
};

Implementarea completa a ei poate fi realizată:

  • în header (în cazul template-urilor, acest mod este cel mai indicat).
  • în fișierul de implementare .cc / .cpp (al cărui schelet parțial îl găsiți mai jos).
KeyStorage.cpp
#include "KeyStorage.h"
 
template<typename T>
KeyStorage<T>::KeyStorage(int k) {
//TODO
}
 
template<typename T>
KeyStorage<T>::~KeyStorage() {
}
 
//TODO: restul metodelor.
 
// La sfarsit, cu tipurile de date pe care le veti folosi.
template class KeyStorage<int>;
template class KeyStorage<long>;

ArrayList și LinkedList

ArrayList și LinkedList reprezintă două structuri de date, de obicei abstracte, ce formalizează conceptul de colecție ordonată de entități. În mod minimal, acestea sunt caracterizate prin:

  • Operații:
    • Add - adaugă un element în cadrul containerului. Adăugarea se poate face la început, la sfârșit sau la o poziție arbitrară
    • Remove - șterge un element din container. Identificarea se poate face pe baza poziției din container (iterator) sau pe baza valorii elementului ce se dorește a fi șters
    • Get - consultă un element din listă. Identificarea se face pe baza poziției elementului din container
    • Update - actualizează informația unui element din container. Identificarea se face pe baza poziției elementului din container
  • Proprietăți:
    • Lungimea - numărul de elemente din listă
    • Tipul - felul elementelor din listă. Această proprietate este întâlnită mai ales la implementările în limbaje care suportă tipuri generice (C++, Java, etc.)

ArrayList

În cazul array-urilor dinamice, elementele sunt stocate într-un vector de tipul specificat. În momentul în care, prin adăugarea unui element, s-ar depăşi lungimea vectorului, acesta este realocat şi extins cu un factor specificat (fixat în implementare sau setat de către utilizator). De asemenea, în cazul în care, în urma ștergerilor, arraylist-ul are un numar de poziții ocupate mai mic decât capacitatea sa, dat de un factor specificat, se poate opta pentru redimensionarea array-ului care conține elementele, la o dimensiune mai mică. Obținem astfel mai multă memorie liberă, însă, trebuie sa plătim prețul overhead-ului dat de realocarea array-ului și în acest caz.

Această implementare 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

În cazul listelor înlănţuite, fiecare nod din listă va conţine pe lângă informaţia utilă şi legături către nodurile vecine (liste dublu înlănţuite), sau către nodul următor (liste simplu înlănţuite). 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 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 containerului

Tipuri de LinkedList

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

Are o singură legatură la fiecare nod. Această legatură indică întotdeauna următorul nod din listă, sau o valoare nulă (dacă suntem la finalul listei), sau o listă liberă (pentru identificarea ei).

Listă liniară dublu-înlănţuită

Fiecare nod din listă liniara dublu înlănţuită are două legături:

  • una leagă nodul actual de nodul de dinaintea lui, sau leagă nodul actual cu o listă libera, sau cu o listă care are o valoare nulă dacă aceasta este la începutul primului nod.
  • cealaltă legatură leagă nodul actual de o listă care are o valoare nulă sau cu o listă liberă dacă această reprezintă nodul final.

Listă circulară simplu-înlănţuită

Primul şi ultimul nod sunt legate împreună. Pentru a parcurge o listă circular înlănţuită se începe de la oricare nod şi se urmăreşte lista prin aceasta direcţie aleasă până când se ajunge la nodul de unde s-a pornit parcurgerea (lucru valabil şi pentru listele circulare dublu-înlănţuite).

Fiecare nod are o singură legatură, similar cu listele liniare simplu-înlănţuite, însă, diferenţa constă în legătura aflată după ultimul nod ce îl leagă pe acesta de primul nod. La fel ca şi în listele liniare simplu-înlănţuite, nodurile noi pot fi inserate eficient numai dacă acestea se află după un nod care are referinţe la acesta. Din acest motiv, este necesar să se menţină numai o referinţă către ultimul element dintr-o listă circulară simplu-înlănţuita, căci aceasta permite o inserţie rapidă la nodul de început al listei, şi de asemenea, permite accesul la primul nod prin legatura dintre acesta şi ultimul nod.

Exerciții

1) [5p] Implementați în header-ul definit funcțiile pentru o listă liniară dublu înlănțuită.

  • [3p] constructor, destructor(eliberați memoria folosită) și isEmpty.
  • [1p] addFirst și addLast.
  • [1p] removeFirst, removeLast si removeFirstOccurence.
list.h
template <typename T>
struct Node {
    T value;
    Node<T> *next, *prev;
    Node (T value) {
	this->value = value;
        next = prev = NULL;
    }
    Node() {
        next = prev = NULL; 
    }
};
 
template <typename T>
class LinkedList {
private:
    Node<T> *pFirst, *pLast;
public: 
    // Constructor
    LinkedList();
    // Destructor
    ~LinkedList();
 
    /* Adauga un nod cu valoarea == value la inceputul listei. */
    void addFirst(T value);
 
    /* Adauga un nod cu valoarea == value la sfarsitul listei. */
    void addLast(T value);
 
    /* Elimina elementul de la inceputul listei si intoarce valoarea acestuia. */
    T removeFirst();
 
    /* Elimina elementul de la sfarsitul listei si intoarce valoarea acestuia. */
    T removeLast();
 
    /* Elimina prima aparitie a elementului care are valoarea == value. */
    T removeFirstOccurrence(T value);
 
    /* Elimina ultima aparitie a elementului care are valoarea == value. */
    T removeLastOccurrence(T value);
 
    /* Afiseaza elementele listei pe o singura linie, separate printr-un spatiu. */
    void printList();
 
    /* Intoarce true daca lista este vida, false altfel. */
    bool isEmpty();
};

1) [5p] Implementați în header-ul definit funcțiile pentru un vector alocat dinamic cu redimensionare.

  • [3p] constructor, destructor(eliberați memoria folosită) și isEmpty.
  • [1p] addFirst și addLast.
  • [1p] removeFirst, removeLast si removeFirstOccurence.
list.h
template <typename T>
class ArrayList {
private:
    T *array;
    int size, capacity;
public: 
    // Constructor
    ArrayList();
    // Destructor
    ~ArrayList();
 
    /* Adauga un nod cu valoarea == value la inceputul vectorului. */
    void addFirst(T value);
 
    /* Adauga un nod cu valoarea == value la sfarsitul vectorului. */
    void addLast(T value);
 
    /* Elimina elementul de la inceputul vectorului si intoarce valoarea acestuia. */
    T removeFirst();
 
    /* Elimina elementul de la sfarsitul vectorului si intoarce valoarea acestuia. */
    T removeLast();
 
    /* Elimina prima aparitie a elementului care are valoarea == value. */
    T removeFirstOccurrence(T value);
 
    /* Elimina ultima aparitie a elementului care are valoarea == value. */
    T removeLastOccurrence(T value);
 
    /* Afiseaza elementele vectorului pe o singura linie, separate printr-un spatiu. */
    void printList();
 
    /* Intoarce true daca vectorul este gol, false altfel. */
    bool isEmpty();
};

2) [2p] Implementaţi o funcţie removeDuplicates şi demonstraţi funcţionarea acesteia printr-un cod simplist. Funcţia va trebui să elimine toate elementele duplicate din lista dublu înlănţuită.

3) [2p] Implementaţi o funcţie reverse şi demonstraţi funcţionarea acesteia printr-un cod simplist. Funcţia va trebui să inverseze elementele din lista dublu înlănţuită.

4) BONUS Implementați inversarea unei liste simplu inlănțuite, fară memorie auxiliara.

sd-ca/2017/laboratoare/lab-03.1513081951.txt.gz · Last modified: 2017/12/12 14:32 by teodora.serbanescu
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