FormularioDati e Algoritmi: definizioni, teoremi e formule delle note, in ordine di capitolo
In questa pagina 11
I. Nozioni fondamentali
Problemi computazionali e algoritmi
Definizione (problema computazionale). Un problema computazionaleDice quali risposte sono corrette per ogni possibile input, senza dire come trovarle. Esempio: per la somma, all'input (3, 4) corrisponde la risposta 7. Il modo di calcolare la risposta è l'algoritmo. è un insieme di coppie con (istanza, dominio delle istanze) e (soluzione, dominio delle soluzioni). Si richiede che ogni istanza abbia almeno una soluzione, cioè che per ogni esista almeno un con . Le coppie stanno in , quindi . Il simbolo è il prodotto cartesianoInsieme di tutte le coppie ordinate (i, s) con i in I e s in S. dei due insiemi (vedi Operazioni tra insiemiUnione, intersezione, differenza, complementare e prodotto cartesiano, con le leggi di De Morgan e il legame con la logica.Operazioni tra insiemi →): ne è un sottoinsieme, cioè una relazione tra istanze e soluzioni. Se ogni istanza avesse una sola soluzione, sarebbe il grafico di una funzioneUna funzione associa a ogni elemento del dominio un solo valore: dominio naturale, grafico, immagine, antimmagine e metodo grafico.Funzioni - definizione, dominio e grafico → da a ; qui si ammette il caso più generale, con più soluzioni possibili per la stessa istanza.
Esempio. Per la somma di interi (gli insiemi numerici sono in Insiemi numerici (N, Z, Q, R)Naturali, interi, razionali e reali, perché la radice di 2 obbliga a passare da Q a R, e gli intervalli.Insiemi numerici (N, Z, Q, R) →) l'istanza ha come soluzione , e e hanno la stessa soluzione .
Definizione (macchina RAM). Il modello usato è la macchina RAMRandom Access Machine: input, output, dati intermedi e programma stanno in memoria, e ogni accesso a una cella costa un passo. Non c'entra la memoria RAM del PC.: i passi elementari sono assegnamento, operazioni logiche e aritmetiche, indicizzazione di array, restituzione di un valore da un metodo; ciascuno costa (la notazione è 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 →: significa "un numero di operazioni limitato da una costante che non dipende dalla taglia dell'input"). Il modello rispecchia un vero elaboratore, in cui processore e memoriaModello di von Neumann: CPU, memoria centrale, bus e periferiche; ciclo fetch-decode-execute e gerarchia di memoria.Struttura di un elaboratore → leggono o scrivono una cella con una sola istruzione e la memoria è indirizzabile per posizione; i casi in cui questa semplificazione non regge (accessi lenti fuori dalla cache) sono in Gerarchia di memoria e principio di localitàCaratteristiche delle memorie (posizione, capacità, unità di trasferimento, metodo di accesso, prestazioni, volatilità); compromesso costo-capacità-velocità e gerarchia a livelli; località temporale e spaziale con esempi; hit, miss e tempo medio di accesso con calcolo svolto.Gerarchia di memoria e principio di località →.
Esempio. A[i] <- A[i] + 1 è una indicizzazione, una addizione e un assegnamento: tre passi elementari, in tutto .
Definizione (taglia di un'istanza). La taglia è una funzione che associa a ogni istanza uno o più numeri che ne misurano la grandezza: la lunghezza di un array, il numero di nodi di un albero, il numero di vertici e archi di un grafo ( ed ). La complessità si esprime in funzione della taglia (vedi Complessità in tempo e caso pessimoPerché lo studio sperimentale non basta; complessità al caso pessimo come massimo sul numero di operazioni tra le istanze di una data taglia; stima con limiti superiore e inferiore senza trovare l'istanza peggiore; esempi arrayMax, prefixAverages e InsertionSort; efficienza asintotica e limiti dell'analisi.Complessità in tempo e caso pessimo →).
Esempio. La taglia di un array di interi è ; quella di un grafo è la coppia di vertici e archi.
Proprietà (ricerca binaria: invariante e complessità)
- Invariante: se è in , allora la sua posizione sta in . Vale all'inizio ( è tutto l'array) e si conserva: se , per l'ordine crescente tutti gli elementi in posizione valgono al più , quindi può stare solo a destra di e si pone ; il caso è simmetrico. Al termine del ciclo () l'intervallo è vuoto, quindi per l'invariante non c'è in e si restituisce . Il metodo dell'invariante è spiegato in Dimostrazioni, induzione e invariantiTecniche di dimostrazione (esempio, controesempio, assurdo), induzione con casi base multipli, invarianti di ciclo (inizializzazione, conservazione, uso alla fine), schema generale per provare la correttezza; esempi svolti su arrayMax, sequenza di bit e numeri di Fibonacci.Dimostrazioni, induzione e invarianti →.
- Complessità: l'intervallo dimezza a ogni iterazione, quindi ci sono al più iterazioni di costo costante: .
Esempio. Su con la ricerca binaria restituisce l'indice dopo iterazioni; con ne servono al più .
Grafico interattivo: Passi al caso pessimo: ricerca lineare, n, contro ricerca binaria, floor(log2 n)+1 iterazioni; a n = 100 sono 100 contro 7
Complessità in tempo e caso pessimo
Definizione (complessità in tempo al caso pessimo). Sia il numero di operazioni eseguite da sull'istanza . La complessità in tempo al caso pessimo è
Esempio. InsertionSort su elementi fa confronti sull'array ordinato e sull'array invertito : il caso pessimo è il massimo, .
Proprietà (stima con limiti superiore e inferiore). Trovare l'istanza peggiore e contare i passi esatti è difficile e inutile. Si cercano due funzioni semplici:
- limite superiore : si argomenta che per ogni abbastanza grande, ogni istanza di taglia costa al più operazioni;
- limite inferiore : si argomenta che per ogni abbastanza grande esiste un'istanza di taglia che costa almeno (a volte è comodo mostrare che tutte le istanze costano tanto).
Se coincidono si ha (stima stretta; le definizioni di , e con le costanti ed sono 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 →). La funzione deve essere la più vicina possibile al vero andamento (tight) e la più semplice (niente costanti, niente termini di ordine inferiore).
Esempio. Se ogni istanza di taglia costa al più operazioni e una di esse ne costa almeno , allora .
Notazione asintotica
Definizione (notazioni , , e )
Notazione Definizione Si legge : per ogni cresce al più come : per ogni almeno come e , cioè definitivamente come : per ; equivale a strettamente meno di
Esempio. con e , perché (si porta a destra: ).
Grafico interattivo: 3n+4 è O(n): con c = 4 la retta 4n sta sopra 3n+4 da n0 = 4 in poi (in n = 4 valgono entrambe 16)
Proprietà (polinomio contro esponenziale, logaritmo contro potenza)
- Polinomio contro esponenziale: se , (limite ; è la gerarchia degli infiniti di Limiti notevoli e gerarchia degli infinitiI limiti di base per sciogliere le forme indeterminate delle successioni: potenze, polinomi, il numero e, esponenziali, la gerarchia logaritmi < potenze < esponenziali < fattoriale < n^n e la razionalizzazione.Limiti notevoli e gerarchia degli infiniti →, che si può giustificare con il criterio del rapporto: , quindi il rapporto tende a , vedi Criteri del rapporto e della radice per successioniPer a_n > 0: se a_(n+1)/a_n oppure la radice n-esima di a_n tende a un limite minore di 1 la successione tende a 0, se maggiore di 1 tende a +∞; con limite 1 non si conclude.Criteri del rapporto e della radice per successioni →).
- Logaritmo contro potenza: per , . Si pone : con e per il punto 2.
Esempio. Per si ha mentre , coerente con .
Proprietà (legami tra le notazioni)
- Legami tra notazioni:
- (si prende );
- (si divide per );
- e ;
- (perché );
- non valgono: () né (, ).
Esempio. Con e : e , ma .
Formula (sommatorie notevoli)
Esempio. e .
Dimostrazioni, induzione e invarianti
Teorema (principio di induzione). Per provare che vale per ogni :
- si sceglie (di solito );
- base: si dimostra ;
- passo: si fissa arbitrario e, assumendo vera per ogni con (ipotesi induttiva), si dimostra .
Esempio 1. per (sommatorie: SommatorieIl simbolo di sommatoria, le sue proprietà (linearità, additività, cambio di indice) e le somme notevoli di Gauss e geometrica.Sommatorie →). Base : a sinistra c'è la somma del solo termine , cioè ; a destra . Passo: si suppone vera la formula per e si scrive la somma fino a isolando l'ultimo termine, dove si è messo a denominatore comune e raccolto . L'ultimo membro è proprio la formula con al posto di . ✓
Definizione (invariante di ciclo). Un invarianteProprietà sulle variabili del ciclo che descrive lo stato a ogni iterazione. è una proprietà delle variabili usate nel ciclo che:
- vale all'inizio del ciclo, come conseguenza dello stato iniziale (inizializzazione);
- vale alla fine di ogni iterazione, assumendo che valesse all'inizio di quella iterazione (conservazione);
- alla fine dell'ultima iterazione, insieme alla condizione di uscita, implica la correttezza del ciclo (uso).
Esempio. In arrayMax l'invariante «alla fine dell'iterazione , » vale all'inizio (), si conserva a ogni passo e a fine ciclo () dà il massimo dell'intero array.
Algoritmi ricorsivi
Definizione (algoritmo ricorsivo). Un algoritmo ricorsivo invoca se stesso su istanze sempre più piccole. La soluzione di un'istanza di taglia si ottiene:
- direttamente per i casi base ;
- altrimenti usando le soluzioni di una o più istanze di taglia .
Esempio. LinearSum(A, n) ha il caso base e per usa LinearSum(A, n-1): su con restituisce .
Formula (potenza in tempo logaritmico). se ; se dispari; se pari e positivo.
Esempio. Power(2, 10) calcola con chiamate () e restituisce .
Formula (numero di chiamate di fib). Il numero di chiamate soddisfa e , cioè : (; per sono chiamate, verificato eseguendo il codice). Cresce come : esponenziale. Lo spreco è nei sottoproblemi ripetuti ( è ricalcolato più volte).
Esempio. chiamate per fib(5).
{"tipo":"funzione","titolo":"Chiamate di fib(n): senza memoizzazione crescono come 2Φ^(n+1)/√5 − 1 (C(5) = 15, C(10) = 177), con memoizzazione sono 2n − 1 (9 per n = 5, 19 per n = 10)","curve":[{"espressione":"2*((1+sqrt(5))/2)^(x+1)/sqrt(5)-1","etichetta":"senza memoizzazione: C(n)"},{"espressione":"2*x-1","etichetta":"con memoizzazione: 2n−1"}],"x":[1,12],"y":[0,500],"punti":[{"x":5,"y":15,"etichetta":"15"},{"x":10,"y":177,"etichetta":"177"}],"assi":{"x":"n","y":"chiamate"}}
``` Con la **memoizzazione** (si salva ogni risultato in una tabella e lo si riusa) per le chiamate scendono a e in generale a (ciascun è calcolato una volta sola). Per : la discesa fa chiamate e riempie la tabella; poi chiama (caso base, una chiamata), chiama (già in tabella) e chiama (già in tabella): . In generale chiamate di discesa più chiamate che trovano già il risultato, circa . La memoizzazione usa di memoria per la tabella: è lo scambio tempo contro spazio.
*Esempio.* chiamate per `fib(5)`.
## Correttezza per induzione
Sia la taglia. Si dimostra la correttezza per i casi base ; poi, assumendo che l'algoritmo risolva correttamente tutte le istanze di taglia , si dimostra che risolve quelle di taglia .
**LinearSum.** Base : restituisce ✓. Passo: per ipotesi `LinearSum(A, n)` , quindi `LinearSum(A, n+1)` ✓.
**Power.** Base : ✓. Passo: si assume che `Power` sia corretta per **tutti** gli esponenti da a (ipotesi forte: serve perché la chiamata ricorsiva è su circa la metà dell'esponente, non su ). Se è pari, la chiamata è su (vero per ), quindi per ipotesi e . Se è dispari, la chiamata è su , quindi e ✓ (si usa , vedi <a class="wiki glossa" href="/analisi-1/appunti/funzioni-potenza/" aria-describedby="sp-17">Funzioni potenza<span class="spiega" role="tooltip"><span class="spiega-testo" id="sp-17">Le potenze x^alfa al variare dell'esponente: domini, grafici, radici n-esime e proprietà delle potenze con base positiva.<span class="spiega-vai">Funzioni potenza →</span></span><span class="graffa" aria-hidden="true"><i class="g-l"></i><i class="g-linea"></i><i class="g-punta"></i><i class="g-linea"></i><i class="g-r"></i></span></span><!--/spiega--></a>).
## Errori comuni
- Dimenticare un caso base, o una chiamata che non riduce la taglia (non termina): in `ReverseArray` serve .
- Contare il costo di un nodo **includendo** le chiamate figlie (le conta due volte).
- Ricalcolare più volte la stessa sottoistanza (`Power` con due chiamate, `fib` senza memoizzazione).
- Dimenticare che la ricorsione consuma stack: profondità ⇒ spazio .
II. Il linguaggio C
Puntatori, struct e memoria dinamica in C
Definizione (puntatore). Un puntatore è una variabile che contiene un indirizzo di memoria.
&xè l'indirizzo dix,*pè il valore puntato dap(dereferenziazione),NULLè il puntatore che non punta a nulla.
Esempio. Con int x = 5; int *p = &x; *p = 7; si ottiene x == 7. Passo per passo, supponendo che x sia all'indirizzo (valore indicativo):
Proprietà (passaggio dei parametri per valore). C passa i parametri per valore: la funzione riceve una copia. Per modificare una variabile del chiamante si passa il suo indirizzo.
Esempio. Con void incrementa(int *p) { (*p)++; } e int n = 5; incrementa(&n); si ha n == 6.
Proprietà (array e aritmetica dei puntatori). Un array è un blocco contiguo; il suo nome si comporta come puntatore al primo elemento, quindi
v[i]equivale a*(v + i)(la somma avanza diielementi, non diibyte). Un array passato a una funzione arriva come puntatore: la lunghezza non si conserva e va passata a parte (int n). Nessun controllo sui limiti:v[n]è un errore silenzioso.
Esempio. Con int v[3] = {10, 20, 30}; si ha *(v + 2) == v[2] == 30. Se v fosse all'indirizzo e sizeof(int) fosse , gli elementi occuperebbero i byte –, –, – e v + 2 varrebbe : il compilatore moltiplica i per la dimensione del tipo puntato. Per questo v[i] costa (un'addizione e una lettura), la stessa assunzione del modello RAMProblema 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 →. Altri dettagli sugli array sono in Array e stringhe in CArray di dimensione fissa in memoria contigua, inizializzazione, nessun controllo sugli indici, passaggio a funzioni con la lunghezza, matrici; stringhe come array di char terminati da '\0' e funzioni di string.h.Array e stringhe in C →.
Definizione (stringa in C). Una stringa è un array di
charterminato da'\0':char nome[64]contiene al più 63 caratteri più il terminatore. Si copia constrcpy, si confronta constrcmp, mai con=o==.
Esempio. strlen("ciao") vale 4, ma la stringa occupa sizeof("ciao") == 5 byte per il terminatore '\0': in memoria ci sono i cinque char 'c', 'i', 'a', 'o', '\0'. I caratteri sono memorizzati come interi piccoli secondo una codifica (vedi Codifiche binarie e informazione non numericaBit, byte e multipli (potenze di 2 e di 10); codici BCD e Gray; caratteri ASCII, Unicode e UTF-8; ordine dei byte (little e big endian); bit di parità e codice di Hamming per rilevare e correggere errori.Codifiche binarie e informazione non numerica →) e '\0' è il valore . strlen conta i caratteri fino al primo '\0', quindi costa e non . == tra due stringhe confronterebbe i due indirizzi, non il contenuto.
Definizione (operatori . e ->).
.si usa su una struct,->su un puntatore a struct. Una struct che contiene un puntatore a se stessa permette le strutture collegate:typedef struct nodo { int val; struct nodo *next; /* dentro la definizione serve il nome "struct nodo" */ } Nodo;
Esempio. Con Studente s; s.voto = 28; Studente *ps = &s; ps->voto++; si ottiene s.voto == 29.
Definizione (memoria dinamica con malloc e free). Le variabili locali vivono nello stack e spariscono quando la funzione termina;
mallocalloca nello heap un blocco che dura finché non lo si libera confree.
Esempio. int *a = malloc(4 * sizeof(int)); riserva 16 byte nello heap; dopo a[i] = i * i si ha a[3] == 9, e free(a); li libera.
Proprietà (puntatore a puntatore). Un puntatore passato per valore è una copia: assegnarlo dentro la funzione non cambia quello del chiamante. Per modificarlo (tipicamente la testa di una lista o la radice di un albero) si passa il suo indirizzo, cioè un
Nodo **.
Esempio. Con void azzeraGiusto(int **p) { *p = NULL; }, la chiamata azzeraGiusto(&a) lascia a == NULL.
Liste concatenate in C
Definizione (lista concatenata). Una lista concatenata è una sequenza di nodi allocati nello heap, ciascuno con un valore e un puntatore al successivo; la lista è identificata dal puntatore alla testa (
NULL= lista vuota). Premessa: Puntatori, struct e memoria dinamica in CPuntatori e passaggio per riferimento in C, array e aritmetica dei puntatori, stringhe, struct e typedef con l'operatore ->, malloc e free, puntatore a puntatore per modificare una testa; compilazione con Makefile; errori tipici (puntatori pendenti, perdite di memoria, off-by-one).Puntatori, struct e memoria dinamica in C →. È l'implementazione concreta degli ADT di Liste, pile e codeRipasso degli ADT elementari: lista index-based (array) e position-based (lista doppiamente concatenata con sentinelle), pila LIFO, coda FIFO, deque, iteratori; interfacce, implementazioni e costi; array estendibile e coda circolare.Liste, pile e code →. La stessa struttura è presentata, in versione più elementare, in 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 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 → (Fondamenti di Informatica); i puntatori usati qui sono quelli di 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 → e i blocchi dimallocvivono nello heap (Gestione della memoria in CSegmenti di memoria di un processo (codice, dati statici, stack, heap); durata delle variabili; allocazione dinamica con malloc, calloc, realloc e free; errori classici: memory leak, dangling pointer, double free, buffer overflow; strumenti di controllo.Gestione della memoria in C →).
Esempio. Dopo addHead di , , in una lista vuota la lista è 3 2 1, con la testa che punta a 3. Stato dopo ciascuna chiamata (la freccia è il campo next):
Proprietà (testa passata per riferimento). Ogni funzione che può cambiare la testa riceve un
Nodo **l(puntatore alla variabile testa del chiamante).
Esempio. Con Nodo *l = NULL; la chiamata addHead(&l, 5) rende l != NULL: senza il doppio puntatore la testa del chiamante non cambierebbe.
Definizione (inversione in loco). Inversione in loco, e senza nodi nuovi: si ritorcono i puntatori scorrendo la lista.
Esempio. La lista 3 2 1 10 20 diventa 20 10 1 2 3 dopo inverti. Traccia su una lista più corta, 1 2 3 (ogni riga è lo stato alla fine dell'iterazione del while):
Definizione (coda con puntatori a testa e coda). Per rendere anche l'inserimento in fondo si tengono due puntatori, in una struct:
typedef struct { Nodo *testa; Nodo *coda; } Coda; /* estrae da testa, inserisce in coda */ void enqueue(Coda *q, int v) { Nodo *n = malloc(sizeof(Nodo)); n->val = v; n->next = NULL; if (q->coda == NULL) q->testa = n; /* coda vuota */ else q->coda->next = n; q->coda = n; } int dequeue(Coda *q) { /* precondizione: non vuota */ Nodo *n = q->testa; int v = n->val; q->testa = n->next; if (q->testa == NULL) q->coda = NULL; /* si e' svuotata */ free(n); return v; } int isEmpty(const Coda *q) { return q->testa == NULL; }
Esempio. Inseriti con enqueue, due dequeue danno e (ordine FIFO).
Proprietà (costi delle operazioni sulle liste concatenate)
Operazione Lista semplice Con puntatore alla coda addHead,popaddTailricerca di un valore accesso al nodo -esimo
Esempio. Con nodi addTail ne scorre in una lista semplice e nessuno con il puntatore alla coda. Costruire una lista di elementi con addTail su lista semplice costa (somma di SommatorieIl simbolo di sommatoria, le sue proprietà (linearità, additività, cambio di indice) e le somme notevoli di Gauss e geometrica.Sommatorie →, come in Complessità in tempo e caso pessimoPerché lo studio sperimentale non basta; complessità al caso pessimo come massimo sul numero di operazioni tra le istanze di una data taglia; stima con limiti superiore e inferiore senza trovare l'istanza peggiore; esempi arrayMax, prefixAverages e InsertionSort; efficienza asintotica e limiti dell'analisi.Complessità in tempo e caso pessimo →), contro con addHead o con il puntatore alla coda.
III. ADT elementari
Liste, pile e code
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.
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
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 →.
Esempio. Dato un nodo , addAfter(p, e) collega il nuovo nodo ai suoi due vicini in passi, senza scorrere la lista.
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.
Esempio. Dopo push(1), push(2), push(3) si ha top() = 3; un pop() restituisce 3 e la nuova cima è 2.
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 :
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.
IV. Alberi
Alberi
Definizione (albero radicato). Un albero radicato è una collezione di nodi che, se non vuota, soddisfa:
- esiste un nodo speciale , la radice;
- ogni ha un unico padre ( è figlio di );
- risalendo di padre in padre da ogni nodo si arriva a .
Definizione ricorsiva: con , dove sono alberi non vuoti le cui radici sono figlie di .
Esempio. Un albero con radice , figli e e un nodo figlio di ha nodi; le foglie sono e .
Definizione (terminologia degli alberi)
Termine Significato antenato di oppure antenato del padre di discendente di tale che è antenato di nodo interno nodo con almeno un figlio nodo esterno o foglia nodo senza figli sottoalbero con tutti i suoi discendenti albero ordinato per ogni nodo interno è fissato un ordine lineare tra i figli
Esempio. Nello stesso albero è antenato di , il sottoalbero è , è un nodo interno e , sono foglie.
Definizione (profondità, livello e altezza)
- Profondità = numero di antenati di meno 1; ricorsivamente: , altrimenti .
- Livello = insieme dei nodi a profondità .
- Altezza di un nodo: se è foglia, altrimenti . Altezza dell'albero: .
Esempio. Nello stesso albero , e .
Proposizione (altezza e profondità delle foglie). Se non è vuoto, .
Esempio. Le foglie e hanno profondità e , quindi . Sull'albero di riferimento le foglie hanno profondità e .
Visite di alberi
Definizione (visite in preorder e postorder). Chiamata iniziale:
preorder(T, T.root()). Preorder: prima il padre, poi i sottoalberi dei figli. Postorder: prima i sottoalberi dei figli, poi il padre.
Esempio. Sull'albero con radice , figli , e figlio di : preorder e postorder .
Formula (complessità di una visita). L'albero della ricorsione (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 →) ha un nodo per ogni nodo di . Sia il costo della visita di e il numero di figli. La chiamata su costa (il ciclo sui figli, escluse le chiamate ricorsive, più la visita). Poiché (vedi AlberiAlbero radicato (definizione per padre e ricorsiva), terminologia (antenati, discendenti, nodi interni ed esterni, sottoalbero, albero ordinato), profondità, livello, altezza; altezza = massima profondità delle foglie; algoritmi depth e height con costo; somma dei figli = n-1; esempio di algoritmo Omega(n^2) (heightBad).Alberi →: ogni nodo non radice ha un solo padre, quindi gli archi sono ):
Se ogni visita costa si ha ; se per ogni si ha .
Esempio. In un albero con nodi , quindi .
Definizione (antenato comune più basso, LCA). Dati in , è l'antenato comune più profondo. Algoritmo: si calcolano e risalendo; si porta il nodo più profondo a salire di passi, poi si salgono insieme e finché coincidono. Costo , al caso pessimo e senza strutture di appoggio. La differenza di profondità è un valore assolutoIl valore assoluto è la distanza di un numero da zero: definizione, proprietà, disuguaglianza triangolare e disequazioni con i moduli.Valore assoluto (modulo) → perché non si sa quale dei due nodi sia più profondo. Perché funziona: dopo aver portato i due nodi alla stessa profondità, i loro antenati alla stessa profondità sono confrontabili e il primo in comune che si incontra salendo è, per definizione, il più profondo tra quelli comuni.
Esempio. Nell'albero con radice , figli , e figlio di : e .
Alberi binari
Definizione (albero binario e albero binario proprio). Un albero binario è un albero ordinato (vedi AlberiAlbero radicato (definizione per padre e ricorsiva), terminologia (antenati, discendenti, nodi interni ed esterni, sottoalbero, albero ordinato), profondità, livello, altezza; altezza = massima profondità delle foglie; algoritmi depth e height con costo; somma dei figli = n-1; esempio di algoritmo Omega(n^2) (heightBad).Alberi →) in cui ogni nodo interno ha al più 2 figli, ogni nodo non radice è etichettato come figlio sinistro o destro del padre, e il figlio sinistro precede il destro. Se c'è un solo figlio si assume sia il sinistro. Il sottoalbero sinistro (destro) di è quello radicato nel suo figlio sinistro (destro).
Un albero binario è proprio (o pieno, full) se ogni nodo interno ha esattamente 2 figli. Un albero binario è un caso particolare di albero (AlberiAlbero radicato (definizione per padre e ricorsiva), terminologia (antenati, discendenti, nodi interni ed esterni, sottoalbero, albero ordinato), profondità, livello, altezza; altezza = massima profondità delle foglie; algoritmi depth e height con costo; somma dei figli = n-1; esempio di algoritmo Omega(n^2) (heightBad).Alberi →) e di grafo senza cicli (Grafi - definizioni e proprietàGrafo G=(V,E) diretto e non diretto, grafo semplice e pesato; incidenza, adiacenza, grado; cammini, cicli, sottografi, grafi connessi e componenti connesse; alberi liberi, foreste, spanning tree e spanning forest; proprietà con dimostrazioni (somma dei gradi = 2m, m <= n(n-1)/2, alberi m = n-1, connessi m >= n-1, foreste m <= n-1).Grafi - definizioni e proprietà →); le interfacce sono ADT (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 →).
Esempio. Un albero con la radice, un figlio sinistro con due foglie e un figlio destro foglia è binario proprio: ogni nodo interno ha figli e .
Teorema (relazioni in un albero binario proprio)
# Relazione 1 2 3 4 5
Esempio. Albero perfetto di altezza : , foglie e .
Grafico interattivo: Altezza di un albero binario proprio con n nodi: limite inferiore log2(n+1) − 1 (albero perfetto) e limite superiore (n − 1)/2 (albero a pettine); per n = 31 l'altezza è tra 4 e 15
Definizione (visita inorder). Oltre a preorder e postorder (vedi Visite di alberiVisite in preorder e postorder come schemi generali (template) da adattare; complessità Theta(n + somma dei costi di visita) perché la somma dei figli è n-1; esempi (indice di un libro, spazio occupato in un file system); profondità con il preorder, altezza con il postorder; antenato comune più basso.Visite di alberi →), negli alberi binari si ha la visita inorder: prima il figlio sinistro, poi il padre, poi il figlio destro.
Esempio. Sull'albero con radice e foglie e la visita inorder dà .
Definizione (parse tree di un'espressione). Un'espressione fully parenthesized in notazione infissa è una costante/variabile oppure con fully parenthesized e Op binario. Il parse tree è l'albero binario proprio che ha nelle foglie costanti e variabili, nei nodi interni gli operatori, con sottoalbero sinistro e destro uguali ai parse tree di ed ; le parentesi sono implicite nella struttura. Con operatori unari l'albero non è più proprio. Le espressioni aritmetiche e la precedenza degli operatori sono quelle di Tipi numerici ed espressioniint, float, complex e bool; operatori aritmetici con divisione intera e modulo; precedenza; conversioni di tipo; modulo math e arrotondamenti.Tipi numerici ed espressioni → (Fondamenti di Informatica); la costruzione del parse tree è una fase della compilazione (Paradigmi di programmazione e linguaggiAlgoritmo e programma; paradigmi imperativo, procedurale, a oggetti, funzionale e dichiarativo; compilazione e interpretazione; confronto tra Python e C.Paradigmi di programmazione e linguaggi →, Dal programma C all'eseguibile ARMCatena di traduzione compilatore, assemblatore, linker e loader; file oggetto, tabella dei simboli e rilocazione; collegamento statico e dinamico; organizzazione della memoria di un processo; chiamare funzioni assembly da C e viceversa, con esempio.Dal programma C all'eseguibile ARM →).
Esempio. con vale .
Definizione (heightsum). La heightsum di un albero binario proprio è la somma delle altezze di tutti i suoi nodi. Con la sola somma restituita da ogni sottoalbero non si potrebbe calcolare l'altezza del padre; si restituisce quindi una coppia (somma, altezza):
Algoritmo heightSum(T, v) Input: v in T Output: (somma delle altezze dei nodi di T_v, altezza di v) if T.isExternal(v) then return (0, 0) (sL, hL) <- heightSum(T, T.left(v)) (sR, hR) <- heightSum(T, T.right(v)) h <- max(hL, hR) + 1 return (sL + sR + h, h)
Esempio. In un albero con la radice, un figlio sinistro con due foglie e un figlio destro con una foglia e un nodo con due foglie, le altezze dei nodi interni sono e la heightsum vale .
V. Code con priorità e heap
Code con priorità
Definizione (coda con priorità). Una coda con priorità (priority queue) è una collezione di entry le cui chiavi, non necessariamente distinte, rappresentano priorità e provengono da un universo totalmente ordinato (due chiavi qualunque sono sempre confrontabili con , come per i numeri reali: Campi ordinati (Q e R)Gli assiomi di somma, prodotto e ordine comuni a Q e R, da cui derivano le regole di equazioni e disequazioni.Campi ordinati (Q e R) →). Ingressi e uscite si alternano in modo arbitrario, le priorità in ingresso non sono ordinate, le uscite avvengono in ordine di priorità.
interfaccia PriorityQueue<K,V>: size(), isEmpty() insert(k, v) inserisce e restituisce la nuova entry (k, v) min() restituisce una entry con chiave minima, senza toglierla removeMin() restituisce e toglie una entry con chiave minimaPer convenzione chiave minima = priorità massima. Una coda basata sul massimo ha
max()eremoveMax()al posto dimin()eremoveMin().
Esempio. Dopo insert(5,a), insert(2,b), insert(7,c), min() restituisce la entry con chiave e removeMin() la toglie.
Proprietà (code con priorità su liste: costi). Lista non ordinata.
insert: si aggiunge in coda, .min: si scorre tutta la lista tenendo la chiave minore, .removeMin: si trova il minimo e lo si toglie, .Lista ordinata (per chiave crescente).
mineremoveMin: si guarda/toglie il primo nodo, .insert: si scorre fino alla prima chiave e si inserisce prima di essa, .
Esempio. Con entry, removeMin scorre fino a entry in una lista non ordinata e ne guarda una sola in una lista ordinata. Nella lista ordinata insert(6, E) sul contenuto confronta con , , e si ferma davanti a (prima chiave ): il risultato è , con fino a confronti nel caso peggiore (chiave maggiore di tutte).
Proprietà (ordinamento con una coda con priorità). Per ordinare chiavi: si fanno
inserte poiremoveMin, che restituiscono le chiavi in ordine crescente. Il costo dipende dall'implementazione:
- lista non ordinata: (è il selection sort);
- lista ordinata: (è l'insertion sort);
- heap: (è l'heap sort).
Esempio. Con le chiavi , quattro insert seguiti da quattro removeMin restituiscono .
Grafico interattivo: Confronti di ordinamento con coda con priorità: liste (selection e insertion sort) n(n+1)/2 contro heap (heap sort) 2·n·log2 n; a n = 100 sono 5050 contro 1329, e il divario cresce con n
Heap
Definizione (albero binario completo). Un albero binario di altezza è completo se:
- per ogni livello con ci sono nodi (livello pieno);
- al livello i nodi sono tutti a sinistra delle eventuali mancanze: nessun buco, e al più l'ultimo nodo interno ha solo il figlio sinistro.
Esempio. Con i livelli e sono pieni ( nodi) e il livello ha da a nodi tutti a sinistra: va da a .
Proposizione (altezza di un albero binario completo). Un albero binario completo con nodi ha altezza .
Esempio. dà , dà e dà .
Grafico interattivo: Altezza di un albero binario completo, floor(log2 n): 3 per n = 11 (gli heap degli esempi), 5 per n = 63; ogni volta che n raddoppia l'altezza cresce di 1
Definizione (heap). Uno heap è un albero binario completo i cui nodi memorizzano entry e che soddisfa la heap-order property: per ogni nodo diverso dalla radice, . Equivale a: ogni nodo ha chiave quella dei figli. (Un max-heap inverte la disuguaglianza.) Il nodo last è il più a destra del livello .
Esempio. L'array è uno heap: , e .
Proprietà (heap con entry). Proprietà (heap con entry):
- la radice contiene una entry con chiave minima;
- le chiavi lungo un cammino dalla radice a una foglia formano una sequenza non decrescente;
- con chiavi distinte, se è l'entry con -esima chiave più piccola ( = radice) e sta a profondità , allora (gli antenati di sono altrettante entry con chiave minore, più stessa);
- con chiavi distinte, l'entry con chiave massima sta in una foglia (per assurdo: se fosse in un nodo interno, un suo figlio avrebbe chiave maggiore);
- per ogni nodo e ogni nodo nel sottoalbero , (per transitività lungo il cammino).
Esempio. Nello heap la radice è il minimo e il cammino è non decrescente.
Formula (level numbering di uno heap su array). Un heap sta in un array numerando i nodi per livelli, da sinistra a destra: la radice è , i figli di sono e , il padre di è e . I collegamenti padre-figlio sono impliciti (struttura dati implicita): nessun puntatore, spazio . (Nel testo adottato la numerazione parte da 0: figli , , padre .)
Esempio. In i figli di sono e , e il padre di è .
Proprietà (insert e up-heap bubbling). Si pone la nuova entry in posizione (la completezza è salva) e si ripristina l'heap-order risalendo.
Esempio. Su , insert(5) mette in posizione e lo fa salire fino alla : .
Proprietà (removeMin e down-heap bubbling). Si toglie la radice, si porta in radice l'ultima entry e si ripristina scendendo, scambiando sempre con il figlio di chiave minore.
Esempio. Su , removeMin() restituisce e lo heap diventa .
Proprietà (rimozione di una entry qualsiasi).
removeEntry(P, j): si pone e si decrementa . Sia l'entry spostata, il nuovo padre, i figli. Tre casi:
- : l'heap è già valido;
- : up-heap bubbling a partire da ;
- : down-heap bubbling a partire da .
Esempio. Su , removeEntry(P, 2) porta in posizione e lo fa scendere (down-heap): .
Costruzione di uno heap
Definizione (costruzione top-down con inserimenti successivi). Si considera uno heap di un elemento e si inserisce con l'up-heap bubbling.
Esempio. Con : è uno heap; insert di lo fa salire sopra (); insert di sale fino alla radice ().
Formula (costo della costruzione top-down). Costo. L'iterazione costa , quindi . È anche : se è in ordine decrescente ogni nuova chiave è minore di tutte le precedenti e sale fino alla radice, e
Quindi (anche
insertconsecutive in uno heap vuoto, vedi Esercizio 13 · costruzione di uno heap top-down e bottom-up).
Esempio. : dopo : ; ( sale sopra ): ; : ; ( sopra ): ; ( sopra , poi stop): . Gli scambi sono stati , in tutto .
Definizione (costruzione bottom-up). Le posizioni sono foglie e quindi già heap da un elemento. Si procede a ritroso sui nodi interni, "sistemando" ciascuno con il down-heap bubbling: ogni nodo ha come sottoalberi due heap già costruiti.
Esempio. Con si ha : sono foglie e si sistemano con il down-heap bubbling.
Formula (costo della costruzione bottom-up). Costo. Sia . Il down-heap da un nodo al livello () costa (al più la distanza dalle foglie), e al livello ci sono al più nodi. Il costo totale è
Si dimostra per induzione che : per è ; passando da a si somma e si ottiene . (Nel corso si dimostra la limitazione più debole , che basta.) Dunque il costo è , perché .
Esempio. (, ):
Grafico interattivo: Scambi al caso peggiore nella costruzione di uno heap con n elementi: top-down su array decrescente, (n+1)·⌊log₂n⌋ − 2^(⌊log₂n⌋+1) + 2 (34 per n = 15, 258 per n = 63, 1538 per n = 255), bottom-up al più n − 1 (somma delle altezze dei nodi: 11, 57 e 247 se n = 2^k − 1)
Definizione (heap sort). Costruzione bottom-up e poi
removeMinda : (vedi Algoritmi di ordinamentoSelection sort e insertion sort (quadratici, con invarianti), merge sort (divide et impera, Theta(n log n)), quick sort (caso pessimo quadratico, medio n log n), heap sort (in loco, Theta(n log n)), ordinamento senza confronti per chiavi intere in un intervallo piccolo, limite inferiore Omega(n log n) per gli algoritmi basati su confronti.Algoritmi di ordinamento →).
Esempio. Con chiavi: costruzione bottom-up e poi removeMin, ciascuno con circa scambi al più.
VI. Mappe e tabelle hash
Mappe e dizionari
Definizione (ADT mappa). Mappa: entry con chiavi distinte provenienti da un dominio su cui è definito
=; concetto analogo a un indice (associative array: la chiave è l'indice di accesso).
Metodo Effetto get(k)se esiste restituisce , altrimenti nullput(k, v)se esiste lo sostituisce con e restituisce ; altrimenti inserisce e restituisce nullremove(k)se esiste lo toglie e restituisce , altrimenti nullkeySet(),values(),entrySet()iteratori sulle chiavi, sui valori, sulle entry
Esempio. Su una mappa vuota: put("a", 1) restituisce null, poi put("a", 2) restituisce 1, get("a") restituisce 2 e remove("a") restituisce 2.
Definizione (dizionario o multimap). Ammette più entry con la stessa chiave. Metodi caratteristici:
get(k): restituisce una collezione (anche vuota) con tutti i valori associati a ;put(k, x): inserisce sempre una nuova entry , senza toccare le altre con la stessa chiave; non restituisce nulla;remove(k, v): toglie una entry con chiave e valore , se esiste; non restituisce nulla.
Esempio. Dopo put(k, 1) e put(k, 2), get(k) restituisce la collezione ; remove(k, 1) lascia .
Tabelle hash
Definizione (tabella hash). La tabella hash è un'implementazione efficiente della mappa (vedi Mappe e dizionariADT mappa (chiavi distinte) con get, put, remove, keySet/values/entrySet; famiglia mappa/mappa ordinata/dizionario (multimappa); applicazioni; implementazioni semplici ma poco efficienti (lista, array indicizzato dalle chiavi); dizionario realizzato con una mappa di liste.Mappe e dizionari →) con tempo medio costante. Ha tre componenti:
- una funzione hash ( universo delle chiavi), composta da hash code e compression function ;
- un bucket array di capacità : contiene le entry con ;
- un metodo di gestione delle collisioni: con e .
Esempio. Con e la chiave va nel bucket : e , quindi il resto è .
Definizione (hash code per stringhe)
- Stringhe :
- somma dei caratteri : non è buona:
stop,spot,topshanno lo stesso hash code (le permutazioni collidono);- polinomiale: (con la regola di Horner); per parole inglesi vanno bene ( nella classe
Stringdi Java);- cyclic shift: si somma ogni carattere dopo uno shift ciclico di posizioni della somma parziale:
h <- s0; for i <- 1 to k-1: h <- (h << 5) | (h >> 27); h <- h + s_i.
Esempio. Con , "ab" ha hash code polinomiale e "ba" ha , mentre con la somma dei caratteri entrambe valgono .
Definizione (compression function: divisione e MAD)
- Metodo della divisione: . Se non è primo le correlazioni tra gli hash code tendono a conservarsi; ad esempio con conta solo gli ultimi bit e con solo le ultime cifre decimali. In pratica si sceglie primo e lontano da potenze di .
- MAD (multiply-add-divide): con primo e scelti a caso, .
Esempio. Con l'hash code dà ; con MAD (, , ) l'hash code dà .
Definizione (load factor e separate chaining). Ogni bucket è una mappa più piccola, realizzata con una lista. Il load factor è ( entry, bucket): la lunghezza media di un bucket.
Esempio. Con entry e bucket: . Perché è la lunghezza media: con entry distribuite sui bucket in modo uniforme, il numero di entry nel bucket ha Valore attesoIl valore atteso E[X] = Σ x p_X(x) è la media dei valori di X pesata con le loro probabilità (esiste se la serie converge assolutamente); per una funzione g vale E[g(X)] = Σ g(x) p_X(x) senza trovare la legge di g(X), ed E è lineare: E[aX + bY + c] = aE[X] + bE[Y] + c.Valore atteso → (ogni entry cade nel bucket con probabilità , e il valore atteso di una somma è la somma dei valori attesi).
Proprietà (costo di una tabella hash: caso pessimo e medio). Per una tabella con entry
get,put,removecostano , dominate dalla ricerca della chiave nel bucket: banale; con tutte le entry nello stesso bucket e chiave cercata assente.Caso medio. Sotto l'ipotesi di uniform hashing la complessità media è , nel senso: ricerca senza successo, media su tutti i valori di equiprobabili; ricerca con successo, media su tutte le chiavi presenti equiprobabili inserite una dopo l'altra senza cancellazioni. In pratica si impone . Il load factor è il compromesso tra spazio e velocità: piccolo = tempo basso e spazio elevato; grande = tempo elevato e spazio ridotto. La qualità di determina quanto ci si avvicina all'ipotesi ideale.
Esempio. Con una ricerca senza successo costa in media circa confronti, mentre nel caso pessimo, con tutte le entry in un bucket, costa .
Grafico interattivo: Confronti medi per una ricerca in una tabella hash con separate chaining in funzione del load factor: senza successo 1 + λ, con successo circa 1 + λ/2; a λ = 0,75 (soglia di rehashing) sono 1,75 e 1,375
Definizione (rehashing). Quando supera una soglia (ad esempio ):
- si crea un nuovo bucket array di capacità (se serve, si cerca un primo );
- si sceglie una nuova funzione hash;
- si reinseriscono tutte le entry.
Esempio. Con e soglia il nono inserimento () causa il rehashing verso , per esempio il primo .
VII. Alberi di ricerca
Alberi binari di ricerca
Definizione (albero binario di ricerca). Un albero binario di ricerca (ABR, binary search tree) implementa una mappa ordinata (vedi Mappe e dizionariADT mappa (chiavi distinte) con get, put, remove, keySet/values/entrySet; famiglia mappa/mappa ordinata/dizionario (multimappa); applicazioni; implementazioni semplici ma poco efficienti (lista, array indicizzato dalle chiavi); dizionario realizzato con una mappa di liste.Mappe e dizionari →) su chiavi di un universo ordinato (Campi ordinati (Q e R)Gli assiomi di somma, prodotto e ordine comuni a Q e R, da cui derivano le regole di equazioni e disequazioni.Campi ordinati (Q e R) →). È l'analogo ad albero della ricerca binaria su array (Ricerca lineare e binariaRicerca lineare su sequenze qualsiasi in O(n); ricerca binaria su sequenze ordinate in O(log n), versione iterativa e ricorsiva, invariante e errori di indice; modulo bisect.Ricerca lineare e binaria →): a ogni confronto si scarta un intero sottoalbero come là si scartava metà intervallo, con il vantaggio che inserire e togliere non richiede di spostare gli elementi. È un albero binario proprio (vedi Alberi binariAlbero binario e albero binario proprio; interfaccia; relazioni tra nodi, foglie e altezza (m = n-m+1, h+1 <= m <= 2^h, 2h+1 <= n <= 2^(h+1)-1) con dimostrazioni; visita inorder; parse tree e valutazione di espressioni; heightSum come esempio di calcolo di un'informazione più ricca.Alberi binari →) i cui nodi interni memorizzano entry e le cui foglie sono vuote (sentinelle esterne), tale che per ogni nodo interno con chiave :
- le chiavi del sottoalbero sinistro di sono ;
- le chiavi del sottoalbero destro di sono .
Esempio. Inserendo in un ABR vuoto si ha radice , figlio destro e figlio sinistro ; l'inorder è .
Definizione (get, put e remove su un ABR)
get(k):TreeSearch(k, root); se è esterno restituiscenull, altrimenti il valore di . .
put(k, x):TreeSearch. Se è interno si sostituisce il valore e si restituisce il vecchio; altrimenti è una foglia e si espande: diventa nodo interno con la entry e due nuove foglie come figli; si incrementa il numero di entry e si restituiscenull. .
remove(k):TreeSearch; se è esterno restituiscenull. Altrimenti due casi:
- ha almeno un figlio foglia: si elimina e la foglia e si collega l'altro figlio al padre di (se era la radice, diventa radice);
- ha due figli interni: si prende = predecessore inorder di (il nodo più a destra del sottoalbero sinistro: entry con la chiave immediatamente minore) e si copia la sua entry in ; non ha figlio destro interno, quindi si elimina con il caso 1. L'albero resta di ricerca: la nuova chiave in è maggiore di tutte le chiavi a sinistra e minore di quelle a destra.
Costo: per la ricerca, per trovare il predecessore, per aggiornare i nodi: .
Esempio. In un ABR con inorder , remove(30) con due figli interni sostituisce la entry con il predecessore inorder, cioè .
Proprietà (altezza e costo delle operazioni su un ABR). Le tre operazioni costano e può arrivare a (chiavi inserite in ordine crescente o decrescente: l'albero degenera in una lista), per cui al caso pessimo sono ; in un albero bilanciato . Per ottenere garantito: alberi che si ristrutturano (AVL, rosso-neri) oppure nodi più capienti, gli alberi (2,4) (vedi Multi-way search tree e alberi (2,4)Multi-way search tree (nodi con più entry, d figli e d-1 chiavi ordinate), ricerca in O(d_max h), un MWS tree con n entry ha n+1 foglie; albero (2,4): nodi con 2-4 figli e tutte le foglie alla stessa profondità, altezza Theta(log n) con dimostrazione, operazioni in Theta(log n); idea di overflow e underflow; tabella di confronto con gli alberi binari di ricerca.Multi-way search tree e alberi (2,4) →).
Esempio. Inserendo in ordine crescente l'albero diventa un cammino: e ogni operazione costa . Il grafico confronta l'altezza dell'albero degenere con quella minima possibile (albero completo, , vedi HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap →): già con chiavi sono contro passi di ricerca.
Grafico interattivo: Altezza di un albero binario di ricerca con n chiavi: da floor(log2 n) (albero bilanciato) a n − 1 (chiavi inserite in ordine); per n = 64 sono 6 contro 63
Multi-way search tree e alberi (2,4)
Definizione (multi-way search tree). Un multi-way search tree (MWS tree) è un albero ordinato (vedi AlberiAlbero radicato (definizione per padre e ricorsiva), terminologia (antenati, discendenti, nodi interni ed esterni, sottoalbero, albero ordinato), profondità, livello, altezza; altezza = massima profondità delle foglie; algoritmi depth e height con costo; somma dei figli = n-1; esempio di algoritmo Omega(n^2) (heightBad).Alberi →) in cui:
- ogni nodo interno ha figli;
- ogni nodo interno con figli (-nodo) memorizza entry con ;
- per ogni chiave del sottoalbero soddisfa (con , ).
Esempio. Un -nodo con chiavi ha figli, con chiavi , in e . Il caso è il nodo dell'ABR (Alberi binari di ricercaAlbero binario di ricerca come albero binario proprio con entry nei nodi interni e foglie vuote; inorder crescente; TreeSearch; get, put e remove (due casi, con predecessore inorder) in Theta(h); altezza fino a n-1; esempi di inserimenti; alberi aumentati con campi size e max e algoritmi di conteggio e interrogazione in O(h).Alberi binari di ricerca →): una chiave e due sottoalberi, uno con chiavi minori e uno con chiavi maggiori. Gli intervalli aperti sono quelli dei intervalli di numeri realiNaturali, interi, razionali e reali, perché la radice di 2 obbliga a passare da Q a R, e gli intervalli.Insiemi numerici (N, Z, Q, R) →: gli intervalli coprono tutta la retta tranne le chiavi del nodo, quindi ogni chiave cercata ha esattamente un posto dove andare.
Proposizione (numero di foglie di un MWS tree). Un MWS tree con entry ha foglie.
Esempio. Con radice e figli , , le entry sono e le foglie .
Definizione (albero (2,4) e altezza). Un albero (2,4) è un MWS tree tale che:
- ogni nodo interno è un -nodo con (cioè ha , o entry);
- tutte le foglie hanno la stessa profondità.
Proposizione. Un (2,4) con entry ha altezza .
Esempio. Con entry l'altezza è tra e , cioè .
Grafico interattivo: Altezza di un albero (2,4) con n entry: compresa tra metà di log2(n+1) (tutti 4-nodi) e log2(n+1) (tutti 2-nodi); per n = 1000 tra 5 e 10
VIII. Alberi e heap in C
Alberi e heap in C
Definizione (albero binario in C come puntatore alla radice). L'albero è il puntatore alla radice (
NULL= albero vuoto). Gli algoritmi ricorsivi seguono lo schema delle visite: caso baser == NULL, poi i due sottoalberi.
Esempio. Con Nodo *r = nuovoNodo(5); r->left = nuovoNodo(3); si ha contaNodi(r) == 2, altezza(r) == 1 e inorder(r) stampa 3 5.
Definizione (inserimento in un albero binario di ricerca con doppio puntatore). La radice può cambiare (primo inserimento), quindi si passa
Nodo **:void addBST(Nodo **r, int v) { if (*r == NULL) *r = nuovoNodo(v); else if (v < (*r)->val) addBST(&(*r)->left, v); else if (v > (*r)->val) addBST(&(*r)->right, v); /* chiave uguale: ignorata */ }
Esempio. Con addBST(&r, 30); addBST(&r, 40); addBST(&r, 24); la visita inorder stampa 24 30 40. Passo per passo: r vale NULL, quindi addBST(&r, 30) crea la radice (e la scrive in r: per questo serve Nodo **, come nelle liste, 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 →). Per : *r non è NULL, , si chiama addBST(&(*r)->right, 40), dove *r ora è il campo right della radice, ancora NULL: si crea lì il nodo. Per : , si scende a sinistra e si crea il nodo nel campo left. È la ricerca di TreeSearch (Alberi binari di ricercaAlbero binario di ricerca come albero binario proprio con entry nei nodi interni e foglie vuote; inorder crescente; TreeSearch; get, put e remove (due casi, con predecessore inorder) in Theta(h); altezza fino a n-1; esempi di inserimenti; alberi aumentati con campi size e max e algoritmi di conteggio e interrogazione in O(h).Alberi binari di ricerca →) con l'espansione della foglia, dove la "foglia" è un campo NULL.
Definizione (min-heap su array con indici da 1). Un min-heap (HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap →) è un array
hcon indici da 1 (h[0]non si usa): figli diiin2ie2i+1, padre ini/2.lastè il numero di entry.
Esempio. Con last = 7, i figli di i = 3 sono h[6] e h[7], e il padre di h[7] è h[3]. In C la divisione tra interi positivi tronca, quindi 7 / 2 == 3 coincide con (Tipi, operatori e controllo del flusso in CTipi interi e reali del C con dimensioni e limiti, conversioni implicite e cast, divisione intera, operatori di incremento, logici e bit a bit; if, switch, while, do-while, for, break e continue.Tipi, operatori e controllo del flusso in C →); per questo h[i / 2] è il padre senza chiamare altre funzioni. Perché gli indici partono da si veda HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap → (con indici da le formule si complicano).
Proprietà (costi di insert, removeMin e bottomUp in C).
inserteremoveMincostano ,bottomUp(vedi Costruzione di uno heapCostruire uno heap da un array di n entry: approccio top-down con n-1 insert, Theta(n log n); approccio bottom-up con down-heap dalle foglie verso la radice, Theta(n), con dimostrazione della somma; esempi svolti, in loco; unione di due alberi con heap-order.Costruzione di uno heap →). Prova: da11 9 7 13 3 4,bottomUpproduce3 9 4 13 11 7; dopoinsert(2)i dueremoveMinrestituiscono e , e resta4 9 7 13 11.
Esempio. Da 11 9 7 13 3 4, bottomUp produce 3 9 4 13 11 7; dopo insert(2), due removeMin restituiscono 2 e 3 e resta 4 9 7 13 11.
IX. Algoritmi di ordinamento
Algoritmi di ordinamento
Proprietà (selection sort: invariante e costo). Invariante all'inizio dell'iterazione : contiene gli elementi minori, già in ordine crescente (e ciascuno tutti gli elementi di ). Alla fine e tutto è ordinato. Costo: il ciclo interno fa confronti, in totale per ogni istanza, anche già ordinata. Corrisponde alla coda con priorità su lista non ordinata (vedi Code con prioritàEntry chiave-valore; ADT coda con priorità (insert, min, removeMin) con chiave minima = priorità massima; esempio di esecuzione; applicazioni; implementazioni con lista non ordinata e ordinata e relativi costi; ordinamento tramite coda con priorità.Code con priorità →).
Esempio. Con il selection sort fa confronti su qualunque array, anche già ordinato.
Proprietà (insertion sort: caso pessimo e caso migliore). Si mantiene ordinato un prefisso e si inserisce al posto giusto spostando a destra i maggiori (pseudocodice e analisi in Complessità in tempo e caso pessimoPerché lo studio sperimentale non basta; complessità al caso pessimo come massimo sul numero di operazioni tra le istanze di una data taglia; stima con limiti superiore e inferiore senza trovare l'istanza peggiore; esempi arrayMax, prefixAverages e InsertionSort; efficienza asintotica e limiti dell'analisi.Complessità in tempo e caso pessimo →). Invariante: all'inizio dell'iterazione , è ordinato (e contiene gli elementi originali di ). Caso pessimo (array decrescente), caso migliore (array ordinato), veloce su sequenze quasi ordinate.
Esempio. Con servono confronti su e su . Traccia su :
Formula (costo del merge sort).
merge(L, R)confronta le due teste e sposta la minore nell'output: . Costo: l'albero della ricorsione ha livelli e al livello le chiamate lavorano su sequenze di taglia , con costo totale per livello (il costo di un nodo è la fusione): per ogni istanza. Richiede di spazio ausiliario. È stabile se in caso di parità si prende l'elemento di sinistra (verificato: per già ordinato i confronti sono ).
Esempio. Per l'albero ha livelli e nel caso pessimo i confronti sono .
Proprietà (costo del quick sort). . Costo: per partizione. Se il pivot divide in parti bilanciate l'albero ha altezza e il costo è ; se è sempre il minimo o il massimo (array già ordinato con pivot ultimo) il costo è . Con pivot scelto a caso il tempo atteso è : è l'esempio classico di caso pessimo patologico (vedi Complessità in tempo e caso pessimoPerché lo studio sperimentale non basta; complessità al caso pessimo come massimo sul numero di operazioni tra le istanze di una data taglia; stima con limiti superiore e inferiore senza trovare l'istanza peggiore; esempi arrayMax, prefixAverages e InsertionSort; efficienza asintotica e limiti dell'analisi.Complessità in tempo e caso pessimo →). Spazio ausiliario attesi (stack).
Esempio. Con pivot ultimo su un array già ordinato di elementi i confronti sono : . Il pivot è il massimo: tutti gli altri elementi vanno a sinistra, la parte destra è vuota e il problema scende solo di , quindi , che dà .
Definizione (heap sort). Si costruisce uno heap sull'array e si estrae ripetutamente il massimo (heap sort con max-heap) mettendolo in fondo all'array: in loco.
bottom-upheap-construction: (vedi Costruzione di uno heapCostruire uno heap da un array di n entry: approccio top-down con n-1 insert, Theta(n log n); approccio bottom-up con down-heap dalle foglie verso la radice, Theta(n), con dimostrazione della somma; esempi svolti, in loco; unione di due alberi con heap-order.Costruzione di uno heap →);- per fino a : scambia con e fai down-heap su : passi da .
Esempio. Per : costruzione bottom-up e down-heap da al più passi, circa operazioni. Perché l'ordine è crescente con un max-heap: il massimo in radice viene scambiato con l'ultima posizione utile dell'array, che è la sua posizione definitiva; poi la parte ancora da sistemare si accorcia di e un down-heap ripristina l'heap (HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap →). L'array è così riempito dal fondo, dal massimo al minimo, e a fine algoritmo risulta crescente senza memoria aggiuntiva.
Formula (ordinamento per conteggio). Se le chiavi sono interi in si può ordinare (o contare) in con un array di contatori : una scansione che fa e una scansione di . Non usa confronti, quindi non è soggetto al limite sotto. Con è lineare (si usa nell'esercizio della mediana di chiavi in ). L'array dei contatori è una mappa con chiavi intere (Mappe e dizionariADT mappa (chiavi distinte) con get, put, remove, keySet/values/entrySet; famiglia mappa/mappa ordinata/dizionario (multimappa); applicazioni; implementazioni semplici ma poco efficienti (lista, array indicizzato dalle chiavi); dizionario realizzato con una mappa di liste.Mappe e dizionari →, array di taglia ) e il costo è la somma dei due passaggi: incrementi e una scansione di contatori.
Esempio. Con e si ottiene e quindi l'ordine : ( compare una volta), , , ; leggendo da sinistra si scrive una volta, una volta, nessun , due volte.
Teorema (limite inferiore per l'ordinamento per confronti). Ogni algoritmo di ordinamento basato su confronti richiede confronti al caso pessimo.
Esempio. Per ci sono permutazioni, quindi servono almeno confronti; per almeno .
Grafico interattivo: Limite inferiore log2(n!) per l'ordinamento per confronti (Stirling) e confronti del merge sort al caso pessimo n·log2 n − n + 1: a n = 64 sono 296 contro 321, quindi il merge sort è quasi ottimo
X. Grafi: definizioni e visite
Grafi - definizioni e proprietà
Definizione (grafo). Un grafo ha un insieme di vertici (o nodi) e una collezione di archi (coppie di vertici). È diretto se ogni arco è una coppia ordinata (), non diretto se la coppia è non ordinata (). è una collezione e non un insieme (Insiemi e insieme delle partiCome si descrive un insieme, sottoinsiemi, uguaglianza, insieme delle parti e insiemi finiti e infiniti.Insiemi e insieme delle parti →, Operazioni tra insiemiUnione, intersezione, differenza, complementare e prodotto cartesiano, con le leggi di De Morgan e il legame con la logica.Operazioni tra insiemi →) perché si ammettono archi multipli tra due vertici; un self loop è un arco . Un grafo semplice non ha archi multipli né self loop; un grafo pesato associa pesi a archi e/o vertici.
Esempio. Il grafo non diretto con e ha vertici e archi.
Definizione (grado, cammino e componenti connesse)
- Un arco è incidente su e , che sono adiacenti; i vicini di sono i vertici con .
- Il grado è il numero di archi incidenti su . Un vertice di grado è isolato.
- Un cammino è una sequenza di vertici con ; la lunghezza è il numero di archi, (nei grafi pesati, la somma dei pesi). Un ciclo è un cammino con ; è semplice se i vertici sono tutti distinti (a parte primo e ultimo).
- Un sottografo ha , e gli archi di incidono solo su . È di copertura (spanning) se .
- è connesso se per ogni coppia esiste un cammino da a ; altrimenti è disconnesso.
- Le componenti connesse sono una partizione di in sottografi () con: ogni connesso; e (partizioni); nessun arco tra e per . Sono i sottografi connessi massimali; la partizione è unica; connesso . Esempio: vertici con archi , , , hanno tre componenti: con 1 arco, con 3 archi, senza archi. Gradi: hanno grado ; hanno grado ciascuno (ognuno è su due archi del triangolo ); ha grado . Somma dei gradi , con archi.
Esempio. Con e archi : , il vertice è isolato e il grafo ha componenti connesse, e .
Proprietà (gradi, numero di archi e alberi). P1. . Dimostrazione: ogni arco è contato esattamente due volte, una per estremo (nella somma l'arco compare una volta nel grado di e una volta in quello di ). Conseguenza: la somma dei gradi è pari, quindi il numero di vertici di grado dispari è pari (se ne avessi un numero dispari la somma sarebbe dispari).
P2. , quindi . Dimostrazione: è semplice, perciò è un sottoinsieme delle coppie non ordinate di vertici distinti. Tali coppie sono : si scelgono vertici su senza badare all'ordine (Fattoriale e coefficienti binomialiFattoriale, permutazioni, disposizioni, combinazioni e coefficiente binomiale n su k, con il triangolo di Tartaglia.Fattoriale e coefficienti binomiali →); equivalentemente, ogni vertice può essere unito a altri, e ogni coppia è stata contata due volte, da cui il fattore . Con sono , con sono (grafi completi , ).
P3. Se è un albero, . Dimostrazione: si vede come albero radicato; coincide con le relazioni padre-figlio, che sono (ogni vertice non radice ha un solo padre).
P4. Se è connesso, . Dimostrazione: finché esiste un ciclo si elimina un suo arco: resta connesso, perché ogni cammino che usava quell'arco si può rifare percorrendo il resto del ciclo (il ciclo offre un percorso alternativo tra gli estremi dell'arco). Alla fine è connesso e senza cicli, cioè un albero libero con archi.
P5. Se è una foresta, . Dimostrazione: finché non è connesso si aggiunge un arco tra due componenti diverse : non può creare cicli, perché un ciclo che passasse per il nuovo arco richiederebbe un altro cammino da a , ma e stanno in componenti diverse e quindi non ce n'è nessuno. Alla fine è un albero con archi.
Esempio. Con vertici si ha ; un albero libero ha archi e la somma dei gradi è .
Grafico interattivo: Numero di archi di un grafo semplice connesso con n vertici: da n − 1 (albero) a n(n−1)/2 (grafo completo); a n = 20 sono 19 contro 190
Corollario (spanning forest). Un grafo con componenti connesse di vertici ha spanning forest con archi (ogni componente contribuisce con uno spanning tree di archi, per P3). Esempio d'esame: , ; , ; , : il numero massimo di archi in una spanning forest è , indipendentemente dai valori (le prime due componenti sono grafi completi e , la terza ha archi su vertici e quindi contiene almeno un ciclo). Ogni spanning forest che non si può più estendere ha esattamente archi.
Esempio. Con componenti di , e vertici lo spanning forest ha archi.
Rappresentazione dei grafi
Definizione (liste di adiacenza). Per ogni vertice una lista di puntatori agli archi di incidenti su (in alternativa, ai vicini; al posto delle liste si possono usare mappe, ad esempio Tabelle hashTabella hash per implementare una mappa: funzione hash = hash code + compression function, bucket array, separate chaining; hash code per numeri e stringhe (polynomial, cyclic shift), division e MAD; load factor, complessità al caso pessimo Theta(n) e medio O(1+lambda), rehashing; esempio svolto con inserimenti e collisioni.Tabelle hash →: con una mappa
areAdjacentcosterebbe in media). È la rappresentazione più usata, e quella assunta se non detto altrimenti, perché:
- occupa spazio lineare nella taglia del grafo, (in un grafo non diretto ogni arco compare nelle liste dei suoi due estremi);
- permette di scorrere i vicini di in tempo lineare nel grado di .
Esempio. Con archi e il vertice isolato: , e .
Definizione (matrice di adiacenza). Matrice di dimensione , con righe e colonne in corrispondenza 1-1 con i vertici (che devono essere rappresentati da interi):
Dà accesso a un arco in tempo costante, ma occupa , che può essere superlineare nella taglia del grafo: conviene per grafi densi ().
Esempio. Con la matrice ha celle; punta all'arco se esiste, altrimenti vale null.
Proprietà (spazio e tempi delle rappresentazioni di un grafo)
Operazione / risorsa Solo Liste di adiacenza Matrice di adiacenza spazio incidentEdges(v)opposite(v, e)areAdjacent(u, v)scansione di tutti i vicini di tutti i vertici
Esempio. Con vertici e archi le liste di adiacenza usano celle (più precisamente riferimenti, perché ogni arco è in due liste), la matrice .
Grafico interattivo: Memoria per un grafo con m = 3n archi: liste di adiacenza n + 2m = 7n riferimenti, matrice di adiacenza n² celle; a n = 100 sono 700 contro 10000, e il rapporto cresce con n
Visite di grafi - BFS e DFS
Definizione (visita in ampiezza, BFS). La BFS da visita la componente connessa di , etichetta gli archi di come
DISCOVERY EDGEoCROSS EDGEe divide i vertici in livelli secondo la distanza da (distanza = lunghezza minima di un cammino; se in componenti diverse).
Esempio. Sul grafo con liste ; ; ; ; ; ; , la BFS da ha livelli , , , .
Proprietà (BFS e spanning tree). Dopo
BFS(G, s): tutti i vertici di sono visitati e tutti i suoi archi etichettati; i discovery edge formano uno spanning tree di radicato in (il BFS tree); per ogni il cammino nel BFS tree da a ha archi; se è un cross edge i livelli di e differiscono al più di .
Esempio. Nello stesso grafo (cammino ) e il BFS tree ha discovery edge.
Formula (complessità della BFS). Ogni vertice di entra in una lista una sola volta (poi
ID) e ogni arco di viene etichettato una volta; dalla lista di adiacenza di ciascun vertice ogni arco è esaminato due volte (una per estremo). Con vertici e archi in il costo è (se non è isolato, ). Se è connesso, . Il conto: ogni vertice di è estratto da una lista una volta e per ciascuno si percorre la sua lista di adiacenza, di lunghezza (Rappresentazione dei grafiStrutture di base (lista dei vertici LV, lista degli archi LE), liste di adiacenza, matrice di adiacenza; operazioni incidentEdges, opposite, areAdjacent; spazio e tempi a confronto; esempio con un grafo di 5 vertici.Rappresentazione dei grafi →); il costo totale è perché la somma dei gradi è (Grafi - definizioni e proprietàGrafo G=(V,E) diretto e non diretto, grafo semplice e pesato; incidenza, adiacenza, grado; cammini, cicli, sottografi, grafi connessi e componenti connesse; alberi liberi, foreste, spanning tree e spanning forest; proprietà con dimostrazioni (somma dei gradi = 2m, m <= n(n-1)/2, alberi m = n-1, connessi m >= n-1, foreste m <= n-1).Grafi - definizioni e proprietà →), e . Con la matrice di adiacenza lo scorrimento dei vicini di ogni vertice costerebbe e il totale .
Esempio. Con vertici e archi il costo è .
Proprietà (DFS). Dopo
DFS(G, s): tutti i vertici di sono visitati, tutti gli archi di sono etichettatiDISCOVERYoBACK, e i discovery edge formano uno spanning tree di radicato in . Complessità con liste di adiacenza ( se connesso), con lo stesso argomento della BFS. Un vertice si dice discoverable da se esiste un cammino da a fatto di vertici non ancora visitati.
Esempio. La DFS da nello stesso grafo visita con discovery edge e back edge, , e .
Proprietà (confronto tra BFS e DFS)
BFS DFS struttura di appoggio liste di livelli (coda) ricorsione (pila) archi non-tree cross edge, tra livelli adiacenti back edge cammini nell'albero minimi in numero di archi non necessariamente minimi costo (liste di adiacenza)
Esempio. La BFS trova , mentre il cammino da a nell'albero DFS () ha lunghezza .
XI. Cammini minimi, alberi di copertura e ordinamento topologico
Cammini minimi e algoritmo di Dijkstra
Definizione (lunghezza di un cammino e distanza). Sia un grafo non diretto e pesato, con (esempio: rete di aeroporti con distanze in km; sono i numeri reali: Insiemi numerici (N, Z, Q, R)Naturali, interi, razionali e reali, perché la radice di 2 obbliga a passare da Q a R, e gli intervalli.Insiemi numerici (N, Z, Q, R) →). Il problema è lo stesso dell'instradamento dei pacchetti in Internet, dove i pesi sono costi dei collegamenti e l'algoritmo è detto link state (Algoritmi di instradamento - link state e distance vectorL'instradamento (routing) trova il percorso di costo minimo in un grafo pesato in cui i router sono nodi e le reti tra due router sono archi. Link state: ogni router diffonde con un flooding i pacchetti LSP sui propri collegamenti, ricostruisce tutto il grafo e applica Dijkstra (nodi con stato (distanza, permanente o temporaneo)). Distance vector: ogni router conosce solo i vicini e scambia con loro il proprio vettore delle distanze, aggiornato con Bellman-Ford $D_{A,w}=\min{D_{A,w},,D_{A,Y}+d_{Y,w}}$; converge in al più $n-1$ giri ma può soffrire del conteggio all'infinito (limite a 16, hold down, aggiornamenti immediati, split horizon con poison reverse). Path vector: ogni annuncio porta l'intero cammino, si sceglie per politica e non per costo, e i cicli si scoprono trovando sé stessi nel cammino. Dijkstra è corretto se i costi sono non negativi (invariante: un nodo permanente ha già la distanza vera); Bellman-Ford è corretto perché dopo $k$ giri conosce i cammini minimi con al più $k+1$ archi.Algoritmi di instradamento - link state e distance vector →, Instradamento e inoltroL'inoltro (forwarding) mette il pacchetto sulla strada verso la destinazione, un salto alla volta (hop by hop). Se la destinazione è nella stessa rete del mittente l'inoltro è diretto (si usa l'ARP per il MAC del destinatario), altrimenti è indiretto: il pacchetto va al router successivo (next hop) indicato dalla tabella di instradamento, o al default gateway. Con le netmask: l'inoltro è diretto attraverso l'interfaccia $x$ se $\text{IP(dst)}\ \text{AND}\ \text{NM}(x)=\text{IP}(x)\ \text{AND}\ \text{NM}(x)$; altrimenti si scorre la tabella dalla maschera più lunga (longest prefix match) e si usa il primo match. La riga con rete $0.0.0.0$ e maschera $0.0.0.0$ (default route) corrisponde sempre. L'aggregazione di rotte (route aggregation) riduce la tabella, e nell'inoltro con etichette (MPLS) la tabella si consulta per indice.Instradamento e inoltro →). La lunghezza di un cammino è ; se per ogni arco è il numero di archi. La distanza è la minima lunghezza di un cammino da a e il cammino che la realizza è un cammino minimo.
Esempio. Nel grafo dell'esempio svolto il cammino ha lunghezza e .
Proposizione (sottostruttura ottima dei cammini minimi). Se è un cammino minimo da a , allora è un cammino minimo da a per ogni . Dimostrazione (per assurdo: Tecniche di dimostrazioneDimostrazione diretta, per contronominale e per assurdo di un teorema del tipo ipotesi implica tesi.Tecniche di dimostrazione →): se esistesse un cammino più corto da a , sostituendolo nel cammino originale si otterrebbe un cammino da a più corto, contro l'ipotesi. In formule: la lunghezza del cammino originale è ; sostituendo il termine centrale con uno strettamente minore si ottiene una somma strettamente minore.
Esempio. Se è un cammino minimo da a , allora è un cammino minimo da a ().
Definizione (algoritmo di Dijkstra). Sviluppato nel 1956, richiede pesi non negativi. Generalizza la BFS: a partire da fa crescere una cloud di vertici di cui si conoscono già distanza da e cammino minimo.
Esempio. Sul grafo dell'esempio svolto Dijkstra da estrae i vertici in ordine di distanza: , , , , , .
Teorema (complessità dell'algoritmo di Dijkstra). La complessità di
ShortestPaths(G, s)è : con la scelta migliore dell'implementazione. Per grafi sparsi () conviene lo heap (); per grafi densi () la lista ( contro ).
Esempio. Con e conviene lo heap: contro (, quindi ).
Grafico interattivo: Costo di Dijkstra con n = 1000 in funzione di m: lista non ordinata n² = 10⁶ (costante), heap (n+m)·log₂n; si incrociano a m ≈ 99000, sotto il quale lo heap vince
Minimum spanning tree
Definizione (minimum spanning tree). Dato un grafo non diretto, pesato e connesso, un minimum spanning tree (MST, albero di copertura di peso minimo) è uno spanning tree (vedi Grafi - definizioni e proprietàGrafo G=(V,E) diretto e non diretto, grafo semplice e pesato; incidenza, adiacenza, grado; cammini, cicli, sottografi, grafi connessi e componenti connesse; alberi liberi, foreste, spanning tree e spanning forest; proprietà con dimostrazioni (somma dei gradi = 2m, m <= n(n-1)/2, alberi m = n-1, connessi m >= n-1, foreste m <= n-1).Grafi - definizioni e proprietà →) che minimizza il peso totale . Ha archi. Applicazione tipica: collegare tutti i nodi di una rete (cavi, broadcast efficiente) con il costo minimo. Perché archi: uno spanning tree è un albero libero con vertici, quindi ha archi (proprietà P3 di Grafi - definizioni e proprietàGrafo G=(V,E) diretto e non diretto, grafo semplice e pesato; incidenza, adiacenza, grado; cammini, cicli, sottografi, grafi connessi e componenti connesse; alberi liberi, foreste, spanning tree e spanning forest; proprietà con dimostrazioni (somma dei gradi = 2m, m <= n(n-1)/2, alberi m = n-1, connessi m >= n-1, foreste m <= n-1).Grafi - definizioni e proprietà →); con meno archi non sarebbe connesso, con di più avrebbe un ciclo, che si potrebbe spezzare togliendo un arco e quindi abbassando il peso (se i pesi sono positivi). Un albero di copertura compare anche nell'analisi dei circuiti (Grafo di una rete ed equazioni indipendentiIl grafo di una rete tiene solo i collegamenti (nodi e lati). Scelto un albero (n−1 lati, connesso, senza maglie), ogni lato di coalbero dà una LKT indipendente e ogni lato d'albero una LKC indipendente: in totale ℓ − n + 1 LKT e n − 1 LKC, che con le ℓ equazioni dei bipoli fanno 2ℓ equazioni per 2ℓ incognite.Grafo di una rete ed equazioni indipendenti →), dove però non si cerca di minimizzare un peso ma di scegliere le equazioni indipendenti. Se i pesi sono tutti distinti l'MST è unico; altrimenti ce ne può essere più d'uno.
Esempio. Nel grafo , , , , , , , , l'MST ha archi e peso .
Proprietà (del taglio). Sia un sottoinsieme degli archi di un qualche MST e sia un taglio che nessun arco di attraversa. Se è un arco di peso minimo tra quelli che attraversano il taglio, allora è ancora contenuto in un MST.
Esempio. Con il taglio contro il resto gli archi che lo attraversano pesano : l'arco appartiene a un MST. Un secondo taglio, con già scelto e , : gli archi che attraversano sono , , , ; il minimo è , quindi è ancora contenuto in un MST.
Definizione (algoritmo di Kruskal). Si scorrono gli archi per peso crescente e si aggiunge l'arco se non crea un ciclo con quelli già scelti, cioè se collega due componenti diverse.
Esempio. Nello stesso grafo Kruskal sceglie , , , , : peso .
Definizione (algoritmo di Prim). Si parte da un vertice e si fa crescere un unico albero, aggiungendo sempre l'arco di peso minimo che collega l'albero a un vertice esterno. Si realizza come Dijkstra (vedi Cammini minimi e algoritmo di DijkstraGrafi pesati, lunghezza di un cammino e distanza; sottocammini di un cammino minimo; problema SSSP; algoritmo di Dijkstra con cloud e priority queue, rilassamento degli archi, esempio svolto; correttezza (due lemmi) e complessità O(min(n^2, (n+m) log n)) con lista non ordinata o heap; pesi non negativi.Cammini minimi e algoritmo di Dijkstra →) con una coda con priorità in cui la chiave di un vertice esterno è il peso del miglior arco che lo collega all'albero.
Esempio. Da Prim aggiunge , , , , : peso , come con Kruskal. L'albero cresce da un solo vertice, mentre Kruskal fa crescere una foresta di più alberi che si fondono; l'invariante di Prim è che l'insieme degli archi scelti forma un albero che contiene , e il taglio della proprietà è "albero contro resto".
Grafi diretti e ordinamento topologico
Definizione (grafo diretto, grado entrante e uscente). In un grafo diretto ogni arco è una coppia ordinata : è la coda (origine) e la testa (destinazione). Per ogni vertice si distinguono il grado uscente (archi che partono da ) e il grado entrante (archi che arrivano in ); vale (ogni arco ha una sola coda e una sola testa, quindi contribuisce a una somma di uscenti e a una somma di entranti: a differenza dei grafi non diretti, dove la somma dei gradi è , Grafi - definizioni e proprietàGrafo G=(V,E) diretto e non diretto, grafo semplice e pesato; incidenza, adiacenza, grado; cammini, cicli, sottografi, grafi connessi e componenti connesse; alberi liberi, foreste, spanning tree e spanning forest; proprietà con dimostrazioni (somma dei gradi = 2m, m <= n(n-1)/2, alberi m = n-1, connessi m >= n-1, foreste m <= n-1).Grafi - definizioni e proprietà →). Nei circuiti elettrici il grafo ha archi orientati secondo il verso di riferimento delle correnti (Grafo di una rete ed equazioni indipendentiIl grafo di una rete tiene solo i collegamenti (nodi e lati). Scelto un albero (n−1 lati, connesso, senza maglie), ogni lato di coalbero dà una LKT indipendente e ogni lato d'albero una LKC indipendente: in totale ℓ − n + 1 LKT e n − 1 LKC, che con le ℓ equazioni dei bipoli fanno 2ℓ equazioni per 2ℓ incognite.Grafo di una rete ed equazioni indipendenti →).
Esempio. Con archi , , : e .
Definizione (DAG). Un DAG (directed acyclic graph) è un grafo diretto senza cicli diretti. Modella dipendenze: prerequisiti di corsi, compiti di un progetto, dipendenze tra moduli di un programma. Anche una rete combinatoria di porte logiche è un DAG (le uscite dipendono dagli ingressi senza retroazione: Reti combinatorie e mappe di KarnaughRete combinatoria (uscite funzione dei soli ingressi attuali); mintermini e maxtermini, forme canoniche SOP e POS; mappe di Karnaugh a 3 e 4 variabili con esempi svolti; condizioni di indifferenza; costo e ritardo di una rete a due livelli.Reti combinatorie e mappe di Karnaugh →), e per calcolare l'uscita si valutano le porte proprio in ordine topologico; una retroazione crea un ciclo e dà una rete sequenziale (Reti sequenziali e automi a stati finitiModello di una rete sequenziale sincrona (stato in flip-flop, logica di stato prossimo e di uscita); automi di Moore e di Mealy; procedimento di sintesi con esempio svolto di un riconoscitore della sequenza 11.Reti sequenziali e automi a stati finiti →). Una relazione " deve precedere " senza cicli è un ordine parziale: un ordine topologico ne è un'estensione a ordine totale.
Esempio. Gli archi e formano un DAG; aggiungendo si crea un ciclo diretto e il grafo non è più un DAG.
Definizione (ordinamento topologico). Un ordinamento topologico di un DAG è una sequenza di tutti i vertici tale che per ogni arco il vertice precede . Esiste se e solo se il grafo è un DAG (con un ciclo, ogni vertice del ciclo dovrebbe precedere il successivo: impossibile). In generale non è unico.
Esempio. Per il DAG con archi e sono ordini topologici sia sia .
Proprietà (cammini minimi in un DAG pesato). Applicazione: cammini minimi in un DAG pesato in , anche con pesi negativi: si rilassano gli archi uscenti dei vertici nell'ordine topologico a partire dalla sorgente (quando si arriva a tutti i predecessori sono già definitivi).
Esempio. Con archi , , l'ordine topologico dà . Passo per passo: ; si rilassano gli archi uscenti di : , ; si passa a (secondo nell'ordine): l'arco dà , quindi ; si passa ad e non ci sono archi uscenti. Qui non c'è il problema di Dijkstra con pesi negativi (Cammini minimi e algoritmo di DijkstraGrafi pesati, lunghezza di un cammino e distanza; sottocammini di un cammino minimo; problema SSSP; algoritmo di Dijkstra con cloud e priority queue, rilassamento degli archi, esempio svolto; correttezza (due lemmi) e complessità O(min(n^2, (n+m) log n)) con lista non ordinata o heap; pesi non negativi.Cammini minimi e algoritmo di Dijkstra →) perché è elaborato prima di : l'ordine topologico garantisce che quando si arriva a un vertice tutti i suoi predecessori sono già definitivi.