miercuri, 25 ianuarie 2012

Coada (Queue)

Coada (Queue) este un container (la fel ca şi Stiva) bazat pe principiul first-in first-out (FIFO). Adică elementele pot fi adăugate în coadă în orice moment, dar numai cel mai vechi (cel de la bază) element poate fi eliminat în orice moment (se extrage cea mai veche informaţie adăugată). Mai simplu, elementele sunt adăugate în spate şi eliminate din faţă. Intraţi pe http://courses.cs.vt.edu/csonline/DataStructures/Lessons/QueuesImplementationView/applet.html sau http://www.concentric.net/~ttwang/java/QueueDemo.html (aveţi nevoie de Java) pentru a înţelege mai bine. Folosiţi Enqueue pentru a adăuga obiecte şi Dequeue pentru a elimina obiectul din faţă (front). Acum că aveţi o idee despre cozi, să trecem la operaţiile pe care coada le implementează. Coada este o structură de date care suportă următoarele operaţii:
  • enqueue(ob): adaugă un obiect la sârşitul cozii (baza).
  • dequeue(): elimină obiectul din faţa cozii (cap).
  • front(): returnează o referinţă către cap fără a elimina obiectul.
  • back(): returnează o referinţă către baza fără a elimina obiectul.
  • size(): returnează numărul obiectelor din coadă.
  • isEmpty(): returnează true dacă coada este vidă, altfel false.
Deoarece am zis că coada este un container de obiecte, acest lucru înseamnă că ar trebui să pot declara cozi de orice tip: int, char, bool, etc. sau un tip definit de utilizator. Deci voi implementa o clasă generică Coada, cu parametrul generic T.
template<typename T>
class Coada
{
public:
    Coada(); // Constructor
    ~Coada(); // Destructor
    void enqueue(const T& ob); // Adaug un element in coada
    void dequeue(); // Elimin obiectul din fata
    const T& front() const; // Returnez o referinta catre cap
    const T& back() const; // Returnez o referinta catre baza
    int size() const; // Returnez numarul de elemente
    bool isEmpty() const; // Este coada vida?
private:
    struct Element
    {
        T data;
        Element * next;
    };
    Element * cap;
    Element * baza;
    int count; // Numar elementele
};

Observaţi că la unele funcţii apare referinţa constantă. De ce fac asta? De ce nu returnez pur şi simplu un obiect T şi nu o referinţă? Datorită eficienţei. Atunci când returnez o referinţă nu se mai alocă, temporar (deci va fi dealocat; un overhead suplimentar), spaţiu pentru obiectul returnat sau pentru parametrul formal, deoarece pot accesa direct variabila referită. Referinţa este constantă tocmai pentru că nu vreau să modific din greşeală argumentul sau obiectul returnat, plus că nu aş putea transmite valori concrete (de ex: 1, 2, 'c', 4.6, etc.) funcţiei enqueue(), dacă referinţa nu ar fi constantă.

Iniţializarea Cozii

Iniţializarea cozii presupune crearea cozii vide, adică elementele cap şi baza indică NULL (nullptr). Pentru cei care folosiţi compilatoare noi, cum ar fi cel din Visual Studio, folosiţi nullptr în loc de NULL, deoarece NULL este doar un simplu macro, este 0, este un întreg şi nu reprezintă concret o adresă inexistentă. Coada este iniţializată în constructor:
template<typename T> Coada<T>::Coada()
{
    cap = baza = nullptr; // coada vida
    count = 0; // niciun element
}

Coada Vidă

Coada este vidă atunci când cap este NULL, deci algoritmul va fi următorul:
template<typename T> bool Coada<T>::isEmpty() const
{
    return cap == nullptr;
}

Calificatorul const informează compilatorul că respectiva funcţie nu modifcă obiectele stivei, nu le modifica datele membre.

Numărul de elemente dintr-o Coadă

Pur şi simplu returnăm valoarea membrului count.
template<typename T> int Coada<T>::size() const
{
    return count;
}

Adăugarea unui element în Coadă

Algoritmul este simplu. Aloc memorie pentru un obiect de tip Element -> Scriu informaţia în obiect -> Succesorul acestui obiect este baza -> Noul element devine baza. Se incrementează count. Funcţia enqueue() va implementa această operaţie.
template<typename T> void Coada<T>::enqueue(const T& ob)
{
    if(isEmpty()) // Daca coada este vida
    {
        cap = new Element;
        cap->data = ob;
        cap->next = nullptr; // Fiind singurul element, succesorul este NULL
        baza = cap;
        count = 1;
    }
    else
    {
        Element * p = new Element;
        p->data = ob;
        p->next = nullptr; // Devine noul element baza
        baza->next = p; // Fosta baza se leaga de noua baza
        baza = p; // p devine baza
        ++count;  // S-a mai adaugat un element
    }
}

Eliminarea unui element din Coadă

Se poate elimina numai elementul din faţă. Se salvează adresa capului -> Elementul următor devine cap -> Se eliberează memoria fostului cap. Se decrementează count. Funcţia dequeue() va implementa această operaţie.
template<typename T> void Coada<T>::dequeue()
{
    if(isEmpty()) throw "Eroare! Coada Vida!";
    Element * q = cap; // Salvez elementul din cap
    cap = cap->next; // Elementul urmator devine cap
    delete q;
    --count;
}

Obţinerea elementului din capul Cozii

Se returnează o referinţă constantă către cap->data.
template<typename T> const T& Coada<T>::front() const
{
    if(isEmpty()) throw "Eroare! Coada Vida!";
    return cap->data;
}

Obţinerea elementului din baza Cozii

Se returnează o referinţă constantă către baza->data.
template<typename T> const T& Coada<T>::back() const
{
    if(isEmpty()) throw "Eroare! Coada Vida!";
    return baza->data;
}

Eliberarea spaţiului de memorie ocupat de Coadă

Se distruge pe rând fiecare element al cozii. De asta se va ocupa destructorul clasei.
template<typename T> Coada<T>::~Coada()
{
    while(cap != nullptr)
    {
        Element * q = cap;
        cap = cap->next;
        delete q;
    }
}

Dacă aveţi probleme cu nullptr, atunci folosiţi NULL. De asemenea vă recomand să folosiţi Visual Studio.
În continuare vă propun să creaţi un nou fişier Coada.h (header) şi să-l adăugaţi la proiectul vostru, după care în fişierul main (unde aveţi funcţia main) să adăugaţi, la început, directiva: #include "Coada.h". Adăugaţi următorul cod în Coada.h:

template<typename T>
class Coada
{
public:
    Coada() { // Constructor
        cap = baza = nullptr; // coada vida
        count = 0; // niciun element
    }
    ~Coada(); // Destructor
    void enqueue(const T& ob); // Adaug un element in coada
    void dequeue(); // Elimin obiectul din fata
    const T& front() const { // Returnez o referinta catre cap
        if(isEmpty()) throw "Eroare! Coada Vida!";
        return cap->data;
    }
    const T& back() const { // Returnez o referinta catre baza
        if(isEmpty()) throw "Eroare! Coada Vida!";
        return baza->data;
    }
    int size() const { return count; } // Returnez numarul de elemente
    bool isEmpty() const { return cap == nullptr; } // Este coada vida?
private:
    struct Element
    {
        T data;
        Element * next;
    };
    Element * cap;
    Element * baza;
    int count; // Numar elementele
};

template<typename T> Coada<T>::~Coada()
{
    while(cap != nullptr)
    {
        Element * q = cap;
        cap = cap->next;
        delete q;
    }
}

template<typename T> void Coada<T>::enqueue(const T& ob)
{
    if(isEmpty()) // Daca coada este vida
    {
        cap = new Element;
        cap->data = ob;
        cap->next = nullptr; // Fiind singurul element, succesorul este NULL
        baza = cap;
        count = 1;
    }
    else
    {
        Element * p = new Element;
        p->data = ob;
        p->next = nullptr; // Devine noul element baza
        baza->next = p; // Fosta baza se leaga de noua baza
        baza = p; // p devine baza
        ++count;  // S-a mai adaugat un element
    }
}

template<typename T> void Coada<T>::dequeue()
{
    if(isEmpty()) throw "Eroare! Coada Vida!";
    Element * q = cap; // Salvez elementul din cap
    cap = cap->next; // Elementul urmator devine cap
    delete q;
    --count;
}

PROBLEMĂ: Într-o coadă sunt memorate numere din intervalul \([1, 10]\). Să se elimine numerele pare.
Deoarece numai informaţia din cap poate fi extrasă, pentru a ajunge la un anumit element trebuie eliminate toate elementele până la acel element. În acest caz, pentru a nu pierde informaţia din celelalte elemente (aici, numerele impare), voi descărca elementele impare într-o coadă de rezervă (temp), iar după ce voi scăpa de numerele pare, voi încărca numerele impare din coada de rezervă înapoi în coada iniţială.

#include <iostream>
#include "Coada.h"
using namespace std;

int main()
{
    Coada<int> qu, temp; 

    for(int i = 1; i <= 10; i++) // Adaug 10 numere [1, 10]
        qu.enqueue(i);

    while(!qu.isEmpty()) // Cat timp coada nu este vida
    {
        if(qu.front() % 2 == 0)
            qu.dequeue(); // Daca este par, il elimin
        else
        {
            temp.enqueue(qu.front()); // Salvez numarul impar
            qu.dequeue();
        }
    }

    // Incarc numerele impare inapoi in coada qu
    while(!temp.isEmpty())
    {
        qu.enqueue(temp.front());
        temp.dequeue();
    }

    // Afisez rezultatul
    while (!qu.isEmpty())
    {
        cout << qu.front() << ' ';
        qu.dequeue();
    }

    system("PAUSE");
    return 0;
}
Output:
1 3 5 7 9

Trebuie să ştiţi că Standard Template Library (STL) implementează o structură de tip coadă în clasa queue, ce se găseşte în headerul queue. Are aceeiaşi membri (isEmpty() este empty(), enqueue() este push(), dequeue() este pop()) ca şi clasa definită de mine, singura diferenţa fiind că operaţiile front(), back() şi pop() pentru o coadă vidă sunt nedefinite, adică nu se generează o excepţie (aşa cum am făcut eu). Deci este responsabilitatea programatorului pentru a se asigura că nu aplică acele operaţii pe o coadă vidă.

#include <queue>
using std::queue; 
queue<float> myQueue;

Stiva (Stack)

Stiva (Stack) este un container (sau colecţie) de obiecte bazat pe principiul last-in-first-out (LIFO). Adică obiectele (numere, caractere, etc.) pot fi adăugate în stivă în orice moment, dar numai cel mai recent (cel din vârf) poate fi eliminat în orice moment. Imaginaţi-vă stiva ca un turn de obiecte, cel mai recent adăugat aflându-se în vârf, iar cel mai vechi la bază. Intraţi pe http://www.cosc.canterbury.ac.nz/mukundan/dsal/StackAppl.html (aveţi nevoie de Java) sau http://acc6.its.brooklyn.cuny.edu/~cis22/animations/tsang/html/STACK/stack640.html pentru a înţelege mai bine. Folosiţi Push pentru a adăuga obiecte şi Pop pentru a elimina obiectul din vârf (top), iar butonul Top afişează valoarea vârfului. Stivele sunt folosite în multe aplicaţii cum ar fi: browser-ele web care memorează adresele site-urilor recent vizitate, opţiunea undo din editoarele de text, ş.a. Acum că aveţi o idee despre stive, să trecem la operaţiile pe care stiva le implementează. Stiva este o structură de date care suportă următoarele operaţii:
  • push(ob): adaugă un obiect în vârful stivei.
  • pop(): elimină obiectul din vârful stivei.
  • top(): returnează o referinţă către vârful stivei fără a elimina obiectul.
  • size(): returnează numărul obiectelor din stivă.
  • isEmpty(): returnează true dacă stiva este vidă, altfel false.
Deoarece am zis că stiva este un container de obiecte, acest lucru înseamnă că ar trebui să pot declara stive de orice tip: int, char, bool, etc. sau un tip definit de utilizator. Să definesc câte o stivă pentru fiecare tip ar însemna să fiu nebun, de aceea voi implementa o clasă generică Stiva, cu parametrul generic T.
template<typename T>
class Stiva
{
public:
    Stiva(); // Constructor | Initializarea stivei
    ~Stiva(); // Destructor
    int size() const; // Returneaza numarul elementelor din stiva
    bool isEmpty() const; // Este stiva vida?
    const T& top() const; // Returneaza o referinta constanta catre varf
    void push(const T& ob); // Adauga un element
    void pop(); // Elimina elementul din varf
private:
    struct Element // Implementeaza un element de stiva
    {
        T data; // Informatia memorata in elementul stivei
        Element * below; // Elementul anterior / dedesubt
    };
    Element * varf; // Varful stivei
    int count; // Data membra cu care numar elementele stivei
};

Observaţi că la unele funcţii apare referinţa constantă. De ce fac asta? De ce nu returnez pur şi simplu un obiect T şi nu o referinţă? Datorită eficienţei. Atunci când returnez o referinţă nu se mai alocă, temporar (deci va fi dealocat; un overhead suplimentar), spaţiu pentru obiectul returnat sau pentru parametrul formal, deoarece pot accesa direct variabila referită. Referinţa este constantă tocmai pentru că nu vreau să modific din greşeală argumentul sau obiectul returnat, plus că nu aş putea transmite valori concrete (de ex: 1, 2, 'c', 4.6, etc.) funcţiei push(), dacă referinţa nu ar fi constantă.

Iniţializarea Stivei

Iniţializarea stivei presupune crearea stivei vide, adică elementul varf indică NULL (nullptr). Pentru cei care folosiţi compilatoare noi, cum ar fi cel din Visual Studio, folosiţi nullptr în loc de NULL, deoarece NULL este doar un simplu macro, este 0, este un întreg şi nu reprezintă concret o adresă inexistentă. Stiva este iniţializată în constructor:
template<typename T> Stiva<T>::Stiva()
{
    varf = nullptr; // stiva vida
    count = 0; // zero elemente
}

Stiva Vidă

Stiva este vidă atunci când varf este NULL, deci algoritmul va fi următorul:
template<typename T> bool Stiva<T>::isEmpty() const
{
    return varf == nullptr;
}

Calificatorul const informează compilatorul că respectiva funcţie nu modifcă obiectele stivei, nu le modifica datele membre.

Numărul de elemente dintr-o Stivă

Pur şi simplu returnăm valoarea membrului count.
template<typename T> int Stiva<T>::size() const
{
    return count;
}

Adăugarea unui element în Stivă

Algoritmul este simplu. Aloc memorie pentru un obiect de tip Element -> Scriu informaţia în obiect -> Predecesorul acestui obiect este varf -> Noul element devine varf. Se incrementează count. Funcţia push() va implementa această operaţie.
template<typename T> void Stiva<T>::push(const T& ob)
{
    if(isEmpty()) // Daca stiva este vida
    {
        varf = new Element;
        varf->data = ob;
        varf->below = nullptr; // Este baza! Primul element adaugat
        count = 1; 
    }
    else
    {
        Element * p = new Element;
        p->data = ob;
        p->below = varf; // Dedesubt se afla fostul varf
        varf = p; // p devine noul varf
        ++count;  // S-a mai adaugat un element
    }
}

Eliminarea unui element din Stivă

Se poate elimina numai elementul din vârf. Se salvează adresa vârfului -> Elementul dedesubt devine vârf -> Se eliberează memoria fostului vârf. Se decrementează count. Funcţia pop() va implementa această operaţie.
template<typename T> void Stiva<T>::pop()
{
    // Daca stiva este vida, genereaza o exceptie
    if(isEmpty()) throw "Eroare! Stiva Vida!";
    Element * q = varf; // Salvez varful
    varf = varf->below; // Elementul anterior devine noul varf
    delete q;
    --count; // S-a mai eliminat un element
}

Obţinerea elementului din vârful Stivei

Se returnează o referinţă constantă către varf->data.
template<typename T> const T& Stiva<T>::top() const
{
    if(isEmpty()) throw "Eroare! Stiva Vida!";
    return varf->data;
}

Eliberarea spaţiului de memorie ocupat de Stivă

Se distruge pe rând fiecare element al stivei. De asta se va ocupa destructorul clasei.
template<typename T> Stiva<T>::~Stiva()
{
    while(varf != nullptr)
    {
        Element * q = varf;
        varf = varf->below;
        delete q;
    }
}

Dacă aveţi probleme cu nullptr, atunci folosiţi NULL. De asemenea vă recomand să folosiţi Visual Studio.
În continuare vă propun să creaţi un nou fişier Stiva.h (header) şi să-l adăugaţi la proiectul vostru, după care în fişierul main (unde aveţi funcţia main) să adăugaţi, la început, directiva: #include "Stiva.h". Adăugaţi următorul cod în Stiva.h:

template<typename T>
class Stiva
{
public:
    // Constructor | Initializarea stivei
    Stiva() {
        varf = nullptr; // stiva vida
        count = 0; // zero elemente
    }
    ~Stiva(); // Destructor
    int size() const { return count; } // Returneaza numarul elementelor din stiva
    bool isEmpty() const { return varf == nullptr; } // Este stiva vida?
    const T& top() const { // Returneaza o referinta constanta catre varf
        if(isEmpty()) throw "Eroare! Stiva Vida!"; 
        return varf->data; 
    } 
    void push(const T& ob); // Adauga un element
    void pop(); // Elimina elementul din varf
private:
    struct Element // Implementeaza un element de stiva
    {
        T data; // Informatia memorata in elementul stivei
        Element * below; // Elementul anterior / dedesubt
    };
    Element * varf; // Varful stivei
    int count; // Data membra cu care numar elementele stivei
};

template<typename T> void Stiva<T>::push(const T& ob)
{
    if(isEmpty()) // Daca stiva este vida
    {
        varf = new Element;
        varf->data = ob;
        varf->below = nullptr; // Este baza! Primul element adaugat
        count = 1; 
    }
    else
    {
        Element * p = new Element;
        p->data = ob;
        p->below = varf; // Dedesubt se afla fostul varf
        varf = p; // p devine noul varf
        ++count;  // S-a mai adaugat un element
    }
}

template<typename T> void Stiva<T>::pop()
{
    // Daca stiva este vida, genereaza o exceptie
    if(isEmpty()) throw "Eroare! Stiva Vida!";
    Element * q = varf; // Salvez varful
    varf = varf->below; // Elementul anterior devine noul varf
    delete q;
    --count; // S-a mai eliminat un element
}

template<typename T> Stiva<T>::~Stiva()
{
    while(varf != nullptr)
    {
        Element * q = varf;
        varf = varf->below;
        delete q;
    }
}

PROBLEMĂ: Într-o stivă sunt memorate numere din intervalul \([1, 10]\). Să se elimine numerele pare.
Deoarece numai informaţia din vârf poate fi prelucrată, pentru a ajunge la un anumit element trebuie eliminate toate elementele până la acel element. În acest caz, pentru a nu pierde informaţia din celelalte elemente (aici, numerele impare), voi descărca elementele impare într-o stivă de rezervă (temp), iar după ce voi scăpa de numerele pare, voi încărca numerele impare din stiva de rezervă înapoi în stiva iniţială.

#include <iostream>
#include "Stiva.h"
using namespace std;

int main()
{
    Stiva<int> st; // Voi memora intregi | int
    Stiva<int> temp; // Stiva de rezerva in care voi memora numerele impare
 
    for(int i = 1; i <= 10; i++)
    st.push(i); // Adaug cele zece numere

    while(!st.isEmpty()) // Cat timp stiva nu este vida
    {
        if(st.top() % 2 == 0)
        st.pop(); // Daca este par, il elimin
        else
        {
            temp.push(st.top()); // Salvez numarul impar
            st.pop();
        }
    }

    // Acum incarc numerele impare inapoi in stiva st
    while(!temp.isEmpty())
    {
        st.push(temp.top());
        temp.pop();
    }

    // Acum afisez stiva st
    while(!st.isEmpty())
    {
        cout << st.top() << '\n';
        st.pop();
    }

    system("PAUSE");
    return 0;
}
Output:
9
7
5
3
1

Trebuie să ştiţi că Standard Template Library (STL) implementează o structură de tip stivă în clasa stack, ce se găseşte în headerul stack. Are aceeiaşi membri (isEmpty() este empty()) ca şi clasa definită de mine, singura diferenţa fiind că operaţiile top() şi pop() pentru o stivă vidă sunt nedefinite, adică nu se generează o excepţie (aşa cum am făcut eu). Deci este responsabilitatea programatorului pentru a se asigura că nu aplică acele operaţii pe o stivă vidă.

#include <stack>
using std::stack; 
stack<int> myStack;

marți, 24 ianuarie 2012

Lista Circulară Simplu Înlanţuită

Lista Circulară Simplu Înlănţuită este aproape similară cu Lista Liniară Simplu Înlănţuită. Diferenţele sunt următoarele: nodul tail (ultim) este legat de nodul head (prim), în acest caz lista nu mai are început sau sfârşit. Vedeţi tutorialul despre listele simplu înlănţuite înainte de a-l citi pe acesta. Mai jos este o imagine ce ilustrează conceptul de listă circulară simplu înlănţuită (LCÎ):

În continuare voi implementa numai algoritmii care diferă faţă de algoritmii listei simplu înlănţuite.

Adăugarea unui nod la listă

Algoritmii sunt aceeiaşi singura diferenţa fiind că ultimul nod va fi legat de primul nod. Mai jos voi implementa algoritmul de eliminare al ultimului nod:

void ListaC::push_back(int elem)
{
    // Daca lista este vida, atunci 
    if(isEmpty()) 
    {
        head = new Nod; // Aloc memorie pentru primul nod
        head->data = elem;
        head->next = head; // Fiind singurul nod, urmatorul este head
        tail = head; // si tail == head
    }
    else  // altfel
    {
        Nod * nod = new Nod; // Aloc memorie pentru noul nod
        nod->data = elem;    // Scriu informatia in data
        nod->next = head;  // Ultimul nod va fi mereu legat de primul
        tail->next = nod;    // Fostul tail este legat de noul tail
        tail = nod;          // nod devine tail
    }
}

Parcurgerea listei

Lista va fi parcursă tot cu Iterator, dar de data aceasta avem nevoie de un nod care să termine lista sau, mai bine zis, parcurgerea. Vom considera nodul head ca nod terminator. Acesta este algoritmul general: Mai întâi prelucrăm nodul prim, apoi prelucrăm toate celelalte noduri până când ajungem din nou la prim (head).
ListaC mylist;
// Mai intai se prelucreaza nodul prim
// *mylist.front()
// Se prelucreaza celelalte noduri
for(ListaC::Iterator it = ++mylist.front(); it != mylist.front(); it++)
{
 // Se prelucreaza *it
}

Nu uitaţi că front() returnează un Iterator.

Eliminarea unui nod din listă

Vom elimina de fapt nodul următor nodului curent q. Trebuie să verificăm dacă succesorul lui q nu este cumva nodul head, caz în care actualizăm nodul head. Voi folosi funcţia searchPrevious() pentru a obţine un iterator către nodul precedent nodului pe care vreau să-l şterg.
void ListaC::remove(Iterator nod)
{
    if(isEmpty()) throw "Empty List"; // Daca lista este vida
    if(head == tail) // Daca lista are un singur nod
    { delete head; head = tail = nullptr; return; } 
    Nod * temp = nod.list->next;
    nod.list->next = nod.list->next->next;
    if(temp == head) head = head->next;
    delete temp;
}
Funcţia searchPrevious():
ListaC::Iterator ListaC::searchPrevious(int value) const
{
    if(head->next->data == value) return front();
    for(Nod* it = head->next; it != head; it = it->next)
    {
        if(it->next->data == value) return Iterator(it); // Daca am gasit nodul il returnez
    }
    return Iterator(nullptr); // Nu am gasit nimic
}
În final, iată clasa ListaC:
class ListaC
{
public:
    struct Iterator; // Declaratie forward
    // Constructor - Initializarea listei
    ListaC() { head = tail = nullptr; /*Se creeaza lista vida*/ }
    // Destructor - Distrugerea listei
    ~ListaC() { if(!isEmpty()) clear(); }  
    void push_front(int elem); // Inserare in fata primului nod
    void push_back(int elem);  // Inserare dupa ultimul nod
    Iterator search(int value) const; // Cauta value in lista
    Iterator searchPrevious(int value) const; // Cauta predecesorul lui value
    void pop_front(); // Elimina nodul din fata
    void remove(Iterator nod); // Elimina nodul nod
    // Returneaza un iterator catre inceputul listei
    Iterator front() const { return Iterator(head); } 
    // Returneaza un iterator catre nodul tail
    Iterator back() const { return Iterator(tail); }
    bool isEmpty() const { return head == nullptr; } // Este lista vida?
    void clear(); // Stergerea completa a listei

private:
    struct Nod  // Clasa Helper; Implementeaza un nod de lista
    {
    int data;   // informatia propriu-zisa
    Nod * next; // urm
    };
    Nod * head; // prim
    Nod * tail; // ultim
public:
    struct Iterator // Un pointer inteligent
    {
        friend class ListaC; // Lista are acces la membrii privati ai lui Iterator
        Iterator() { list = nullptr; }
        Iterator(Nod * ls) { list = ls; }
        // Supraincarc operatorul * (dereferentiere)
        int& operator*() { if(list != nullptr) return list->data; else throw "Null iterator!"; }
        // Prefix - Trec la urmatorul nod
        Iterator& operator++() { list = list->next; return *this; }  
        // Postfix
        Iterator operator++(int) { Iterator temp = *this; ++(*this); return temp; }
        bool operator==(const Iterator& it) const { if(it.list == this->list) return true; else return false; }
        bool operator!=(const Iterator& it) const { if(!(it == *this)) return true; else return false; }
    private:
        Nod * list;
    };
};

void ListaC::push_front(int elem)
{
    // Daca lista este vida, atunci 
    if(isEmpty()) 
    {
        head = new Nod; // Aloc memorie pentru primul nod
        head->data = elem;
        head->next = head; // Fiind singurul nod, urmatorul este head
        tail = head; // si tail == head
    }
    else  // altfel
    {
        Nod * nod = new Nod; // Aloc memorie pentru noul nod
        nod->data = elem;    // Scriu informatia in data
        nod->next = head;    // Leg nod de head
        head = nod;          // nod devine noul head
        tail->next = head;   // Actualizez legatura lui tail
    }
}

void ListaC::push_back(int elem)
{
    // Daca lista este vida, atunci 
    if(isEmpty()) 
    {
        head = new Nod; // Aloc memorie pentru primul nod
        head->data = elem;
        head->next = head; // Fiind singurul nod, urmatorul este NIMIC adica NULL
        tail = head; // si tail == head
    }
    else  // altfel
    {
        Nod * nod = new Nod; // Aloc memorie pentru noul nod
        nod->data = elem;    // Scriu informatia in data
        nod->next = head;  // Ultimul nod va fi mereu legat de primul
        tail->next = nod;    // Fostul tail este legat de noul tail
        tail = nod;          // nod devine tail
    }
}

ListaC::Iterator ListaC::search(int value) const
{
    if(head->data == value) return front();
    for(Nod* it = head->next; it != head; it = it->next)
    {
        if(it->data == value) return Iterator(it); // Daca am gasit nodul il returnez
    }
    return Iterator(nullptr); // Nu am gasit nimic
}

void ListaC::pop_front()
{
    if(isEmpty()) throw "Empty List"; // Daca lista este vida
    if(head == tail) // Daca lista are un singur nod
    { delete head; head = tail = nullptr; return; } 
    Nod * temp = head; // Salvez adresa obiectului head
    head = head->next; // Succesorul lui head devine noul head
    tail->next = head; // Actualizez legatura lui tail
    delete temp; // Eliberez memoria ocupata de vechiul obiect head
}

void ListaC::remove(Iterator nod)
{
    if(isEmpty()) throw "Empty List"; // Daca lista este vida
    if(head == tail) // Daca lista are un singur nod
    { delete head; head = tail = nullptr; return; } 
    Nod * temp = nod.list->next;
    nod.list->next = nod.list->next->next;
    if(temp == head) head = head->next;
    delete temp;
}

void ListaC::clear()
{
    Nod *it = head, *temp; 
    while(it != nullptr)
    {
        temp = it; // Salvez adresa nodului curent
        it = it->next; // Trec mai departe
        delete temp; // Distrug nodul curent
    }
    head = tail = nullptr; // Lista Vida
}

ListaC::Iterator ListaC::searchPrevious(int value) const
{
    if(head->next->data == value) return front();
    for(Nod* it = head->next; it != head; it = it->next)
    {
        if(it->next->data == value) return Iterator(it); // Daca am gasit nodul il returnez
    }
    return Iterator(nullptr); // Nu am gasit nimic
}
Iată un exemplu cu lista circulară simplu înlănţuită:
#include <iostream>
#include "ListaC.h"
using namespace std;

int main()
{
    ListaC mylist;
    mylist.push_back(3);
    mylist.push_back(12);
    mylist.push_front(18);
    mylist.push_front(56);
 
    cout << *mylist.front() << ' ';
    for(ListaC::Iterator it = ++mylist.front(); it != mylist.front(); it++)
    cout << *it << ' ';
    cout << '\n';

    ListaC::Iterator p = mylist.searchPrevious(18);
    mylist.remove(p);
    mylist.pop_front();

    cout << *mylist.front() << ' ';
    for(ListaC::Iterator it = ++mylist.front(); it != mylist.front(); it++)
    cout << *it << ' ';
    cout << '\n';

    system("PAUSE");
    return 0;
}
Output:
56 18 3 12
3 12

duminică, 22 ianuarie 2012

Lista Liniară Dublu Înlănţuită

Lista Liniară Dublu Înlanţuită este similară cu Lista Simplu Înlănţuită. Singura diferenţă este că fiecare nod are o legătură atât către nodul succesor cât şi nodul predecesor. În continuare voi implementa clasa ListaD. Vă reamintesc: Pentru a semnala sfârşitul listei folosim NULL. Pointerului next al ultimului nod (nodul terminal) îi atribuim adresa NULL (Pentru cei care folosiţi compilatoare noi, cum ar fi cel din Visual Studio, folosiţi nullptr în loc de NULL, deoarece NULL este doar un simplu macro, este 0, este un întreg şi nu reprezintă concret o adresă inexistentă). Vedeţi tutorialul despre listele simplu înlănţuite înainte de a-l citi pe acesta. Mai jos este o imagine ce ilustrează conceptul de listă dublu înlănţuită (LDÎ):

Implementarea Listei Dublu Înlănţuite

Voi defini o clasă numită ListaD, ce implementează LDÎ. Fiecare nod va memora trei valori, membrul data care stochează informaţia propriu-zisă (în acest caz va fi un întreg), membrul next care este un pointer (legătură) către următorul nod al listei şi membrul previous care este un pointer către nodul anterior al listei.
class ListaD
{
public:
    struct Iterator; // Declaratie forward
    static const Iterator END; // Iterator NULL
    // Constructor - Initializarea listei
    ListaD() { head = tail = nullptr; /*Se creeaza lista vida*/ }
    // Destructor - Distrugerea listei
    ~ListaD() { if(!isEmpty()) clear(); }  
    void push_front(int elem); // Inserare in fata primului nod
    void push_back(int elem);  // Inserare dupa ultimul nod
    void insert_before(int elem, Iterator nod); // Inserare inainte de nod
    void insert_after(int elem, Iterator nod);  // Inserare dupa nod
    Iterator search(int value) const; // Cauta value in lista
    void pop_front(); // Elimina nodul din fata
    void pop_back();  // Elimina ultimul nod
    void remove(Iterator nod); // Elimina nodul nod
    // Returneaza un iterator catre inceputul listei
    Iterator front() const { return Iterator(head); }
    // Returneaza un iterator catre nodul tail
    Iterator back() const { return Iterator(tail); } 
    bool isEmpty() const { return head == nullptr; } // Este lista vida?
    void clear(); // Stergerea completa a listei

private:
    struct Nod  // Clasa Helper; Implementeaza un nod de lista
    {
        int data;   // informatia propriu-zisa
        Nod * next; // urm
        Nod * previous; // ant
    };
    Nod * head; // prim
    Nod * tail; // ultim
public:
    struct Iterator // Un pointer inteligent
    {
        friend class ListaD; // Lista are acces la membrii privati ai lui Iterator
        Iterator() { list = nullptr; }
        Iterator(Nod * ls) { list = ls; }
        // Supraincarc operatorul * (dereferentiere)
        int& operator*() { if(list != nullptr) return list->data; else throw "Null iterator!"; }
        // Prefix - Trec la urmatorul nod
        Iterator& operator++() { list = list->next; return *this; }  
        // Postfix
        Iterator operator++(int) { Iterator temp = *this; ++(*this); return temp; }
        // Prefix - Trec la nodul anterior
        Iterator& operator--() { list = list->previous; return *this; }  
        // Postfix
        Iterator operator--(int) { Iterator temp = *this; --(*this); return temp; }
        bool operator==(const Iterator& it) const { if(it.list == this->list) return true; else return false; }
        bool operator!=(const Iterator& it) const { if(!(it == *this)) return true; else return false; }
    private:
        Nod * list;
    };
};
În continuare voi implementa numai algoritmii care diferă faţă de algoritmii listei simplu înlănţuite.

Adăugarea unui nod la listă

Avem 3 cazuri:
  • Inserarea în faţa primului nod;
  • Inserarea după ultimul nod;
  • Inserarea în interiorul listei.
Inserarea în faţa primului nod [push_front()]. Aici avem două cazuri: dacă lista este vidă atunci adăugăm atât la head cât şi la tail primul nod, altfel adăugăm nodul în faţa listei. Aloc memorie pentru noul nod (să zicem p) -> Scriu informaţia în p -> Leg nodul p de nodul head şi previous indică NULL -> p devine noul head (adică head indică adresa lui p).
void ListaD::push_front(int elem)
{
    // Daca lista este vida, atunci 
    if(isEmpty()) 
    {
        head = new Nod; // Aloc memorie pentru primul nod
        head->data = elem;
        head->next = nullptr; // Fiind singurul nod, urmatorul este NIMIC adica NULL
        head->previous = nullptr; // Fiind singurul nod, anteriorul este NIMIC adica NULL
        tail = head; // si tail == head
    }
    else  // altfel
    {
        Nod * nod = new Nod; // Aloc memorie pentru noul nod
        nod->data = elem;    // Scriu informatia in data
        nod->previous = nullptr; // Devenind head, previous indica NULL
        nod->next = head;    // Leg nod de head
        head->previous = nod; // Predecesorul fostului head este 'nod'
        head = nod;          // nod devine noul head
    }
}

Inserarea după ultimul nod [push_back()]. Şi aici avem cele două cazuri. Dacă lista nu este vidă, adăugăm nodul la sfârşitul listei. Se alocă memorie pentru noul nod p -> Scriu informaţia în nod -> Leg nodul tail de nodul p şi pointerul previous de nodul tail -> p devine noul tail (adică p se leagă de NULL şi tail indică acum nodul p).

void ListaD::push_back(int elem)
{
    // Daca lista este vida, atunci 
    if(isEmpty()) 
    {
        head = new Nod; // Aloc memorie pentru primul nod
        head->data = elem;
        head->next = nullptr; // Fiind singurul nod, urmatorul este NIMIC adica NULL
        head->previous = nullptr; // Fiind singurul nod, anteriorul este NIMIC adica NULL
        tail = head; // si tail == head
    }
    else  // altfel
    {
        Nod * nod = new Nod; // Aloc memorie pentru noul nod
        nod->data = elem;    // Scriu informatia in data
        nod->next = nullptr; // Devenind nod terminal, va fi legat de NULL
        nod->previous = tail; // previous indica tail
        tail->next = nod;    // Fostul tail este legat de noul tail
        tail = nod;          // nod devine tail
    }
}

Inserarea în interiorul listei.
Inserarea după nodul q (unde q este un nod al listei pe care îl obţineţi iterând prin listă). Se alocă memorie pentru noul nod (newNod) -> Leg nodul newNod de succesorul nodului q, iar predecesorul său va fi q -> Nodul q se leagă de nodul newNod, iar predecesorul succesorului lui q este newNod. Dacă q a fost nod terminal, atunci newNod este acum nod terminal.

void ListaD::insert_after(int elem, Iterator nod)
{
    Nod * newNod = new Nod; // Aloc memorie pentru noul nod
    newNod->data = elem;    // Scriu informatia in data
    newNod->next = nod.list->next; // newNod se leaga de succesorul lui 'nod'
    newNod->previous = nod.list; // Predecesorul lui newNod este 'nod'
    // Daca nodul 'nod' a fost ultimul nod, atunci nodul newNod devine nod terminal
    if(nod.list == tail) tail = newNod;
    else nod.list->next->previous = newNod; // Predecesorul succesorului lui 'nod' este newNod
    nod.list->next = newNod; // Nodul 'nod' se leaga de newNod
}

Chiar dacă am spus nodul nod, nod nu este de fapt Nod, este un obiect Iterator. Prin nodul nod mă refer la nodul încapsulat în obiectul Iterator nod (adică membrul list).

Inserarea înainte de nodul q. Nodul newNod se inserează între predecesorul lui q şi q. Succesorul lui newNod este q, predecesorul este q-previous. Nodul newNod va fi succesorul lui q->previous şi anteriorul lui q. De asemenea verificăm dacă q este head, caz în care newNod devine head.

void ListaD::insert_before(int elem, Iterator nod)
{
    Nod * newNod = new Nod; // Aloc memorie pentru noul nod
    newNod->data = elem; // Scriu informatia in data
    newNod->next = nod.list; // Succesorul lui newNod este 'nod'
    newNod->previous = nod.list->previous; // Predecesorul lui newNod este predecesorul lui 'nod'
    // Daca nodul 'nod' a fost primul nod, atunci nodul newNod devine head
    if(nod.list == head) head = newNod;
    else nod.list->previous->next = newNod; // Succesorul predecesorului lui 'nod' este newNod
    nod.list->previous = newNod; // Predecesorul lui 'nod' este newNod
}
Inserţia (cei patru algoritmi) în listă se realizează în timp constant \(O(1)\).

Parcurgerea listei

Lista se parcurge folosind un Iterator. Algoritmul de parcurgere l-am implementat deja în funcţia operator++() (ambele) a clasei Iterator şi funcţia operator--() pentru parcurgerea în ordine inversă. Ideea este următoarea: Se obţine un pointer (în cazul meu un Iterator) către primul nod (head), apoi se parcurge lista actualizând pointerul (iteratorul) curent cu următorul element (nod = nod->next) până când pointerul curent indică NULL (în cazul meu iteratorul ajunge la end()), adică pointerul curent indică nodul imediat următor după tail. Pentru parcurgerea inversă, pointerul (în cazul meu iteratorul) indică nodul tail, iar acesta se actualizează cu predecesorul până când pointerul curent indică NULL (adică end()).
Iterator& operator++() { list = list->next; return *this; }
Iterator& operator--() { list = list->previous; return *this; }
Împreună cu funcţiile front(), back() şi constanta END putem parcurge lista. Cele două funcţii şi constanta END sunt definite în felul următor:
ListaD::Iterator ListaD::front() const { return Iterator(head); }
ListaD::Iterator ListaD::back() const { return Iterator(tail); }
static const ListaD::Iterator ListaD::END = nullptr; // Iterator NULL
Observaţi că am înlocuit funcţia end() (vezi tutorialul despre LSÎ) cu constanta statică END care este un Iterator ce încapsulează un Nod NULL. Acesta este nodul imediat următor după nodul tail şi imediat anterior nodului head al listei. Deci de fiecare dată când prelucrez lista, voi prelucra informaţiile din intervalul \([front, back]\). Iată algoritmul general de parcurgere a listei:
ListaD myList;  // declar o lista
for(ListaD::Iterator iter = myList.front(); iter != myList.end(); iter++)
{
 // Se prelucreaza (*iter)
}
Şi în ordine inversă:
ListaD myList;  // declar o lista
for(ListaD::Iterator iter = myList.back(); iter != myList.end(); iter--)
{
 // Se prelucreaza (*iter)
}

Eliminarea unui nod din listă

Avem 3 cazuri:
  • Eliminarea primului nod;
  • Eliminarea ultimuluiu nod;
  • Eliminarea unui nod din interiorul listei.

Eliminarea primului nod [pop_front()]. Sunt câteva cazuri care trebuie luate în considerare: lista este vidă sau lista are un singur nod.

Oricum ar fi, algoritmul general este următorul: Salvez nodul head într-un nod oarecare temp -> Succesorul nodului head devine nod head şi predecesorul succesorului lui head indică NULL (deoarece succesorul devine head) -> Se eliberează memoria ocupată de obiectul referit de nodul temp (fostul head).

void ListaD::pop_front()
{
    if(isEmpty()) throw "Empty List"; // Daca lista este vida
    if(head == tail) // Daca lista are un singur nod
    { delete head; head = tail = nullptr; return; } 
    Nod * temp = head; // Salvez adresa obiectului head
    head->next->previous = nullptr; // Predecesorul succesorului lui head devine NULL
    head = head->next; // Succesorul lui head devine noul head
    delete temp; // Eliberez memoria ocupata de vechiul obiect head
}

Eliminarea ultimului nod [pop_back()]

Succesorul predecesorului lui tail indică NULL (deoarece predecesorul devine tail) -> Predecesorul nodului tail devine nod terminal -> Eliberez zona de memorie ocupată de fostul tail -> Predecesorul devine nod tail.
void ListaD::pop_back()
{
    if(isEmpty()) throw "Empty List"; // Daca lista este vida
    if(head == tail) // Daca lista are un singur nod
    { delete head; head = tail = nullptr; return; }
    Nod * temp = tail; // Salvez adresa obiectului tail
    tail->previous->next = nullptr; // Succesorul predecesorului lui tail devine NULL
    tail = tail->previous; // Predecesorul lui tail devine noul tail
    delete temp; // Eliberez memoria ocupata de vechiul obiect tail
}

Eliminarea unui nod din interiorul listei

Eliminarea unui nod din interiorul listei este destul de simplă. Ca să eliminăm nodul p din listă, trebuie să legăm predecesorul nodului p de succesorul nodului p, apoi predecesorul succesorului nodului p de predeceosorul lui p. Algoritmul este implementat în funcţia remove() care ia ca parametru un obiect de tip Iterator.
void ListaD::remove(Iterator nod)
{
    if(isEmpty()) throw "Empty List"; // Daca lista este vida
    if(head == tail) // Daca lista are un singur nod
    { delete head; head = tail = nullptr; return; } 
    nod.list->previous->next = nod.list->next; // Leg predecesorul lui 'nod' de succesorul acestuia
    nod.list->next->previous = nod.list->previous; // Predecesorul succesorului lui 'nod' indica predecesorul lui 'nod'
    delete nod.list; // Elimin nodul 'nod' 
}
Algoritmul de eliminare al unui nod (toţi cei trei algoritmi) este constant \(O(1)\).
În continuare vă propun să creaţi un nou fişier ListaD.h (header) şi să-l adăugaţi la proiectul vostru, după care în fişierul main (unde aveţi funcţia main) să adăugaţi, la început, directiva: #include "ListaD.h". Adăugaţi următorul cod în ListaD.h:
class ListaD
{
public:
    struct Iterator; // Declaratie forward
    static const Iterator END; // Iterator NULL
    // Constructor - Initializarea listei
    ListaD() { head = tail = nullptr; /*Se creeaza lista vida*/ }
    // Destructor - Distrugerea listei
    ~ListaD() { if(!isEmpty()) clear(); }  
    void push_front(int elem); // Inserare in fata primului nod
    void push_back(int elem);  // Inserare dupa ultimul nod
    void insert_before(int elem, Iterator nod); // Inserare inainte de nod
    void insert_after(int elem, Iterator nod);  // Inserare dupa nod
    Iterator search(int value) const; // Cauta value in lista
    void pop_front(); // Elimina nodul din fata
    void pop_back();  // Elimina ultimul nod
    void remove(Iterator nod); // Elimina nodul nod
    // Returneaza un iterator catre inceputul listei
    Iterator front() const { return Iterator(head); }
    // Returneaza un iterator catre nodul tail
    Iterator back() const { return Iterator(tail); }
    bool isEmpty() const { return head == nullptr; } // Este lista vida?
    void clear(); // Stergerea completa a listei

private:
    struct Nod  // Clasa Helper; Implementeaza un nod de lista
    {
        int data;   // informatia propriu-zisa
        Nod * next; // urm
        Nod * previous; // ant
    };
    Nod * head; // prim
    Nod * tail; // ultim
public:
    struct Iterator // Un pointer inteligent
    {
        friend class ListaD; // Lista are acces la membrii privati ai lui Iterator
        Iterator() { list = nullptr; }
        Iterator(Nod * ls) { list = ls; }
        // Supraincarc operatorul * (dereferentiere)
        int& operator*() { if(list != nullptr) return list->data; else throw "Null iterator!"; }
        // Prefix - Trec la urmatorul nod
        Iterator& operator++() { list = list->next; return *this; }  
        // Postfix
        Iterator operator++(int) { Iterator temp = *this; ++(*this); return temp; }
        // Prefix - Trec la nodul anterior
        Iterator& operator--() { list = list->previous; return *this; }  
        // Postfix
        Iterator operator--(int) { Iterator temp = *this; --(*this); return temp; }
        bool operator==(const Iterator& it) const { if(it.list == this->list) return true; else return false; }
        bool operator!=(const Iterator& it) const { if(!(it == *this)) return true; else return false; }
    private:
        Nod * list;
    };
};
// Definirea constantei END
const ListaD::Iterator ListaD::END = nullptr;

void ListaD::push_front(int elem)
{
    // Daca lista este vida, atunci 
    if(isEmpty()) 
    {
        head = new Nod; // Aloc memorie pentru primul nod
        head->data = elem;
        head->next = nullptr; // Fiind singurul nod, urmatorul este NIMIC adica NULL
        head->previous = nullptr; // Fiind singurul nod, anteriorul este NIMIC adica NULL
        tail = head; // si tail == head
    }
    else  // altfel
    {
        Nod * nod = new Nod; // Aloc memorie pentru noul nod
        nod->data = elem;    // Scriu informatia in data
        nod->previous = nullptr; // Devenind head, previous indica NULL
        nod->next = head;    // Leg nod de head
        head->previous = nod; // Predecesorul fostului head este 'nod'
        head = nod;          // nod devine noul head
    }
}

void ListaD::push_back(int elem)
{
    // Daca lista este vida, atunci 
    if(isEmpty()) 
    {
        head = new Nod; // Aloc memorie pentru primul nod
        head->data = elem;
        head->next = nullptr; // Fiind singurul nod, urmatorul este NIMIC adica NULL
        head->previous = nullptr; // Fiind singurul nod, anteriorul este NIMIC adica NULL
        tail = head; // si tail == head
    }
    else  // altfel
    {
        Nod * nod = new Nod; // Aloc memorie pentru noul nod
        nod->data = elem;    // Scriu informatia in data
        nod->next = nullptr; // Devenind nod terminal, va fi legat de NULL
        nod->previous = tail; // previous indica tail
        tail->next = nod;    // Fostul tail este legat de noul tail
        tail = nod;          // nod devine tail
    }
}

void ListaD::insert_after(int elem, Iterator nod)
{
    Nod * newNod = new Nod; // Aloc memorie pentru noul nod
    newNod->data = elem;    // Scriu informatia in data
    newNod->next = nod.list->next; // newNod se leaga de succesorul lui 'nod'
    newNod->previous = nod.list; // Predecesorul lui newNod este 'nod'
    // Daca nodul 'nod' a fost ultimul nod, atunci nodul newNod devine nod terminal
    if(nod.list == tail) tail = newNod;
    else nod.list->next->previous = newNod; // Predecesorul succesorului lui 'nod' este newNod
    nod.list->next = newNod; // Nodul 'nod' se leaga de newNod
}

void ListaD::insert_before(int elem, Iterator nod)
{
    Nod * newNod = new Nod; // Aloc memorie pentru noul nod
    newNod->data = elem; // Scriu informatia in data
    newNod->next = nod.list; // Succesorul lui newNod este 'nod'
    newNod->previous = nod.list->previous; // Predecesorul lui newNod este predecesorul lui 'nod'
    // Daca nodul 'nod' a fost primul nod, atunci nodul newNod devine head
    if(nod.list == head) head = newNod;
    else nod.list->previous->next = newNod; // Succesorul predecesorului lui 'nod' este newNod
    nod.list->previous = newNod; // Predecesorul lui 'nod' este newNod
}

ListaD::Iterator ListaD::search(int value) const
{
    for(Nod* it = head; it != nullptr; it = it->next)
    {
        if(it->data == value) return Iterator(it); // Daca am gasit nodul il returnez
    }
    return END; // Nu am gasit nimic
}

void ListaD::pop_front()
{
    if(isEmpty()) throw "Empty List"; // Daca lista este vida
    if(head == tail) // Daca lista are un singur nod
    { delete head; head = tail = nullptr; return; } 
    Nod * temp = head; // Salvez adresa obiectului head
    head->next->previous = nullptr; // Predecesorul succesorului lui head devine NULL
    head = head->next; // Succesorul lui head devine noul head
    delete temp; // Eliberez memoria ocupata de vechiul obiect head
}

void ListaD::remove(Iterator nod)
{
    if(isEmpty()) throw "Empty List"; // Daca lista este vida
    if(head == tail) // Daca lista are un singur nod
    { delete head; head = tail = nullptr; return; } 
    nod.list->previous->next = nod.list->next; // Leg predecesorul lui 'nod' de succesorul acestuia
    nod.list->next->previous = nod.list->previous; // Predecesorul succesorului lui 'nod' indica predecesorul lui 'nod'
    delete nod.list; // Elimin nodul 'nod' 
}

void ListaD::pop_back()
{
    if(isEmpty()) throw "Empty List"; // Daca lista este vida
    if(head == tail) // Daca lista are un singur nod
    { delete head; head = tail = nullptr; return; }
    Nod * temp = tail; // Salvez adresa obiectului tail
    tail->previous->next = nullptr; // Succesorul predecesorului lui tail devine NULL
    tail = tail->previous; // Predecesorul lui tail devine noul tail
    delete temp; // Eliberez memoria ocupata de vechiul obiect tail
}

void ListaD::clear()
{
    Nod *it = head, *temp; 
    while(it != nullptr)
    {
        temp = it; // Salvez adresa nodului curent
        it = it->next; // Trec mai departe
        delete temp; // Distrug nodul curent
    }
    head = tail = nullptr; // Lista Vida
}
Iată un exemplu cu lista dublă înlănţuită:
#include <iostream>
#include "ListaD.h"
using namespace std;

int main()
{
    ListaD myList; // Creez o lista
    myList.push_back(8);
    myList.push_front(5);
    myList.push_back(11);

    for(ListaD::Iterator it = myList.front(); it != ListaD::END; it++)
        cout << *it << ' ';
    cout << '\n';

    ListaD::Iterator p = myList.search(8);
    myList.insert_after(16, p);
    myList.insert_before(22, p);

    // Afisez invers
    for(ListaD::Iterator it = myList.back(); it != ListaD::END; it--)
        cout << *it << ' ';
    cout << '\n';

    p = myList.search(8);
    myList.remove(p);
    myList.pop_back();
    myList.pop_front();

    // Afisez invers
    for(ListaD::Iterator it = myList.back(); it != ListaD::END; it--)
        cout << *it << ' ';
    cout << '\n';

    // Afisez normal
    for(ListaD::Iterator it = myList.front(); it != ListaD::END; it++)
        cout << *it << ' ';
    cout << '\n';

    system("PAUSE");
    return 0;
}
Output:
5 8 11
11 16 8 22 5
16 22
22 16
Dacă aveţi probleme cu nullptr, atunci folosiţi NULL. De asemenea vă recomand să folosiţi Visual Studio.

sâmbătă, 21 ianuarie 2012

Lista Liniară Simplu Înlănţuită

Vectorii (tablourile de memorie) sunt structuri simple şi eficiente de memorare a obiectelor într-o anumită ordine, dar au şi dezavantaje. Nu sunt foarte adaptabili, de exemplu, inserarea şi ştergerea elementelor sunt dificile deoarece elementele trebuie să fie deplasate pentru a face loc inserţiei sau pentru a umple poziţiile rămase libere după ştergere. De aceea avem nevoie de o alternativă la vectori care să implementeze o secvenţă de elemente. În acest tutorial voi prezenta lista liniară simplu înlănţuită.
O listă simplu înlănţuită este o colecţie de noduri aşezate într-o ordine liniară (dar NU în locaţii succesive din memorie - aşa cum se întâmplă la vectori). Fiecare nod are cel puţin un pointer către următorul nod, pe care îl voi numi next (în imagine este urm). Primul nod se numeşte head (sau prim), iar ultimul nod se numeşte tail (sau ultim), dar bineînţeles că nu există o regulă strictă cu privire la aceste nume (puteţi să le numiţi cum vreţi). Lista se numeşte simplu înlănţuită deoarece fiecare nod memorează un singur pointer către următorul nod. Spre deosebire de vectori, lista nu are o mărime fixă. Poate fi redimensionată prin adăugarea sau ştergerea nodurilor. Gândiţi-vă la un nod ca la un obiect (item) care conţine atât informaţia pe care vreţi s-o stocaţi cât şi legătura către următorul obiect. Pentru a semnala sfârşitul listei folosim NULL. Pointerului next al ultimului nod (nodul terminal) îi atribuim adresa NULL (Pentru cei care folosiţi compilatoare noi, cum ar fi cel din Visual Studio, folosiţi nullptr în loc de NULL, deoarece NULL este doar un simplu macro, este 0, este un întreg şi nu reprezintă concret o adresă inexistentă). Mai jos este o imagine ce ilustrează conceptul de listă simplu înlănţuită (LSÎ):

În tutorialele ce urmează voi folosi programarea obiect-orientată, deci ar fi bine să vă familiarizaţi cu această paradigmă. Totuşi, dacă sunteţi aici doar pentru algoritmii listei, atunci nu este necesar să cunoaşteţi POO (deşi ar fi bine). De asemenea, folosesc Visual Studio 12.

Implementarea Listei Simplu Înlănţuite

Voi defini o clasă numită Lista, ce implementează LSÎ. Fiecare nod va memora două valori, membrul data care stochează informaţia propriu-zisă (în acest caz va fi un întreg) şi membrul next care este un pointer către următorul nod al listei (legătura).
class Lista
{
public:
    struct Iterator; // Declaratie forward
    Lista();   // Constructor - Initializarea listei
    ~Lista();  // Destructor - Distrugerea listei
    void push_front(int elem); // Inserare in fata primului nod
    void push_back(int elem);  // Inserare dupa ultimul nod
    void insert_before(int elem, Iterator nod); // Inserare inainte de nod
    void insert_after(int elem, Iterator nod);  // Inserare dupa nod
    Iterator search(int value) const; // Lista contine value?
    void pop_front(); // Elimina nodul din fata
    void pop_back();  // Elimina ultimul nod
    void remove(Iterator nod); // Elimina nodul nod
    Iterator front() const; // Returneaza un iterator catre inceputul listei
    Iterator end() const; // Returneaza un iterator catre sfarsitul listei
    bool isEmpty() const; // Este lista vida?
    void clear(); // Stergerea completa a listei

private:
    struct Nod  // Clasa Helper; Implementeaza un nod de lista
    {
        int data;   // informatia propriu-zisa
        Nod * next; // urm
    };
    Nod * head; // prim
    Nod * tail; // ultim
public:
    struct Iterator // Un pointer inteligent
    {
        friend class Lista; // Lista are acces la membrii privati ai lui Iterator
        Iterator() { list = nullptr; }
        Iterator(Nod * ls) { list = ls; }
        // Supraincarc operatorul * (dereferentiere)
        int& operator*() { if(list != nullptr) return list->data; else throw "Null iterator!"; }
        // Prefix - Trec la urmatorul nod
        Iterator& operator++() { list = list->next; return *this; }  
        // Postfix
        Iterator operator++(int) { Iterator temp = *this; ++(*this); return temp; }
        bool operator==(const Iterator& it) const { if(it.list == this->list) return true; else return false; }
        bool operator!=(const Iterator& it) const { if(!(it == *this)) return true; else return false; }
    private:
        Nod * list;
    };
};
Clasa Nod şi cei doi pointeri sunt privaţi deoarece nu vreau ca utilizatorul clasei să aibă acces direct la ei; nu vreau să fie alteraţi de codul client. De asemenea declar clasa Iterator, pentru a putea parcurge lista (Iterator este membru static public al clasei Lista). De ce fac asta? Deoarece Nod este privat, nu pot folosi un pointer simplu către Lista deoarece ar trebui ca Nod să fie public în acel caz, ori eu nu vreau asta, vreau să respect principiul încapsulării şi să protejez datele interne ale clasei. Dacă Nod ar fi public există riscul ca cel care foloseşte clasa mea să modifice (poate face asta fără să vrea, din greşeală, sau intenţionat) adresa către care pointează next (membrul lui Nod), iar în acest caz lista nu ar mai fi listă, deoarece ar exista o ruptură. Aceeaşi explicaţie pentru head şi tail (aceste detalii ţin de POO, nicidecum de LSÎ, deci pot fi ignorate). Din această cauză implementez clasa Iterator. Dacă tot nu înţelegeţi de ce am declarat această clasă, mai aşteptaţi puţin şi veţi înţelege. Ok, acum să trecem la lista simplu înlănţuită.

Lista vidă

Dacă lista este vidă înseamnă că atât head cât şi tail au valoarea NULL (nullptr). Deci trebuie să testez dacă head este NULL. Funcţia isEmpty() returnează true dacă lista este vidă şi false dacă nu este.
bool Lista::isEmpty() const
{
 // Se evalueaza expresia si se returneaza rezultatul
 return head == nullptr;
}

Iniţializarea listei

Voi iniţializa lista în constructorul clasei Lista. Iniţializarea presupune crearea listei vide; head şi tail au valoarea NULL.
Lista::Lista()
{
 // Se creeaza lista vida
 head = tail = nullptr;
}

Adăugarea unui nod la listă

Avem 3 cazuri:
  • Inserarea în faţa primului nod;
  • Inserarea după ultimul nod;
  • Inserarea în interiorul listei.
Inserarea în faţa primului nod [push_front()]. Aici avem două cazuri: dacă lista este vidă atunci adăugăm atât la head cât şi la tail primul nod (iar pointerul next va fi NULL deoarece este singurul nod din listă), altfel adăugăm nodul în faţa listei. Aloc memorie pentru noul nod (să zicem p) -> Scriu informaţia în p -> Leg nodul p de nodul head -> p devine noul head (adică head indică adresa lui p).
void Lista::push_front(int elem)
{
    // Daca lista este vida, atunci 
    if(isEmpty()) 
    {
        head = new Nod; // Aloc memorie pentru primul nod
        head->data = elem;
        head->next = nullptr; // Fiind singurul nod, urmatorul este NIMIC adica NULL
        tail = head; // si tail == head
    }
    else  // altfel
    {
        Nod * nod = new Nod; // Aloc memorie pentru noul nod
        nod->data = elem;    // Scriu informatia in data
        nod->next = head;    // Leg nod de head
        head = nod;          // nod devine noul head
    }
}

Inserarea după ultimul nod [push_back()]. Şi aici avem cele două cazuri. Dacă lista nu este vidă, adăugăm nodul la sfârşitul listei. Se alocă memorie pentru noul nod p -> Scriu informaţia în nod -> Leg ultimul nod de nodul p -> p devine noul tail (adică p se leagă de NULL şi tail indică acum nodul p).

void Lista::push_back(int elem)
{
    // Daca lista este vida, atunci 
    if(isEmpty()) 
    {
        head = new Nod; // Aloc memorie pentru primul nod
        head->data = elem;
        head->next = nullptr; // Fiind singurul nod, urmatorul este NIMIC adica NULL
        tail = head; // si tail == head
    }
    else  // altfel
    {
        Nod * nod = new Nod; // Aloc memorie pentru noul nod
        nod->data = elem;    // Scriu informatia in data
        nod->next = nullptr; // Devenind nod terminal, va fi legat de NULL
        tail->next = nod;    // Fostul tail este legat de noul tail
        tail = nod;          // nod devine tail
    }
}

Inserarea în interiorul listei.
Inserarea după nodul q (unde q este un nod al listei pe care îl obţineţi iterând prin listă). Se alocă memorie pentru noul nod (newNod) -> Leg nodul newNod de succesorul nodului q -> Nodul q se leagă de nodul newNod. Dacă q a fost nod terminal, atunci newNod este acum nod terminal.

void Lista::insert_after(int elem, Iterator nod)
{
    Nod * newNod = new Nod; // Aloc memorie pentru noul nod
    newNod->data = elem;    // Scriu informatia in data
    newNod->next = nod.list->next; // newNod se leaga de succesorul lui 'nod'
    nod.list->next = newNod; // Nodul 'nod' se leaga de newNod
    // Daca nodul 'nod' a fost ultimul nod, atunci nodul newNod devine nod terminal
    if(newNod->next == nullptr) tail = newNod;
}

Chiar dacă am spus nodul nod, nod nu este de fapt Nod, este un obiect Iterator. Prin nodul nod mă refer la nodul încapsulat în obiectul Iterator nod (adică membrul list).

Inserarea înainte de nodul q. Pentru a adăuga nodul newNod înainte de nodul p, ar trebui să leg predecesorul nodului q de newNod, dar eu nu cunosc acest predecesor. În acest caz voi insera nodul newNod după nodul p şi voi interschimba informaţia din cele două noduri dând impresia unei inserări înainte. Algoritmul este acelaşi, dar cu mici modificări.

void Lista::insert_before(int elem, Iterator nod)
{
    Nod * newNod = new Nod; // Aloc memorie pentru noul nod
    // --- Interschimb informatia --- //
    newNod->data = nod.list->data; // Copiez informatia din 'nod' in newNod
    nod.list->data = elem; // 'nod' va memora informatia care trebuie adaugata la lista
    // ------------------------------ //
    newNod->next = nod.list->next; // newNod se leaga de succesorul lui 'nod'
    nod.list->next = newNod; // Nodul 'nod' se leaga de newNod
    // Daca nodul 'nod' a fost ultimul nod, atunci nodul newNod devine nod terminal
    if(nod.list == tail) tail = newNod;
}
Inserţia (cei patru algoritmi) în listă se realizează în timp constant \(O(1)\).

Parcurgerea listei

Lista se parcurge folosind un Iterator. Algoritmul de parcurgere l-am implementat deja în funcţia operator++() (ambele) a clasei Iterator. Ideea este următoarea: Se obţine un pointer (în cazul meu un Iterator) către primul nod (head), apoi se parcurge lista actualizând pointerul (iteratorul) curent cu următorul element (nod = nod->next) până când pointerul curent indică NULL (în cazul meu iteratorul ajunge la end()), adică pointerul curent indică nodul imediat următor după tail.
Iterator& operator++() { list = list->next; return *this; }
Împreună cu funcţiile front() şi end() putem parcurge lista. Cele două funcţii sunt definite în felul următor:
Lista::Iterator Lista::front() const { return Iterator(head); }
Lista::Iterator Lista::end() const { return Iterator(nullptr); }
Observaţi că funcţia end() returnează un Iterator ce încapsulează un Nod NULL. Acesta este nodul imediat următor după nodul tail al listei. Deci de fiecare dată când prelucrez lista, voi prelucra informaţiile din intervalul \([front, end)\). Iată algoritmul general de parcurgere a listei:
Lista myList;  // declar o lista
for(Lista::Iterator iter = myList.front(); iter != myList.end(); iter++)
{
 // Se prelucreaza (*iter)
}

Căutarea unui nod în listă

Căutarea este destul de simplă. Pur şi simplu parcurgem lista şi verificăm dacă valoarea care ne interesează se află în nodul curent. Dacă o găsim, returnăm un iterator către acest nod. Căutarea o voi implementa în funcţia search().
Lista::Iterator Lista::search(int value) const
{
    for(Nod* it = head; it != nullptr; it = it->next)
    {
        if(it->data == value) return Iterator(it); // Daca am gasit nodul il returnez
    }
    return end(); // Nu am gasit nimic
}

Algoritmul de căutare este liniar $O(n)$

Eliminarea unui nod din listă

Avem 3 cazuri:
  • Eliminarea primului nod;
  • Eliminarea ultimuluiu nod;
  • Eliminarea unui nod din interiorul listei.

Eliminarea primului nod [pop_front()]. Sunt câteva cazuri care trebuie luate în considerare: lista este vidă sau lista are un singur nod.

Oricum ar fi, algoritmul general este următorul: Salvez nodul head într-un nod oarecare temp -> Succesorul nodului head devine nod head -> Se eliberează memoria ocupată de obiectul referit de nodul temp (fostul head).

void Lista::pop_front()
{
    if(isEmpty()) throw "Empty List"; // Daca lista este vida
    if(head == tail) // Daca lista are un singur nod
    { delete head; head = tail = nullptr; return; } 
    Nod * temp = head; // Salvez adresa obiectului head
    head = head->next; // Succesorul lui head devine noul head
    delete temp; // Eliberez memoria ocupata de vechiul obiect head
}
Algoritmul de eliminare a primului nod este constant $O(1)$.

Eliminarea ultimului nod [pop_back()]

Caut predecesorul nodului tail prin parcurgerea listei -> Predecesorul nodului tail devine nod terminal -> Eliberez zona de memorie ocupată de fostul tail -> Predecesorul devine nod tail.
void Lista::pop_back()
{
    if(isEmpty()) throw "Empty List"; // Daca lista este vida
    if(head == tail) // Daca lista are un singur nod
    { delete head; head = tail = nullptr; return; }
    // Caut predecesorul lui tail
    Nod * pred;
    for(pred = head; pred->next->next != nullptr; pred = pred->next); // For Vid
    pred->next = nullptr; // Predecesorul devine nod terminal
    delete tail; // Eliberez memoria ocupata de vechiul obiect tail
    tail = pred; // Predecesorul devine tail
}
Algoritmul de eliminare a ultimului nod este liniar $O(n)$.

Eliminarea unui nod din interiorul listei

Deoarece nu cunosc predecesorul unui nod, eliminarea unui nod înseamnă de fapt eliminarea informaţiei acelui nod. Aşadar, din listă nu va fi eliminat nodul nod ci succesorul său (care este cunoscut), asta după ce informaţia din succesor este salvată în nod. Dacă succesorul nodului nod era nodul tail, atunci nodul nod devine nod tail. Algoritmul este implementat în funcţia remove() care ia ca parametru un obiect de tip Iterator.
void Lista::remove(Iterator nod)
{
    if(isEmpty()) throw "Empty List"; // Daca lista este vida
    if(head == tail) // Daca lista are un singur nod
    { delete head; head = tail = nullptr; return; } 
    Nod * temp = nod.list->next; // Salvez adresa succesorului lui 'nod'
    // Copiez toata informatia succesorului in 'nod'
    nod.list->data = nod.list->next->data;
    nod.list->next = nod.list->next->next;
    delete temp; // Eliberez memoria ocupata de succesor; il elimin
    if(nod.list->next == nullptr) tail = nod.list;
}
Algoritmul de eliminare al unui nod din interiorul listei este constant \(O(1)\).

Ştergerea listei

Algoritmul pentru eliberarea spaţiului de memorie ocupat de listă (ştergerea listei) este următorul:
void Lista::clear()
{
    Nod *it = head, *temp; 
    while(it != nullptr)
    {
        temp = it; // Salvez adresa nodului curent
        it = it->next; // Trec mai departe
        delete temp; // Distrug nodul curent
    }
    head = tail = nullptr; // Lista Vida
}
Algoritmul de ştergere a listei este liniar \(O(n)\). Şi să nu uităm de destructorul clasei:
Lista::~Lista()
{ if(!isEmpty()) clear(); }
În continuare vă propun să creaţi un nou fişier Liste.h (header) şi să-l adăugaţi la proiectul vostru, după care în fişierul main (unde aveţi funcţia main) să adăugaţi, la început, directiva: #include "Liste.h". Adăugaţi următorul cod în Liste.h:
class Lista
{
public:
    struct Iterator; // Declaratie forward
    // Constructor - Initializarea listei
    Lista() { head = tail = nullptr; /*Se creeaza lista vida*/ }
    // Destructor - Distrugerea listei
    ~Lista() { if(!isEmpty()) clear(); }  
    void push_front(int elem); // Inserare in fata primului nod
    void push_back(int elem);  // Inserare dupa ultimul nod
    void insert_before(int elem, Iterator nod); // Inserare inainte de nod
    void insert_after(int elem, Iterator nod);  // Inserare dupa nod
    Iterator search(int value) const; // Cauta value in lista
    void pop_front(); // Elimina nodul din fata
    void pop_back();  // Elimina ultimul nod
    void remove(Iterator nod); // Elimina nodul nod
    // Returneaza un iterator catre inceputul listei
    Iterator front() const { return Iterator(head); } 
    // Returneaza un iterator catre sfarsitul listei
    Iterator end() const { return Iterator(nullptr); }
    bool isEmpty() const { return head == nullptr; } // Este lista vida?
    void clear(); // Stergerea completa a listei

private:
    struct Nod  // Clasa Helper; Implementeaza un nod de lista
    {
        int data;   // informatia propriu-zisa
        Nod * next; // urm
    };
    Nod * head; // prim
    Nod * tail; // ultim
public:
    struct Iterator // Un pointer inteligent
    {
        friend class Lista; // Lista are acces la membrii privati ai lui Iterator
        Iterator() { list = nullptr; }
        Iterator(Nod * ls) { list = ls; }
        // Supraincarc operatorul * (dereferentiere)
        int& operator*() { if(list != nullptr) return list->data; else throw "Null iterator!"; }
        // Prefix - Trec la urmatorul nod
        Iterator& operator++() { list = list->next; return *this; }  
        // Postfix
        Iterator operator++(int) { Iterator temp = *this; ++(*this); return temp; }
        bool operator==(const Iterator& it) const { if(it.list == this->list) return true; else return false; }
        bool operator!=(const Iterator& it) const { if(!(it == *this)) return true; else return false; }
    private:
        Nod * list;
    };
};

void Lista::push_front(int elem)
{
    // Daca lista este vida, atunci 
    if(isEmpty()) 
    {
        head = new Nod; // Aloc memorie pentru primul nod
        head->data = elem;
        head->next = nullptr; // Fiind singurul nod, urmatorul este NIMIC adica NULL
        tail = head; // si tail == head
    }
    else  // altfel
    {
        Nod * nod = new Nod; // Aloc memorie pentru noul nod
        nod->data = elem;    // Scriu informatia in data
        nod->next = head;    // Leg nod de head
        head = nod;          // nod devine noul head
    }
}

void Lista::push_back(int elem)
{
    // Daca lista este vida, atunci 
    if(isEmpty()) 
    {
        head = new Nod; // Aloc memorie pentru primul nod
        head->data = elem;
        head->next = nullptr; // Fiind singurul nod, urmatorul este NIMIC adica NULL
        tail = head; // si tail == head
    }
    else  // altfel
    {
        Nod * nod = new Nod; // Aloc memorie pentru noul nod
        nod->data = elem;    // Scriu informatia in data
        nod->next = nullptr; // Devenind nod terminal, va fi legat de NULL
        tail->next = nod;    // Fostul tail este legat de noul tail
        tail = nod;          // nod devine tail
    }
}

void Lista::insert_after(int elem, Iterator nod)
{
    Nod * newNod = new Nod; // Aloc memorie pentru noul nod
    newNod->data = elem;    // Scriu informatia in data
    newNod->next = nod.list->next; // newNod se leaga de succesorul lui 'nod'
    nod.list->next = newNod; // Nodul 'nod' se leaga de newNod
    // Daca nodul 'nod' a fost ultimul nod, atunci nodul newNod devine nod terminal
    if(nod.list == tail) tail = newNod;
}

void Lista::insert_before(int elem, Iterator nod)
{
    Nod * newNod = new Nod; // Aloc memorie pentru noul nod
    // --- Interschimb informatia --- //
    newNod->data = nod.list->data; // Copiez informatia din 'nod' in newNod
    nod.list->data = elem; // 'nod' va memora informatia care trebuie adaugata la lista
    // ------------------------------ //
    newNod->next = nod.list->next; // newNod se leaga de succesorul lui 'nod'
    nod.list->next = newNod; // Nodul 'nod' se leaga de newNod
    // Daca nodul 'nod' a fost ultimul nod, atunci nodul newNod devine nod terminal
    if(nod.list == tail) tail = newNod;
}

Lista::Iterator Lista::search(int value) const
{
    for(Nod* it = head; it != nullptr; it = it->next)
    {
        if(it->data == value) return Iterator(it); // Daca am gasit nodul il returnez
    }
    return end(); // Nu am gasit nimic
}

void Lista::pop_front()
{
    if(isEmpty()) throw "Empty List"; // Daca lista este vida
    if(head == tail) // Daca lista are un singur nod
    { delete head; head = tail = nullptr; return; } 
    Nod * temp = head; // Salvez adresa obiectului head
    head = head->next; // Succesorul lui head devine noul head
    delete temp; // Eliberez memoria ocupata de vechiul obiect head
}

void Lista::remove(Iterator nod)
{
    if(isEmpty()) throw "Empty List"; // Daca lista este vida
    if(head == tail) // Daca lista are un singur nod
    { delete head; head = tail = nullptr; return; } 
    Nod * temp = nod.list->next; // Salvez adresa succesorului lui 'nod'
    // Copiez toata informatia succesorului in 'nod'
    nod.list->data = nod.list->next->data;
    nod.list->next = nod.list->next->next;
    delete temp; // Eliberez memoria ocupata de succesor; il elimin
    if(nod.list->next == nullptr) tail = nod.list;
}

void Lista::pop_back()
{
    if(isEmpty()) throw "Empty List"; // Daca lista este vida
    if(head == tail) // Daca lista are un singur nod
    { delete head; head = tail = nullptr; return; }
    // Caut predecesorul lui tail
    Nod * pred;
    for(pred = head; pred->next->next != nullptr; pred = pred->next); // For Vid
    pred->next = nullptr; // Predecesorul devine nod terminal
    delete tail; // Eliberez memoria ocupata de vechiul obiect tail
    tail = pred; // Predecesorul devine tail
}

void Lista::clear()
{
    Nod *it = head, *temp; 
    while(it != nullptr)
    {
        temp = it; // Salvez adresa nodului curent
        it = it->next; // Trec mai departe
        delete temp; // Distrug nodul curent
    }
    head = tail = nullptr; // Lista Vida
}
În final, iată un exemplu cu liste:
#include <iostream>
#include "Liste.h"
using namespace std;

int main()
{
    Lista myList;  // Declar o lista
    myList.push_back(4);
    myList.push_back(3);
    myList.push_front(7);
 
    for(Lista::Iterator it = myList.front(); it != myList.end(); it++)
        cout << *it << ' ';
    cout << '\n';

    Lista::Iterator p = myList.search(4);
    myList.insert_after(5, p);
    myList.insert_before(2, p);

    for(Lista::Iterator it = myList.front(); it != myList.end(); it++)
        cout << *it << ' ';
    cout << '\n';

    myList.pop_back();
    myList.pop_front();
    p = myList.search(4);
    myList.remove(p);

    for(Lista::Iterator it = myList.front(); it != myList.end(); it++)
        cout << *it << ' ';
    cout << '\n';

    myList.clear();
    if(myList.isEmpty()) cout << "Lista vida!\n";

    system("PAUSE");
    return 0;
}
Output:
7 4 3
7 2 4 5 3
2 5

Lista vida!
Dacă aveţi probleme cu nullptr, atunci folosiţi NULL. De asemenea vă recomand să folosiţi Visual Studio.

duminică, 15 ianuarie 2012

Algoritmi: Algoritmul lui Euclid (CMMDC)

Algoritmul lui Euclid reprezintă o metodă eficientă de calculare a celui mai mare divizor comun (CMMDC) a două numere întregi. Algoritmul bazat pe împărţire (implementat în C++):
int cmmdc(int a, int b)
{
    int t;
    while (b != 0)
    {
        t = b;
        b = a % b;
        a = t;
    }
    return a;
}
Funcţia de mai sus returnează CMMDC al numerelor a şi b. Există şi versiunea bazată pe scăderi repetate:
int cmmdc(int a, int b)
{
    if(a == 0)
        return b;
    while(b != 0)
    {
        if(a > b) a -= b;
        else b -= a;
    }
    return a;
}
Bineînţeles că poate fi implementat şi recursiv:
int cmmdc(int a, int b)
{
    if(b == 0)
        return a;
    else return cmmdc(b, a % b);
}
Pentru a calcula cel mai mic multiplu comun, înmulţiţi numerele a şi b, şi împărţiţi prin cmmdc. Exemplu:
12 * 6 / cmmdc(12, 6);

Algoritmul lui Euclid extins

Algoritmul lui Euclid extins găseşte, pe lângă cmmmd al numerelor a şi b, întregii x şi y care satisfac identitatea lui Bézout:

ax+by = cmmdc(a,b)

Implementarea algoritmului în C++:
int euclidExtins(int a, int b, int& lastX, int& lastY)
{
    int x = 0, y = 1, q; lastX = 1; lastY = 0;
    // variabile temporare
    int tA, tX, tY;
    while(b != 0)
    {
        q = a / b; // aveti grija ca a > b
        tA = b; b = a % b; a = tA;
  
        tX = x; x = lastX - q * x; lastX = tX;
        tY = y; y = lastY - q * y; lastY = tY;
    }
    return a;
}
Algoritmul returnează cmmdc(a, b), iar prin referinţele lastX şi lastY returnează cei doi întregi x şi y. Exemplu:
int main()
{
    int x, y;

    cout << euclidExtins(27, 6, x, y) << '\n'; // 3
    cout << x << ' ' << y; // 1 -4

    system("PAUSE");
    return 0;
}
Explicaţia o găsiţi aici: http://en.wikipedia.org/wiki/Extended_Euclidean_algorithm sau luaţi pixul şi hârtia şi executaţi algoritmul pas cu pas (recomand să faceţi asta, veţi înţelege mai bine).

vineri, 6 ianuarie 2012

Algoritmi: Generarea Numerelor Prime

Metoda Clasică

Metoda clasică de generare a numerelor prime până la o anumită limită presupune iterarea până la acea limită şi verificarea primalităţii fiecărui număr. Dacă este prim atunci va fi afişat pe ecran. Algoritmul în C++:
bool IsPrime(int n)
{
    for(int i = 2; i <= sqrt(n); i++)
        // Daca i divide n, atunci n nu este prim
        if(n % i == 0) return false;
    // Daca am ajuns aici inseamna ca n este prim
    return true;
}

void MetodaClasica(int lim)
{
    cout << 2 << ' ' << 3 << ' ';
    for(int i = 5; i <= lim; i+=2)
        if(IsPrime(i)) cout << i << ' ';
    cout << '\n';
}
Observaţi că sar peste numerele pare (i+=2) deoarece singurul număr par prim este 2. Acest algoritm generează toate numerele prime până la lim. Totuşi există metode mai bune de generare a numerelor prime, iar una din ele este Ciurul lui Eratostene.

Ciurul lui Eratostene

Ciurul lui Eratostene (Sieve of Eratosthenes) este un algoritm de găsire a tuturor numerelor prime până la un număr dat. De data aceasta voi număra numerele prime. Paşii algoritmului:
  1. Se iterează de la 2 până la sqrt(lim).
  2. Fie p, primul număr prim găsit.
  3. Toţi multiplii numărului p*p se marchează ca fiind compuşi (adică ne-primi).
  4. În final se numără sau se afişează toate numerele prime din intervalul [sqrt(lim)+1, lim].
Algoritmul în C++:
void Eratostene(int lim) 
{
    int sqr = (int)sqrt(lim); 
    // Variabila in care numar numerele prime
    int nr = 0; 
    // Marcarea numerelor prime se face in acest vector
    bool * isComposite = new bool[lim+1]; 
    // Initial toate sunt prime
    memset(isComposite, 0, sizeof(bool)*(lim+1));
    for(int p = 2; p <= sqr; p++)
    {
        // Daca nu este compus, adica este prim
        if(!isComposite[p])
        {
            ++nr;
            // Marcheaza toti multiplii p*p ca fiind compusi
            for(int k = p*p; k <= lim; k+=p)
            isComposite[k] = true;
        }
    }
    // Numara numerele prime ramase
    for(int i = sqr+1; i <= lim; i++)
        if(!isComposite[i]) ++nr;
    cout << nr << '\n';
    delete[] isComposite;
}
Pentru mai multe detalii intraţi pe Wikipedia.

Ciurul lui Atkin

Ciurul lui Atkin (Sieve of Atkin) este un algoritm rapid şi modern de găsire a tuturor numerelor prime până la un număr întreg. A fost creat de A.O.L. Atkin şi Daniel J. Bernstein. Vă ofer doar implementarea algoritmului în C++, nu şi explicaţia:
void Atkin(int lim)
{
    double sqr = sqrt(lim); int n, nr = 2;
    bool * isPrime = new bool[lim+1];
    // Initial toate numerele sunt neprime
    memset(isPrime, 0, sizeof(bool)*(lim+1));
    for(int x = 1; x <= sqr; x++)
    {
        for(int y = 1; y <= sqr; y++)
        {
            n = 4 * x * x + y * y;
            if(n <= lim && (n % 12 == 1 || n % 12 == 5))
                isPrime[n] = !isPrime[n];
            n = 3 * x * x + y * y;
            if(n <= lim && n % 12 == 7)
                isPrime[n] = !isPrime[n];
            n = 3 * x * x - y * y;
            if(x > y && n <= lim && n % 12 == 11)
                isPrime[n] = !isPrime[n];
        }
    }
    for(int i = 5; i <= sqr; i+=2) 
    {
        // Daca este prim, marcheaza toti multiplii i*i ca fiind neprimi
        if(isPrime[i])
        {
            for(int k = i*i; k <= lim; k+=i*i)
            isPrime[k] = false;
        }
    }
    // Numara numerele prime
    for(int i = 5; i <= lim; i+=2)
        if(isPrime[i]) ++nr;
    cout << nr << '\n';
    delete[] isPrime;
}
Descrierea completă a algoritmului o găsiţi aici.