Orario: Martedì 1013 Giovedì 1113 1416 Lab.Martedì 1416 RicevimentoGio.16 aula a.

Slides:



Advertisements
Presentazioni simili
RB-alberi (Red-Black trees)
Advertisements

Hash Tables Indirizzamento diretto Tabelle Hash Risoluzioni di collisioni Indirizzamento aperto.
Strutture dati elementari
Alberi binari di ricerca
Code con priorità Ordinamento
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl 1 Usa la tecnica del.
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl 1 Ordinamenti ottimi.
Capitolo 4 Ordinamento Algoritmi e Strutture Dati.
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl 1 Usa la tecnica del.
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl 1 Usa la tecnica del.
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl Capitolo 4 Ordinamento:
Algoritmi e strutture Dati - Lezione 7
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl Capitolo 4 Ordinamento:
Il problema del dizionario
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl 1 Capitolo 1 Unintroduzione.
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Capitolo 4 Ordinamento: Heapsort Algoritmi e Strutture Dati.
Algoritmi e Strutture Dati
Capitolo 4 Ordinamento: Selection e Insertion Sort Algoritmi e Strutture Dati.
Capitolo 4 Ordinamento Algoritmi e Strutture Dati.
Capitolo 4 Ordinamento: Selection e Insertion Sort Algoritmi e Strutture Dati.
Algoritmi e Strutture Dati
Capitolo 4 Ordinamento: Selection e Insertion Sort Algoritmi e Strutture Dati.
Capitolo 4 Ordinamento Algoritmi e Strutture Dati.
Algoritmi e Strutture Dati Alberi Binari di Ricerca.
Algoritmi e Strutture Dati (Mod. A)
Alberi di Ricorrenza Gli alberi di ricorrenza rappresentano un modo conveniente per visualizzare i passi di sostitu- zione necessari per risolvere una.
Algoritmi e Strutture Dati
Algoritmi e Strutture Dati 20 aprile 2001
Modello dati ALBERO Albero: Albero: insieme di punti chiamati NODI e linee chiamate EDGES EDGE: linea che unisce due nodi distinti Radice (root): in una.
Modello dati ALBERO Albero: Albero: insieme di punti chiamati NODI e linee chiamate EDGES EDGE: linea che unisce due nodi distinti Radice (root): in una.
QuickSort Quick-Sort(A,s,d) IF s < d THEN q = Partiziona(A,s,d) Quick-Sort(A,s,q-1) Quick-Sort(A,q + 1,d)
Anche la RB-Delete ha due fasi: Nella prima viene tolto un nodo y avente uno dei sottoalberi vuoto sostituendolo con la radice dellaltro sottoalbero. Per.
Metodo della moltiplicazione
Radix-Sort(A,d) // A[i] = cd...c2c1
Algoritmi e Strutture Dati
RB-insert(T, z) // z.left = z.right = T.nil Insert(T, z) z.color = RED // z è rosso. Lunica violazione // possibile delle proprietà degli alberi // rosso-neri.
Algoritmi e Strutture Dati
La complessità media O(n log n) di Quick-Sort vale soltanto se tutte le permutazioni dell’array in ingresso sono ugualmente probabili. In molte applicazioni.
1/32 Algoritmi e Strutture Dati HEAP Anno accademico
Algoritmi e Strutture Dati Luciano Gualà
Implementazione di dizionari Problema del dizionario dinamico Scegliere una struttura dati in cui memorizzare dei record con un campo key e alcuni altri.
GLI ALGORITMI VISIBILE SUL BLOG INFORMATICA ANNO SCOLASTICO 2013 / 2014 GABRIELE SCARICA 2°T.
Capitolo 6 Alberi di ricerca Algoritmi e Strutture Dati.
Risoluzione delle collisioni con indirizzamento aperto Con la tecnica di indirizzamento aperto tutti gli elementi stanno nella tavola. La funzione hash.
Algoritmi e Strutture Dati
Paola Disisto, Erika Griffini, Yris Noriega.  Insieme ordinato di operazioni non ambigue ed effettivamente computabili che, quando eseguito, produce.
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl Capitolo 4 Ordinamento:
Hashing. 2 argomenti Hashing Tabelle hash Funzioni hash e metodi per generarle Inserimento e risoluzione delle collisioni Eliminazione Funzioni hash per.
1 Ordinamento (Sorting) INPUT: Sequenza di n numeri OUTPUT: Permutazione π = tale che a 1 ’  a 2 ’  … …  a n ’ Continuiamo a discutere il problema dell’ordinamento:
Algoritmi e strutture Dati - Lezione 7 1 Algoritmi di ordinamento ottimali L’algoritmo Merge-Sort ha complessità O(n log(n))  Algoritmo di ordinamento.
Capitolo 10 Tecniche algoritmiche Algoritmi e Strutture Dati.
Capitolo 10 Tecniche algoritmiche Algoritmi e Strutture Dati.
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl 1 Soluzione esercizio.
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl Capitolo 4 Ordinamento:
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl 1 Soluzione esercizio.
1 Ordinamento (Sorting) Input: Sequenza di n numeri Output: Permutazione π = tale che: a i 1  a i 2  ……  a i n Continuiamo a discutere il problema dell’ordinamento:
Codici prefissi Un codice prefisso è un codice in cui nessuna parola codice è prefisso (parte iniziale) di un’altra Ogni codice a lunghezza fissa è ovviamente.
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Capitolo 4 Ordinamento: Heapsort Algoritmi e Strutture Dati.
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl 1 Progettare algoritmi.
Problemi risolvibili con la programmazione dinamica Abbiamo usato la programmazione dinamica per risolvere due problemi. Cerchiamo ora di capire quali.
Complessità Computazionale
Didattica e Fondamenti degli Algoritmi e della Calcolabilità Sesta giornata Risolvere efficientemente un problema in P: Il problema dell’ordinamento: Insertion.
GLI ALGORITMI DI ORDINAMENTO
Capitolo 4 Ordinamento: lower bound Ω(n log n) e MergeSort ((*) l’intera lezione) Algoritmi e Strutture Dati.
Camil Demetrescu, Irene Finocchi, Giuseppe F. ItalianoAlgoritmi e strutture dati Copyright © The McGraw - Hill Companies, srl 1 Capitolo 1 Un’introduzione.
Algoritmi e Strutture Dati HeapSort. Select Sort: intuizioni L’algoritmo Select-Sort  scandisce tutti gli elementi dell’array a partire dall’ultimo elemento.
Algoritmi e Strutture Dati Università di Camerino Corso di Laurea in Informatica (12 CFU) I periodo didattico Emanuela Merelli
Algoritmi e Strutture Dati Luciano Gualà
Prof.ssa Rossella Petreschi Lezione del 17 /10/2014 del Corso di Algoritmica Lezione n°5.
Prof.ssa Rossella Petreschi Lezione del 15 /10/2012 del Corso di Algoritmica B-alberi Lezione n°5.
Transcript della presentazione:

Orario: Martedì 1013 Giovedì Lab.Martedì 1416 RicevimentoGio.16 aula a

Problemi, Algoritmi e Strutture Dati

Problema: Ordinamento Input: una sequenza di n numeri Output: i numeri ricevuti in input ordinati dal più piccolo al più grande ovvero: dove a π (i) ≤ a π (i+1) π è una opportuna permutazione degli indici 1,…,n

Idea per ordinare… Ad ogni passo ho una sottosequenza ordinata in cui inserisco un nuovo elemento dell’input: ordinati elemento da inserire ordinati Non necessariamente ordinati

Insertion Sort Insertion-sort(A) 1.for j=2 to length(A) 2.do key = A[j] 3. {insert A[j] in A[1,…,j-1]} 4. i = j-1 5. while i>0 and A[i]>key 6.do A[i+1] = A[i] 7. i=i-1 8. A[i+1] = key

Esempio: A = {5,2,4,6,1,3} j= j= j= j= j= j=

Ordinamento Insertion Sort A[1,…,n] = vettore i,j,key = variabili Numeri ordinati n numeri

Problema Algoritmo Strutture Dati OutputInput

Strutture dati usate da Insertion-Sort DATI: Insieme di numeri: S OPERAZIONI: read(i) size(S) modify(i,x)

Insertion Sort Insertion-sort(A) 1.for j=2 to size(A) 2.do key = read(j) 3. {insert A[j] in A[1,…,j-1]} 4. i = j-1 5. while i>0 and read(i)>key 6.do modify(i+1,read(i)) 7. i=i-1 8. modify(i+1,key)

Strutture Dati Astratte DATI + OPERAZIONI “Che cosa” DATI OP 1 OP 2 OP n ……………

Esempio di ADS Dati = insieme S di numeri OP 1 = estrai il minimo OP 2 = estrai il massimo OP 3 = restituisci la taglia di S OP 4 = inserisci un nuovo numero in S

Insertion sort ADS = DS = Insieme S di numeri + Read, Size, Modify S=“A[1,…,n]” (vettore) Read(i)=“A[i]” Modify(i,x)=“A[i]=x”

ADS = che cosa vogliamo ? DS = come lo implementiamo ?

Quando una struttura dati è “buona” ? Una DS è buona quando non usa troppe risorse. Risorse Tempo Spazio di memoria Numero di processori …

Dimensione del problema Le risorse usate (tempo, spazio) si misurano in funzione della dimensione dell’istanza del problema che vogliamo risolvere. Esempio: se ordiniamo n numeri la taglia dell’input sarà n. se moltiplichiamo due matrici nxn sarà n 2. Etc… Il tempo e lo spazio saranno funzioni in n ( TIME(n), SPACE(n) )

Analisi di Insertion Sort Dati: A= Dimensione di A = nc 1 Read(i) impiega tempo c 2 Modify(i,x) impiega tempo c 3 NOTA: c 1, c 2, c 3 sono indipendenti da n e vengono perciò dette costanti

Insertion Sort: analisi del costo computazionale t j numero di elementi maggiori di A[j] dipende dai dati in input caso ottimo: t j = 0 caso pessimo: t j = j-1 caso medio: t j = (j-1)/2 tjtj A[j]

Insertion Sort: analisi del costo computazionale Insertion-sort(A) 1.for j=2 to length(A) 2.do key = A[j] 3. i = j-1 4. while i>0 and A[i]>key 5.do A[i+1] = A[i] 6. i=i-1 7. A[i+1] = key C1nC1n C 2 n-1 C 3 n-1 C 4 ∑(t j +1) C 5 ∑t j C 6 ∑t j C 7 n-1 j=2 n n n

T(n) = c 1 n + c 2 (n-1) + c 3 (n-1) + c 4 ∑(t j +1) + (c 5 +c 6 ) ∑t j + c 7 (n-1) caso ottimo: t j = 0 T(n) = an + b; lineare caso pessimo: t j = j-1 T(n) = an 2 + bn + c; quadratico caso medio: t j = (j-1)/2Esercizio!!!

Procedimento di astrazione Costo in microsecondi Costanti c i Costanti a, b, c an 2 + bn + c Ordini di grandezza: quadratico

Analisi Asintotica

Analisi asintotica Obiettivo: semplificare l’analisi del tempo di esecuzione di un algoritmo prescindendo dai dettagli implementativi o di altro genere. Classificare le funzioni in base al loro comportamento asintotico. Astrazione: come il tempo di esecuzione cresce in funzione della taglia dell’input asintoticamente. Asintoticamente non significa per tutti gli input. Esempio: input di piccole dimensioni.

“O grande” Limite superiore asintotico f(n) = O(g(n)), se esistono due costanti c e n 0, t.c. f(n)  cg(n) per n  n 0 f(n) e g(n) sono funzioni non negative Notazione usata ad esempio nell’analisi del costo computazionale nel caso pessimo

Limite inferiore asintotico f(n) =  (g(n)) se esistono due costanti c e n 0, t.c. cg(n)  f(n) per n  n 0 Usato per: tempo di esecuzione nel caso ottimo; limiti inferiori di complessità; Esempio: il limite inferiore per la ricerca in array non ordinati è  (n). “  grande”

Eliminiamo termini di ordine inferiore e costanti. 50 n log n è O(n log n) 7n - 3 è O(n) 8n 2 log n + 5n 2 + n è O(n 2 log n) Nota: anche se (50 n log n) è O(n 5 ), ci aspettiamo di avere approssimazioni migliori !!!

“tight bound” = approssimazione stretta. f(n) =  (g(n)) se esistono c 1, c 2, e n 0, t.c. c 1 g(n)  f(n)  c 2 g(n) per n  n 0 f(n) =  (g(n)) se e solo se f(n) = O(g(n)) e f(n) =  (g(n)) O(f(n)) è spesso usato erroneamente al posto di  (f(n)) “”“”

c 2 g(n) c 1 g(n) f(n) Taglia dell’input = n Tempo di esecuzione n0n0 f(n) =  (g(n))

”o piccolo” e ”  piccolo” f(n) = o(g(n)) Analogo stretto di O grande Per ogni c, esiste n 0, t.c. f(n)  cg(n) per n  n 0 Usato per confrontare tempi di esecuzione. Se f(n)=o(g(n)) diciamo che g(n) domina f(n). f(n)=  (g(n)) analogo stretto di   grande.

Notazione Asintotica Analogie con i numeri reali f(n) = O(g(n))f  g f(n) =  (g(n))f  g f(n) =  (g(n))f = g f(n) = o(g(n))f < g f(n) =  (g(n))f > g Abuso di notazione: f(n) = O(g(n)) Versione corretta: f(n) appartiene a O(g(n))

Non tutte le funzioni si possono confrontare !!! nn 1+sen(n) ??? 1+sen(n) oscilla tra 0 e 2

Esempi M = numero grande, es: ;  = numero piccolo, es: 0, ; Mn =  (n  ) Log(n) = o(n) [Log(n)] M = o(n  ) n M =o(2 n  ) 2 nM = o(n!) Mn! = o(n n )

Limiti e notazione asintotica f(n)/g(n) ---> c allora f(n) =  (g(n)) f(n)/g(n) ---> 0 allora f(n) = o(g(n)) f(n)/g(n) ---> ∞ allora f(n) =  (g(n))

Limiti e notazione asintotica log[f(n)] = o(log[g(n)])allora f(n) = o(g(n)) log[f(n)] =  (log[g(n)])non è detto che f(n) = o(g(n)) esempio: log[n] =  (log[n 2 ]) ma n = o(n 2 )

Tecniche Algoritmiche: Divide et Impera

Divide et Impera Divide et impera: Dividi: Se l’istanza del problema da risolvere è troppo “complicata” per essere risolta direttamente, dividila in due o più “parti” Risolvi ricorsivamente: Usa la stessa tecnica divide et impera per risolvere le singole parti (sottoproblemi) Combina: Combina le soluzioni trovate per i sottoproblemi in una soluzione per il problema originario.

MergeSort: Algoritmo Dividi: se S contiene almeno due elementi (un solo elemento è banalmente già ordinato), rimuovi tutti gli elementi da S e inseriscili in due vettori, S 1 e S 2, ognuno dei quali contiene circa la metà degli elementi di S. (S 1 contiene i primi n/2 elementi e S 2 contiene i rimanenti n/2 elementi). Risolvi ricorsivamente: ordina gli elementi in S 1 e S 2 usando MergeSort (ricorsione). Combina: metti insieme gli elementi di S 1 e S 2 ottenendo un unico vettore S ordinato (merge)

Mergesort: esempio

Merge Sort: Algoritmo Merge-sort(A,p,r) if p < r then q   p+r)/2 Merge-sort(A,p,q) Merge-sort(A,q+1,r) Merge(A,p,q,r) Rimuovi il più piccolo dei due elementi affioranti in A[p..q] e A[q+1..r] e inseriscilo nel vettore in costruzione. Continua fino a che i due vettori sono svuotati. Copia il risultato in A[p..r].

Merge … e così via

Equazioni ricorsive: un esempio semplice T(n) = 1 se n = 1 T(n/2) + 1 se n > 1 Come si risolve ???

Equazioni ricorsive Risultati e tempi di esecuzione di algoritmi ricorsivi possono essere descritti usando equazioni ricorsive Un equazione ricorsiva esprime il valore di f(n) come combinazione di f(n 1 ),...,f(n k ) dove n i < n. Esempio: Merge Sort

Metodo iteratvo T(n) = T(n/2) + 1 T(n/4) T(n/8) T(n/n) k Ci fermiamo quando 2 k =n k chiamate ricorsive

Dobbiamo valutare k. sappiamo che 2 k = n, quindi log 2 ( 2 k ) = log 2 (n), ovvero k = log 2 (n)

Induzione Dobbiamo dimostrare che una affermazione è vera per ogni n≥0 Teorema. Se 1. affermazione(0) è vera. 2. affermazione(n-1) vera implica affermazione(n) vera. Allora affermazione(n) vera per ogni n ≥ 0

Dimostrazione per induzione: esempio  n i=1 i = n(n+1)/2 affermazione(n):  1 i=1 i = 1(1+1)/2 = 1 OK affermazione(1): affermazione(n-1) “implica” affermazione(n):  n-1 i=1 i = (n-1)(n)/2  n i=1 i = n(n+1)/2 implica

Dimostrazione per induzione: esempio  n i=1 i =...ma  n-1 i=1 i + n = (n-1)(n)/2 + n = n(n+1)/2 L’uguaglianza tra questi due termini non è altro che affermazione(n-1) e quindi la assumiamo vera per ipotesi induttiva.

Metodo di sostituzione Primo passo: Ci buttiamo a “indovinare” una possibile soluzione: T(n) ≤ clog 2 (n) Secondo passo: la verifichiamo per induzione come segue: Assumiamo che T(n’) ≤ clog 2 (n’) per n’ < n e dimostriamo che T(n) ≤ clog 2 (n) c è una costante (indipendente da n) che determineremo strada facendo…

T(n) = T(n/2) + 1 ≤ clog 2 (n/2) + 1 = clog 2 (n) - clog 2 (2) + 1 = clog 2 (n) - c + 1 se c ≥ 1 allora ≤ clog 2 (n) Ipotesi induttiva !!! 

Equazioni ricorsive: un esempio più complicato T(n) =  (1) se n = 1 2T(n/2) +  (n) se n > 1 Soluzione T(n) =  (n log(n))

Albero di ricorsione Cn + 2T(n/2) C(n/2) + 2T(n/4) C(n/4) + 2T(n/8) = c n = c n + …… + = n(log(n))  (1) …… = c n Il fattore log(n) deriva dal fatto che l’albero ha un altezza log(n)

“Master Method” T(n) = aT(n/b) + f(n) a  1, b > 1, f(n) > 0 Poniamo x = log b a f(n) = O(n x-  ) con  >0 allora T(n) =  (n x ) f(n) =  (n x ) allora T(n) =  (n x log(n)) f(n) =  (n x+  ) con  >0 af(n/b) ≤ cf(n) con c n 0

… Merge sort T(n) =  (n log(n)) Insertion sortMerge sort Worst case Average case Best case  (n 2 )  (n log(n))  (n)  (n log(n))

Perchè ordinare è importante … velocizza molto la ricerca !!! Binary-search(A,x) i=0 j=length(A)-1 while i<j do k=  (i+j)/2  if A[k]=x then return true if A[k]>x then j=k-1 if A[k]<x then i=k+1 if A[i]=x then return true else return false

Analisi di Binary search Poniamo D(t)=j-i. D(t) è l’ampiezza del vettore sul quale ancora dobbiamo eseguire la ricerca dopo t confronti. Abbiamo D(0) = n-1 ……… D(t+1) = D(t)/2 Usciamo dal while quando D(t)<2 … ovvero se t ≥ log 2 n. Quindi T(n) =  (log 2 n)

Priority Queue (Code a Priorità) Dati: un insieme di elementi, ognuno dei quali ha una chiave (un intero per esempio). Operazioni: inserimento, trova il massimo, estrazione del massimo (massima chiave). Applicazioni delle PQ: Job scheduling Event-driven simulations

Implementazione (facile) usando vettori Prima soluzione: vettore ordinato. Ricerca massimo:  (1) operazioni estrazione massimo:  (1) operazioni inserimento:  (n) operazioni Seconda soluzione vettore non ordinato. Ricerca massimo:  (n) operazioni estrazione massimo:  (n) operazioni inserimento:  (1) operazioni Si può fare meglio ???

Grafi e Alberi G=(V,E) V={1,2,3,4,5,6,7,8} E={(1,2),(1,3),(1,4),(3,4),(6,7),(7,8)} {1,3,4,1} è un ciclo. Un grafo senza cicli è aciclico.

Un albero è un grafo aciclico con un numero di nodi uguale al numero di archi più uno ( |V|=|E|+1 ) Albero Foresta

r x qy w Radice r è la radice x è il padre di y y è un figlio di x x e q sono avi di w w e q sono discendenti di x q è fratello di y Foglie h(a) altezza del nodo a: h(x)=1 h(y)=h(q)=2 h(w)=3

Heap A={ 128, 64, 72, 8, 7, 12, 30, 1, 6, 3 } A(6) = 12

Heap: definizione formale Un Heap è un albero binario quasi completo. Quasi significa che possono mancare alcune foglie consecutive a partire dall’ultima foglia di destra. Per ogni nodo i: Value(i) ≤ Value(Parent(i)) Nota 1: il massimo si trova nella radice Nota 2: non c’è nessuna relazione tra il valore di un nodo e quello di un suo fratello

Memorizzazione di un heap in un vettore

Memorizzazione di un heap in un vettore Radice posizione 1 Per ogni nodo in posizione i: left-child(i) posizione 2i right-child(i) posizione 2i+1 parent(i)  i/2 

i AB HeapsHeap i2i2i+1 parte del vettore già heapizzato elemento da aggiungere al sotto heap (verde) 4i4i +38i8i +7

i AB IDEA: facciamo scendere il nodo i nell’albero fino a trovare la sua posizione. ? AB i

Heapify(A,i) l=left(i) r=right(i) if l≤heap-size(A) and A[l]>A[i] then largest=l else largest=i if r≤heap-size(A) and A[r]>A[largest] then largest=r if largest  i then Exchange(A[i],A[largest]) Heapify(A,largest)

Heapify: costo computazionale Caso pessimo: il nodo si sposta fino ad arrivare alle foglie. Heapify impiega tempo costante ad ogni livello per sistemare A[i], A[left(i)] e A[right(i)]. Esegue aggiustamenti locali al massimo height(i) volte dove height(i) = O(log(n))

Build-heap(A) heap-size(A)=length(A) for i=  length(A)/2  downto 1 do heapify(A,i) Analisi “approssimativa”: ogni chiamata a heapify costa O(log(n)). Chiamiamo heapify O(n) volte, quindi build-heap = O(nlog(n)) Domanda (esercizio): build-heap =  (nlog(n)) ?

PQ implementate con Heap Extract-max(A) if heap-size(A)<1 then “error” max=A[1] A[1]=A[heapsize(A)] heapsize(A)=heapsize(A)-1 Heapify(A,1) return max O(log(n))

PQ implementate con Heap max = max = ?? max = Heapify( )

PQ implementate con Heap Insert(A,x) heap-size(A)=heap-size(A)+1 i=heap-size(A) while i>1 and A[parent(i)]<x do A[i]=A[parent(i)] i=parent(i) A[i]=x O(log(n))

Heap Sort: l’idea. Heap Heapify Heap Heapify... avanti così...

Heap Sort Heap-Sort(A) build-heap(A) for i=length(A) downto 2 do exchange(A[1],A[i]) heap-size(A)=heap-size(A)-1 heapify(A,1) O(nlog(n)) È un metodo “in place”

Quicksort: l’idea Dividi: Dividi il vettore in due parti non vuote. Conquista: ordina le due parti ricorsivamente Combina: fondi le due parti ottenendo un vettore ordinato. A={10,5,41,3,6,9,12,26} mergesort quicksort A metà A 1 ={10,5,41,3} A 2 ={6,9,12,26} Intorno a un Pivot, es 12 A 1 ={10,5,3,6,9,12} A 2 ={41,26} Dividi

Quicksort Quicksort(A,p,r) if p<r then q=partition(A,p,r) Quicksort(A,p,q) Quicksort(A,q+1,r) Nota: Mergesort lavora dopo la ricorsione Quicksort lavora prima della ricorsione Partition è cruciale !!!

ij i i i i i j j j j j A(p,r) < 5≥ 5 Pivot  (n) in place

Analisi di QS nel caso ottimo Caso ottimo: partizioni bilanciate T(n) = 2T(n/2) +  (n) quindi: T(n) =  (nlog(n))

Analisi di QS nel caso pessimo Caso pessimo: partizioni sbilanciate T(n) = T(n-1) +  (n) quindi: T(n) =  (n 2 ) ricorsione partition

Analisi di QS nel caso non buono ! 90% 10% T(n) ???

Albero di ricorsione n 1/10 n9/10 n 1/100 n9/100 n 81/100 n n +  (n log(n)) < n 81/1000 n729/1000 n log 10 n log 10/9 n

Analisi del caso medio di QS: una intuizione. Caso medio: a volte facciamo una buona partition a volte no... buona partition: cattiva partition

Caso medio le buone e le cattive partition si alternano... cattiva 1n-1 1(n-1)/2 dopo una cattiva e una buona partizione in successione siamo più o meno nella situazione in cui la cattiva partizione non è stata fatta ! buona

QS: distribuzione degli input Abbiamo assunto implicitamente che tutte le sequenze di numeri da ordinare fossero equiprobabili. Se ciò non fosse vero potremmo avere costi computazionali più alti. Possiamo “rendere gli input equiprobabili” ? mischiamo la sequenza casualmente prima di ordinare Scegliamo il pivot a caso. come procediamo

QS “randomizzato” QSR una una versione randomizzata della procedura Partition. Randomized-partition(A,p,r) i=random(p,r) exchange(A[p],A[i]) return partition(A,p,r) Un algoritmo randomizzato non ha un input pessimo, bensì ha una sequenza di scelte pessime di pivot.

Insertion sort Merge sort Heap sort Quick sort Caso pessimo n2n2 n log(n) n2n2 Caso medio n2n2 n log(n) Caso ottimo nn log(n) = in place

È possibile ordinare in meno di n log(n) ??? ovvero in o(n log(n))

Limite inferiore di complessità Insertion-sort Merge-sort Heap-sort Quick-sort “Comparison-sort” algoritmi basati su confronti Questi metodi calcolano una soluzione che dipende esclusivamentedall’esito di confronti fra numeri TEOREMA (Lower Bound per algoritmi Comparison-sort): Qualsiasi algoritmo “comparison-sort” deve effettuare nel caso pessimo  (n log(n)) confronti per ordinare una sequenza di n numeri.

lower bound per comparison sort IDEA: con n numeri ho n! possibili ordinamenti. Possiamo scegliere quello giusto tramite una sequenza di confronti. ≤> >> ≤ ≤ Ogni nodo rappresenta un confronto.

Esempio: n=3 {a 1,a 2,a 3 } a 1 :a 2 a 2 :a 3 a 1 :a 3 a 1,a 2,a 3 a 1 :a 3 a2,a1,a3a 2 :a 3 ≤> >> ≤ ≤ Ogni nodo bianco rappresenta un confronto. Ogni nodo rosso rappresenta una possibile soluzione. a 1,a 3,a 2 a 3,a 1,a 2 >≤ a 2,a 3,a 1 a 3,a 2,a 1 >≤ albero dei confronti

3! = 6 = numero di foglie dell’albero dei confronti. ogni (cammino dalla radice ad una) foglia rappresenta un ordinamento ci sono n! ordinamenti. quanto deve essere alto un albero binario per avere n! foglie ??? un albero binario alto h ha al massimo 2 h foglie dobbiamo avere 2 h ≥ n! Formula di Stirling: n! > (n/e) n e= h ≥ log[(n/e) n ] = nlog(n) - nlog(e) =  (nlog(n))

Il caso pessimo di un qualsiasi algoritmo comparison-sort eseguito su una sequenza di n numeri è dato dall’altezza dell’albero di decisione associato a quell’algoritmo. MA Un albero binario con n! foglie (ordinamenti) ha un altezza  (nlog(n)) QUINDI qualsiasi algoritmo comparison-sort, nel caso pessimo, esegue  (nlog(n)) confronti.

Counting sort: come ordinare in tempo lineare (!?!?) Ipotesi di lavoro: I numeri da ordinare appartengono all’intervallo [1,k] Risultato: counting sort ha un costo computazionale O(n + k) Se k=O(n) allora counting sort ha un costo computazionale O(n) e quindi “batte” tutti i comparison sort

Counting sort: un esempio A = {3,6,4,1,3,4,1,4} C’ = {2,0,2,3,0,1} C’’ = {2,2,4,7,7,8}. C’[3]=2 perché il numero 3 è contenuto 2 volte in A C’’[4]=7 perché ci sono 7 numeri minori o uguali a 4

Algoritmi di ordinamento stabili A B C D E F C E D A B F

Algoritmi di ordinamento NON stabili A B C D E F E C D A B F

Algoritmi di ordinamento stabili Un algoritmo di ordinamento è stabile se: Se A[i] = A[j] e i < j allora A[i] compare nell’ordinamento prima di A[j] ESERCIZIO: dimostrare che counting sort è stabile.

Counting-sort(A,B,k) 1.for i=1 to k do C[i]=0 2.for j=1 to length(A) do C[A[j]]=C[A[j]]+1 3.for i=2 to k do C[i]=C[i]+C[i-1] 4.for j=length(A) downto 1 do 5.B[C[A[j]]]=A[j] 6.C[A[j]]=C[A[j]]-1 1.costa  (k) 2.costa  (n) 3.costa  (k) 4.costa  (n) Quindi Counting sort =  (n + k)

Radix sort vettore ordinato

Radix sort Radix-sort(A,d) for i=1 to d do usa un “stable sort” per ordinare A sulla cifra iesima Ogni cifra è compresa tra 1 e k. Usiamo counting sort (stabile). Costo computazionale: d  (n+k) =  (nd+dk). Counting sort non lavora in place !

Bucket sort Ipotesi: i numeri da ordinare sono uniformemente distribuiti nell’intervallo [0,1), ovvero Ci si aspetta che nell’intervallo [x,x+  ) ci siano tanti numeri quanti in [y,y+  ) per qualunque x,y, 

Bucket-sort(A) n=length(A) for i=1 to n do inserisci A[i] nella lista B[  nA[i]  ] for i=0 to n-1 do ordina la lista B[i] usando insertion-sort Concatena le liste B[0],...,B[n-1] NOTA:  nA[i]  restituisce il “bucket” dove inserire A[i]

Variabile Aleatoria Discreta: variabile che può assumere un numero finito di valori con una certa distribuzione di probabilità. Esempio 1: X = (Testa, Croce) Pr(X=Testa) = Pr(X=Croce) = 1/2 Esempio 2: Y = (1,2,3,4) Pr(Y=1) = Pr(Y=2) = 1/3, Pr(Y=3) = Pr(Y=3) = 1/6.

Media di una VAD E[Y]= 1·1/3 + 2·1/3 + 3·1/6 + 4·1/6 = 13/6 Valori possibili di Y Probabilità

Vogliamo calcolare il costo computazionale medio di Bucket Sort. Bucket Sort usa Insertion Sort sulle singole liste. Assumendo che la lunghezza media di ogni lista sia n i, il costo della singola applicazione di Insertion Sort è (n i ) 2 (nel caso pessimo !!!) Dobbiamo quindi valutare E[(n i ) 2 ]. NOTA: E[X 2 ] diversa da E[X] 2

n i = variabile aleatoria = numero di elementi nel bucket i Pr(x ---> B i ) = 1/n Distribuzione binomiale Pr(n i = k) = ( ) (1/n) k (1-1/n) n-k E[n i ] = n 1/n = 1 E[n i 2 ] = Var[n i ] + E 2 [n i ] = 2 - 1/n =  (1) E[n i 2 ] = costo computazionale di insertion sort Costo computazionale di bucket sort = O(n) nknk

Selezione: esempio minimo massimo quarto elemento nell’ordinamento Input Input ordinato

Selezione Ordinamento  (nlog(n)) minimo/massimo  (n) Selezione ???? Selezione: Calcolare l’iesimo elemento nell’ordinamento.

Selezione Input: un insieme A di n numeri distinti e un numero i tra 1 e n Output: l’elemento x in A maggiore di esattamente i-1 altri numeri in A Soluzione banale: ordino gli elementi di A e prendo l’iesimo elemento nell’ordinamento.  (nlog(n)) (analisi del caso pessimo)

A[1,...,n] 1 n q Supponiamo che A[i] ≤ A[j] per ogni 1 ≤ i ≤ q e ogni q < j ≤ n Domanda: il k-esimo elemento nell’ordinamento sta in L o in R ? LR Risposta: Facile. Se k ≤ q ---> L. Altrimenti R.

Selezione in tempo medio lineare Rand-select(A,p,r,i) if p=r then return A[p] q=rand-partition(A,p,r) k=q-p+1 if i≤k then return Rand-select(A,p,q,i) else return Rand-select(A,q+1,r,i-k) caso pessimo  (n 2 ) caso medio  (n) (senza dimostrazione)

Selezione in tempo lineare nel caso pessimo IDEA: dobbiamo progettare un buon algoritmo di partition  n (1-  )n Nota: basta che  sia maggiore di zero e indipendente da n !!!

Select(i) 1.Dividi i numeri in input in gruppi da 5 elementi ciascuno. 2.Ordina ogni gruppo (qualsiasi metodo va bene). Trova il mediano in ciascun gruppo. 3.Usa Select ricorsivamente per trovare il mediano dei mediani. Lo chiamiamo x. 4.Partiziona il vettore in ingresso usando x ottenendo due vettori A e B di lunghezza k e n-k. 5.Se i≤k allora Select(i) sul vettore A altrimenti Select(i-k) sul vettore B.

5 Mediano della terza colonna  n/5  = M Calcoliamo il mediano di M usando Select ricorsivamente !!!

Supponiamo di aver riordinato le colonne a seconda del valore del loro mediano. = mediano dei mediani. Maggiori o uguali di Minori o uguali di

più o meno 3 n/10 Se partizioniamo intorno a lasciamo almeno (circa) 3n/10 elementi da una parte e almeno (circa) 3n/10 elementi dall’altra !!! OK

Select: costo computazionale T(n) =  (1)se n < c  (n) + T(n/5) + T(7n/10 ) se n ≥ c Costo per ordinare le colonne Costo per calcolare il mediano dei mediani Costo per la chiamata ricorsiva di select T(n) ≤ k n, k costante opportuna Dim: Esercizio

Strutture Dati Elementari: Pile e Code

Pile (Stacks) Dati: un insieme S di elementi. Operazioni: PUSH, POP PUSH: inserisce un elemento in S POP: restituisce l’ultimo elemento inserito e lo rimuove da S Politica: Last-In-First-Out (LIFO)

Pila PUSH…

Pila POP…

Code (Queues) Dati: un insieme S di elementi. Operazioni: ENQUEUE, DEQUEUE ENQUEUE : inserisce un elemento in S DEQUEUE : restituisce l’elemento da più tempo presente (il più vecchio) e lo rimuove da S Politica: First-In-First-Out (FIFO)

Coda ENQUEUE…

Coda DEQUEUE…

Implementazione di Pile con Vettori STACK-EMPTY(S) If top[S] = 0 then return TRUE else return FALSE PUSH(S,x) top[S] = top[S] + 1 S[top[S]] = x POP(S) if STACK-EMPTY(S) then “error” else top[S] = top[S] - 1 return S[top[S] + 1]

Implementazione di code con Vettori ENQUEUE(Q,x) Q[tail[Q]] = x if tail[Q] = length[Q] then tail[Q] = 1 else tail[Q] = tail[Q] + 1 DEQUEUE(Q,x) x = Q[head[Q]] if head[Q] = length[Q] then head[Q] = 1 else head[Q] = head[Q] + 1

Problemi con i vettori Vettori Semplici, Veloci ma Bisogna specificare la lunghezza staticamente Legge di Murphy Se usi un vettore di lunghezza n = doppio di ciò che ti serve, domani avrai bisogno di un vettore lungo n+1 Esiste una struttura dati più flessibile?

Linked Lists Dato 1nextDato 2nextDato 3--- Head[L] Elemento della lista: Dato + puntatore all’ elemento successivo nella lista

Doubly Linked Lists Dato Head[L] next Dato 2prevnext Dato 3prev---- Elemento della lista: Dato + puntatore al predecessore + puntatore al successore

Ricerca e Inserimento LIST-SEARCH(L,k) x = head[L] while x nil and key[x] k do x = next[x] return x LIST-INSERT(L,x) next[x] = head[L] if head[L] nil then prev[head[L]] = x head[L] = x prev[x] = nil

Cancellazione LIST-DELETE(L,k) x = LIST-SEARCH[L,k] if prev[x] nil then next[prev[x]] = next[x] else head[L] = next[x] if next[x] nil then prev[next[x]] = prev[x]

Costi Computazionali InserimentoLIST-INSERT  1) CancellazioneLIST-DELETE  1) RicercaLIST-SEARCH  n)

Sentinelle ---prev nil[L] next Lista vuota usando le sentinelle

Sentinelle xlast nil[L] first Dato 1prevnext sentinella Dato 2prevnext Dato 3prevnext Dato 4prevnext

Cancellazione usando le sentinelle LIST-DELETE(L,k) x = LIST-SEARCH[L,k] if prev[x] nil then next[prev[x]] = next[x] else head[L] = next[x] if next[x] nil then prev[next[x]] = prev[x] LIST-DELETE-SENTINEL(L,k) x = LIST-SEARCH[L,k] next[prev[x]] = next[x] prev[next[x]] = prev[x]

Esercizio: usare le sentinelle per SEARCH e INSERT

Alberi binari rappresentati usando liste Padre Figlio sinistro Filgio destro

Alberi generali rappresentati usando liste Padre Primo figlio Primo fratello --- Lista dei fratelli

Costo computazionale delle operazioni sulle liste Singly linked non ordinata Singly linked ordinata Doubly linked non ordinata Doubly linked ordinata Ricerca Inserimento Cancellazione Successore Predecessore Massimo

Tabelle Hash

Elemento 1 Elemento 12 Elemento 2 Elemento 41 Elemento i 1 |S| Vettore V Ogni elemento ha una chiave K i tale che 0 < K i < n+1 Insieme S

Tabella con indirizzamento diretto i iElemento i-esimo j jElemento j-esimo Posizione nel vettore = valore della chiave

Tabella con indirizzamento diretto Search(V,k) return V[k] Insert(V,x) V[key[x]] = x Delete(V,x) V[key[x]] = nil Costo computazionale =  (1)

Problemi… Supponiamo che solo una parte S’ dello spazio S delle chiavi sia utilizzata/attiva. Cosa succede quando |S’| << |S| ? Si spreca spazio di memoria !!! Soluzioni?

Problemi… S’ S = Spazio sprecato

Una soluzione … Possiamo ridurre l’occupazione di spazio da  (|S|) a  (|S’|) Usando LINKED LISTS !!! PROBLEMA (non finiscono mai): Inserimento, Cancellazione e Ricerca costano  (|S’|) invece di  (1).

Vero Problema: compromesso tra TEMPO e SPAZIO Usando Hash tables possiamo raggiungere: Tempo di accesso:  (1) Spazio di memoria:  (|S’|) Ma … in media e non nel caso pessimo !

IDEA … i iElemento i-esimo h(i) iElemento i-esimo h = funzione hash

Funzione hash h restituisce un numero intero da 1 a M. Usiamo una tabella con M posizioni x viene memorizzato in posizione h(key[x])

Proprietà per una “buona” h  Deterministica ma deve sembrare “random” in modo da minimizzare le collisioni.  x e y generano una collisione se x ≠ y e h(x) = h(y)  h deve minimizzare il numero di collisioni

kiki kjkj kiki kjkj -- h(k i )=h(k j ) Risoluzione di collisioni con “chaining”

Chained-hash-insert(T,x) Inserisci x in testa alla lista: T[h(key[x])] Chained-hash-search(T,k) Ricerca l’elemento con chiave k nella lista: T[h(k)] Chained-hash-delete(T,x) cancella x dalla lista: T[h(key[x])]

Chaining: analisi Load factor  =n/m, n = numero di elementi memorizzati in T m = dimensione di T Caso pessimo: tutte le n chiavi finiscono nella stessa posizione. Ricerca =  (n) Caso medio: Simple uniform hashing: Pr(h(k)=i) = Pr(h(k)=j)

Simple uniform hashing: un esempio m= (1/2) 2 (1/8) 3 (1/8) 5 (1/16) 6 (1/8) 4 (1/16) U= in rosso è indicata la probabilità che una certa chiave debba essere inserita nella tabella h NON UNIFORME !!! PERCHE’ ???

Simple uniform hashing: un esempio m= (1/2) 2 (1/8) 3 (1/8) 5 (1/16) 6 (1/8) 4 (1/16) U= in rosso è indicata la probabilità che una certa chiave debba essere inserita nella tabella h UNIFORME !!! PERCHE’ ???

Simple uniform hashing Una funzione hash si dice uniforme quando rende uniforme il riempimento della tabella. Non quando la distribuzione delle chiavi è uniforme !!!

Teorema: Ipotesi: collisioni gestite con chaining simple uniform hashing caso medio Tesi: una ricerca ha costo computazionale  (1+  )

Dimostrazione: Caso di ricerca senza successo. Load factor  è la lunghezza media di una catena. In una ricerca senza successo il numero di elementi esaminati è uguale alla lunghezza media delle catene. Calcolare h() costa 1.

Dimostrazione: Caso di ricerca con successo. Assumiamo di inserire elementi in testa alla catena. Simple uniform hashing numero medio di elementi in una catena dopo i inserimenti = i/m l’elemento j ci si aspetta che venga inserito nella posizione 1 +(j-1)/m all’interno di una catena.

Un elemento generico finirà in media nella posizione data dalla formula: 1/n  ( 1+ (i-1)/m ) = 1/n (n + [n(n+1)]/[2m] - n/m) = = 1 +  /2 - 1/(2m) = =  (1+  ) i=1 n

Supponiamo che n=O(m). Ovvero che il numero di elementi inseriti nella tabella sia proporzionale alla dimensione della tabella. Abbiamo:  = n/m = O(m)/m = O(1) In questo caso la ricerca impiega tempo costante !!! Cosa succede se gli elementi vengono inseriti all’inizio delle liste ?

Riepiloghiamo... Se usiamo doubly linked lists per le catene e se inseriamo i nuovi elementi in testa alle liste abbiamo Ricerca Cancellazione Inserimento O(1) operazioni in media

Funzioni hash: progettazione Pr(k) = probabilità della chiave k S j ={ k  U tali che h(k)=j } Vogliamo uniform hashing ovvero  Pr(k) = 1/m (m=dimensione della tabella) kSjkSj

Esempio U = { x  R : 0≤x<1 } x preso a caso da U. Definiamo h(x)=  xm  Dimostrare per esercizio che h() è una buona hash function. (suggerimento: definire S j esplicitamente)

Se Pr() è sconosciuta Usiamo euristiche IDEA:  h deve dipendere da tutti i bit di k  deve essere indipendente da eventuali pattern che possono essere presenti nelle chiavi

Supponiamo per semplicità che le chiavi siano numeri naturali. Metodo della divisione h(k) = k mod m Esempio: m=12, k=100, h(100) = 100 mod 12 = 4 Per controllare se uno ha scelto un buon m è consigliabile usare un “benchmark” reale.

Metodo della moltiplicazione h(k) =  m(kA mod m)  Esempio: A = (5 1/2 -1)/2 = , k = , m = h(123456) = conti conti conti = 41

Risoluzione collisioni: open addressing h( ,0) =1  h( ,0) =1h( ,1) =2  h(,0) =2h(,1) =4 h( ,0) =2h( ,1) =4h( ,2) =5 

Open addressing Nessun puntatore: spazio risparmiato!  ≤ 1 sempre. Nessuna lista per gestire le collisioni Hash function più complessa. deve essere una permutazione di

h(k,i) = posizione della tabella in cui inserire la chiave k quando tutte le posizioni h(k,0),..., h(k,i-1) sono già occupate.

Open addressing: uniform hashing Se gestiamo le collisioni con il metodo open addressing, la funzione hash restituisce una permutazione degli indici. Invece di simple uniform hashing parliamo di uniform hashing. Uniform hashing: tutte le permutazioni devono apparire con la stessa probabilità

Open addressing: inserimento Hash-insert(T,k) i=0 repeat j=h(k,i) if T[j]=nil then T[j]=k return j else i=i+1 until i=m error “hash table overflow”

Open addressing: ricerca Hash-search(T,k) i=0 repeat j=h(k,i) if T[j]=k then return j else i=i+1 until (T[j]=nil) or (i=m) return nil

Open addressing: cancellazione Hash-delete(T,k) i=Hash-search(T,k) if i  nil then T[i]=nil NON FUNZIONA

h(6,0)=0 h(6,1)=1 h(6,2)=2 h(6,3)=3 Inseriamo Cancelliamo ricerchiamo 6 Risposta: 6 non c’è

Esercizio: Modificare Hash-search e Hash-delete per risolvere il problema illustrato nel lucido precedente. 3 7 D Suggerimento: usare un carattere con il quale contrassegnare gli elementi cancellati.

Open addressing: linear probing Sia h’ una funzione hash “ordinaria”. Definiamo h(k,i)=(h’(k) + i) mod m Esempio di linear probing: m=5, k=3, h’(3)=4 h(k,0) = 4 h(k,1) = 5 h(k,2) = 0 h(k,3) = 1 h(k,4) = 2 h(k,5) = 3 = probe

Linear probing: primary clustering Tempo medio di accesso per una ricerca senza successo: 1.5 Perche’? Tempo medio di accesso per una ricerca senza successo: 2.5 Perche’? Clustering primario

Usando linear probing il clustering primario si forma con alta probabilità. Pr = (i+1)/m i slot pieni i+1 slot pieni Pr = 1/m i slot vuoti

Quadratic probing h(k,i) = (h’(k) + c 1 i + c 2 i 2 ) mod m con c 2  0 Cosa si può dire sul clustering primario ?

Double hashing h(k,i) = (h 1 (k) + ih 2 (k)) mod m Cosa succede se MCD(m,h 2 (k)) = d > 1 ??? Quante permutazioni distinte produce il double hashing ???

Open addressing: ricerca Teorema: Data una hash table con open addressing e load factor  = n/m < 1 la lunghezza media di una “probe” in una ricerca senza successo è 1/(1-  ). (Ipotesi: uniform hashing)

1/(1-  ) = m  = (m-1)/m (valore massimo di  ) 1/(1-  ) = m/(m-1)  = 1/m (valore minimo di  ) 1/(1-  ) = 2  = 1/2

Dimostrazione: IDEA: cosa succede quando facciamo una ricerca senza successo ??? Empty X = lunghezza probe = quante volte devo calcolare h(k,i) prima di trovare uno slot vuoto = elemento non trovato Dobbiamo valutare E[X] = media di X

Lemma: X variabile aleatoria discreta X= 0 --> p > p 1 i --> p i E[X] =  i=0 ∞ ip i  i=0 ∞ iPr(X=i)  i=0 ∞ i ( Pr(X≥i) - Pr(X≥i+1) )  i=1 ∞ Pr(X ≥i) = == ESERCIZIO !!!

E[X] =  i=1 ∞ (n/m) i  i=1 ∞ ii  ∞ Pr(X≥i) ≤ ≤ Costo per la ricerca: 1 + E[X] = 1 +  i=1 ∞  i = 1 +  +  2 +  / (1-  )

Open addressing: inserimento Teorema: Data una hash table con open addressing e load factor  = n/m < 1, la lunghezza media di una “probe” è 1/(1-  ). (Ipotesi: uniform hashing)

Dimostrazione: Nota:  deve essere < 1. Per inserire un elemento abbiamo bisogno di determinare la posizione nella tabella dove inserirlo. Ricerca: costo 1/(1-  ). Per inserire nella tabella nella posizione appena determinata:  (1).

Alberi Binari di Ricerca

Alberi di ricerca binari ≤ >

Alberi di ricerca binari ≤ >

BST: definizione formale Sia x un nodo dell’albero: Se y è un nodo nel sottoalbero sinistro di x allora key[y]≤key[x] Se y è un nodo nel sottoalbero destro di x allora key[y]>key[x] Nota che un BST può essere molto sbilanciato !!! bilanciato sbilanciato

Inorder-tree-walk(x) if x ≠ nil then Inorder-tree-walk(left[x]) print key[x] Inorder-tree-walk(right[x]) ORDINAMENTO

Ricerca ricerchiamo il 16  (h) confronti h=altezza albero

Ricerca Tree-search(x,k) if x=nil or k=key[x] then return x if k<key[x] then return Tree-search(left[x],k) else return Tree-search(right[x],k) Esercizio: dimostrare che il costo computazionale di Tree-search è  (h)

Successore 2017 x ha il figlio destro. successore(x)=minimo nel sottoalbero di destra Dimostrazione: Esercizio.

Successore 2017 x non ha il figlio destro. successore(x) = il più basso avo di x il cui figlio sinistro è avo di x Dimostrazione: Esercizio.

Operazioni su BST Ricerca Minimo Massimo Predecessore Successore  (h) confronti

Inserimento

Cancellazione 3 casi: x non ha figli: elimina x x ha un figlio: x ha 2 figli: Lemma: il successore di x sta nel sotto albero destro e ha al massimo 1 figlio. Dimostrazione: esercizio.

eliminiamo successore di 15

ha preso il posto di 15

Cancellazione di un nodo x con 2 figli: 1. sia y = successore di x. y ha un solo figlio (al massimo) 2. sostituisci x con y. 3. rimuovi y.

Problema Tutte le operazioni su BST hanno un costo lineare nell’altezza dell’albero. Purtroppo, quando l’albero è sbilanciato, h = n-1 Le operazioni hanno un costo lineare invece che logaritmico come speravamo !!! Soluzione ???

Soluzione Introduciamo alcune proprietà addizionali sui BST per mantenerli bilanciati. Paghiamo in termini di una maggiore complessità delle operazioni dinamiche sull’albero. Tali operazioni devono infatti preservare le proprietà introdotte per mantenere il bilanciamento.

Red-Black Trees

Red Black Trees = BST + alcune proprietà aggiuntive

Proprietà A ogni nodo è rosso o nero

Proprietà B ogni foglia è nera (ne aggiungiamo un livello fittiziamente)

Proprietà C un nodo rosso ha figli neri

Proprietà D tutti i cammini da un nodo x alle foglie ha lo stesso numero di nodi neri nodi neri

Idea di base Proprietà D: se un RB tree non ha nodi rossi è completo. Possiamo immaginarci un RB tree come un albero nero completo a cui abbiamo aggiunto “non troppi” nodi rossi (Proprietà C). Ciò rende l’albero “quasi bilanciato”

Black-height di un RB tree bh(x) = numero di nodi neri (senza contare x) nel cammino da x a una foglia) bh(root) = black-height dell’albero

bh(x)

Teorema: Un RBT con n nodi interni è alto al massimo 2log 2 (n+1) Lemma: Il numero di nodi interni di un sotto albero radicato in x è maggiore o uguale a 2 bh(x) -1

Dimostrazione del lemma. Per induzione sull’altezza di x. CASO BASE: h(x)=0. Se h(x)=0 allora bh(x)=0 inoltre x è una foglia quindi il numero di nodi interni è 0. INDUZIONE: h(x)>0. x ha 2 figli: L e R. Abbiamo 2 casi: L è rosso: bh(L) = bh(x) L è nero: bh(L) = bh(x) - 1 R viene trattato in modo analogo

Visto che h(L) < h(x) e h(R) < h(x) applichiamo l’ipotesi induttiva. Numero di nodi interni dell’albero radicato in L maggioreo o uguale a 2 bh(L) - 1. Stesso discorso per R. Inoltre 2 bh(L) -1  2 bh(x) e 2 bh(R) - 1  2 bh(x) Quindi: numero di nodi interni dell’albero radicato in x maggiore o uguale a 2 bh(x) bh(x) che è uguale a 2 bh(x) - 1.

Dimostrazione del teorema. Sia h l’altezza dell’albero. Qualsiasi cammino dalla radice --> foglia contiene almeno metà nodi neri. Il cammino radice --> foglia che determina l’altezza dell’albero contiene almeno h/2 nodi neri. bh(T) = bh(root)  h/2 Lemma --> n  2 bh(root) - 1 quindi: n  2 bh(root) - 1  2 h/ concludiamo: log 2 (n+1)  log 2 (2 h/2 ) = h/2.

Rotazioni Operazioni di ristrutturazione locale dell’albero che mantengono soddisfatte le proprietà A,B,C,D Y X A CB Y X AC B destra sinistra esercizio: scrivere il pseudo codice per le rotazioni

Inserimento Idea: inseriamo x ---> T color[x]=red qualche rotazione + ricolorazione nodo inserito ---> ---> !!!!!! il figlio di un nodo rosso deve essere nero

p

p rotazione sinistra --->

p 8 rotazione destra --->

FINE...

metodo generale: zio(x) è rosso C A x B D p yz ws rosso up A C x B p yz ws D

metodo generale: zio(x) è rosso C B x A D p y zws rosso up B C x A p y zws D

metodo generale: zio(x) è nero C A x B B p yz nero left(A) x A p y z D D C

... e poi... B right(C) x A p y z D C C x A y zD B

Ci sono un certo numero di casi analoghi riconducibili a quelli esaminati: esercizio.

Cancellazione di un nodo da un RB-tree Come nel caso dei BST, possiamo sempre assumere di eliminare un nodo che ha al massimo un figlio. Infatti se dobbiamo cancellare un nodo x con due figli, spostiamo la chiave del successore y di x in x e poi rimuoviamo y dall’albero. Il successore di un nodo con due figli ha sempre al massimo un figlio.

Cancellazione di un nodo con ≤ 1 figlio Sia x il nodo da cancellare. Sia y il figlio di x e sia z il padre di x. 1. rimuoviamo x collegando z con y. 2. se x era rosso allora y e z sono neri e non dobbiamo fare altro. 3. se x era nero e y è rosso allora coloriamo y di nero. Se x era nero e y è nero allora ricoloriamo y con un colore nero “doppio”. Dopodichè svolgiamo alcune altre operazione descritte nel seguito.

Esempio z y x z y z y x z y ricolora doppio nero che va poi ridistribuito su un nodo rosso annerendolo. z y x

Cancellazione di un nodo con ≤ 1 figlio Idea: cercare di far salire il doppio nero nell’albero fino a trovare un nodo rosso sul quale scaricare una parte del nero del nodo “doppio nero” e riottenere una colorazione legale. Per far ciò operiamo operazioni di ristrutturazioni locali dell’albero e ricolorazioni che possono propagare il doppio nero due livelli più in alto

Caso 1: fratello nero con almeno un figlio rosso x y s t x y s t x y s t rotazione sinistra rotazione derstra e poi sinistra ab x ys t ab

Caso 2: fratello nero con figli neri x y s x y s x y s x y s

Caso 3: fratello rosso x y s ab x y s a b rotazione sinistra

Programmazione Dinamica

Divide et impera: si suddivide il problema in sotto problemi indipendenti, si calcola ricorsivamente una soluzione per i sottoproblemi e poi si fondono le soluzioni così trovate per calcolare la soluzione globale per il problema originale. Programmazione dinamica: simile all’approccio divide et impera, ma in questo caso si tiene traccia (in una tabella) delle soluzioni dei sottoproblemi perchè può capitare di dover risolvere il medesimo sottoproblema per più di una volta.

Prodotto di una sequenza di matrici Dobbiamo calcolare A=A 1 A 2 A m dove A i sono matrici di opportune dimensioni (righe x colonne). In che ordine conviene effettuare le moltiplicazioni ?

Moltiplicazioni di matrici Matrix-multiply(A,B) if columns(A)  rows(B) then “error” else for i=1 to rows(A) do for j=1 to columns(B) do C[i,j]=0 for k=1 to columns(A) do C[i,j]=C[i,j]+A[i,k]B[k,j]  (rows(A) columns(B) columns(C)) moltiplicazioni.

Esempio: A=MNQ M = 10 righe, 100 colonne N = 100 righe, 5 colonne Q = 5 righe, 50 colonne Primo metodo A=((MN)Q). Numero di moltiplicazioni: per calcolare A’=MN per calcolare A=A’Q Totale 7500 Secondo metodo A=(M(NQ)). Numero di moltiplicazioni: per calcolare A’=NQ per calcolare A=MA’ Totale 75000

Numero di possibili parentesizzazioni Il numero di possibili parentesizzazioni P(n) può essere ottenuto come segue: P(n) = 1 n=1 n>1  P(k)P(n-k) k=1 n-1 numero di parentesizzazioni delle prime k matrici numero di parentesizzazioni delle altre n-k matrici

P(n) risulta essere  (4 n /n 3/2 ) Quindi esponenziale in n. L’algoritmo di enumerazione non può essere usato !!! Osservazione chiave: Supponiamo che la soluzione ottima sia ottenuta 1. moltiplicando le prime k matrici tra loro in qualche modo 2. moltiplicando le altre n-k matrici tra loro in qualche modo 3. moltiplicando le due matrici ottenute ai primi due passi Le parentesizzazioni dei passi 1 e 2 sono ottime

Soluzione ricorsiva m[i,j] = costo minimo per moltiplicare le matrici A i,...,A j m[i,j] = 0 i=j i<j min{ m[i,k] + m[k+1,j] + p i-1 p k p j } i≤k<j

Costo della soluzione ricorsiva Esercizio: Determinare il costo computazionale dell’algoritmo ricorsivo progettato a partire dall’equazione ricorsiva del lucido precedente. Esponenziale o polinomiale ???

Numero di sottoproblemi Quanti sottoproblemi abbiamo? uno per ogni coppia di indici i,j nel range 1,...n ovvero  (n 2 ) (pochi !). Quindi l’algoritmo ricorsivo deve risolvere più volte lo stesso sottoproblema altrimenti non si spiega il costo esponenziale !

m= A1A1 A2A2 A3A3 A4A4 A5A5 A6A6 A 1 30x35 A 2 35x15 A 3 15x5 A 4 5x10 A 5 10x20 A 6 20x25

s = s[i,j] contiene l’indice ottmo per spezzare la moltiplicazione A i A j in due: A i A s[i,j] e A s[i,j] +1 A j

Matrix-chain-order(p) n=length(p)-1 for i=1 to n do m[i,i]=0 for l=2 to n do for i=1 to n-l+1 do j=i+l-1 m[i,j]=∞ for k=i to j-1 do q=m[i,k]+m[k+1,j]+p i-1 p k p j if q<m[i,j] then m[i,j]=q s[i,j]=k return m,s Pseudo codice

Matrix-chain-multiply(A,s,i,j) if j>i then X = Matrix-chain-multiply(A,s,i,s[i,j]) Y = Matrix-chain-multiply(A,s,s[i,j]+1,j) return Matrix-multiply(X,Y) else return A i Parentesizzazione ottima dell’esempio = ((A 1 (A 2 A 3 ))((A 4 A 5 )A 6 )) Costo computazionale: Matrix-chain-order tempo  (n 3 )spazio  (n 2 ) Matrix-chain-multiply tempo esercizio spazio esercizio Pseudo codice

Passi fondamentali della programmazione dinamica 1.Caratterizzazione della struttura di una soluzione ottima 2.Definizione ricorsiva del valore di una soluzione ottima 3.Calcolo del valore di una soluzione ottima con strategia bottom-up 4.Costruzione di una soluzione ottima a partire dalle informazioni già calcolate.

nell’esempio della moltiplicazione di matrici Una parentesizzazione ottima associata a una lista A 1,A 2,...,A m di matrici da moltiplicare può essere sempre suddivisa in due parentesizzazioni ottime associate alle liste A 1,...,A k e A k+1,...,A m per un opportuno valore di k. 2.Una parentesizzazione ottima costa quanto la somma dei costi delle due sotto parentesizzazioni ottime più il costo dovuto alla moltiplicazione delle matrici associate alle due sotto parentesizzazioni 3.vedi come procedere con la matrice m nei lucidi precedenti 4.vedi come procedere con la matrice s nei lucidi precedenti

Caratteristiche del problema per applicare la programmazione dinamica Sottostruttura ottima. Una soluzione ottima per il problema contiene al suo interno le soluzioni ottime dei sottoproblemi Sottoproblemi comuni. Un problema di ottimizzazione ha sottoproblemi comuni quando un algoritmo ricorsivo richiede di risolvere più di una volta lo stesso sottoproblema

Versione ricorsiva con memorizzazione dei risultati parziali in una tabella Mem-matrix-chain(p) n=length(p)-1 for i=1 to n do for j=1 to n do m[i,j]=∞ return Lookup-chain(p,1,n) Lookup-chain(p,i,j) if m[i,j]<∞ then return m[i,j] if i=j then m[i,j]=0 else for k=1 to j-1 do q=Lookup-chain(p,i,k)+ Lookupchain(p,k+1,j) + p i-1 p k p j if q<m[i,j] then m[i,j]=q return m[i,j]

Esercizi Determinare il costo computazionale di: Mem-matrix-chain ??? Lookup-chain ???

Sottosequenza comune più lunga Problema di ottimizzazione. Possiamo applicare la programmazione dinamica ?? X = ABCDBDAB Y = BDCABA Z = BCBA è LCS(X,Y)

Sotto struttura ottima Siano X= e Y= Sia Z= una qualunque LCS di X e Y. 1.Se x m =y n, e z k =x m =y n allora Z k-1 è LCS di X m-1 e Y n-1 2.Se x m  y n, e z k  x m allora Z k-1 è LCS di X m-1 e Y 3.Se x m  y n, e z k  y n allora Z k-1 è LCS di X e Y n-1

B D C A B A A B C B D A B

LCS-length(X,Y) m=length(X) n=length(Y) for i=1 to m do c[i,0]=0 for j=1 to n do c[0,j]=0 for i=1 to m do for j=1 to n do if x i =y j then c[i,j]=c[i-1,j-1] + 1 b[i,j]=  else if c[i-1,j]≥c[i,j-1] then c[i,j]=c[i,j-1] b[i,j]=  else c[i,j]=c[i,j-1] b[i,j]=  return b,c

Costruzione di una LCS Print-LCS(b,X,i,j) if i=0 or j=0 then return if b[i,j] =  then Print-LCS(b,X,i-1,j-1) print x i else if b[i,j] =  then Print-LCS(b,X,i-1,j) else Print-LCS(b,X,i,j-1)

Algoritmi Greedy

Selezione di attività S={1,...,n} insieme di attività. Ogni attività ha un tempo di inizio s i e un tempo di fine f i. Problema: Selezionare un sottoinsieme S’ di attività in modo tale che: 1.se i e j appartengono a S’ allora: s i ≥f j oppure s j ≥f i. 2.La cardinalita di S’ è massimizzato.

Attività ordinate in base a f i

Pseudocodice Greedy-activity-selector(s,f) n=length(s) A={1} j=1 for i=2 to n do if s i ≥f j then A=A  {i} j=i return A  (n)

Dimostrazione di correttezza Assumiamo che le attività siano ordinate per tempi di fine in modo crescente. Dimostriamo che esiste una soluzione ottima che contiene l’attività 1 che è quella che termina per prima (con il tempo di fine più piccolo). Supponiamo per assurdo che esista una soluzione S’’migliore di S’ (S’ contiene l’attività 1 ed è stata costruita con l’algoritmo greedy). Assumiamo che sia S’ che S’’ siano ordinate per tempi di fine crescenti. S’={1,.....} e S’’={k,......} dove 1  k. Sia T=S’’-{k}  {1}. Non è difficile dimostrare (esercizio) che T è una soluzione ottima e che ovviamente contiene l’attività 1. Adesso eliminiamo da S tutte le attività che sono incompatibili con l’attività 1 e ripetiamo la dimostrazione da capo.

A volte gli algoritmi greedy non trovano la soluzione ottima... Problema del commesso viaggiatore. Dato un insieme di n città occorre trovare la strada più breve per visitarle tutte una ed una sola volta e tornare alla città di partenza. Nessun algoritmo greedy può funzionare...

Algoritmo greedy: parto dalla città 1 e poi procedo visitando la città più vicina non ancora visitata. Quando tutte le città sono state visitate torno alla città Soluzione ottima Soluzione con algoritmo greedy

Non sempre una soluzione di ottimo “globale” si ottiene facendo una serie di scelte “localmente” ottime. Per il problema del commesso viaggiatore nessuna politica decisionale greedy fornisce una soluzione finale ottima.

Proprietà della scelta greedy. Ad ogni passo l’algoritmo compie una scelta in base ad una certa politica ed alle scelte compiute fino a quel momento. Cosi facendo ci si riduce ad un sottoproblema di dimensioni più piccole. Ad ogni passo si calcola un pezzo della soluzione. Sottostruttura ottima. Come nel caso della programmazione dinamica, anche per applicare un algoritmo greedy occorre che la soluzione ottima contenga le soluzioni ottime dei sottoproblemi.

Programmazione dinamica Vs Algoritmi greedy Non sempre è possibile risolvere un problema con programmazione dinamica o con algoritmi greedy (commesso viaggiatore). Non sempre i problemi che soddisfano la proprietà della sottostruttura ottima possono essere risolti con entrambi i metodi. Ci sono problemi che non possono essere risolti con algoritmi greedy ma possono essere risolti con programmazione dinamica Se un problema può essere risolto con algoritmi greedy è inutile scomodare la programmazione dinamica.

Problema dello zaino: 2 versioni Knapsack 0-1: Un ladro durante una rapina si trova davanti a n oggetti. Ogni oggetto ha un valore v i e un peso w i (numeri interi). Il ladro ha uno zaino che può contenere fino a W (numero intero) chilogrammi di refurtiva. Il ladro deve scegliere quali oggetti rubare per massimizzare il valore complessivo degli oggetti rubati. Knapsack: In questo caso il ladro può anche prendere una parte frazionaria degli oggetti. Non è costretto a “prendere o lasciare” un oggetto, può decidere di prenderne un pezzo grande a suo piacimento. Nota: Knapsack è una generalizzazione di Knapsack 0-1

Proprietà della sottostruttura ottima Entrambe le versioni del knapsack soddisfano tale proprietà. Supponiamo infatti che il ladro possa rubare refurtiva avente peso W’ non maggiore di W di valore massimo V. Se togliamo dallo zaino l’oggetto j otteniamo la soluzione ottima del sottoproblema in cui lo zaino può contenere al massimo W-w j chilogrammi ottenuti mettendo insieme oggetti da un insieme di n-1 (abbiamo eliminato l’oggetto j).

Knapsack: soluzione greedy Idea: il ladro prende la quantità più grande possibile dell’oggetto i tale per cui v i /w i (valore per unità di peso) è massimo. Dopodiche’, se nello zaino c’è ancora posto, ripete l’operazione. Esercizio. Dimostrare che l’algoritmo è greedy ed è corretto.

Knapsack: soluzione greedy € 60 € 100 € € al chilo 5 € al chilo 4 € al chilo Zaino da 50 chili Soluzione ottima di knapsack con algoritmo greedy € = 2/3 di 30

Knapsack 0-1: nessuna soluzione greedy € 60 € 100 € € al chilo 5 € al chilo 4 € al chilo Soluzione ottima di knapsack 0-1 Zaino da 50 chili 3020 € 220 Soluzione di knapsack 0-1 scegliendo per primo l’oggetto da 6 € al chilo (valore per unità di peso massimo) €

Knapsack 0-1: Soluzione con programmazione dinamica Prima di decidere se inserire nello zaino l’oggetto i bisogna controllare il valore della sottosoluzione ottima di due altri sottoproblemi su n-1 oggetti: il sottoproblema nel quale l’oggetto i è inserito nello zaino e lo zaino ha capienza W-w i il sottoproblema nel quale l’oggetto i non è inserito nello zaino e lo zaino ha ancora peso W Esercizio: risolvere knapsack 0-1 con la programmazione dinamica