Programele pentru calculatoare utilizeaza de obicei tabele de informatii care presupun anumite relatii structurale intre datele care le compun. Aceste relatii structurale diferentiaza mai multe modele de organizare a datelor, dintre care cele mai importante sunt: liste, arbori, multimi. Toate aceste modele se reprezinta in programare prin colectii de date care, in programarea orientata pe obiecte, se implementeaza prin clase (tipuri definite de utilizator).
Fiecare programator isi poate defini propriile implementari ale modelelor de date, sau poate sa foloseasca unele tipuri definite in bibliotecile sistemului, pe care le modifica in mod corespunzator cerintelor aplicatiei. In ambele situatii, insa, este necesara cunoasterea modului de organizare interna a unor astfel de modele de date, precum si posibilitatile de reprezentare si de exploatare a acestora prin intermediul claselor de colectii, a claselor container si a claselor template. Reprezentarea colectiilor de date prin intermediul claselor derivate si al claselor template vor fi prezentate in sectiunile 5 si, respectiv, 7.
O lista liniara este o secventa finita de elemente de un tip dat. Secventa in care apar elementele este definitorie. De exemplu, lista (a1, a2, an este diferita de lista (a2, a1, an). De asemenea, in lista este posibil sa existe elemente cu acceasi valoare (elemente duplicat, ceea ce in multimi nu este admis). Lungimea listei este data de numarul de elemente in lista. Modelul de lista include si lista vida, care are zero elemente. Daca lista nu este vida, atunci ea contine un element de inceput, numit capul listei (head), iar restul elementelor se numeste coada listei (tail). Elementele listei pot fi identificate prin pozitia lor in lista, un index i, care poate avea valori £ i £ n , unde n este lungimea listei. De multe ori, (mai ales in implementarile in limbajul C), elementele listei se indexeaza de la la n-1, asa cum sunt indexate datele in tablourile din C.
O sublista este formata dintr-o parte a elementelor unei liste, cu indici cuprinsi intre i si j ai, ai aj) unde £ i £ j £ n.
Operatia de sortare intr-o lista presupune ca elementele listei apatin unei multimi in care se poate defini o relatie de ordine liniara. Fie o lista:
L = (a, c, b, f, e, d), unde a, b, I S
Multimea S careia in apartin elementele listei trebuie sa fie o multime ordonabila liniar. O multime de elemente S este ordonabila liniar daca si numai daca:
pentru fiecare doua elemente a si b ale multimii S este indeplinita una din conditiile: a < b a = b sau a > b
pentru oricare trei alemente a b c ale multimii S, daca a < b si b < c, atunci a < c
Simbolul " < " semnifica "precede". Un exemplu de multime care satisface conditiile de ordine liniara este multimea numerelor intregi. Un alt exemplu este multimea literelor alfabetului Latin. Intr-o secventa S = compusa din elemente care apartin unei multimi ordonabile liniar, rangul k al unui element si este definit ca numarul de elmente in S care preced elementul si plus 1.
Sortarea unei liste (secventa de elemente) inseamna efectuarea unei permutari intre elementele listei, astfel incat fiecare sa respecte regula de precdenta fata de elementul aflat inaintea lui in lista. Ca rezultat al unei operatii de sortare in lista L rezulta lista L'
L' = (a, b, c, d, e, f), unde a < b < c < d < e < f
O astfel de lista se numeste lista ordonata.
In programarea orientata pe obiecte elementele unei liste sunt de un tip dat, predefinit sau definit de utilizator (clasa). Pentru tipurile de date fundamentale ale limbajului C relatia de precedenta < " este definita printr-un operator de relatie. Pentru tipurile definite de utilizator, obiectele (instante ale unei clasei) dintr-o lista pot fi ordonate daca pentru clasa respectiva se defineste operatia de precedenta. In general, aceasta operatie se defineste prin supraincarcarea operatorul de relatie " <
Vectorul de elemente are dimensiune p locatii posibile (elemente) si o structura circulara. Doua variabile memoreaza pozitia (indicele) de scriere (writer) si pozitia de citire din vector (reader) (Fig. 3.3).
reader
Fig 3.3 Implementarea unei cozi printr-un vector parcurs circular
Initial coada este goala, numarul de elemente ocupate in vector (n) este egal cu zero, iar indicii de inserare si de extragere sunt de asemenea initializati cu valoarea 0. Un element se insereaza in vector in pozitia (indicele) writer, dupa care acesta este incrementat circular writer = (writer+1)%d; si de asemenea este incrementat numarul n de elemente ale listei. Daca lista nu este vida (daca n > 0) se poate extrage un element din vector de la adresa reader, dupa care acest indice este incrementat circular: reader = (reader+1)%d; iar numarul de elemente n este decrementat. Operatia de inserare trebuie sa fie permisa numai daca mai exista pozitii libere in vector (daca n < d). Operatia de extragere trebuie sa fie permisa numai daca exista cel putin un element in vector (n > 0
Se definesc doua clase pentru reprezentarea unei liste simplu inlantuite de numere intregi: o clasa IntSListNode pentru reprezentarea unui nod al listei si o clasa IntSList pentru reprezentarea listei. In clasa IntSListNode este memorat elementul listei (un numar intreg, variabila int v) si pointerul link la nodul urmator. In clasa IntSList este memorat pointerul la primul nod al listei (first), pointerul la ultimul nod al listei (last) si numarul de elemente ale listei (count . Memorarea ultimului nod al listei (last) si al numarului de elemente (count) nu sunt strict necesare, deoarece ele pot fi calculate de fiecare data pornind de la primul nod. Dar aceste operatii parcurg secvential intreaga lista, ceea ce reprezinta un consum de timp de care poate fi considerabil pentru listele de dimensiuni mari si de aceea memorarea acestor variabile in clasa IntSList maresc eficienta de executie a operatiilor cu listele simplu inlantuite.
Pentru fiecare clasa s-au definit constructori, destructor si functii membre care implementeaza operatii asupra listei. Clasa IntSList este declarata friend in clasa IntSListNode, pentru ca aceasta din urma sa poata accesa datele protejate ale clasei IntSList
class IntSListNode
~IntSListNode();
class IntSList
IntSList(int x);
IntSList(int* p, int n);
IntSList(const IntArray& array);
IntSList(IntSList& r)
int GetHead(); // citeste elem. din cap
int GetTail(); // citeste elem. din coada
void AddHead(int x); //adauga elem. in capul listei
void AddTail(int x); //adauga elem. in coada listei
int RemoveHead(); // extrage elem. din cap
int RemoveTail(); // extrage elem. din coada
int Lookup(int x) // cautare element x
void Display(); // afiseaza toate elem. listei
Constructorul de initializare al clasei IntSListNode construieste un nod al listei in care elementul primeste valoarea transmisa ca argument si pointerul de inlantuire nul (0).
Contructorul de inizializare cu un argument de tip intreg al clasei IntSList creaza o lista compusa dintr-un singur nod (element al listei) pe care il aloca dinamic in memoria heap folosind operatorul new
inline IntSList::IntSList(int x)
Constructorul de initializare cu doua argumente creaza o lista inlantuita in care elementele (pe care le creaza in memoria heap) sunt in ordinea din vectorul de numere intregi primit ca argument.
IntSList::IntSList(int* p, int n)
count = n;
Se mai poate defini un constructor avand ca argument o referinta (const) la un obiect din clasa IntArray, care contine un vector de numere intregi (Exercitiul 3.6):
IntSList::IntSList(const IntArray& array)
count = n;
Constructorul de copiere al clasei IntSList se defineste astfel
IntSList::IntSList(IntSList& r)
Destructorul clasei IntSListNode nu are de executat nici-o operatie utila, alta decat sa afiseze un mesaj care sa permita ulterior observarea compotarii acestei clase. Destructorul clasei IntSList parcurge lista inlantuita de noduri si le sterge succesiv din memorie folosind operatorul delete
~IntSListNode()
IntSList::~IntSList()
In Exercitiul 3.2 este propus si un alt mod de distrugere a obiectelor de clasa IntSListNode si IntSList.
Functia Display() a clasei IntSList afiseaza continutul unei liste incepand cu primul nod. O operatie asemanatoare se poate obtine prin supraincarcarea functiei operator << pentru clasa IntSList (Exercitiul 4. . )
void IntSList::Display()
cout << endl;
Fie urmatoarea functie fslist1()
void fslist1();
IntSList list1(val, sizeof(val)/sizeof(int));
list1.Display();
Obiectul list1 de clasa IntSList care se creaza la executia acestei functii este reprezentat in Fig. 3.4.
Fig. 3.4 Obiect (lista simpla inlantuita de numere intregi) din clasa IntSetList
Mesajele afisate la executia functiei fslist1() sunt:
Destructor nod v = 1
Destructor nod v = 2
Destructor nod v = 3
Destructor nod v = 4
Destructor nod v = 5
Operatiile de dictionar implementate pentru aceasta lista permit adaugarea unui nou element la inceputul sau sfarsitul listei (AddHead() si AddTail()) si extragerea (citire valoare si eliminare) unui element din capul sau coada listei RemoveHead() si RemoveTail()). In aceste operatii de adaugare si stergere element nu trateaza cazurile de eroare (eroare de alocare a unui nod nou la inserare sau eroarea de lista vida la extragere). Aceste situatii se vor trata ca exceptii (in sectiunea 8).
void IntSList::AddHead(int x)
void IntSList::AddTail(int x)
else
count++;
int IntSList::RemoveHead()
return v;
}
int IntSList::RemoveTail()
else p = p->link;
}
else
}
}
Alte functii membre ale clasei IntSList sunt functii de aflare a elementelor de inceput si de sfarsit ale liste, GetHead() si GetTail() si functia Lookup() care returneaza numarul de elemente cu o valoare data aflate in lista. Definitiile acestor functii sunt lasate ca un exercitiu simplu.
Lista simplu inlantuita definita prin clasele IntSListNode si IntSList se poate folosi pentru a implementa o stiva de numere intregi. Functia AddHead() este echivalenta operatiei push, iar functia RemoveHead() este echivalenta operatiei pop. Functia fslist2() evidentiaza acest lucru:
void fslist2()
De acemenea, aceasta lista se poate folosi pentru implementarea unei cozi de numere intregi. Functia AddTail() este echivalenta operatiei de inserare, iar functia RemoveHead() este echivalenta operatiei de extragere. De exemplu
void fslist3()
Pentru implementarea unei liste dublu inlantuite de numere intregi se folosesc doua clase: clasa IntDListNode, care reprezinta un nod al listei si clasa IntDList, care reprezinta lista dublu inlantuita. In clasa IntDListNode este memorat elementul listei (un numar intreg, variabila int v), pointerul next la nodul urmator si pointerul prev la nodul precedent. In clasa IntDList este memorat pointerul la primul nod al listei (first), pointerul la ultimul nod al listei (last) si numarul de elemente ale listei (count . Pentru fiecare clasa s-au definit constructori, destructor si functii membre care implementeaza operatii asupra listei. Clasa IntDList este declarata friend in clasa IntDListNode, pentru ca aceasta din urma sa poata accesa datele protejate ale clasei IntDList
class IntDListNode
~IntDListNode();
};
class IntDList
IntDList(int x);
IntDList(int* p, int n);
IntDList(const IntArray& array);
IntDList(IntDList& r);
~IntDList();
int GetCount()
void AddHead(int x);
void AddTail(int x);
int GetHead();
int GetTail();
int RemoveHead();
int RemoveTail();
void Display();
void ReverseDisplay();
};
inline IntDList::IntDList(int x)
Ceilalti constructori se pot implementa mai simplu folosind una din functiile de inserare a unui element, AddHead() sau AddTail(). La inserarea unui element intr-o lista dublu inlantuita trebuie sa fie refacute legaturile intre nodul nou introdus si nodurile existente pentru ambele directii de inlantuire. In Fig. 3.5 sunt reprezentate modificarile de pointeri la inserarea unui element nou in fata primului element al listei (functia AddHead()
void IntDList::AddHead(int x)
else
count++;
void IntDList::AddTail(int x)
else
count++;
Cazul particular al listei vide este tratat separat.
void fdlist1();
IntDList list1(v, sizeof(v)/sizeof(int));
list1.Display();
list1.ReverseDisplay();
}
void fdlist2()
Un mecanism mai evoluat de inspectare a colectiilor de date ordonate (de exemplu liste ordonate) il reprezinta iteratorii. Un iterator permite parcurgerea in ordinea dorita a elementelor, fara ca aceasta ordine sa depinda de modul de ordonare interna a elementelor colectiei. Iteratorii se pot implementa in mai multe moduri. Ei pot fi constituiti dintr-un grup de functii membre publice ale unei clase a colectiei (subsectiunea 3.3) sau pot fi definiti printr-o clasa diferita de clasele prin care se descrie colectia insasi, aceptata ca o clasa friend de catre acestea. In Exemplul 7.3 din sectiunea 7 este prezentata implementarea unui astfel iterator pentru parcurgerea unui vector asociativ.
Un arbore (tree) este un model de date care permite reprezentarea structurilor ierarhice si constitue una dintre cele mai importante structuri neliniar ce intervin in algoritmii pentru calculatoare.
Un arbore este compus dintr-o multime finita T de unul sau mai multe noduri (nodes) si o multime de muchii (edges), fiecare muchie conectand doua noduri intre ele. Un arbore are urmaroarele proprietati
Un nod este distinct si este se numeste radacina (root).
Fiecare nod c, cu exceptia radacinii este conectat printr-o muchie la un alt nod p, numit nodul parinte al lui c; nodul c se numeste nod fiu (child) al lui p
Un arbore este conectat, in sensul ca, pornind de la un nod oarecare n, diferit de nodul radacina, si mergand la parintele lui, apoi la parintele parintelui lui n, sa asa mai departe, se ajunge la nodul radacina.
Un arbore binar este un arbore in care fiecare nod are cel mult doi fii, numiti fiul din stanga si fiul din dreapta. Fiecare nod dintr-un arbore binar se reprezinta de obicei printr-un camp al informatiei (eticheta) si doi pointeri catre noduri, pointerul catre nodul fiu din stanga si pointerul catre nodul fiu din dreapta. Daca unul sau amandoi fiii unui nod lipsesc, pointerii corespunzatori au valoarea zero. In Fig. 3.7 este reprezentat un arbore binar in care fiecare nod este descris printr-o structura care contine informatia (eticheta) si cei doi pointeri.
Parcurgerea unui arbore binar se poate realiza in trei moduri: parcurgerea in preordine, in inordine sau in postordine. Metodele de parcurgere sunt definite recursiv: un arbore binar vid este parcurs fara sa se intreprinda nimic; altfel, parcurgerea se executa astfel:
typedef void* Pdate;
typedef int (*COMPARE) (Pdate d1, Pdate d2);
typedef void (*EXECUTE) (Pdate d);
class info
info(int x)
int get()
};
int comp(Pdate i1, Pdate i2)
void execute(Pdate i)
class TNode
~TNode()
};
class BTree
int getcount()
void insert(Pdate data);
int search(Pdate data);
void remove(Pdate data);
void inorder();
void preorder();
void postorder();
~BTree();
};
TNode* BTree::insert1(TNode *root, TNode *r, Pdate data)
if (f(data, r->d) == 0) return 0;
if (f(data, r->d) < 0)
return insert1(r, r->left, data);
else return insert1(r, r->right, data);
}
void BTree::insert(Pdate data)
void BTree::inorder1(TNode *root)
void BTree::inorder()
void ft1()
tree.inorder();
}
class IntSet
~IntSet()
int insert(int t);
int remove(int t);
int lookup(int t) const;
// functii iterator
void start(int& x)const
int inside(int& x) const
int next(int& x) const
};
int IntSet::insert(int t)
while(i>0 && t<set[i-1])
set[i] = t;
count++;
return 0;
}
int IntSet::remove(int t)
return l;
}
}
return -1; // nu este gasit
}
Operatia de aflare daca un membru apartine multimii foloseste un algoritm de cautare binara, la fel ca si operatia de stergere:
int IntSet::lookup(int t)const
return 0; // nu este gasit
}
Pentru parcurgerea elementelor multimii in scopul de a fi utilizate (de exemplu, afisate la consola) se defineste un mecanism care itereaza prin multimea data in ordinea dorita. Cel mai simplu mod de a defini un iterator este prin intermediul unor functii publice ale clasei IntSet, care stabileste punctul de pornire al iteratiei si ordinea in care sunt parcurse elementele multimii. Un astfel de mecanism ascunde utilizatorului modul de organizare interna a datelor clasei IntSet, si poate fi utilizat si daca se modifica modul de implementare a multimii, de exemplu se poate folosi o lista inlantuita sau arbore de cautare binara.
Un iterator simplu pentru multimea definita prin clasa IntSet se implementeaza prin intermediul a trei functii publice: functia start() care stabileste punctul de pornire a parcurgerii, functia next() care pozitioneaza iteratorul pe elementul urmator si functia inside() care returneaza 0 daca pozitia de iterare a depasit dimensiunea multimii.
Pentru testarea multimilor modelate de clasa IntSet se genereaza in mod aleator o secvente de numere intregi folosind functia rand() din biblioteca standard stdlib si se insereaza intr-o multime data. Continutul listei se afiseaza in ordine crescatoare folosind functiile de iteratie ale clasei si o variabila locala pos, prin care se controleaza parurgerea multimii:
void fset1()
O alta modalitate de a defini un iterator pentru multimi din clasa IntSet este prin definirea unei clase diferite care memoreaza si actualizeaza pozitia de parcurgere a unei multime date. O astfel de clasa, clasa SetIter se declara clasa friend in clasa IntSet si se defineste in felul urmator
class SetIter
void start()
int next()
else return 0;
}
int elem()
};
Un iterator se construieste pentru un obiect din clasa IntSet, al carui pointer il primeste ca argument al constructorului si-l memoreaza in variabila protejata s. La constructie, sau la apelul functiei start() se initializeaza pozitia de inceput a parcurgerii, pos = 0. Functia membra next() avanseaza cu o pozitie in multime; functia elem() returneaza valoarea elementului din pozitia curenta a iteratiei.
void fset2()
Pentru implementarea multimilor se mai pot folosi si alte structuri de date, cum sunt vectorii caracteristici, vectorii asociativi sau tabele de dispersie (hash table). Alegerea celei mai adecvate metode de reprezentare a unei multimi depinde, evident, de cerintele aplicatiei in care este folosita aceasta.
Exercitii
E3.1 Se defineste clasa IntNode care reprezinta un nod al unei liste simplu inlantuite de numere intregi.
class IntNode
~IntNode();
IntNode* AddHead(int x);
int GetHead()
IntNode *RemoveHead();
void Display();
};
Se poate admite ca o lista inlantuita sa fie reprezentata printr-un pointer la primul nod de tip IntNode, care le inlantuie pe urmatoarele, si atunci se pot defini functiile AddHead() si RemoveHead() pentru adaugarea si stergerea unui element din lista. Se cere sa se defineasca functiile declarate in clasa IntNode()
O astfel de lista poate implementa direct o stiva de numere intregi
void fnode()
Care sunt dezavantajele acestei implementari a listei inlantuite
E3.2 Sa se defineasca destructorii claselor IntSListNode si IntSList astfel incat destructorul clasei IntSListNode sa stearga elementul urmator din lista, daca acesta exista. Cum se explica faptul ca se modifica mesajele afisate la executia functiei fslist1() dupa aceast modificare
E3.3 Sa se defineasca constructorii, destructorii si celelalte functii ale claselor IntDListNode si IntDList. Pentru definirea constructorilor se vor testa doua modalitati de implementare: folosind functia AddTail() sau folosind functia AddHead().
E3.4 Sa se defineasca o lista dublu inlantuita de elemente de un tip oarecare definit de utilizator, folosind ca element al listei un pointer la obiectele de tipul respectiv.
E3.5 Se se defineasca o lista dublu inlantuita ordonata de numere intregi. Pentru inserarea unui numar in lista, se va folosi un algoritm de cautare binara.
E3.6 Sa se defineasca destructorul si functiile clasei BTree
E3.6 Fie urmatoarea definitie a claselor GenListNode si GenList, care implementeaza o lista dublu inlantuita generalizata:
typedef void* Pdate;
class GenListNode
~GenListNode();
};
class GenList
GenList(Pdate p);
~GenList();
int GetCount()
int insert(Pdate d);
int remove(Pdate d);
int lookup(Pdate d) const;
GenList& union(const GenList& m1,
const GenList& m2);
GenList& intersection(const GenList& m1,
const GenList& m2);
GenList& diference(const GenList& m1,
const GenList& m2);
};
Sa se completeze definitia clasei GenList si sa se defineasca functiile membre ale acestei clasei, astfel incat sa implementeze o multime de obiecte de un tip oarecare predefinit sau dat printr-o clasa.
Sa se testeze aceasta clasa pentru tipul de date sir de caractere si sa se utilizeze pentru afisarea in ordine lexicografica a unor cuvinte introduse aleator in multime.
E3. Sa se defineasca o clasa care sa reprezinte conceptul de multime de numere intregi, clasa IntSet. Pentru aceasta clasa sa se implementeze urmatoarele functii
constructori, destructor;
functie de introducere a unui numar in multimea respectiva;
functie de aflare daca un numar dat apartine sau nu multimii;
operatii cu multimi (reuniunea, intersectia, diferenta).
|