Dizionario
Dizionario
• Il dizionario è un TDA dove si vogliono
eseguire ricerche dove l’informazione è
individuata da una chiave che è unica.
• La parola del TDA deriva proprio dal
dizionario (vocabolario) nel quale:
• le parole sono le chiavi di accesso e sono tutte
distinte
• la definizione che segue la parola (informazione
associata alla parola, attributo) caratterizza la
parola.
Dizionario
• Un Dizionario è un contenitore di coppie:
Coppia = (chiave, attributo)
• la chiave è confrontabile: intero, stringa
• la chiave è unica: se si vuole inserire una chiave si
deve verificare che non sia presente, se lo è si esegue
una sostituzione.
• Le operazioni del TDA Dizionario sono:
•
•
•
•
Albero
verifica se è vuoto
inserimento
ricerca
cancellazione
1
Albero
Albero binario
• Un albero è un insieme di punti, detti nodi, a
cui è associata una struttura d’ordine parziale
(non tutti gli elementi sono confrontabili).
• Un albero binario è un TDA definito nel
modo seguente:
• un insieme vuoto di nodi
• una radice con due sottoalberi sinistro e
destro che sono alberi binari.
• Il TDA albero binario si può realizzare con un
array o con una lista concatenata.
• Nella realizzazione con lista concatenata
utilizziamo due riferimenti per indicare i due
sottoalberi:
struct tiponodo{
int chiave;
tiponodo *sinistro;
tiponodo *destro;
};
(par. 9.5)
Albero binario
• Possiamo rappresentarlo
modo seguente:
chiave
graficamente nel
Albero binario
• Le foglie sono rappresentate con i due
riferimenti uguali a NULL:
chiave
sinistro
•
destro
•
• Si considerano alberi binari completi,
perfettamente bilanciati e bilanciati.
2
Albero binario
Albero binario
• Albero binario completo:
da ogni nodo partono sempre due rami e le
foglie sono tutte allo stesso livello.
• Albero binario perfettamente bilanciato:
per ogni nodo il numero dei nodi del
sottoalbero sinistro differisce al più di 1 dal
numero dei nodi del sottoalbero destro.
Albero binario
Albero binario
• Albero binario bilanciato:
per ogni nodo le altezze dei sottoalberi sinistro
e destro differiscono al più di 1.
• Un albero binario di profondità k
perfettamente bilanciato è completo fino al
livello k-1.
• Un albero binario completo Bk ha n=2k+1-1
nodi e 2k foglie (dimostrazione per induzione).
• Tutte le operazioni di ricerca di un nodo
(inserimento e cancellazione) su un albero
binario bilanciato (perfettamente bilanciato)
hanno complessità O(log2 n).
3
Albero binario
Albero binario
• Albero binario di ricerca:
i valori dei nodi del sottoalbero sinistro sono
minori della radice e quelli del sottoalbero
destro sono maggiori della radice.
• Un albero binario di ricerca può essere
totalmente sbilanciato; esistono delle tecniche
di ribilanciamento che, con un numero finito
1
di assegnazioni sui
puntatori, permettono
2
di costruire alberi di
ricerca
bilanciati.
3
8
4
1
11
9
12
4
Albero binario
• Si può “visitare” un albero di ricerca
esaminando i nodi in un ordine stabilito. Si
hanno le seguenti tipologie di visita
dell’albero:
• inordine
ARB
R
• preordine R A B
A
B
• postordine A B R
Gestione hash di un
array di interi
dove R è la radice e A e B i sottoalberi sinistro
e destro.
4
Gestione hash di un array di
interi
Gestione hash di un array di
interi
• Vogliamo inserire dei numeri interi in un array e
pensiamo ad una memorizzazione efficiente con
lo scopo di ottimizzare successivamente la
ricerca del numero.
• Esempio.
• Pensiamo di memorizzare in un array i numeri
di matricola di un corso di n=180 studenti; alla
matricola si possono poi far corrispondere
ulteriori informazioni riguardanti lo studente.
• Oppure potremmo pensare di individuare lo
studente tramite la sua postazione al PC del
laboratorio: in questo caso avremo 180 numeri
diversi per i 180 studenti. In questo secondo
caso consideriamo un array di dimensione 180
e memorizziamo al posto i-esimo lo i-esimo
studente: l’indice dell’array coincide con
l’informazione che vogliamo memorizzare:
indice = informazione
Gestione hash di un array di
interi
Gestione hash di un array di
interi
• Se andiamo ad eseguire una ricerca il costo sarà
sempre O(1); l’accesso diretto alla posizione iesima ci fornisce l’informazione cercata:
postazione[i] = i
• Potremmo pensare di risolvere in modo analogo
il problema di memorizzare il numero di
matricola, supponiamo compreso tra 575000 e
580000. Se avessimo a disposizione un array di
580000 componenti nuovamente troveremmo la
matricola cercata in un tempo O(1)
matricola[575231] = 575231
• Possiamo vedere la matricola e la postazione
come delle chiavi di accesso all’informazione
vera, costituita dai dati dello studente. Se
l’insieme delle possibili chiavi coincide con il
numero
di
elementi
che
vogliamo
rappresentare, come nel caso della postazione,
allora la scelta è buona.
• Nel caso della matricola avremmo un notevole
spreco di memoria: un array di 580000
componenti per memorizzare 180 dati.
5
Gestione hash di un array di
interi
• L’idea pertanto è la seguente:
Gestione hash di un array di
interi
• Vogliamo determinare una funzione h tale che,
per ogni valore della chiave (matricola) si
abbia:
h(chiave) = posizione
con
0 ≤ posizione ≤ dim-1
• Una funzione di trasformazione può essere il
calcolo del resto della divisione con dim:
posizione = h(k) = k mod dim
• Si può dimostrare che, con questa scelta della
funzione, è bene che sia dim ≥ n del 10-20% e
che dim sia un numero primo. Infatti una
buona scelta di h comporta che i resti siano,
nella maggior parte dei casi, diversi tra loro.
• Se fosse dim=100, tutti i numeri del tipo
575100, 575200, . . ., 576100, 576200, . . .,
avrebbero lo stesso resto.
Gestione hash di un array di
interi
Gestione hash di un array di
interi
• Non sempre le chiavi troveranno resti diversi: si
parla allora di “collisione” dato che due chiavi
dovrebbero andare nello stesso posto. Come
risolvere il problema?
• Lo spazio nell’array c’è, visto che dim ≥ n ,
pertanto si effettua una scansione cercando un
“posto libero”, provando con il successivo
modulo dim:
se
posizione = k mod dim
è occupata,
allora si cerca in
posizione = (posizione+1) mod dim
• Come nella Coda, questa tecnica permette di
“fare il giro” dell’array.
• Esempio. Consideriamo i seguenti n=9 valori:
1, 3, 4, 15, 18, 20, 22, 35, 48
e proviamo ad inserirli con la tecnica del resto
in un array di dim=11 elementi.
• Calcoliamo i resti:
1%11 = 1; 3%11 = 3; 4%11 = 4;
15%11 = 4 ma la posizione 4 è occupata
• lo spazio in più può dare dei vantaggi
• per non sprecarlo cerchiamo una funzione che
trasformi la matricola (chiave) in un valore di
indice possibile per un array di dimensione dim,
con dim un po’ più grande di n=180.
6
Gestione hash di un array di
interi
• Facciamo:
(4+1)%11 = 5%11= 5 che è libera
continuiamo:
18%11= 7; 20%11= 9; 22%11= 0;
35%11= 2; 48%11= 4 che è occupata:
(4+1)%11 = 5%11= 5 che è occupata
(5+1)%11 = 6%11= 6 che è libera
I numeri nell’array sono memorizzati in ordine
diverso da quello di inserimento (rimescolati):
Gestione hash di un array di
interi
0 1
2
3
22 1 35 3
4 5 6 7 8
9 10
4 15 48 18
20
• Quanto costa inserire con questa tecnica?
• L’inserimento di un numero che trova subito il suo
posto è O(1): calcolo del resto, invece di +1; quando c’è
una collisione si deve cercare il posto: se n=dim il caso
peggiore è O(n), si fa tutto il giro; nel nostro caso
abbiamo 12 confronti (per cercare il posto libero) per 9
elementi.
• Si può dimostrare che, se dim è “buona”, in media il
costo è O(1).
Gestione hash di un array di
interi
Gestione hash di un array di
interi
• Proviamo ora ad effettuare una ricerca di un
valore nell’array. La tecnica per la ricerca sarà la
stessa: calcolare il resto della divisione con dim:
valore%dim
fornisce l’indice in cui cercare il valore (chiave).
• Il costo della ricerca di un valore presente è lo
stesso di quello dell’inserimento: in media O(1).
Per un valore non presente il costo è superiore:
può capitare di fare tutto il giro, O(n) nel caso
peggiore.
• Per costruire l’array dobbiamo dare un
significato al posto “vuoto”. Possiamo
inizializzare l’array con un valore non
appartenente all’universo delle chiavi; ad
esempio 0 per le matricole, -1 per le postazioni
dei PC, ecc.
• Durante la costruzione dell’array il posto vuoto
indica la posizione libera in cui poter inserire
l’elemento.
• Durante la ricerca il posto vuoto indica
“elemento non presente”.
7
Gestione hash di un array di
interi
Gestione hash di un array di
interi
• Nel caso della ricerca si effettua la stessa
scansione facendo il giro dell’array. In caso di
array pieno (n=dim) e di elemento non
presente, si fa tutto il giro ritornando alla
posizione iniziale senza trovarlo.
• Per gestire questa situazione, si salva il valore
della chiave presente all’indice chiave%dim in
una variabile temporanea, si copia in tale
posizione l’elemento da cercare, si effettua la
ricerca (che avrà esito positivo).
• Se l’elemento viene trovato nella posizione
iniziale (si è fatto tutto il giro) significa che
non c’è, altrimenti lo si è trovato più avanti e
ciò significa che c’era stata una collisione.
Infine si ripristina il valore salvato.
• Si può ottimizzare la ricerca del caso non
trovato, inserendo le chiavi in ordine: se si
trova un elemento più grande ciò significa che
quello più piccolo non c’è.
Tavola Hash
Tavola Hash
• Un Dizionario con chiave intera viene
chiamato Tavola.
• Nella costruzione dell’array di interi con la
tecnica hash, avevamo risolto il problema delle
collisioni con un sovradimensionamento della
tavola; cerchiamo di risolverlo in altro modo:
mantenere le stesse prestazioni e non sprecare
spazio.
• L’array offre con l’accesso diretto O(1), la lista
concatenata offre un inserimento O(1).
8
Tavola Hash
Tavola Hash
• Costruiamo un array di liste concatenate: la
scelta dell’indice nell’array si effettua con
chiave trasformata (ridotta) e la collisione si
risolve con un inserimento in testa alla lista.
• Abbiamo quindi un Tavola realizzata tramite
due strutture di dati che prende il nome di
Tavola Hash con bucket (bucket è la lista
concatenata).
• Le prestazioni della Tavola dipendono dalla
funzione hash e dalle chiavi.
• La funzione h potrebbe fornire sempre lo
stesso valore di chiave ridotta: caso peggiore,
O(n)
Tavola Hash
Tavola Hash
• Caso favorevole: tutte le liste hanno la stessa
lunghezza, n/dim, O(n/dim); se n~dim si ha
circa O(1)
• Se le chiavi sono uniformemente distribuite, la
funzione hash con il resto della divisione con
dim è una buona scelta: anche le chiavi ridotte
sono uniformemente distribuite.
coppia
...
coppia
coppia
coppia
coppia
•
•
•
•
•
• Come fare se la chiave è una stringa?
• Dobbiamo associare ad una stringa un valore
intero per poter poi calcolare il resto.
coppia
9
Tavola Hash
Tavola Hash
• Ricordiamo la notazione posizionale dei numeri:
257 = 2 ×102 + 5 × 101 + 7 × 100
possiamo trasformare una stringa in un numero
pensando alla base
b = 256 (ASCII):
“ABC” = ‘A’ × b2 + ‘B’ × b1 + ‘C’ × b0
• Sarebbe una buona idea, dato che in C++ un
char è anche un intero piccolo, ma si avrebbe un
numero troppo elevato e spesso overflow.
• Spesso per le stringhe viene usato il valore 31
(numero primo) come moltiplicatore, al posto
di b:
Riepilogo
“ABC” = ‘A’ × 312 + ‘B’ × 311 + ‘C’ × 310
• Possiamo quindi calcolare il valore hash per
ogni stringa.
TDA e strutture di
dati
10
TDA
TDA
• Un Tipo di Dato Astratto è:
• un insieme di elementi chiamato dominio del
tipo di dato
• caratterizzato da un nome
• possiede delle funzioni che operano sul
dominio
• I seguenti TDA sono contenitori caratterizzati
dalle loro diverse operazioni :
• Pila (Stack)
• Coda (Queue)
• Lista
• Dizionario e Tavola
• Albero
• operatori, predicati, operatori di creazione
•
possiede delle costanti che lo caratterizzano
Strutture di dati
Strutture di dati
• Una struttura di dati è un modo di
organizzare i dati in un contenitore di dati ed
ha una sua propria modalità di accesso. Sono
state viste le seguenti strutture:
Array
Lista concatenata
diretto
i+1
sì
info
sì
sequenziale
p->punt
no
info e punt
no
• array
• lista concatenata.
accesso
successivo
dimensione massima
spazio
spostamento dati
(inserire e cancellare)
11
Pila Stack (Last In First Out)
Coda o Queue (First In First Out)
• Le operazioni coinvolgono solo il primo
elemento della Pila.
• Le operazioni coinvolgono il primo elemento
e l'ultimo elemento della Coda. Il TDA viene
descritto nella seguente interfaccia:
• verifica di Pila vuota
• visione della testa
• inserimento in testa
• estrazione dalla testa
•
•
•
•
verifica di Coda vuota
visione della testa
inserimento in coda
estrazione dalla testa
Lista
Dizionario o Dictionary
• Generalizza il concetto di sequenza: è un
contenitore di informazioni che hanno un ordine
posizionale.
• Si esegue una scansione lineare tramite una
variabile che a partire dal primo elemento della
lista si posiziona sull'elemento cercato (se esso è
presente).
• In una lista si può accedere a qualunque
elemento.
• E' costituito da coppie (chiave, attributo); la
chiave è unica e deve permettere il confronto.
• Le operazioni che si possono fare sono:
• verifica se è vuoto
• ricerca di una chiave
• inserimento di una coppia
• rimozione della coppia
12
Tavola Hash
• Si esegue una trasformazione della chiave per
ottimizzarne la memorizzazione:
posizione = h(chiave)
• h : resto della divisione intera chiave/dim
• dim : dimensione della tavola
Il linguaggio C++
• Si utilizzano assieme le due strutture dati
costruendo un array di liste concatenate (bucket).
• Se le chiavi sono n e le liste hanno la stessa
lunghezza, le prestazioni sono O(n/dim).
C++
C++
• Il linguaggio C++ è un linguaggio di
programmazione ad alto livello.
• Il linguaggio C++ è dotato di un compilatore
che traduce il codice sorgente in un codice
binario.
• Il compilatore, durante la traduzione in codice
macchina, esegue una analisi lessicale,
sintattica e semantica.
• Il codice binario viene elaborato dal linker che
produce il codice eseguibile (aggancio dei vari
moduli).
• Un programma C++ è composto da una o più
unità compilabili (moduli).
• Ciascuna unità contiene una o più funzioni e
può contenere istruzioni chiamate "direttive"
per il preprocessore.
• L’istruzione include effettua l’inserimento del
file indicato.
• Una sola unità contiene la funzione main.
13
Alfabeto
Unità lessicali
• Le unità lessicali (token) con cui
costruiscono le frasi del linguaggio sono:
• identificatori
• parole chiave
• costanti
• separatori
• operatori
si
• L'alfabeto del linguaggio è costituito dai
simboli del codice ASCII.
• I primi 128 caratteri vengono usati per scrivere
le istruzioni e le unità lessicali del programma.
• Si distinguono le lettere minuscole dalle
maiuscole.
• Lo spazio è un separatore.
• Le parole chiave (parole che hanno un
particolare significato per il linguaggio) sono
riservate (minuscole), ossia non si possono
usare come nomi di identificatori.
• I nomi degli identificatori sono costruiti con
caratteri alfanumerici e devono iniziare con un
carattere alfabetico .
Tipi di dato
Tipo Intero
• Abbiamo visto i seguenti
tipi di dato
predefiniti:
• intero
• reale
• carattere
• logico (non è nello standard)
• puntatori
• Si sono usate anche le struct del C per gestire
record (collezione di elementi di tipo diverso).
• A seconda dell'occupazione in byte si hanno i
seguenti tipi:
short (1, 2), int (2, 4), long (8).
• Degli n bit a disposizione, il primo è il bit del segno
0 positivi
1 negativi
• I rimanenti n-1 sono per il numero.
14
Tipo Intero
• Si possono rappresentare 2n -1 valori diversi.
• Costanti del tipo di dato:
- 2n -1 e 2n -1 -1
che delimitano l'intervallo di rappresentazione.
Tipo Intero
• La rappresentazione dell'opposto è definita in
complemento a 2
r(x) + r(-x) = 2n
• Dato x, per trovare la sua rappresentazione
r(x) in base b (b=2) si divide per la base e si
prendono i resti.
• Per trovare la rappresentazione dell'opposto,
indicata con r(-x), si invertono tutti i bit di r(x)
fatta eccezione degli 0 a destra e del primo 1 a
destra (oppure si invertono tutti i bit e si
aggiunge 1).
Tipo Reale
Mantissa
• A seconda dell'occupazione in byte si hanno i
seguenti tipi: float (4), double (8) e long double
(12).
• I numeri reali sono rappresentati in modulo e segno,
riferiti alla base 2, con mantissa normalizzata e bit
nascosto.
• Dato x per costruire la rappresentazione r(x) si deve
trasformare in binario il numero reale:
parte intera: si divide per la base e si prendono i
resti
parte frazionaria: si moltiplica per la base e si
prendono e parti intere
• Si deve poi portare il numero binario in forma
normalizzata
1.c1c2c3.... × 2e
• Dati n bit a disposizione il primo è il bit del
segno
0 positivi
1 negativi
• Dei rimanenti n-1 bit si ha una ripartizione tra
esponente e mantissa.
15
Esponente
Funzione
• L'esponente e viene rappresentato con
l'eccesso (in traslazione) vale a dire che e deve
essere aumentato di 127 per i float (eccesso). Il
nuovo esponente (intero) viene trasformato in
binario.
• Una funzione può avere zero, uno o più
argomenti ed ha un esplicito valore scalare di
ritorno.
• Costanti del tipo di dato
minimo reale ≠ 0 float ~ 10-38
massimo reale float
~ 1038
• Ogni funzione è caratterizzata dal tipo del
valore restituito.
• Se una funzione non restituisce esplicitamente
un valore, allora il suo tipo di ritorno è void.
Variabile locale
Variabile di scambio o parametro
• Si chiama blocco un gruppo di istruzioni
all'interno di una coppia di parentesi graffe.
• Una variabile di scambio è una variabile che
permette la comunicazione dell’informazione
tra funzioni.
• Parametro formale: parametro che appare
nella intestazione della funzione.
• Parametro attuale: parametro (argomento) che
appare nell'istruzione di chiamata della
funzione.
• La corrispondenza tra i parametri è
posizionale.
• Ogni funzione contiene uno o più blocchi.
• Una variabile locale è nota (visibile) solo nel
blocco in cui è stata definita.
• All’interno del blocco deve essere definita una
ed una sola volta.
16
Passaggio dei parametri
Variabile locale
• Il passaggio dei parametri nella chiamata di
una funzione (sottoprogramma) può essere:
• per valore:
• nel sottoprogramma chiamato viene
costruito un nuovo spazio per la variabile e
in esso viene copiato il valore (scalare)
• per indirizzo:
• al programma viene passato l'indirizzo di
memoria della variabile (per gli array viene
passato l’indirizzo della prima componente).
Una variabile locale:
• appartiene a un blocco o ad una funzione
• è visibile all'interno del blocco o della
funzione
• deve essere inizializzata esplicitamente
• viene creata, "nasce", quando viene
eseguito l'enunciato in cui è definita
• viene
eliminata,
"muore",
quando
l'esecuzione del programma esce dal blocco
nel quale è stata definita
Variabile parametro
Allocazione in memoria
Una variabile parametro (formale):
• appartiene a una funzione
• è visibile all'interno della funzione
• viene
inizializzata
all'atto
della
invocazione della funzione (con il valore
fornito dall'invocazione)
• viene creata, "nasce", quando viene
invocata la funzione
• viene
eliminata,
"muore",
quando
l'esecuzione della funzione termina
• La definizione delle variabili comporta una
allocazione di spazio durante la compilazione
del programma. Lo spazio di memoria
utilizzato si chiama Stack.
• Durante l'esecuzione del programma si può
allocare dello spazio con la funzione new. Lo
spazio di memoria utilizzato si chiama Heap.
17
Stack e Heap
• Rappresentano parti diverse della memoria: Stack
(tipi base) e Heap (elementi creati con new).
• Schema della disposizione degli indirizzi di memoria:
Algoritmi
indirizzi di memoria crescenti
Codice di
Memoria
RuntimeStack
libera
programma
lunghezza
cresce verso →
fissa
la memoria alta
Heap
← cresce verso
la memoria bassa
Algoritmi
Strutture di controllo
• Un
algoritmo
(deterministico)
è
un
procedimento di soluzione costituito da un
insieme di operazioni eseguibili tale che: esiste
una prima operazione, dopo ogni operazione è
individuata la successiva, esiste un'ultima
operazione.
• La risoluzione avviene in due passi:
• Con le seguenti strutture di controllo si può
scrivere qualsiasi algoritmo:
• sequenza
• alternativa
• ciclo
• Queste strutture sono caratterizzate dall'avere
un unico punto di ingresso e un unico punto di
uscita.
• analisi, definizione del problema e progettazione
• codifica, trasformazione in un linguaggio
programmazione
di
18
Iterazione e Ricorsione
Divide et impera
• La scomposizione in sottoproblemi può essere:
• iterativa:
• i sottoproblemi sono simili tra loro
• ricorsiva:
• almeno uno dei sottoproblemi è simile a
quello iniziale, ma di dimensione
inferiore
• E' una strategia generale per impostare
algoritmi:
• In entrambe le scomposizioni si pone il
problema della terminazione.
• suddividere l'insieme dei dati in un numero
finito di sottoinsiemi sui quali si può
operare ricorsivamente.
• Se la suddivisione in sottoinsiemi è bilanciata
(sottoinsiemi di uguale dimensione) si
ottengono algoritmi efficienti.
Complessità
Complessità computazionale
• L'efficienza di un algoritmo si valuta in base
all'utilizzo che esso fa delle risorse di calcolo:
memoria (spazio), CPU (tempo).
• Il tempo che un algoritmo impiega per
risolvere un problema è una funzione
crescente della dimensione dei dati del
problema. Se la dimensione varia, può essere
stimato in maniera indipendente dal
linguaggio, dal calcolatore e dalla scelta di
dati, considerando il numero di operazioni
eseguite dall'algoritmo.
• Indicata con n (numero naturale) la
dimensione dei dati di ingresso, la funzione
F(n) che calcola il numero di operazioni viene
chiamata complessità computazionale e
utilizzata come stima della complessità di
tempo.
• Si fanno delle semplificazioni individuando
quali operazioni dipendono da n.
19
Andamento asintotico della
complessità
• Se F(n) è crescente con n e se si ha che:
F(n) → +∞
per n → +∞
• date le semplificazioni per la valutazione di F(n), si
stima il comportamento della funzione per n→+∞
utilizzando le seguenti notazioni:
• O (o-grande)
• Ω (omega)
• Θ (theta)
limitazione superiore
limitazione inferiore
uguale andamento
Calcolo della complessità
• Nel calcolo della complessità si valuta il
comportamento dell'algoritmo su particolari
scelte di dati e si distinguono:
• un caso peggiore in cui l'algoritmo effettua il
massimo numero di operazioni,
• un caso favorevole in cui l'algoritmo effettua il
minimo numero di operazioni,
• un caso medio in cui l'algoritmo effettua un
numero medio di operazioni.
Calcolo della complessità
Classi di complessità
• Quando si scrive un algoritmo si deve sempre
dare una stima del caso peggiore e di quello
favorevole.
• Le funzioni di complessità sono distinte in classi che
rappresentano i diversi ordini di infinito
O(1)
costanti
O(log (log n))
O(log n)
logarimto
O(n)
lineari
O(n*log n)
O(nk)
polinomi k = 2, 3, ...
O(n!), O(nn), O(an) esponenziali (a ≠ 0,1)
• Gli algoritmi con complessità esponenziale sono
impraticabili
• Se la dimensione n dei dati non varia, la
funzione di complessità è costante.
• Se n si mantiene limitato le considerazioni
asintotiche non valgono.
20
Limiti inferiori e superiori
• Ogni algoritmo determina una limitazione
superiore della complessità del problema.
Complessità di algoritmi
fondamentali
• Calcolo dell‘n-esimo numero di Fibonacci
f0 = 1
• Cercare le limitazioni inferiori alla
complessità del problema vuol dire cercare
algoritmi più efficienti.
• Un algoritmo la cui complessità coincide con
la limitazione inferiore è detto ottimo.
Complessità di algoritmi
fondamentali
• Ricerca
lineare
caso peggiore
O(n)
binaria
hash
fn = fn-1 + fn-2
definizione ricorsiva
iterazione con vettore
iterazione con scalari
moltiplicazione di matrici
matrice quadrati (ricorsione)
approssimazione numerica
tempo
O(2n)
O(n)
O(n)
O(n)
O(log2 n)
O(1)
n>1
spazio
O(n)
O(n)
O(1)
O(1)
O(1)
O(1)
Complessità di algoritmi
fondamentali
• Ordinamento
(vettore ordinato)
(array di interi)
f1 = 1
caso favorevole
Ω(1)
O(log2 n)
Ω(1)
O(1)*
Ω(1)
(in media)
(*dipende dalla dimensione dell’array e dalla funzione hash)
lineare
Θ(n2 /2)
bubblesort
O(n2 /2)
Ω(n) (con varianti)
inserimento
O(n2 /2)
Ω(n)
quicksort
O(n2 /2) O(n log2 n) anche medio
mergesort
Θ(n log2 n)
sempre
• Si può dimostrare che la limitazione inferiore della
complessità
nel problema dell'ordinamento è
Ω(nlog2n): mergesort è ottimo nel caso medio e
peggiore, quicksort è ottimo nel caso medio.
21
Un problema divertente:
il problema del torneo
• Questo problema interessò vari matematici tra i quali
Lewis Carrol, più noto per aver scritto Alice nel
paese delle meraviglie; egli lo propose nella stagione
tennistica inglese del 1883.
• In un torneo ad eliminazione diretta si affrontano n
giocatori: chi perde viene subito eliminato. Si postula
la transitività della bravura: se a perde con b e b
perde con c si conclude che a perderebbe
comunque con c.
Tabellone di un torneo rappresentato su un
albero binario tra 24 = 16 giocatori
m è il vincitore
Il problema del torneo
• Supposto n = 2k, i giocatori si affrontano a coppie in
una serie di n/2 incontri; successivamente si
affrontano i vincitori in una nuova serie di n/4
incontri e così via fino ai quarti di finale (8 giocatori),
alle semifinali (4 giocatori) e alla finale (2 giocatori)
che deciderà il vincitore del torneo.
• Si può rappresentare il tabellone del torneo con un
albero binario, completo, con 2k foglie e di
profondità k.
Il problema del torneo
• Un albero binario Bk completo possiede 2k+1-1 nodi dei
quali 2k sono foglie (dimostrazione per induzione),
pertanto i nodi interni sono 2k-1, vale a dire n-1.
• Gli incontri che vengono effettuati sono tanti quanti i
nodi interni.
• Questo problema è quello di determinare il massimo
in un insieme di n elementi, di cui sappiamo occorrono
n-1 confronti nei quali gli elementi non-massimi
risultano perdenti nel confronto con il massimo.
22
Il problema del torneo
Girone di consolazione per il torneo
p è il secondo dopo m
• Il vincitore della finale è il vincitore del torneo.
Ma chi è il secondo?
• Se il “secondo in bravura” capita nella stessa metà del
tabellone del primo, verrà eliminato prima di arrivare
alla finale.
• Per non commettere ingiustizie bisogna far eseguire un
secondo torneo tra tutti coloro che hanno perduto
in un incontro diretto con il primo.
• Questi giocatori sono k = log2n, perché k sono gli
incontri disputati dal vincitore (k profondità
dell'albero).
Algoritmo del doppio torneo
• Eseguendo lo stesso algoritmo in k-1 (log2n -1)
incontri si determinerà il secondo.
• L'algoritmo del doppio torneo determina il valore del
primo e del secondo eseguendo un numero di
confronti uguale a
C(n) = n-1 + log2(n) -1 = n + log2(n) -2
per n = 16 C(n) = 18
Algoritmo: primo e secondo
se a[1] > a[2]
allora
altrimenti
primo ←
secondo ←
primo ←
secondo ←
a[1]
a[2]
a[2]
a[1]
//finese
per i da 3 a n
se primo < a[i]
allora secondo ← primo
primo ← a[i]
altrimenti se secondo < a[i]
allora secondo ← a[i]
//finese
// finese
//fineper
23
Algoritmo: primo e secondo
Algoritmo del doppio torneo
• L'algoritmo, che determina il valore del primo e del
secondo, esegue invece nel caso peggiore (il
massimo è in uno dei primi due posti, per cui si deve
sempre eseguire anche il secondo confronto) un
numero di confronti uguale a
• Si può dimostrare che, nel problema della
determinazione del primo e del secondo, il
limite inferiore dei confronti
nel caso
peggiore è
Ω(n + log2(n) -2)
C(n) = n-1 + n-2 = 2n -3
per n = 16 C(n) = 46
• Nel caso favorevole (ordine crescente in un array)
C(n) = n-1.
e per finire …
come fa Google a
scegliere le pagine più
importanti?
• Pertanto l'algoritmo del doppio torneo è ottimo
nel caso peggiore.
Google: il problema del page
ranking
• Due studenti dell’Università di Stanford, Sergey
Brin e Larry Page, hanno dato vita a Google nel
1998, uno dei più importanti motori di ricerca nel
Web.
• La parola google deriva da googol, termine
“inventato” da Milton Sirotta nel 1938 (all’epoca
ragazzino e nipote di un matematico), per
indicare un numero seguito da 100 cifre zero.
• L’azienda che gestisce tale motore di ricerca ha
utilizzato questo nome per la vastità di
informazioni trattate.
24
Google: il problema del page
ranking
Google: il problema del page
ranking
• Uno dei problemi era:
• come ordinare le pagine presenti sul web in base
alla
loro
importanza?
come
definire
l’importanza?
• I due fondatori si ispirarono all’algoritmo
HyperSearch dell’italiano prof. Massimo
Marchiori, che all’epoca era ricercatore negli
USA e che attualmente insegna al Dipartimento
Matematica Pura ed Applicata dell’Università di
Padova.
• Si possono scegliere varie possibilità:
• il numero di volte che una parola viene cercata
• il numero di link che partono o arrivano a quella
parola
• il numero delle pagine importanti che
partono o arrivano alla pagina.
Google: il problema del page
ranking
Google: il problema del page
ranking
• L’idea di Page e Brin era che una pagina ha
una sua importanza che deriva dalle sue
connessioni (e non dal suo contenuto).
• L’importanza di una pagina si trasferisce in
parti uguali alle pagine che essa punta (una
persona importante dà importanza alle persone
che frequenta).
• L’importanza di una pagina è data dalla
somma dell’importanza che viene dalle pagine
che puntano ad essa (una persona è importante
se frequenta molte persone importanti).
• Vediamo il modello matematico.
• Supponiamo che siano n le pagine del web, e
definiamo la matrice seguente:
H=
con
h11
h21
...
hn1
h12
h22
hn2
h1n
h2n
...
hnn
1
se c’è un link dalla pagina i alla pagina j
0
altrimenti
hij =
25
Google: il problema del page
ranking
• Esempio.
matrice:
Sia n = 4 e sia H la seguente
0
1
0
0
1
0
0
1
1
0
0
1
1
1
1
0
Google: il problema del page
ranking
• Indichiamo ora con xj l’importanza della pagina j;
risulta:
xj = h1j x1/r1 + h2j x2/r2 + … + hnj xn/rn
• Sommando i valori sulla riga i-esima si trova il
numero di link che partono dalla pagina i: ri
• Sommando i valori sulla colonna j si trova il numero
di pagine che puntano alla pagina i: cj.
per j = 1, 2, 3, …, n
• Questo è un sistema lineare in n equazioni ed n
incognite. Le soluzioni x1, x2, …, xn forniscono il
livello di importanza delle n pagine: page rank.
• L’equazione usata in Google è un po’ diversa, perché
“pesata” con un parametro e le soluzioni che derivano
sono tutti valori compresi tra 0 e 1.
Google: il problema del page
ranking
Google: il problema del page
ranking
• Le pagine attive odierne sono oltre
n = 8.5 ×109
• Un sistema lineare con la regola di Cramer si
risolve con un numero di operazioni
dell’ordine di n!.
• Il metodo di eliminazione di Gauss risolve il
sistema con circa 2n3/3 operazioni.
• Se usassimo quest’ultimo avremmo un numero
di operazioni dell’ordine di:
2/3 × (8.5 109)3 ~4.1 × 1029
(miliardi di miliardi di miliardi) di operazioni
aritmetiche.
• Uno dei calcolatori più veloci al mondo è
attualmente il Blue Gene dell’IBM.
• Ha una velocità di circa 360 teraflops:
3.6×
×1014 operazioni al secondo
(1tera=1000giga)
• Per
eseguire
4.1×
×1029
operazioni
impiegherebbe più di 36 milioni di anni.
• Come può essere se Page e Brin calcolano il
page rank ogni mese?
• Anche un calcolatore mille volte più veloce
non risolverebbe il problema.
26
Google: il problema del page
ranking
• Esistono metodi numerici (metodi che costruiscono
soluzioni approssimate che possono essere
implementate sul calcolatore) con i quali si riesce a
risolvere il sistema lineare, in tempi più brevi.
• Si parte da una ipotetica soluzione, si stima un errore
e in maniera iterativa la si migliora; si costruisce così
una successione che converge indipendentemente
dalla approssimazione iniziale:
xj (1) , xj (2) , xj (3) , ….
xj
con un errore di approssimazione che risulta essere:
e(k) ≤ λk dove λ (0 < λ < 1) è il modulo del
secondo autovalore più grande di una certa matrice.
27