Salta al contenuto
Note per Studenti Liste, pile e code

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 i≥0i \ge 0 = numero di elementi che lo precedono.

Metodo Effetto
size(), isEmpty() numero di elementi, lista vuota
get(i) elemento in posizione ii, senza toglierlo
set(i, e) sostituisce l'elemento in ii con e e restituisce il vecchio
add(i, e) inserisce e all'indice ii spostando a destra i successivi
remove(i) toglie e restituisce l'elemento in ii, 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, size in O(1)O(1); add e remove in O(n)O(n) per gli spostamenti. Se l'array è pieno si alloca un array di taglia doppia e si copiano gli elementi: il costo Θ(n)\Theta(n) della copia è ammortizzato dai nn inserimenti precedenti, quindi l'inserimento in fondo costa O(1)O(1) ammortizzato.

Esempio. Partendo da capacità 11 e raddoppiando, 88 inserimenti in fondo copiano in tutto 1+2+4=71+2+4=7 elementi: meno di una copia per inserimento.

Perché l'ammortizzato è O(1)O(1). Con capacità iniziale 11, la copia avviene quando l'array contiene 1,2,4,8,…1, 2, 4, 8, \dots elementi ed è pieno: copia rispettivamente 1,2,4,8,…1, 2, 4, 8, \dots elementi. Dopo nn inserimenti le copie sono avvenute per le potenze di 22 minori di nn, e il loro totale è la somma geometrica 1+2+4+⋯+2k=2k+1−1<2ncon 2k<n≤2k+11 + 2 + 4 + \dots + 2^{k} = 2^{k+1} - 1 < 2n \qquad \text{con } 2^k < n \le 2^{k+1} (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 2k<n2^k<n si ha 2k+1−1<2n2^{k+1}-1<2n). Quindi il costo totale di nn inserimenti è al più nn (scritture) più 2n2n (copie), cioè Θ(n)\Theta(n), e il costo medio per inserimento è Θ(1)\Theta(1): un singolo inserimento può costare Θ(n)\Theta(n), ma sono rari. Se invece si allungasse l'array di una sola cella ogni volta si copierebbero 1+2+⋯+(n−1)=n(n−1)21+2+\dots+(n-1)=\frac{n(n-1)}{2} 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 O(1)O(1), ma la lista si scandisce solo in sequenza (arrivare al nodo ii costa Θ(i)\Theta(i)). 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 pp, addAfter(p, e) collega il nuovo nodo ai suoi due vicini in O(1)O(1) passi, senza scorrere la lista.

array (index-based) doppia lista (position-based)
accesso per indice O(1)O(1) Θ(i)\Theta(i)
inserire/togliere dato un nodo O(n)O(n) O(1)O(1)
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() e isEmpty(). Con un array e un indice t di cima, o con una lista concatenata con inserimento e rimozione in testa, tutti i metodi sono O(1)O(1) (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[, ] →\to pop dà [ ✓, ) →\to 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 Θ(n)\Theta(n) perché ogni carattere fa al più un push o un pop di costo O(1)O(1).

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 O(1)O(1). Con un array si usa un buffer circolare: indice f del primo e size; enqueue scrive in (f + size) mod N, dequeue avanza f = (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 N=5N=5, primo in f = 3 e size = 2, enqueue scrive in (3+2) mod 5=0(3+2)\bmod5=0; dopo un dequeue si ha f = 4. Il resto della divisione per NN fa "tornare a capo" l'indice: le celle 33 e 44 sono occupate, la prossima libera è 5 mod 5=05 \bmod 5 = 0. Traccia con N=5N=5:

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 (3+2) mod 5=0(3+2)\bmod5=0
enqueue(y) 3 4 3, 4, 0, 1 scrive in (3+3) mod 5=1(3+3)\bmod5=1
dequeue() 4 3 4, 0, 1 legge dalla cella 33, poi f=(3+1) mod 5=4f=(3+1)\bmod5=4

La coda è piena quando size == N. Rispetto a un array in cui dequeue sposta tutti gli elementi a sinistra (O(n)O(n)), qui nessun elemento si sposta e tutte le operazioni costano O(1)O(1). 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/remove per indice su array costano O(n)O(n), non O(1)O(1).
  • 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

Esercizi su questo argomento

Teoria collegata