Algoritmi e strutture dati Contenitori e Iteratori STL Contenitori ● La STL fornisce una serie di classi standard che realizzano dei contenitori. ● Esempi di contenitori sono le liste, i vettori, gli alberi, etc. ● Un contenitore (container) è una collezione di oggetti di tipo omogeneo, organizzati in un certo modo. Algoritmi e strutture dati Contenitori e Iteratori STL Programmazione tradizionale (C) algo1() algo2() vector list algo3() hash search tree unsigned char unsigned int int 3 algoritmi 72 definizioni 4 strutture dati 24 definizioni char float 6 tipi elementari double Fare una libreria di contenitori con la programmazione tradizionale del C porta ad un esplosione di definizioni di strutture dati e di algoritmi. È necessario definire una struttura list per ogni tipo elementare (char, unsigned char, etc.). Inoltre è necessario definire un algoritmo algo1 per ogni struttura list definita. Se E è il numero di tipi elementari, C il numero di contenitori e A il numero di algoritmi, bisogna definire E*C strutture dati ed E*C*A algoritmi. Algoritmi e strutture dati Contenitori e Iteratori STL Programmazione generica ● La STL del C++ utilizza la programmazione generica. ● La classe “list” viene implementata una volta sola. Il tipo degli elementi è un generico tipo T. ● Non importa sapere a priori quale sarà T, basta che soddisfi alcuni requisiti (constraints). ● ● Per esempio: il T dei search tree dev'essere ordinabile (<) Programmazione generica significa programmare utilizzando dei concetti. Un concetto è una famiglia di astrazioni che hanno un insieme comune di operazioni. Per risolvere questo problema, la libreria STL (Standard Template Library) usa la programmazione generica. La classe list viene implementata una volta sola. Il tipo degli elementi è un generico tipo T. La programmazione generica non va confusa con la programmazione a oggetti, tipica di linguaggi a più alto livello come il Java. In Java il problema si risolve con l'ereditarietà. Ovvero si definisce un tipo object di cui tutti gli altri tipi sono derivati. In questo modo basta definire una lista di object, e automaticamente abbiamo definito tutte le possibili liste di tutti i possibili tipi elementari. L'approccio a oggetti è più semplice e porta a meno errori di programmazione. Tuttavia è poco efficiente, perché bisogna fare continue conversioni di tipo da object a int (nel caso di lista di interi). Nella programmazione generica invece non si definisce nessuna classe object. Basta che il tipo T fornisca alcuni requisiti (constraints), necessari per stare in una lista. L'approccio generico è più flessibile ed efficiente, ma può portare ad errori di programmazione se usato in modo sbagliato. Algoritmi e strutture dati Contenitori e Iteratori STL Programmazione generica (C++) algo1() Concetto di “contenitore” algo2() vector list Concetto di tipo elementare “T” algo3() hash search tree unsigned char unsigned int int 3 algoritmi 3 definizioni 4 strutture dati 4 definizioni char float 6 tipi elementari double Con l'approccio generico si racchiudono tutti i possibili tipi elementari nel concetto di tipo elementare T. Quindi si definisce una sola struttura dati list generica di elementi di tipo T. Poi si racchiudono tutti i possibili contenitori nel concetto di contenitore. Quindi si programmano algoritmi che operano su un generico contenitore. Per rendere generico il concetto di tipo elementare basta ricorrere al meccanismo dei template. Per rendere generico il concetto di contenitore si ricorre al meccanismo dei template più al concetto di iteratore. Algoritmi e strutture dati Contenitori e Iteratori STL Contenitori ● Contenitori si dividono in: ● ● ● ● Sequenze Adattatori Quasi-contenitori Contenitori associativi I contenitori (containers) in C++ si dividono in quattro famiglie: (1) le sequenze sono i contenitori che impongono un ordinamento lineare nelle posizioni degli elementi, ovvero dati due elementi si può sempre definire quale sta prima e quale sta dopo (esempio: list); (2) gli adattatori non sono contenitori indipendenti ma si appoggiano ad un altro container e ne estendono le funzionalità (esempio: priority_queue); (3) i quasi-contenitori non esportano tutte le funzionalità di un contenitore propriamente detto (esempio: string); (4) i contenitori associativi sono contenitori che non impongono un ordinamento lineare nelle posizioni degli elementi (esempio: map). Algoritmi e strutture dati Contenitori e Iteratori STL Sequenze size ● vector<T>: 10 1 4 front 12 4 back capacity ● list<T>: 10 1 4 12 4 front back size ● deque<T>: 4 12 size 4 10 back front 1 Il vector è un semplice array dinamico di grandezza “size”, memorizzato in un buffer di capacità “capacity”. La capacità del buffer è sempre maggiore o uguale alla grandezza del vettore. L'allocazione del buffer è gestita: se il vettore cresce oltre la capacità del buffer, viene automaticamente riallocato in un buffer più capiente, e tutti gli elementi copiati nel nuovo buffer. La list è una lista doppia. Ogni elemento contiene non solo il puntatore all'elemento successivo ma anche a quello precedente. Sono poi definiti il puntatore alla testa della lista ed il puntatore alla coda della lista. La deque (pron. “deck”, sta per double-ended queue) è un vettore circolare. Il vector, la list e la deque sono sequenze, in quanto organizzano la collezione di elementi secondo un ordine lineare. Per “front” si intende l'elemento di testa, cioè il primo della sequenza. Per “back” quello di coda, cioè l'ultimo della sequenza. Algoritmi e strutture dati Contenitori e Iteratori STL Iteratori ● Un iteratore è un oggetto che punta ad un elemento del contenitore. Sono un'astrazione del concetto di puntatore. i 10 1 4 12 4 Un iteratore è un oggetto che punta ad un elemento di un contenitore. Può essere pensato come un'astrazione di un puntatore. Il vantaggio degli iteratori è che offrono un set di operazioni comuni con un'interfaccia che non dipende dal tipo di contenitore iterato. La tecnica degli iteratori è usata dalle librerie standard di quasi tutti i linguaggi moderni (Java, C#, perl, etc.) Gli iteratori del C++ sono particolarmente potenti in quanto permetto non solo di accedere agli elementi in lettura e scrittura ma anche di modificare il contenitore stesso (inserire elementi, cancellare elementi, etc.) Algoritmi e strutture dati Contenitori e Iteratori STL Iteratori ● Su ogni iteratore i sono ridefinite le operazioni di: ● ● ● Dereferenziazione (accesso all'elemento puntato): *i Scorrimento all'elemento successivo/precedente nella sequenza: i++, i-Test di (dis)uguaglianza: i==j, i!=j i 10 1 4 12 4 Algoritmi e strutture dati Contenitori e Iteratori STL Iteratori ● Su ogni contenitore c sono definiti due metodi: ● ● c.begin() restituisce un iteratore che punta al primo elemento di c c.end() restituisce un iteratore che punta all'elemento successivo dell'ultimo 10 begin 1 4 12 4 end Algoritmi e strutture dati Contenitori e Iteratori STL Iteratori 10 begin 1 2 3 4 5 6 7 1 i ++ 4 12 4 end void stampa_vector_int(vector<int> vett){ vector<int>::iterator i; // scorro gli elementi in ordine: for(i = vett.begin(); i != vett.end(); i++){ cout << *i; // stampo l'elemento } } Questo algoritmo stampa gli elementi di un vettore di interi. La funzione vett.begin() restituisce un iteratore all'elemento di testa (front element). La funzione vett.end() restituisce un iteratore che punta ad un immaginario elemento successivo all'ultimo. Algoritmi e strutture dati Contenitori e Iteratori STL Iteratori 10 begin 1 2 3 4 5 6 7 8 1 i ++ 4 12 4 end template<class T> void stampa_vector(vector<T> vett){ typename vector<T>::iterator i; // scorro gli elementi in ordine: for(i = vett.begin(); i != vett.end(); i++){ cout << *i; // stampo l'elemento } } Questo algoritmo stampa gli elementi di un vettore di tipo generico. Quando si usa un'espressione generica a sinistra del “::”, si rende necessaria la parola chiave typename. Constraints: Il tipo T deve avere il vincolo di essere stampabile (definito l'operatore << su ostream). Quindi la funzione può essere invocata su vettori di interi, caratteri, stringhe, etc. ma non su vettori di classi, a meno che di non ridefinire il <<. Algoritmi e strutture dati Contenitori e Iteratori STL Iteratori i 10 1 4 ++ 12 begin 1 2 3 4 5 6 7 8 4 end template<class Cont> void stampa(Cont c){ typename Cont::iterator i; // scorro gli elementi in ordine: for(i = c.begin(); i != c.end(); i++){ cout << *i; // stampo l'elemento } } Questo è lo stesso algoritmo ma generico sul tipo di contenitore. Constraints: L'unico vincolo è che il tipo Cont deve supportare gli iteratori. Quindi funziona con qualsiasi contenitore STL (vector, list, etc.) Algoritmi e strutture dati Contenitori e Iteratori STL Esempio di programmazione generica ● Funzione generica che azzera gli elementi precedenti agli elementi dispari di una sequenza 10 1 4 12 1 0 1 4 0 1 Algoritmi e strutture dati Contenitori e Iteratori STL Esempio di programmazione generica 1 template<class Cont> 2 void azzera_predispari(Cont c){ 3 typename Cont::iterator i; 4 for(i = c.begin(); i != c.end(); i++){ 5 if(*i%2 == 1 && i != c.begin()) 6 i--; 7 *i = 0; // azzero l'elemento precedente 8 i++; 9 } 10 } 11 } i 10 begin 1 4 12 1 end Constraints: Il tipo Cont deve supportare gli iteratori. In più gli elementi devono avere definito l'operatore %. Quindi l'algoritmo è applicabile a vettori, liste, etc. di elementi interi (char, int, unsigned int, etc.) Algoritmi e strutture dati Contenitori e Iteratori STL Validità degli iteratori ● Un iteratore può essere: ● ● Valido: se punta ad un elemento (può essere dereferenziato) Non valido: se non punta ad un elemento (non può essere dereferenziato) i 5 begin 5 4 ++ 5 5 end Un iteratore è valido se punta ad un elemento. L'iteratore restituito da end() è un po' speciale, in quanto non è valido (non punta a nessun elemento) ma su di esso può essere applicato ugualmente l'operatore di spostamento all'indietro (i--). Algoritmi e strutture dati Contenitori e Iteratori STL Validità degli iteratori ● Alcune operazioni che modificano il contenitore invalidano i ++ alcuni (o tutti) gli iteratori. 5 5 4 5 5 begin 1 2 3 4 5 template<class Cont> void cancella_pari(Cont& c){ typename Cont::iterator i; for(i = c.begin(); i != c.end(); i++) if(*i % 2 == 0) c.erase(i); // errore! } 1 2 3 4 5 6 template<class Cont> void cancella_pariOK(Cont& c){ typename Cont::iterator i; for(i = c.begin(); i != c.end(); ) { if(*i % 2 == 0) i = c.erase(i); // OK else i++; } } end errore classico La funzione c.erase(iterator i) cancella dalla sequenza l'elemento puntato da i. L'iteratore i viene invalidato (non punta più ad un elemento valido). Un errore comune (esempio 1) consiste nello scorrere una sequenza cancellando o inserendo elementi, senza fare attenzione all'invalidazione degli iteratori. Per mantenere l'iteratore i valido bisogna programmare come nell'esempio 2. Notare che la funzione c.erase() restituisce un iteratore alla posizione successiva a quella cancellata. In questo caso non ho bisogno di incrementare l'iteratore. L'istruzione i++ va messa quindi nella parte else (riga 5) e non nel passo del for (riga 3). Algoritmi e strutture dati Contenitori e Iteratori STL Iteratori a costante ● Un iteratore a costante è un iteratore che non può modificare il container ● ● ● vector<int>::iterator → iteratore a mutabile vector<int>::const_iterator → iteratore a costante Con contenitori costanti è obbligatorio usare iteratori a costante 1 2 3 4 5 template<class Cont> void stampa_pari(const Cont& c){ typename Cont::iterator i; // errore! Il contenitore è costante. for(i = c.begin(); i != c.end(); i++) if(*i % 2 == 0) cout << *c << ' '; } 1 2 3 4 5 template<class Cont> void stampa_pariOK(const Cont& c){ typename Cont::const_iterator i; // ok for(i = c.begin(); i != c.end(); i++) if(*i % 2 == 0) cout << *c << ' '; } Algoritmi e strutture dati Contenitori e Iteratori STL Iteratori inversi ● Gli iteratori inversi vedono il contenitore come se fosse “rovesciato”. Sono utili per visitare in ordine inverso. ● ● ● Ogni contenitore esporta le funzioni: ● ● ● vector<int>::reverse_iterator → iteratore inverso vector<int>::const_reverse_iterator → iteratore inverso a costante c.rbegin() → iteratore all'ulƟmo elemento c.rend() → iteratore precedente al primo elemento i++ sposta l'iteratore i all'indietro. ++ rend 5 5 i 4 rbegin 5 5 Algoritmi e strutture dati Contenitori e Iteratori STL Iteratori ++ rend 5 1 2 3 4 5 6 7 8 5 i 4 rbegin 5 5 template<class Cont> void stampa_contrario(Cont c){ typename Cont::reverse_iterator i; // scorro gli elementi in ordine: for(i = c.rbegin(); i != c.rend(); i++){ cout << *i; // stampo l'elemento } } Questo algoritmo stampa dall'ultimo al primo gli elementi di un contenitore. Algoritmi e strutture dati Contenitori e Iteratori STL Mobilità degli iteratori ● Un iteratore può essere: ● ● ● Forward: permettono di avanzare in una sequenza (i++) Bidirectional: permettono di scorrere in entrambi i sensi (i++, i--) Random Access: permettono di saltare da un elemento all'altro (i++, i--, i+=5, i-=5) ● list<T> esporta iteratori Bidirectional ● vector<T> e deque<T> esportano iteratori Random Access Algoritmi e strutture dati Contenitori e Iteratori STL Mobilità degli iteratori ● L'operatore + è implementato solamente dagli iteratori Random Access. ● Per accedere all'elemento successivo nel caso di iteratori Bidirectional non si può fare *(i+1). ● Bisogna fare: i++; // sposto l'iteratore if(i != c.end()) *i = 4; i--; // lo riporto indietro errore classico Algoritmi e strutture dati Contenitori e Iteratori STL Ranges ● Un range è una sotto-sequenza di elementi delimitata da due iteratori a e b: [a, b) a 1 2 b 3 4 begin ● [a, a) è un range vuoto ● [begin(), end()) rappresenta tutta la sequenza 5 end Un concetto molto usato nella STL è quello di range. Il range è una sottosequenza di elementi delimitata da due iteratori a e b. Si indica con [a, b). Per convenzione la sottosequenza include l'elemento puntato da a ma non quello puntato da b. Algoritmi e strutture dati Contenitori e Iteratori STL vector<T> ● Array dinamico con riallocazione automatica. size 10 front 1 4 12 4 back capacity ● La grandezza può aumentare e diminuire. ● Quando supera la capacità, il buffer viene automaticamente riallocato. ● Si possono accedere direttamente gli elementi. Invalidazione iteratori: ● ● ● Se si elimina un elemento, si invalidano gli iteratori all'elemento ed ai successivi. Se si inserisce un elemento, si invalidano gli iteratori agli elementi successivi, o tutti in caso di riallocazione. Algoritmi e strutture dati Contenitori e Iteratori STL vector<T> – Funzioni base ● vector<int> vett; → Crea un veƩore vuoto di interi. ● vett.push_back(int e); → Inserisce “e” come ulƟmo elemento. ● int vett.pop_back(); → Cancella l'ulƟmo elemento. ● int vett.size(); → ResƟtuisce il numero di elemenƟ. ● int& vett[5]; → Accede all'elemento di indice 5 (in leƩura e/o scrittura). Non esiste per il contenitore “list”. Algoritmi e strutture dati Contenitori e Iteratori STL Esempio di vector<T> ● Programma che stampa la somma di N numeri. ● ● ● Prende il numero N da tastiera. Prende N numeri da tastiera. Stampa la somma. Algoritmi e strutture dati Contenitori e Iteratori STL Esempio di vector<T> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 size #include<iostream> #include<vector> 15 6 3 using namespace std; back capacity int main(){ vector<int> vett; // vettore vuoto int n, c; cin >> n; // attenzione: no controllo errori! vett.reserve(n); // capacità n elementi for(int i = 0; i < n; i++){ cin >> c; // prendo da tastiera n elementi vett.push_back(c); } int sum = 0; for(vector<int>::iterator j = vett.begin(); j!=vett.end(); j++) sum += *j; // calcolo la somma cout << sum << endl; } La funzione vett.reserve() server ad allocare un buffer di capacità n. Non cambia il comportamento del programma, ma ne aumenta l'efficienza in quanto evita inutili riallocazioni automatiche. L'uso di vett.reserve() diminuisce la genericità del programma, in quanto è una funzione specifica di vector e non è definita per gli altri contenitori. Algoritmi e strutture dati Contenitori e Iteratori STL Regola aurea per programmare con contenitori ● Quando avete bisogno di un contenitore, ma non sapete quali saranno le operazioni più comuni su di esso, usate vector<T>. ● Scorrete il vector<T> con iteratori e programmate in modo generico. ● Quando avete identificato le operazioni più comuni, sostituite il vector con il contenitore più adatto (vector, list, deque, set, etc.) Quando si programma, capita molte volte di aver bisogno di una collezione di dati, ma non sappiamo ancora quale è la struttura dati più efficiente per realizzarla (vector, list, etc.). Si consiglia di usare inizialmente la struttura dati vector e programmare in modo generico, usando iteratori ed evitando metodi specifici del vector. Poi quando si sono identificate le operazioni più comuni, cambiare eventualmente la struttura dati. Se sono frequenti le operazioni di accesso diretto, è meglio lasciare vector; se invece sono frequenti gli inserimenti e le cancellazioni a metà, è più efficiente una list; se infine sono frequenti inserimenti e cancellazioni in testa e in coda, è meglio usare la deque. In ogni caso, la programmazione generica renderà minimi i cambiamenti da effettuare nel codice. Algoritmi e strutture dati Contenitori e Iteratori STL list<T> ● Lista doppia: ogni elemento ha il puntatore al successivo e al precedente. ● C'è una puntatore di testa (front) ed uno di coda (back). 10 1 front 4 12 4 back ● Non si possono accedere direttamente gli elementi, ma solo attraverso iteratori. ● Invalidazione degli iteratori: ● ● Se si elimina un elemento, si invalidano gli iteratori che puntano all'elemento. Se si inserisce un elemento, non si invalida nessun iteratore. Algoritmi e strutture dati Contenitori e Iteratori STL Esempio di list<T> ● Programma che prende N righe da tastiera (fino ad una riga nulla) e le scrive ordinate su un file. Algoritmi e strutture dati Contenitori e Iteratori STL Esempio di list<T> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include<list> #include<string> using namespace std; int main(){ list<string> li; // lista vuota string s; do{ getline(cin, s); // input di una riga (no controllo errori!) li.push_back(s); }while(!s.empty()); li.pop_back(); // tolgo la riga vuota finale li.sort(); // merge sort ofstream f("out.txt", ios::out); list<string>::iterator i; for(i = li.begin(); i != li.end(); i++) f << *i << endl; // stampa su file f.close(); } La funzione li.sort() ordina la lista per mezzo dell'algoritmo merge sort. È specifica per il tipo list<T>. Per usarla, T deve essere ordinabile. Algoritmi e strutture dati Contenitori e Iteratori STL Operazioni su sequenze Operazione: Funzione: vector: list: deque: vector<T> c; Default constructor. Crea un contenitore vuoto. O(1) O(1) O(1) vector<T> c(int n, T elem); Fill constructor. Crea un contenitore di n elementi uguali a elem O(n) O(n) O(n) vector<T> c(iterator a, iterator b); Range contructor. Crea un contenitore copiando gli elementi da [a,b) O(n) O(n) O(n) operator==(vector A, vector B); Restituisce vero sei i contenitori sono uguali (stessi elementi nello stesso ordine). O(n) O(n) O(n) ~vector<T>; Distruttore. Invoca delete su ogni elemento. O(n) O(n) O(n) ! Il distruttore di un contenitore invoca automaticamente la delete su ogni elemento. Questo comportamento è essenziale per definire liste di oggetti di tipo class. Algoritmi e strutture dati Contenitori e Iteratori STL Operazioni su sequenze Operazione: Funzione: vector: list: deque: iterator c.begin(); iterator c.end(); Iteratori di inizio e fine sequenza O(1) O(1) O(1) int c.size()const; Numero di elementi O(1) O(n) O(1) void c.resize(int m, T elem); Diminuisce o aumenta il numero di elementi O(|m-n|)* O(|m-n|) O(|m-n|) bool c.empty()const; size()==0 O(1) O(1) int c.capacity()const; Capacità del buffer O(1) no no void c.reserve()const; Riserva capacità del buffer O(n) no no T& c[int i]; Accede all'elemento i-esimo O(1) no O(1)** ! O(1) * O(n) se provoca riallocazione ** Meno efficiente del vector<T> La funzione c.empty() equivale a fare c.size()==0 ma ha complessità O(1). Algoritmi e strutture dati Contenitori e Iteratori STL Operazioni su sequenze Operazione: Funzione: vector: list: iterator c.insert(iterator it, T elem); Inserisce elem prima dell'elemento puntato da it. O(n) O(1) deque: vector::insert(iterator it, int n, T elem); Fill insert. Inserisce n copie di elem prima dell'elemento puntato da it. O(n) O(n) O(n) vector::insert(iterator it, iterator a, iterator b); Range insert. Inserisce gli elementi in [a,b) prima dell'elemento puntato da it. O(n) O(n) O(n) iterator c.erase(iterator it); Cancella l'elemento puntato da it. Restituisce un iteratore valido all'elemento successivo. O(n) O(1) void c.erase(iterator a, iterator b); Range erase. Cancella gli elementi nel range [a,b). O(n) O(n) ! ! O(n) O(n) O(n) Le funzioni insert ed erase hanno complessità O(1) per la lista, e complessità O(n) per gli altri tipi di sequenze. Questo perché nel vector e nella deque, ogni volta che si inserisce o cancella un elemento nel mezzo, bisogna spostare gli altri elementi per ricompattare la sequenza. Algoritmi e strutture dati Contenitori e Iteratori STL Operazioni su sequenze Operazione: Funzione: vector: list: deque: T front(); Accede al primo elemento. O(1) O(1) O(1) push_front(T e); Aggiunge e come primo elemento. O(n) O(1) O(1) ! T pop_front(); Restituisce e cancella il primo elemento. O(n) O(1) O(1) ! T back(); Accede all'ultimo elemento. O(1) O(1) O(1) push_back(T elem); Aggiunge elem come ultimo elemento. O(1)* O(1) O(1) T pop_back(); Restituisce e cancella l'ultimo elemento. O(1) O(1) O(1) * O(n) se provoca riallocazione La push_front() e la pop_front() hanno complessità O(n) per il vector, ma solo O(1) per la deque. Questo è dovuto alla sua natura di array circolare. Algoritmi e strutture dati Contenitori e Iteratori STL Operazioni su sequenze Operazione: Funzione: vector: list: deque: void swap(container c1, container c2); Scambia due contenitori in modo efficiente. O(1) O(1) O(1) iterator copy(iterator a, iterator b, iterator to); Copia gli elementi da [a,b) a [to,to+(b-a)). O(n) O(n) O(n) void sort(iterator a, iterator b); Esegue quick sort sul range [a,b). O(nlogn) no void c.sort(); Esegue merge sort su una lista. no O(nlogn) no bool is_sorted(iterator a, iterator b); Restituisce true se la sequenza [a,b) è ordinata. O(n) O(n) O(n) void make_heap(iterator a, iterator b); Trasforma [a,b) in uno heap. O(n) no O(n) iterator max_element(iterator a, iterator b); Restituisce l'elemento massimo in [a,b). O(n) O(n) O(n) iterator min_element(iterator a, iterator b); Restituisce l'elemento minimo in [a,b). O(n) O(n) O(n) ! ! O(nlogn) La funzione globale sort(iterator a, iterator b) esegue quick sort sul range [a,b). Può essere applicata soltanto a contenitori che esportano iteratori random access. Quindi non su “list”, ma su “vector” e “deque”. Per ordinare una “list” si ricorre alla funzione c.sort() (dove c è una list), che esegue merge sort. Algoritmi e strutture dati Contenitori e Iteratori STL Adattatori ● Estendono le funzionalità di una sequenza (vector, list, deque) ● queue<T> → coda Last In – First Out (estende deque<T>) ● stack<T> → pila First In – First Out (estende deque<T>) ● priority_queue<T> → struƩura heap (estende vector<T>) Gli adattatori si appoggiano ad una sequenza, estendendone le funzionalità. Algoritmi e strutture dati Contenitori e Iteratori STL Quasi-contenitori ● Non hanno tutte le funzionalità di un contenitore ordinario. Sono specializzati per usi specifici. ● string → stringa di caraƩeri. Specializzata per operazioni su sottostringhe. ● valarray<T> → veƩore matemaƟco. Specializzato per operazioni di calcolo su molti dati. T dev'essere un tipo matematico. ● bitset<N> → insieme di N flag (N è un intero). Viene memorizzato in modo compatto (1 bit per flag). Specializzato per operazioni bit a bit. Algoritmi e strutture dati Contenitori e Iteratori STL Contenitori associativi ● Contenitori che offrono una ricerca efficiente basata sul valore di una chiave. ● La chiave può essere di un qualsiasi tipo T (ordinabile). ● set<T> → albero binario di ricerca di chiavi T. ● map<T, ValueT> → albero binario di ricerca di coppie chiave-valore (pair<T, ValueT>). Le visita tramite iteratore esegue una visita simmetrica dell'albero, quindi gli elementi vengono visitati in modo ordinato (crescente) ● Algoritmi e strutture dati Contenitori e Iteratori STL set<T> ● Realizza un albero binario di ricerca di elementi di tipo T. Gli elementi sono ordinati e non ci sono ripetizioni. 6 3 1 0 ● 10 4 2 9 12 14 Una volta inseriti, non è possibile modificare gli elementi. Algoritmi e strutture dati Contenitori e Iteratori STL set<T> – Funzioni base ● set<int> s; → Crea un set vuoto di interi. ● s.insert(int e); → Inserisce “e” nel set. ● iterator s.find(int e); → Cerca “e” nel set. Restituisce un iteratore che punta ad “e”, o s.end() se “e” non esiste. ● int s.size(); → ResƟtuisce il numero di elemenƟ. Algoritmi e strutture dati Contenitori e Iteratori STL Esempio di set<T> ● Programma che prende da tastiera delle parole (fino alla parola “fine”) e stampa un messaggio se la parola è già stata inserita. Dopodiché stampa le parole in modo ordinato. Algoritmi e strutture dati Contenitori e Iteratori STL Esempio di set<T> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include<stream> #include<string> #include<set> using namespace std; int main(){ set<string> parole; // albero vuoto string p; do{ cin >> p; if(parole.find(p) != parole.end()) cout << "Hai gia' inserito: " << p << endl; else parole.insert(p); }while(p != "fine"); } Algoritmi e strutture dati Contenitori e Iteratori STL map<T, ValueT> ● Realizza un dizionario di coppie chiave-valore, ottimizzato per la ricerca sulla chiave. ● È implementato con un albero binario di ricerca ordinato sulle chiavi. Ogni nodo contiene una coppia chiave-valore. Non ci sono ripetizioni sulle chiavi. Pluto 7 Minnie 2 Eta Beta 9 ● Topolino 2 Pippo 4 Zapotec 13 Una volta inserite, le chiavi sono costanti, il relativo valore può essere modificato Algoritmi e strutture dati Contenitori e Iteratori STL map<T, ValueT> ● È possibile accedere ad un elemento tramite indicizzazione diretta (operator[]), con indice di tipo T (non necessariamente intero). Per esempio: map<string, int> m; m["pippo"] = 4; ● L'operatore m["pippo"] cerca un elemento con chiave “pippo”. ● ● Se tale elemento esiste, accede al campo valore. Altrimenti lo crea (con chiave “pippo” e valore 0), e accede al campo valore. L'operatore di indicizzazione diretta (parentesi quadre) serve sia ad accedere (in lettura o in scrittura) il campo valore associato ad una chiave, sia ad inserire un nuovo elemento nell'albero. Algoritmi e strutture dati Contenitori e Iteratori STL map<T, ValueT> ● Con map<T, ValueT> è possibile costruire dei semplici database in memoria. ValueT T Nome Età Num. telefono Mario Rossi 25 123 456789 Gianni Verdi 26 234 567890 Paolo Bianchi 30 345 678901 Franco Rosi 27 456 789012 struct info{ int eta; long numTelefono; }; map<string, info> db; ... db["Mario Rossi"].eta = 26; Algoritmi e strutture dati Contenitori e Iteratori STL Esempio di map<T, ValueT> ● Programma che legge una lista di contatti da file, ci aggiunge un contatto preso da tastiera, e poi li stampa. ● Ogni contatto è su una riga ed ha il formato: email numero-di-telefono Algoritmi e strutture dati Contenitori e Iteratori STL Esempio di map<T, ValueT> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include<map> #include<string> using namespace std; int main(){ map<string, long int> contatti; string email; long int telefono; ifstream f("contatti.txt", ios::in); while(!f.eof()){ f >> email >> telefono; contatti[email] = telefono; // inserimento di un elemento } f.close(); cin >> email >> telefono; contatti[email] = telefono; map<string, long int>::iterator i; for(i = contatti.begin(); i != contatti.end(); i++) // visita ordinata cout << i->first << ' ' << i->second << endl; } Algoritmi e strutture dati Contenitori e Iteratori STL Contenitori associativi Operazione: Funzione: set: map: set<T> c; Default constructor. Crea un contenitore vuoto. O(1) O(1) set<T> c(iterator a, iterator b); Range constructor. Crea un contenitore copiando dal range [a,b). O(nlogn) O(nlogn) ~set<T>; Destructor. Invoca delete su tutti gli elementi. O(n) O(n) operator==(set A, set B) Restituisce vero se i set sono uguali (contengono gli stessi elementi). O(n) O(n) ! Algoritmi e strutture dati Contenitori e Iteratori STL Contenitori associativi Operazione: Funzione: set: map: iterator c.begin(); iterator c.end(); Iteratori di inizio e fine sequenza. O(1) O(1) int c.size(); Numero di elementi. O(n) O(n) bool c.empty(); c.size()==0 O(1) O(1) ValueT& c[T k]; Restituisce l'elemento con chiave k. Lo crea se non esiste. no O(logn) iterator c.find(T k); Trova l'elemento k.* O(logn) O(logn) iterator c.lower_bound(T k); Trova il più piccolo elemento > k.* O(logn) O(logn) iterator c.upper_bound(T k); Trova il più grande elemento <= k.* O(logn) O(logn) * ! Restituisce c.end() se tale elemento non esiste. L'accesso tramite parentesi quadra ad un elemento di una map, crea l'elemento se non esiste. L'elemento creato assume un valore iniziale di default. Se si vuole testare la presenza di un elemento senza crearlo, si deve utilizzare la funzione find(). Algoritmi e strutture dati Contenitori e Iteratori STL Contenitori associativi Operazione: Funzione: set: map: c.insert(T elem); Inserisce elem. O(logn) no c.insert(iterator a, iterator b); Range insert. Inserisce gli elementi in [a,b). O(logn) per ogni elemento O(logn) per ogni elemento void c.erase(T elem); Cancella l'elemento elem. O(logn) O(logn) void c.erase(iterator i); Cancella l'elemento puntato da i. O(logn) O(logn) void c.erase(iterator a, iterator b); Range erase. Cancella gli elementi nel range [a,b). O(logn) per ogni elemento O(logn) per ogni elemento * La funzione make_pair(T k, ValueT elem) crea una struttura dati pair<T,ValueT>. Algoritmi e strutture dati Contenitori e Iteratori STL Altri contenitori associativi ● multiset<T>: come set, ma sono ammessi valori ripetuti. ● multimap<T, ValueT>: come map, ma sono ammessi valori ripetuti delle chiavi. ● hash_set<T>: come set, ma gli elementi sono memorizzati con struttura ad hash. ● ● Più efficiente per la ricerca Non garantisce ordinamento ● hash_map<T, ValueT>: come map, ma gli elementi sono memorizzati con struttura ad hash. ● hash_multiset<T> ● hash_multimap<T, ValueT> Algoritmi e strutture dati Contenitori e Iteratori STL Esercizio riassuntivo ● Scrivere una funzione generica: template<class Cont> void f(Cont& c); che unisce a due a due gli elementi adiacenti, sommandoli fra loro. 10 1 4 11 16 4 12 4 Algoritmi e strutture dati Contenitori e Iteratori STL Esercizio riassuntivo – Soluzione 1 template<class Cont> 2 void fondi(Cont& c){ 3 typename Cont::iterator i; 4 for(i = c.begin(); i != c.end(); i++) 5 { 6 int a = *i; 7 i++; 8 if(i != c.end()){ 9 *i += a; 10 i--; 11 i = c.erase(i); i 12 } 13 else 10 1 4 14 i--; 15 } begin 16 } ++ 12 4 end Algoritmi e strutture dati Contenitori e Iteratori STL Riferimenti ● Matthew H. Austern, “Generic programming and the STL,” Addison-Wesley ● Bjarne Stroustrup, “The C++ Programming Language,” Addison Wesley ● Guida di riferimento sul C/C++: http://en.cppreference.com/w/