Liste, pile e code
In questa pagina 5
Un ADT (vedi Problemi computazionali e algoritmiProblema computazionale come insieme di coppie (istanza, soluzione); algoritmo e modello di calcolo RAM; pseudocodice; taglia di un'istanza; ADT e struttura dati concreta; esempio svolto con ricerca lineare e binaria in un array ordinato.Problemi computazionali e algoritmi → e, per le basi, Tipi di dato astrattiUn ADT è definito dalle operazioni e dal loro comportamento, non dalla rappresentazione; interfaccia e implementazione; realizzazione in Python con le classi e in C con header e tipo opaco; esempio di un ADT Frazione.Tipi di dato astratti →) specifica tipo dei dati, operazioni e parametri, non l'implementazione. Le stesse strutture sono presentate in modo più elementare in Pila e codaPila (LIFO) e coda (FIFO) come ADT: operazioni e costi; realizzazione in Python con list e collections.deque; realizzazione in C con array e indici, coda circolare; applicazioni (parentesi bilanciate, stack delle chiamate, visite).Pila e coda →, Liste concatenateLista concatenata semplice: nodi allocati dinamicamente collegati da puntatori; inserimento e rimozione in testa, scorrimento, ricerca, inserimento ordinato, deallocazione; confronto dei costi con l'array; versione in Python.Liste concatenate → e Realizzare contenitori su array e listeCome si realizza un ADT contenitore partendo da un array: lunghezza logica e capacità con raddoppio (costo ammortizzato), dizionario su array ordinato con ricerca binaria, coda doppia su array circolare, coda con priorità a livelli, ADT costruiti sopra altri ADT (pila di code, pila reversibile); tabella dei costi.Realizzare contenitori su array e liste → (Fondamenti di Informatica). In Java si descrive con un'interfaccia (solo costanti e metodi senza corpo); in C non esistono costrutti per gli ADT, si usano struct e funzioni (vedi Liste concatenate in CLista singolarmente concatenata in C con nodo struct e testa passata per riferimento (Nodo **); addHead, addTail ricorsiva e iterativa, pop, inversione in loco, liberazione; coda con puntatori a testa e coda; ricorsione sulle liste; costi.Liste concatenate in C →).
Lista
Collezione di elementi con un ordine lineare: primo, secondo, ..., ultimo. Si accede a un elemento in due modi.
Lista index-based
Definizione (lista index-based). L'elemento si identifica con l'indice = numero di elementi che lo precedono.
Metodo Effetto size(),isEmpty()numero di elementi, lista vuota get(i)elemento in posizione , senza toglierlo set(i, e)sostituisce l'elemento in con ee restituisce il vecchioadd(i, e)inserisce eall'indice spostando a destra i successiviremove(i)toglie e restituisce l'elemento in , spostando a sinistra i successivi
Esempio. Su [7, 8, 9] si ha get(2) = 9, e add(1, 5) produce [7, 5, 8, 9].
Proprietà (array estendibile con raddoppio). Si realizza con un array:
get,set,sizein ;adderemovein per gli spostamenti. Se l'array è pieno si alloca un array di taglia doppia e si copiano gli elementi: il costo della copia è ammortizzato dai inserimenti precedenti, quindi l'inserimento in fondo costa ammortizzato.
Esempio. Partendo da capacità e raddoppiando, inserimenti in fondo copiano in tutto elementi: meno di una copia per inserimento.
Perché l'ammortizzato è . Con capacità iniziale , la copia avviene quando l'array contiene elementi ed è pieno: copia rispettivamente elementi. Dopo inserimenti le copie sono avvenute per le potenze di minori di , e il loro totale è la somma geometrica (formula della serie geometrica finita, vedi Notazione asintoticaDefinizioni di O, Omega, Theta e o piccolo con le costanti c ed n0; esempi con costanti esplicite; proprietà (polinomi, esponenziali, logaritmi, somme, implicazioni tra notazioni); sommatorie notevoli; terminologia (logaritmica, lineare, polinomiale, esponenziale).Notazione asintotica → e Serie notevoli - geometrica, telescopica, armonicaLe serie di cui si conosce il carattere e da usare come termine di paragone: geometrica (converge a 1/(1-q) se |q|<1), telescopiche (somma b_1 - lim b_n, come Mengoli), armonica generalizzata (1/n^alpha converge se e solo se alpha>1).Serie notevoli - geometrica, telescopica, armonica →; per si ha ). Quindi il costo totale di inserimenti è al più (scritture) più (copie), cioè , e il costo medio per inserimento è : un singolo inserimento può costare , ma sono rari. Se invece si allungasse l'array di una sola cella ogni volta si copierebbero elementi in totale:
Grafico interattivo: Elementi copiati in totale dopo n inserimenti in fondo: con raddoppio (2^ceil(log2 n) − 1, sempre sotto 2n) contro allungamento di una cella alla volta (n(n−1)/2); a n = 30 sono 31 contro 435
Lista position-based
L'elemento si identifica con la posizione (Position<E>, con getElement()): il contenitore, o nodo, che lo ospita. Metodi: first(), last(), before(p), after(p), addFirst(e), addLast(e), addBefore(p, e), addAfter(p, e), remove(p), set(p, e).
Definizione (lista position-based con doppia lista e sentinelle). Si realizza con una lista doppiamente concatenata: ogni nodo punta a predecessore e successore, e due nodi sentinella (senza elemento) delimitano inizio e fine, così nessun inserimento o rimozione ha casi particolari. Tutti i metodi costano , ma la lista si scandisce solo in sequenza (arrivare al nodo costa ). L'implementazione in C con puntatori è in Liste concatenate in CLista singolarmente concatenata in C con nodo struct e testa passata per riferimento (Nodo **); addHead, addTail ricorsiva e iterativa, pop, inversione in loco, liberazione; coda con puntatori a testa e coda; ricorsione sulle liste; costi.Liste concatenate in C → e i puntatori in Puntatori in CUn puntatore contiene un indirizzo di memoria; operatori & e *, NULL, puntatori come parametri per modificare variabili del chiamante, aritmetica dei puntatori, legame tra array e puntatori, const, puntatori a puntatori.Puntatori in C →.
Come funziona la sentinella. Le sentinelle header e trailer sono nodi senza elemento, con header.next che punta al primo nodo vero e trailer.prev all'ultimo. Per inserire un nodo n con valore e tra p e il suo successore s: si pongono n.prev = p, n.next = s, poi p.next = n e s.prev = n (quattro assegnamenti, indipendenti dalla lunghezza). Poiché p e s esistono sempre (al limite sono le sentinelle) non servono casi speciali per lista vuota o inserimento agli estremi.
Esempio. Dato un nodo , addAfter(p, e) collega il nuovo nodo ai suoi due vicini in passi, senza scorrere la lista.
| array (index-based) | doppia lista (position-based) | |
|---|---|---|
| accesso per indice | ||
| inserire/togliere dato un nodo | ||
| spazio | compatto | un nodo con due puntatori per elemento |
Si può realizzare una lista index-based con una doppia lista o una position-based con un array, ma con pochi vantaggi.
Pila (stack)
Definizione (pila LIFO). Inserimenti e rimozioni secondo LIFO (last in, first out):
push(e)inserisce in cima,top()legge la cima senza togliere,pop()toglie e restituisce la cima, piùsize()eisEmpty(). Con un array e un indicetdi cima, o con una lista concatenata con inserimento e rimozione in testa, tutti i metodi sono (con array estendibile, ammortizzato). Esempi: stack delle chiamate di una funzione (i record di attivazione di Algoritmi ricorsiviAlgoritmi ricorsivi come induzione eseguita; albero della ricorsione e record di attivazione nello stack; esempi ReverseArray, LinearSum, Power in tempo logaritmico, Fibonacci ricorsivo (esponenziale) e con memoizzazione; analisi della complessità con l'albero della ricorsione e correttezza per induzione.Algoritmi ricorsivi → e Stack e chiamata di funzioni in ARMChiamata con BL e ritorno con BX LR; convenzione di chiamata AAPCS (argomenti in r0-r3, risultato in r0, registri da preservare r4-r11); stack discendente pieno con PUSH e POP; record di attivazione; funzioni foglia e non foglia; esempio ricorsivo del fattoriale con evoluzione dello stack.Stack e chiamata di funzioni in ARM →), annulla/ripeti, controllo delle parentesi.
Controllo delle parentesi. Si scorre la stringa: ogni parentesi aperta si inserisce con push; per ogni parentesi chiusa si fa pop e si controlla che sia del tipo giusto. Su ( [ ] ): push(, push[, ] pop dà [ ✓, ) pop dà ( ✓, pila vuota alla fine: corretta. Su ( ]: push(, poi ] richiede [ ma pop dà ( ✗: errata. La pila vuota a fine stringa è necessaria (altrimenti restano parentesi aperte). Costo perché ogni carattere fa al più un push o un pop di costo .
Esempio. Dopo push(1), push(2), push(3) si ha top() = 3; un pop() restituisce 3 e la nuova cima è 2.
Coda (queue)
Definizione (coda FIFO e buffer circolare). Secondo FIFO (first in, first out):
enqueue(e)inserisce in fondo,first()legge il primo,dequeue()toglie e restituisce il primo. Con una lista concatenata con puntatori a testa e coda, tutto in . Con un array si usa un buffer circolare: indicefdel primo esize;enqueuescrive in(f + size) mod N,dequeueavanzaf = (f + 1) mod N. Usata dalla visita in ampiezza dei grafi (vedi Visite di grafi - BFS e DFSVisite in ampiezza (BFS) e in profondità (DFS) come design pattern; etichette discovery, cross e back edge; BFS tree e distanze; complessità Theta(n+m) con liste di adiacenza; applicazioni (connettività, componenti, spanning tree, cammini minimi non pesati, cicli, vertici a distanza al più d); esempio svolto su un grafo di 7 vertici.Visite di grafi - BFS e DFS →).
Esempio. Con , primo in f = 3 e size = 2, enqueue scrive in ; dopo un dequeue si ha f = 4. Il resto della divisione per fa "tornare a capo" l'indice: le celle e sono occupate, la prossima libera è . Traccia con :
| operazione | f |
size |
celle occupate | cella scritta o letta |
|---|---|---|---|---|
| (iniziale, dopo vari giri) | 3 | 2 | 3, 4 | |
enqueue(x) |
3 | 3 | 3, 4, 0 | scrive in |
enqueue(y) |
3 | 4 | 3, 4, 0, 1 | scrive in |
dequeue() |
4 | 3 | 4, 0, 1 | legge dalla cella , poi |
La coda è piena quando size == N. Rispetto a un array in cui dequeue sposta tutti gli elementi a sinistra (), qui nessun elemento si sposta e tutte le operazioni costano . Il resto della divisione è definito in Notazione asintoticaDefinizioni di O, Omega, Theta e o piccolo con le costanti c ed n0; esempi con costanti esplicite; proprietà (polinomi, esponenziali, logaritmi, somme, implicazioni tra notazioni); sommatorie notevoli; terminologia (logaritmica, lineare, polinomiale, esponenziale).Notazione asintotica → (e per i numeri interi in Aritmetica binariaSomma e sottrazione in binario, overflow per senza segno (riporto) e per complemento a 2 (segni), flag del processore, moltiplicazione per somme e scorrimenti, algoritmo di Booth, divisione, shift logici e aritmetici.Aritmetica binaria →).
Definizione (deque). La deque (double-ended queue) permette inserimento e rimozione sia in testa sia in coda: unisce pila e coda.
Esempio. Dopo addFirst(1), addLast(2), addFirst(3) la deque contiene 3, 1, 2.
Iteratori
Un iteratore (Iterator<E>) esamina gli elementi di una collezione uno alla volta: hasNext() dice se c'è un prossimo elemento, next() lo restituisce. Una collezione iterabile (Iterable<E>) ha un metodo iterator() che ne restituisce uno. L'ordine di restituzione non è fissato dall'ADT ma dall'implementazione dell'iteratore. È il meccanismo con cui, per esempio, si scorrono i vicini di un vertice (incidentEdges(v)). È l'analogo del for x in lista di Python (Ciclo for e rangefor scorre gli elementi di un iterabile; range per gli indici; enumerate, zip, reversed, sorted; cicli annidati; quando usare for e quando while.Ciclo for e range →), in cui il ciclo chiama a ogni passo hasNext() e next() dietro le quinte.
Errori comuni
- Dimenticare che
add/removeper indice su array costano , non . - Confondere LIFO e FIFO: la pila restituisce l'ultimo inserito, la coda il primo.
- Dimenticare le sentinelle nella lista doppia (casi speciali agli estremi) o aggiornare solo uno dei due puntatori.
- Leggere (
top,first) o togliere da una pila o coda vuota.
Versione ripasso
- ADT = interfaccia (cosa); implementazione = come e a quale costo (vedi Problemi computazionali e algoritmiProblema computazionale come insieme di coppie (istanza, soluzione); algoritmo e modello di calcolo RAM; pseudocodice; taglia di un'istanza; ADT e struttura dati concreta; esempio svolto con ricerca lineare e binaria in un array ordinato.Problemi computazionali e algoritmi →, Tipi di dato astrattiUn ADT è definito dalle operazioni e dal loro comportamento, non dalla rappresentazione; interfaccia e implementazione; realizzazione in Python con le classi e in C con header e tipo opaco; esempio di un ADT Frazione.Tipi di dato astratti →). In C si usano
structe funzioni (vedi Liste concatenate in CLista singolarmente concatenata in C con nodo struct e testa passata per riferimento (Nodo **); addHead, addTail ricorsiva e iterativa, pop, inversione in loco, liberazione; coda con puntatori a testa e coda; ricorsione sulle liste; costi.Liste concatenate in C →). - Lista index-based (
get(i),set(i,e),add(i,e),remove(i),size(),isEmpty()): su arraygetesetsono ,adderemoveper gli spostamenti. - Array estendibile: se è pieno si alloca il doppio e si copia. Partendo da capacità , dopo inserimenti le copie totali sono meno di (serie geometrica, ): costo ammortizzato in fondo. Allungare di una cella alla volta costerebbe copie.
- Lista position-based (
first,last,before,after,addBefore,addAfter,remove(p),set(p,e)): doppia lista con due sentinelleheaderetrailer; tutti i metodi sono , ma arrivare al nodo -esimo costa . - Inserimento con sentinelle: tra e il suo successore bastano quattro assegnamenti (
n.prev = p,n.next = s,p.next = n,s.prev = n), senza casi speciali per lista vuota o estremi. - Pila LIFO:
push(e),top(),pop(), tutti . Esempi: stack delle chiamate (vedi Algoritmi ricorsiviAlgoritmi ricorsivi come induzione eseguita; albero della ricorsione e record di attivazione nello stack; esempi ReverseArray, LinearSum, Power in tempo logaritmico, Fibonacci ricorsivo (esponenziale) e con memoizzazione; analisi della complessità con l'albero della ricorsione e correttezza per induzione.Algoritmi ricorsivi →); controllo delle parentesi conpushper le aperte epopper le chiuse, pila vuota alla fine. Costo . - Coda FIFO:
enqueue(e),first(),dequeue(), con lista con testa e coda, oppure con array circolare:enqueuescrive in ,dequeuepone . Esempio con , , :enqueuescrive nella cella . - Deque: inserimento e rimozione sia in testa sia in coda. Esempio:
addFirst(1),addLast(2),addFirst(3)dà3, 1, 2. - Iteratori:
Iterator<E>conhasNext()enext();Iterable<E>coniterator(). L'ordine dipende dall'implementazione; è il meccanismo delfordi Python (Ciclo for e rangefor scorre gli elementi di un iterabile; range per gli indici; enumerate, zip, reversed, sorted; cicli annidati; quando usare for e quando while.Ciclo for e range →). - Esempi di uso: pila per annulla/ripeti; coda per la BFS dei grafi (vedi Visite di grafi - BFS e DFSVisite in ampiezza (BFS) e in profondità (DFS) come design pattern; etichette discovery, cross e back edge; BFS tree e distanze; complessità Theta(n+m) con liste di adiacenza; applicazioni (connettività, componenti, spanning tree, cammini minimi non pesati, cicli, vertici a distanza al più d); esempio svolto su un grafo di 7 vertici.Visite di grafi - BFS e DFS →).
- Costo di
addsu array: inserire in testa a un array di elementi sposta tutti gli elementi, quindi ; in fondo, invece, si paga solo l'ammortizzato. - Errori tipici: pensare
add/removeper indice su array; LIFO e FIFO scambiati; sentinelle o puntatori dimenticati;top,firstodequeuesu una struttura vuota.
Esercizi su questo argomento
Teoria collegata
- Cammini minimi e algoritmo di Dijkstra
- Code con priorità
- Grafi diretti e ordinamento topologico
- Heap
- Liste concatenate in C
- Mappe e dizionari
- Problemi computazionali e algoritmi
- Puntatori, struct e memoria dinamica in C
- Rappresentazione dei grafi
- Tabelle hash
- Visite di grafi - BFS e DFS
- Java - generics, reflection e passaggio dei parametri