Implementazione di dizionari

Documenti analoghi
Come aumentare gli alberi La soluzione di alcuni problemi algoritmici richiede la progettazione di una struttura dati appropriata.

Alberi rosso-neri. Le operazioni sugli alberi binari di ricerca hanno complessità proporzionale all altezza h dell albero.

Implementazione di dizionari

Risoluzione delle collisioni con indirizzamento aperto

algoritmi e strutture di dati

Alberi rosso-neri. Oltre ad essere alberi binari di ricerca, gli alberi rosso-neri soddisfano le seguenti proprietà:

In questa lezione Alberi binari di ricerca

Algoritmi e Strutture di Dati

TABELLE AD INDIRIZZAMENTO DIRETTO

Esercizi su ABR. Prof. E. Fachini - Intr. Alg.!1

In questa lezione Alberi rosso-neri

Strutture Dati. Parte II: alberi e grafi

Algoritmi e Strutture Dati

Alberi rosso neri. API a.a. 2013/2014 Gennaio 23, 2014 Flavio Mutti, PhD

Dipartimento di Elettronica, Informazione e Bioingegneria API 2013/4

Struttura di dati che può essere usata sia come dizionario, sia come coda con priorità

Alberi binari di ricerca

Algoritmi e Strutture Dati. Capitolo 3 Strutture dati elementari

Algoritmi e Strutture Dati. Capitolo 3 Strutture dati elementari

Algoritmi e Strutture Dati

Dizionario. Marina Zanella Algoritmi e strutture dati Tabelle hash 1

Alberi Binari di Ricerca e Alberi Rosso-Neri

Tabelle Hash. Implementazioni Dizionario. Implementazione. Tabelle ad accesso diretto. Tempo richiesto dall operazione più costosa:

Alberi binari. Alberi binari di ricerca

Alberi Rosso-Neri: definizione

Strutture dati. (parte II: alberi e grafi)

ESERCIZI DI ALGORITMI E STRUTTURE DATI 2 Parte 2. Livio Colussi Dipartimento di Matematica Pura ed Applicata Università di Padova

Analisi dell Hashing

Algoritmi e Strutture Dati

Strutture dati - Parte 2

Dizionari. Realizzazione con alberi binari di ricerca. Alberi rosso-neri. Ugo de' Liguoro - Algoritmi e Seprimentazioni 03/04 Lez.

Alberi binari: definizione e alcune proprietà

Alberi. Se x è il genitore di y, allora y è un figlio di x. Un albero binario è un albero in cui ogni nodo ha al massimo due figli.

Sommario. Tabelle ad indirizzamento diretto e hash Funzioni Hash

Algoritmi e Strutture Dati

Strutture dati per insiemi disgiunti

Esercizi Capitolo 7 - Hash

In questa lezione Alberi binari di ricerca: la cancellazione

Algoritmi e Strutture Dati

Algoritmi e strutture dati

Introduzione agli algoritmi Prova di esame del 19/9/2016 Prof.sse E. Fachini - R. Petreschi. Parte prima

In questa lezione. Alberi binari: [CLRS09] cap. 12 per la visita inorder. visite e esercizi su alberi binari. Prof. E. Fachini - Intr. Alg.

Fondamenti teorici e programmazione

Algoritmi e Strutture Dati

ADT Dizionario. Ordered search table. Supponiamo che sia definita una relazione d'ordine totale sulle chiavi del dizionario D:

Tavole (o tabelle) hash

Tavole hash. Alcune possibili implementazioni dell ADT Dizionario. Tavole ad accesso diretto. Tempo richiesto dall operazione più costosa:

Problemi di ordinamento

INTRODUZIONE AGLI ALGORITMI 25 Giugno 2018 Prof.ssa Fachini/Prof.ssa Petreschi I PARTE

Indici multilivello dinamici (B-alberi e B + -alberi) Alberi di ricerca - 1. Un esempio. Alberi di ricerca - 3. Alberi di ricerca - 2

ricerca di un elemento, verifica dell appartenenza di un elemento

Algoritmi e Strutture Dati

Algoritmi e Strutture di Dati I 1. Algoritmi e Strutture di Dati I Massimo Franceschet francesc

Algoritmi e Strutture di Dati

Alberi binari e alberi binari di ricerca

In questa lezione. Heap binario heapsort. [CLRS10] cap. 6, par Prof. E. Fachini - Intr. Alg.

Esercizio Per quali valori di t l albero in figura è un B-Albero legale?

Alberi ed Alberi Binari di Ricerca

Code a priorità. Progettazione di Algoritmi Matricole congrue a 1. Docente: Annalisa De Bonis

Alberi binari e alberi binari di ricerca

Dizionari. Dizionari con gli alberi. Alberi binari di ricerca. Realizzazione con alberi binari di ricerca. Alberi rosso-neri.

Alberi Binari di Ricerca

Divide et impera (Divide and Conquer) Dividi il problema in sottoproblemi piu` semplici e risolvili ricorsivamente

Heap binomiali. Oltre alle precedenti operazioni fondamentali degli heap riunibili, sugli heap binomiali definiremo anche le due ulteriori operazioni:

Algoritmi e Strutture Dati. Alberi Bilanciati: Alberi Red-Black

Alberi binari e alberi binari di ricerca

Esercizi Capitolo 6 - Alberi binari di ricerca

Algoritmi e Strutture Dati

Esercizio 1. E vero che in un AVL il minimo si trova in una foglia o nel penultimo livello? FB = -1. livello 0 FB = -1. livello 1 FB = -1.

Algoritmi e strutture dati

Lezione 6: 12 Novembre 2013

Alberi Binari di Ricerca: Definizione. Ricerca, inserimento, cancellazione, min, max, predecessore, successore.

Algoritmi di ordinamento

Dati e Algoritmi I (Pietracaprina) Esercizi su Priority Queue e Heap

Algoritmi e Strutture Dati

B-alberi. Strutture dati avanzate

Strutture Dati. Parte I: pile, code, liste, tabelle hash

Alberi di ricerca binari

Alberi Binari di Ricerca

Algoritmi e Strutture Dati

Algoritmi e Strutture Dati. Capitolo 6 Il problema del dizionario

ESERCIZI SUGLI HEAP BINOMIALI (CAPITOLO 20) Catalano Pietro 56/100592

Massimo Benerecetti Tabelle Hash: gestione delle collisioni

Dati e Algoritmi I (Pietracaprina) Esercizi su Alberi Binari di Ricerca e (2,4)-Tree

Multi-way search trees

Algoritmi e Strutture Dati

Ordinamenti per confronto: albero di decisione

Algoritmi e Strutture Dati

Note per la Lezione 4 Ugo Vaccaro

Alberi n-ari: specifiche sintattiche e semantiche. Realizzazioni. Visita di alberi n-ari.

In questa lezione Strutture dati elementari: Pila Coda Loro uso nella costruzione di algoritmi.

Algoritmi e Strutture Dati

Cammini minimi fra tutte le coppie

LE STRUTTURE DATI DINAMICHE: GLI ALBERI. Cosimo Laneve

Esercizi su alberi binari

Esempio: rappresentare gli insiemi

alberi binari di ricerca (BST)

F(<M>, <w>) = 1, se quando M opera su w esistono due configurazioni di M. 0 altrimenti. Parte b

Transcript:

Implementazione di dizionari Problema del dizionario dinamico Scegliere una struttura dati in cui memorizzare dei record con un campo key e alcuni altri campi in cui sono memorizzati i dati associati alla chiave key. Su tale struttura si devono poter eseguire in modo efficiente le operazioni: Insert che aggiunge un nuovo record Search che cerca un record di chiave key Delete che toglie un record dalla struttura

Tavole ad indirizzamento diretto Funzionano bene quando le chiavi sono degli interi positivi non troppo grandi. Ad esempio se le chiavi appartengono all insieme U = {0,1,...,m-1} (con m non troppo grande) possiamo usare un array T[0..m-1] in cui ogni posizione (cella) T[k] corrisponde ad una chiave k. Generalmente T[k] è un puntatore al record con chiave k. Se la tavola non contiene un record con chiave k allora T[k] = nil.

La realizzazione delle operazioni in tempo costante O(1) è semplice: Search(T,k) return T[k] Insert(T,x) T[x.key] = x Delete(T,x) T[x.key] = nil

Inconvenienti Con l indirizzamento diretto occorre riservare memoria sufficiente per tante celle quante sono le possibili chiavi. Se l insieme U delle possibili chiavi è molto grande l indirizzamento diretto è inutilizzabile a causa delle limitazioni di memoria. Anche quando la memoria sia sufficiente se le chiavi memorizzate nel dizionario sono soltanto una piccola frazione di U la maggior parte della memoria riservata risulta inutilizzata.

Tavole hash Una tavola hash richiede memoria proporzionale al numero massimo di chiavi presenti nel dizionario indipendentemente dalla cardinalità dell insieme U di tutte le possibili chiavi. In una tavola hash di m celle ogni chiave k viene memorizzata nella cella h(k) usando una funzione detta funzione hash. h : U {0..m-1}

Siccome U > m esisteranno molte coppie di chiavi distinte k 1 k 2 tali che h(k 1 ) = h(k 2 ). Diremo in questo caso che vi è una collisione tra le due chiavi k 1 e k 2. Nessuna funzione hash può evitare le collisioni. Dovremo quindi accontentarci di funzioni hash che minimizzino la probabilità delle collisioni e, in ogni caso, dovremo prevedere qualche meccanismo per gestire le collisioni.

Risoluzione delle collisioni con liste Gli elementi che la funzione hash manda nella stessa cella vengono memorizzati in una lista La tavola hash è un array T[0..m-1] di m puntatori alle cime delle liste U nil nil k 34 nil k 3 nil nil k 1 k 2 k 4 k 5 k 3 h(k) nil nil k 25 nil k 2 nil nil T k 1 nil

La realizzazione delle operazioni è facile: Search(T,k) cerca nella lista T[h(k)] un elemento x tale che x.key == k return x Insert(T,x) aggiungi x alla lista T[h(x.key)] Delete(T,x) togli x dalla lista T[h(x.key)]

Search richiede tempo proporzionale alla lunghezza della lista T[h(k)] Insert si può realizzare in tempo O(1) Delete richiede una ricerca con Search dopo di che l eliminazione dell elemento dalla lista si può realizzare in tempo O(1)

Analisi di hash con liste Supponiamo che la tavola hash T abbia m celle e che in essa siano memorizzati n elementi. Una Search di un elemento con chiave k richiede tempo O(n) nel caso pessimo in cui tutti gli n elementi stanno nella stessa lista h(k) della chiave cercata. Valutiamo la complessità media di Search in funzione del fattore di carico α = n/m. Siccome n è compreso tra 0 e U 0 α U /m.

Supporremo che h(k) distribuisca in modo uniforme le n chiavi tra le m liste. Più precisamente assumeremo la seguente ipotesi di hash uniforme semplice. ogni elemento in input ha la stessa probabilità di essere mandato in una qualsiasi delle m celle

Siano n 0,n 1,...,n m-1 le lunghezze delle m liste. La lunghezza attesa di una lista è E[ n j ] 1 m - = å m 1 n = n i m i= 0 = a Proprietà: Nell ipotesi di hash uniforme semplice la ricerca di una chiave k non presente nella tavola hash richiede tempo Q(1+α) in media.

Dimostrazione: La Search calcola j =h(k) (tempo Q(1)) e poi controlla tutti gli n j elementi della lista T[j] (tempo Q(n j )). Nell ipotesi di hash uniforme semplice E[n j ] = α e quindi l algoritmo richiede tempo medio Q(1+α).

Proprietà: Nell ipotesi di hash uniforme semplice la ricerca di una chiave k presente nella tavola hash richiede tempo Q(1+α) in media. Dimostrazione: Assumiamo che ogni chiave presente nella tavola abbia la stessa probabilità di essere la chiave cercata.

Una ricerca di un una chiave k presente nella tavola richiede il calcolo dell indice j = h(k), il test sulle chiavi che precedono k nella lista T[j] e infine il test su k (numero operazioni = 2 + numero elementi che precedono k nella lista). Le chiavi che precedono k nella lista T[j] sono quelle che sono state inserite dopo di k.

Supponiamo che k = k i sia l i-esima chiave inserita nella lista. Per j = i +1,...,n sia X i,j la variabile casuale che vale 1 se k j viene inserita nella stessa lista di k e 0 altrimenti X i, j = ì0 í î1 se se h( k h( k j j ) ) ¹ = h( k h( k i i ) ) Nell ipotesi di hash uniforme semplice E(X i,j ) = 1/m.

Il valore atteso del numero medio di operazioni eseguite è 1 E[ = 1 n n n n å(2 + å X = å + å i, j )] (2 E[ X i, j ]) n i= 1 j= i+ 1 n i= 1 j= i+ 1 n å (2 + n å 1 ) = 2 + n -1 a 1 = 2 + = 2 + - = Q(1 + a) 2m 2 2m Se n cm per qualche costante positiva c allora α = O(1) e Q(1+α) = Q(1). 1 å n i= 1 j= i+ 1m nm i= 1 n 1 1 - = 2 + nm å t = 0 t = 2 + 1 1 nm n ( n - i) n( n -1) 2

Funzioni hash Che proprietà deve avere una una buona funzione hash? Essa dovrebbe soddisfare l ipotesi di hash uniforme semplice: Ogni chiave ha la stessa probabilità 1/m di essere mandata in una qualsiasi delle m celle, indipendentemente dalle chiavi inserite precedentemente

Ad esempio, se le chiavi sono numeri reali x estratti casualmente e indipendentemente da una distribuzione di probabilità uniforme in 0 x < 1 allora h(x) = ëmxû soddisfa tale condizione. Sfortunatamente l ipotesi di hash uniforme semplice dipende dalle probabilità con cui vengono estratti gli elementi da inserire; probabilità che in generale non sono note.

Le funzioni hash che descriveremo assumono che le chiavi siano degli interi non negativi. Questo non è restrittivo in quanto ogni tipo di chiave è rappresentato nel calcolatore con una sequenza di bit e ogni sequenza di bit si può interpretare come un intero non negativo.

Metodo della divisione h(k) = k mod m Difetto: non funziona bene per ogni m. Ad esempio se m = 2 p è una potenza di due allora k mod m sono gli ultimi p bit di k. In generale anche valori di m prossimi ad una potenza di 2 non funzionano bene. Una buona scelta per m è un numero primo non troppo vicino ad una potenza di 2. Ad esempio m = 701.

Metodo della moltiplicazione h(k) = ëm(ka mod 1)û in cui A è una costante reale con 0 < A < 1 ed x mod 1 = x ëxû è la parte frazionaria. Vantaggi : la scelta di m non è critica e nella pratica funziona bene con tutti i valori di A anche se ci sono ragioni teoriche per preferire l inverso del rapporto aureo A = ( 5-1) / 2

h(k) si calcola facilmente se si sceglie m = 2 p e A = q/2 w con 0 < q < 2 w dove w è la lunghezza di una parola di memoria. w bit k q = ë r 1 ê p r0 ú ê h( k) = ê2 2 w 2 ú = ê ë û ë = m( kamod1) p û æ ç è h(k) p bit kq w 2 r 0 öú mod1 ú øû

Randomizzazione di funzioni hash Nessuna funzione hash può evitare che un avversario malizioso inserisca nella tavola una sequenza di valori che vadano a finire tutti nella stessa lista. Più seriamente: per ogni funzione hash si possono trovare delle distribuzioni di probabilità degli input per le quali la funzione non ripartisce bene le chiavi tra le varie liste della tavola hash.

Possiamo usare la randomizzazione per rendere il comportamento della tavola hash indipendente dall input. L idea è quella di usare una funzione hash scelta casualmente in un insieme universale di funzioni hash. Questo approccio viene detto hash universale.

Un insieme H di funzioni hash che mandano un insieme U di chiavi nell insieme {0,1,...,m-1} degli indici della tavola hash si dice universale se: per ogni coppia di chiavi distinte j e k vi sono al più H /m funzioni hash in H tali che h(j) = h(k) Se scegliamo casualmente la funzione hash in un insieme universale H la probabilità che due chiavi qualsiasi j e k collidano è 1/m, la stessa che si avrebbe scegliendo casualmente le due celle in cui mandare j e k.

Proprietà : Supponiamo che la funzione hash h sia scelta casualmente in un insieme universale H e venga usata per inserire n chiavi in una tavola T di m celle e sia k una chiave qualsiasi. La lunghezza attesa E[n h(k) ] della lista h(k) è α = n/m se k non è presente nella tavola ed è minore di α+1 se k è presente. Quindi, indipendentemente dalla distribuzione degli input, una Search richiede tempo medio Q(1+α) che, se n = O(m), è Q(1).

Risoluzione delle collisioni con indirizzamento aperto Con la tecnica di indirizzamento aperto tutti gli elementi stanno nella tavola. La funzione hash non individua una singola cella ma un ordine in cui ispezionare tutte le celle. L inserimento di un elemento avviene nella prima cella libera che si incontra nell ordine di ispezione. Nella ricerca di un elemento si visitano le celle sempre nello stesso ordine.

La funzione hash è una funzione h(k,i) che al variare di i tra 0 ed m-1 fornisce, per ciascuna chiave k, una sequenza di indici h(k,0), h(k,1),..., h(k,m-1) che rappresenta l ordine di ispezione. Siccome vogliamo poter ispezionare tutte le celle, la sequenza deve essere una permutazione dell insieme degli indici 0,1,..., m-1 della tavola.

La realizzazione delle operazioni è: Insert(T, k) i = 0 repeat j = h(k, i) if T[ j ] == nil T[ j ] = k return j i = i +1 until i == m Errore : tavola piena

Search(T, k) i = 0 repeat j = h(k, i) if T[ j ] == k return j i = i +1 until i == m or T[ j ] == nil La realizzazione di Delete è più complicata Non possiamo infatti limitarci a porre nil nella cella!!! Perché???

La Delete si limita ad assegnare alla chiave dell elemento da togliere un particolare valore diverso da ogni possibile chiave: Delete(T, i) T[ i ] = deleted

La Search continua a funzionare invariata: Search(T, k) i = 0 repeat j = h(k, i) if T[ j ] == k return j i = i +1 until i == m or T[ j ] == nil

La Insert deve essere modificata: Insert(T, k) i = 0 repeat j = h(k, i) if T[ j ] == nil or T[ j ] == deleted T[ j ] = k return j i = i+1 until i == m Errore: tavola piena

Con l indirizzamento aperto la funzione hash fornisce una sequenza di ispezione. In questo caso l ipotesi di hash uniforme diventa: Ogni chiave ha la stessa probabilità 1/m! di generare una qualsiasi delle m! possibili sequenze di ispezione

Vi sono tre tecniche comunemente usate per determinare l ordine di ispezione: 1. Ispezione lineare 2. Ispezione quadratica 3. Doppio hash Nessuna delle tre genera tutte le m! sequenze di ispezione. Le prime due ne generano soltanto m e l ultima ne genera m 2.

Ispezione lineare La funzione hash h(k,i) si ottiene da una funzione hash ordinaria h'(k) ponendo h( k, i) = [ h'( k) + i] mod m L esplorazione inizia dalla cella h(k,0) = h'(k) e continua con le celle h'(k)+1, h'(k)+2, ecc. fino ad arrivare alla cella m-1, dopo di che si continua con le celle 0,1,ecc. fino ad aver percorso circolarmente tutta la tavola.

L ispezione lineare è facile da implementare ma soffre del problema dell addensamento primario: i nuovi elementi inseriti nella tavola tendono ad addensarsi attorno agli elementi già presenti Una cella libera preceduta da t celle occupate ha probabilità (t +1)/m di venir occupata dal prossimo elemento inserito. Quindi sequenze consecutive di celle occupate tendono a diventare sempre più lunghe.

Ispezione quadratica La funzione hash h(k, i) si ottiene da una funzione hash ordinaria h'(k) ponendo h( k, i) = [ h'( k) + c i 1 + dove c 1 e c 2 sono due costanti con c 2 0. I valori di m, c 1 e c 2 non possono essere qualsiasi ma debbono essere scelti opportunamente in modo che la sequenza di ispezione percorra tutta la tavola. Un modo per fare ciò è suggerito nel problema 11-3 del libro. c 2 i 2 ] mod m

Osserviamo che se h'( j) = h'(k) anche le due sequenze di ispezione coincidono. Questo porta ad un fenomeno di addensamento secondario (meno grave dell addensamento primario). L addensamento secondario è dovuto al fatto che il valore iniziale h'(k) determina univocamente la sequenza di ispezione e pertanto abbiamo soltanto m sequenze di ispezione distinte.

Problema 11-3 del libro: Consideriamo la seguente procedura: j = h'(k) i = 0 while i < m and T[j] non è la cella cercata i = i+1 j = ( j+ i ) mod m Dimostrare che la sequenza delle j che viene generata è una sequenza di ispezione quadratica.

Dobbiamo dimostrare che esistono due costanti c 1 e c 2 con c 2 0 tali che j = h( k, i) = [ h'( k) + c i sia una invariante del ciclo. Calcoliamo i primi valori di h: Per i = 0 Per i = 1 Per i = 2 Per i = 3 j = h' ( k j j j ) 1 + c = [ h'( k) + 1] mod = [ h'( k) + 1+ = [ h'( k) + 1+ 2 + 2 i 2 m 2] mod ] mod m 3] mod m m

e in generale Quindi j = = é i( i + 1) ù ê h'( k) + ë 2 ú û é ê h'( k) + ë 1 2 i + 1 2 mod m 2 ù i úû mod m

Doppio hash La funzione hash h(k,i) si ottiene da due funzione hash ordinarie h 1 (k) ed h 2 (k) ponendo h( k, i) = [ h ( k) + ih 1 2 ( k)] mod m Perché la sequenza di ispezione percorra tutta la tavola il valore di h 2 (k) deve essere relativamente primo con m (esercizio 11.4-3 del libro). Possiamo soddisfare questa condizione in diversi modi.

Possiamo scegliere m = 2 p potenza di 2 ed h 2 (k) = 2 h'(k) + 1 con h'(k) funzione hash qualsiasi per una tavola di dimensione m' = m/2 = 2 p-1. Un altro modo è scegliere m primo e scegliere h 2 (k) che ritorna sempre un valore minore di m. Un esempio è: h 1 (k) = k mod m e h 2 (k) = 1 + (k mod m') dove m' è minore di m (di solito m' = m-1).

Con l hash doppio abbiamo Q(m 2 ) sequenze di ispezione distinte. Questo riduce notevolmente i fenomeni di addensamento e rende il comportamento della funzione hash molto vicino a quello ideale dell hash uniforme.

Analisi dell indirizzamento aperto Assumiamo l ipotesi di hash uniforme, ossia che ogni permutazione di 0,1,..., m-1 sia ugualmente probabile come ordine di ispezione. Valutiamo la complessità media di Search in funzione del fattore di carico α = n/m. Notiamo che con l indirizzamento aperto n m e quindi 0 α 1.

Proprietà: Assumendo l ipotesi di hash uniforme, il numero medio di celle ispezionate nella ricerca di una chiave k non presente in una tavola hash con indirizzamento aperto è m se α = 1 e al più 1/(1-α) se α < 1. Dimostrazione: Se α = 1 non ci sono celle vuote e la ricerca termina dopo aver ispezionato tutte le m celle.

Se α < 1 la ricerca termina con la prima cella vuota incontrata durante la sequenza di ispezione. Per l ipotesi di hash uniforme la prima cella ispezionata può essere con uguale probabilità una qualsiasi delle m celle. Siccome ci sono n celle occupate la probabilità che la prima cella ispezionata risulti occupata e che quindi si debba ispezionare anche la successiva è α = n/m.

La probabilità che si debba ispezionare una terza cella è la probabilità α = n/m che la prima cella risulti occupata moltiplicata per la probabilità (n-1)/(m-1) che anche la seconda cella risulti occupata, ossia n m n -1 m -1 < a In generale la probabilità che si debba ispezionare la i-esima cella della sequenza è 2

Dunque noi ispezioniamo una prima cella con probabilità 1, una seconda cella con probabilità α, una terza cella con probabilità minore di α 2, una quarta con probabilità minore di α 3 e così via. Il numero atteso di celle ispezionate è quindi minore di

Conseguenza : Assumendo l ipotesi di hash uniforme, il numero medio di celle ispezionate quando inseriamo una nuova chiave in una tavola hash con indirizzamento aperto è m se a = 1 e al più 1/(1-α) se α < 1.

Proprietà: Assumendo l ipotesi di hash uniforme, il numero medio di celle ispezionate nella ricerca di una chiave k presente in una tavola hash con indirizzamento aperto è (m+1)/2 se α = 1 e al più 1/α ln [1/(1-α)] se α < 1. Dimostrazione: Se α = 1 la chiave cercata può trovarsi, con uguale probabilità, nella prima, seconda,..., ultima cella e quindi il numero medio di celle ispezionate è 1 m å m i = 1 i = 1 m m( m + 1) 2 = m + 1 2

Se α < 1 la ricerca ispeziona le stesse celle visitate quando la chiave cercata è stata inserita nella tavola. Supponiamo che la chiave cercata sia stata inserita dopo altre i chiavi. Il numero medio di celle ispezionate è al più 1/(1-α) =1/(1-i/m), ossia m/(m i). Mediando su tutte le n chiavi presenti nella tavola otteniamo: å å å + - = - = - = = - = - m n m k n i n i k i m n m i m m n 1 1 0 1 0 1 1 1 1 a

Possiamo maggiorare la sommatoria con un integrale ottenendo a a a a a a - = - = - - = ò å - + - = 1 1 ln 1 ln 1 )) ln( (ln 1 1 1 1 1 1 n m m n m m dx x k m n m m n m k

Ecco una tavola dei valori di 1/α ln [1/(1-α)] α 1/a ln [1/(1-a)] 0.3 1.19 0.5 1.39 0.7 1.72 0.9 2.56 0.95 3.15 0.99 4.65

Alberi Alberi liberi : grafi non orientati connessi e senza cicli. Alberi radicati : alberi liberi in cui un vertice è stato scelto come radice. Alberi ordinati : alberi radicati con un ordine tra i figli di un nodo.

f c h e a d g b = f c h e a d g b libero f c h e a d g b radicato f c h e a d g b 1 1 3 2 1 2 1 ordinato 1 3 2 1 1 2 1 f c h e a d g b

Alberi posizionali : alberi radicati in cui ad ogni figlio di un nodo è associata una posizione. Le posizioni che non sono occupate da un nodo sono posizioni vuote (nil). Alberi k-ari : alberi posizionali in cui ogni posizione maggiore di k è vuota. Alberi binari : alberi k-ari con k = 2. Il figlio in posizione 1 si dice figlio sinistro e quello in posizione 2 si dice figlio destro. Alberi binari

c posizionale b a d e f c k-ario (k = 4) b d a e

Alberi binari Il modo più conveniente per descrivere gli alberi binari è mediante la seguente. Definizione ricorsiva di albero binario: a) l insieme vuoto Ø è un albero binario; b) se T s e T d sono alberi binari ed r è un nodo allora la terna ordinata (r, T s,t d ) è un albero binario.

L albero vuoto Ø si rappresenta graficamente con quadratino nero Per rappresentare l albero T = (r, T s, T d ) si disegna un nodo etichettato r e sotto di esso le due rappresentazioni dei sottoalberi T s e T d, con T s alla sinistra di T d r T s T d

L albero: T = (c, (b, (d, Ø, Ø), (a, (f, Ø, Ø), Ø)), (g, (e, Ø, Ø), Ø)) si rappresenta graficamente: c b g d a e f

Nella memoria l albero: T = (c, (b, (d, Ø, Ø), (a, (f, Ø, Ø), Ø)), (g, (e, Ø, Ø), Ø)) si rappresenta nel modo seguente: nil c p key left b right g nil nil d nil a nil nil e nil nil f nil

Alberi binari di ricerca Un albero binario di ricerca è un albero binario in cui la chiave di ogni nodo è maggiore o uguale delle chiavi dei nodi del sottoalbero sinistro e minore o uguale delle chiavi dei nodi del sottoalbero destro. Ad esempio: 7 3 9 1 6 8 4

Operazioni sugli alberi binari di ricerca Stampa della lista ordinata dei nodi: Stampa(x) if x nil Stampa(x.left) print x Stampa(x.right)

Complessità: T(0) = c T(n) = T(k)+b+T(n-k-1) Verifichiamo per sostituzione che T(n) = (c + b) n + c T(0) = c = (c + b)0 + c T(n) = T(k) + b + T(n-k-1) = = (c + b)k +c+b+(c + b)(n-k-1)+c = (c +b)n +c

Ricerca di una chiave: Search(x, k) if x == nil or k == x.key return x if k < x.key return Search(x.left, k) else return Search(x.right, k) Complessità O(h) dove h è l altezza dell albero.

Si può anche fare iterativa: Search(x, k) while x nil and k x.key if k < x.key x = x.left else x = x.right return x Complessità O(h) dove h è l altezza dell albero.

Ricerca del minimo e del massimo: Minimum(x) while x.left nil x = x.left return x // x nil Maximum(x) // x nil while x.right nil x = x.right return x Complessità O(h) dove h è l altezza dell albero.

Ricerca di successivo e precedente Successor(x) if x.right nil return Minimum(x.right) y = x.p while y nil and x == y.right x = y, y = y.p return y Il precedente si ottiene cambiando right in left e Minimum in Maximum. Complessità O(h) dove h è l altezza dell albero.

Inserzione di un nuovo elemento Insert(T, z) // z.left = z.right = nil x = T.root, y = nil // y padre di x while x nil // cerco dove mettere z y = x if z.key < y.key x = y.left else x = y.right z.p = y // metto z al posto della foglia x if y == nil T.root = z elseif z.key < y.key y.left = z else y.right = z Complessità O(h) dove h è l altezza dell albero.

Eliminazione di un elemento Si riporta una versione semplificata, dove si spostano chiavi tra nodi diversi. Questo potrebbe rendere inconsistenti altri puntatori, a tali nodi. A lezione, discussa una versione che non soffre di questo problema. Vedi Libro Paragrafo 12.3

Eliminazione di un elemento: Delete(T, z) // z nil if z.left == nil or z.right == nil // tolgo z y = z // che ha al più un solo figlio else // tolgo il successore di z che non ha // sottoalbero sinistro y = Successor(z), z.key = y.key // cerco l eventuale unico figlio x di y if y.left == nil x = y.right else x = y.left

// metto x al posto di y if x nil x.p = y.p if y.p == nil T.root = x elseif y == y.p.left y.p.left = x else y.p.right = x Complessità O(h) dove h è l altezza dell albero.

Alberi rosso-neri Le operazioni sugli alberi binari di ricerca hanno complessità proporzionale all altezza h dell albero. Gli alberi rosso-neri sono alberi binari di ricerca in cui le operazioni Insert e Delete sono opportunamente modificate per garantire un altezza dell albero h = O(log n) Bisogna aggiunge un bit ad ogni nodo: il colore che può essere rosso o nero.

Oltre ad essere alberi binari di ricerca, gli alberi rosso-neri soddisfano le proprietà: 1. ogni nodo è o rosso o nero; 2. la radice è nera; 3. le foglie (nil) sono tutte nere; 4. i figli di un nodo rosso sono entrambi neri; 5. per ogni nodo x i cammini da x alle foglie sue discendenti contengono tutti lo stesso numero bh(x) di nodi neri: l altezza nera di x; Notare che il nodo x non viene contato in bh(x) anche se è nero.

Esempio di albero rosso-nero: 26 17 41 14 21 30 47 10 16 19 23 28 38 7 12 15 20 35 39 3

nil c b g nil nil d nil a nil nil e nil nil f nil

E utile usare una sentinella al posto di nil 26 17 41 14 21 30 47 10 16 19 23 28 38 7 12 15 20 35 39 3?

c b g d a e f? T.nil

Proprietà: Un albero rosso-nero con n nodi interni ha altezza h 2 log 2 (n+1) Dimostrazione: Osserviamo che i nodi rossi in un cammino dalla radice r alle foglie possono essere al più bh(r) e quindi h 2 bh(r). Basta quindi dimostrare che bh(r) log 2 (n+1) ossia che n 2 bh(r) - 1

Dimostriamo n 2 bh(r) 1 per induzione sulla struttura dell albero rosso-nero. Se T = Ø la radice r è una foglia, bh(r) = 0 e n = 0 = 2 bh(r) 1

Sia T = (r,t 1,T 2 ) e siano r 1 ed r 2 le radici di T 1 e T 2 ed n 1 ed n 2 il numero di nodi interni di T 1 e T 2. Allora: bh(r 1 ) bh(r)-1 bh(r 2 ) bh(r)-1 n = 1+ n 1 + n 2 Per ipotesi induttiva n ³ 1+ 2 ³ 1+ 2 bh( r 1 ) -1+ 2-1+ bh( r 2 2-1 -1 = bh( r )- 1 bh( r )-1 bh( r ) ) 2-1

Conseguenza: Su di un albero rosso-nero con n nodi interni le operazioni Search, Minimum, Maximum, Successor e Predecessor richiedono tutte tempo O(log n)

Anche le operazioni Insert e Delete su di un albero rosso-nero richiedono tempo O(log n) ma siccome esse modificano l albero possono introdurre delle violazioni alle proprietà degli alberi rosso-neri ed in tal caso occorre ripristinare le proprietà. Per farlo useremo delle operazioni elementari, dette rotazioni, che preservano la proprietà di albero binario di ricerca.

x a y Left-Rotate(T, x) b g a b Left-Rotate(T, x) y = x.right // y non deve essere la sentinella T.nil x.right = y.left, y.left.p = x // y.left può essere T.nil y.p = x.p if x.p == T.nil T.root = y elseif x == x.p.left x.p.left = y else x.p.right = y Complessità Q(1) x.p = y, y.left = x x y g

x a y Right-Rotate(T, y) b g a b Right-Rotate(T, y) x = y.left // x non deve essere la sentinella T.nil y.left = x.right, x.right.p = y // x.right può essere T.nil x.p = y.p if y.p == T.nil T.root = x elseif y == y.p.left y.p.left = x else y.p.right = x Complessità Q(1) y.p = x, x.right = y x y g

Inserimento di un nuovo elemento RB-insert(T, z) // z.left = z.right = T.nil Insert(T, z) z.color = RED // z è rosso. L unica violazione // possibile delle proprietà degli alberi // rosso-neri è che z sia radice (prop. 2) // oppure che z.p sia rosso (prop. 4) RB-Insert-Fixup(T, z)

RB-Insert-Fixup(T, z) while z.p.color == RED // violata la proprietà 4 if z.p == z.p.p.left // l altro caso è simmetrico y = z.p.p.right if y.color == RED // Caso 1 z.p.color = y.color = BLACK z.p.p.color = RED z = z.p.p z.p.p 7 z.p.p 7 z.p 5 9 y z.p 5 9 y z 3 z 3

else if z == z.p.right // Caso 2 z = z.p Left-Rotate(T, z) z.p.p 7 z.p.p 7 z.p 3 9 y z.p 5 9 y z 5 z 3

// z figlio sinistro // Caso 3 z.p.color = BLACK z.p.p.color = RED Right-Rotate(T, z.p.p) else // simmetrico con right e left scambiati // alla fine del ciclo l unica proprietà violata può // essere soltanto la 2 T.root.color = BLACK // Caso 0 z.p.p 7 z.p.p 7 5 z.p 5 9 y z.p 5 9 y 3 7 z 3 g d e z 3 a b g 9 a b d e

Complessità. Ogni volta che si ripete il ciclo while il puntatore z risale di due posizioni. Quindi il ciclo può essere ripetuto al più h volte e la complessità di RB-Insert-Fixup è O(log n). Quindi RB-Insert ha complessità O(log n).

Cancellazione di un elemento Rb-Delete(T, z) // z T.nil if z.left == T.nil or z.right == T.nil y = z else y = Successor(z), z.key = y.key // elimino y che ha almeno un sottoalbero vuoto if y.left == T.nil x = y.right else x = y.left // x sottoalbero di y, l altro è sicuramente vuoto

// metto x al posto del padre y x.p = y.p if y.p == T.nil T.root = x elseif y == y.p.left y.p.left = x else y.p.right = x // Se y è rosso non ci sono violazioni if y.color == BLACK // Se y era nero l unica violazione delle // proprietà degli alberi rosso neri è che // i cammini che passano per x contengano // un nodo nero in meno RB-Delete-Fixup(T, x)

RB-Delete-Fixup(T, x) while x T.root and x.color == BLACK if x == x.p.left // l altro caso è simmetrico w = x.p.right if w.color == RED // Caso 1 w.color = BLACK x.p.color = RED Left-Rotate(T, x.p) w = x.p.right 3 3 7 x 1 7 w x 1 7 w 3 9 a b 5 9 a b 5 9 x 1 5 w e z g d e z g d e z a b g d

// il fratello w è nero if w.left.color == BLACK and w.right.color == BLACK // Caso 2 w.color = RED x = x.p 3 3 x 1 7 w 1 7 w a b 5 9 a b 5 9 g d e z g d e z

else if w.right.color == BLACK // Caso 3 w.left.color = BLACK w.color = RED Right-Rotate(T, w) w = x.p.right 3 3 3 x 1 7 w x 1 7 w x 1 5 w a b 5 9 a b 5 9 a b g 7 g d e z g d e z d 9 e z

// Caso 4 w.color = x.p.color x.p.color = w.right.color = BLACK Left-Rotate(T, x.p) x = T.root else // simmetrico con right e left scambiati 3 3 7 x 1 7 w x 1 7 w 3 9 a b 5 9 a b 5 9 1 5 e z g d e z g d e z a b g d

// quando termina il ciclo o x è la radice // oppure x è rosso x.color = BLACK // Caso 0 Caso 0: x rosso x 5 x 5 Caso 0: x radice x 5 x 5

Complessità di RB-Delete-Fixup. Con i casi 0, 3 e 4 il ciclo while termina immediatamente e dunque essi richiedono tempo costante. Dopo aver eseguito il caso 1 viene eseguito una sola volta il caso 2 e poi uno dei casi 0, 3 o 4. Quindi anche il caso 1 richiede tempo costante. Solo il caso 2 può essere ripetuto sul padre di x. Quindi il ciclo può essere ripetuto al più h volte e la complessità è O(log n).

Come aumentare gli alberi La soluzione di alcuni problemi algoritmici richiede la progettazione di una struttura dati appropriata. Spesso una tale struttura si può ottenere aumentando strutture dati note. Supponiamo ci serva una struttura dati su cui poter eseguire, oltre alle operazioni previste per gli alberi di ricerca, anche l operazione di statistica ordinale Select(k) che ritorna il nodo con la chiave k-esima.

Un modo per farlo è aumentare gli alberi rosso-neri aggiungendo a ciascun nodo x un ulteriore campo intero x.size in cui memorizzare il numero di nodi interni del sottoalbero di radice x.

10 4 14 7 key size 16 2 17 12 19 2 21 4 23 1 26 20 28 1 30 5 38 3 41 7 47 1 7 2 12 1 15 1 20 1 35 1 39 1 3 1? 0

Osservazione. Se usiamo la sentinella T.nil e poniamo T.nil.size = 0 allora per ogni nodo interno x vale l equazione x.size = 1 + x.left.size + x.right.size

Possiamo realizzare facilmente Select: Select(x, k) // 1 k x.size // Trova il nodo con chiave k-esima // nel sottoalbero di radice x i = 1 + x.left.size if i == k return x elseif i > k return Select(x.left, k) else return Select(x.right, k-i)

Possiamo realizzare facilmente anche l operazione inversa Rank(x) che trova la posizione k della chiave di x nella sequenza ordinata delle chiavi dell albero Rank(T, x) // Trova la posizione k della chiave di x i = 1 + x.left.size y = x while y.p T.nil if y == y.p.right i = i + 1 + y.p.left.size y = y.p return i

Naturalmente dobbiamo modificare RB-Insert e RB-Delete per mantenere aggiornato il campo size dei nodi RB-Insert (T, z) // z.left = z.right = T.nil Insert (T, z) z.color = RED RB-Insert-Fixup (T, z) RB-Insert ha due fasi. Nella prima si scende dalla radice fino ad una foglia che viene quindi sostituita con il nuovo nodo. Nella seconda si ripristinano le proprietà violate.

Per la prima fase basta mettere 1 nel campo size del nuovo nodo z e aumentare di 1 il campo size di tutti i nodi incontrati scendendo verso la foglia che verrà sostituita con z.

Insert(T, z) z.size = 1 // istruzione aggiunta x = T.root, y = T.nil while x T.nil x.size = x.size + 1 // istruzione aggiunta y = x if z.key < y.key x = y.left else x = y.right z.p = y if y == T.nil T.root = z elseif key[z] < key[y] x.left = z else x.right = z

Nella seconda fase le modifiche alla struttura sono dovute alle rotazioni. a b x y g Left-Rotate(T, x) Right-Rotate(T, y) I campi size dei nodi diversi da x e y rimangono invariati. Basta quindi ricalcolare i campi size dei due nodi x e y usando la relazione: x.size = 1 + x.left.size + x.right.size x a y b g

Left-Rotate(T, x) y = x.right x.right = y.left, y.left.p = x y.p = x.p if x.p == T.nil T.root = y elseif x == x.p.left x.p.left = y else x.p.right = y x.p = y, y.left = x y.size = x.size // istruzioni aggiunte x.size = 1 + x.left.size + x.right.size x y a b g LeftRot(T,x) y x g a b

Anche la RB-Delete ha due fasi: Nella prima viene tolto un nodo y avente uno dei sottoalberi vuoto sostituendolo con la radice dell altro sottoalbero. Per questa fase basta diminuire di 1 il campo size di tutti i nodi nel cammino dalla radice a tale nodo.

RB-Delete(T, z) // z T.nil if z.left == T.nil or z.right == T.nil y = z else y = Successor(z), z.key = y.key if y.left == T.nil x = y.right else x = y.left x.p = y.p // mette x al posto di y if y.p == T.nil T.root = x elseif y == y.p.left y.p.left = x else y.p.right = x w = x.p if y.color == BLACK RB-Delete-Fixup(T, x) // istruzioni aggiunte while w T.nil w.size = w.size 1, w = w.p

Nella seconda fase di RB-Delete le modifiche alla struttura dell albero sono dovute alle rotazioni e quindi è sufficiente la modifica delle rotazioni già vista per RB-Insert. Le istruzioni aggiunte (quelle in verde) non aumentano la complessità delle operazioni RB-Insert e RB-Delete.

Teorema generale dell aumento L aumento di una struttura dati richiede quattro passi: 1. scelta della struttura dati di base; 2. scelta delle ulteriori informazioni da memorizzare nella struttura; 3. verifica che esse si possano mantenere durante le operazioni della struttura di base senza aumentarne la complessità asintotica; 4. sviluppo delle nuove operazioni.

Per gli alberi rosso-neri c è un teorema che ci facilita il passo 3. Teorema dell aumento Sia x.field un nuovo campo che aumenta un albero rosso-nero T. Se il valore di x.field si può calcolare in tempo O(1) usando soltanto le altre informazioni presenti in x e quelle presenti nei figli x.left e x.right comprese x.left.field e x.right.field allora il campo x.field si può mantenere aggiornato eseguendo RB-Insert e RB-Delete senza aumentare la complessità O(log n) di tali operazioni.

Osservazione Il campo x.size soddisfa tale proprietà x.size = 1 + x.left.size + x.right.size Se usiamo la sentinella T.nil e poniamo T.nil.size = 0 questa formula vale per ogni nodo interno, compresi quelli senza figli.

Dimostrazione L idea è che una modifica di x.field implica la modifica del campo field degli antenati di x ma non degli altri nodi. RB-Insert(T, z) // z.left = z.right = T.nil Insert(T, z) z.color = RED RB-Insert-Fixup(T, z) Nella prima fase Insert il nodo z aggiunto non ha figli e quindi z.field si può calcolare direttamente in tempo costante. Basta quindi ricalcolare il campo field di z e di tutti i suoi antenati (tempo O(log n)).

Insert(T, z) x = T.root, y = T.nil while x T.nil y = x if z.key < y.key x = y.left else x = y.right z.p = y if y == T.nil T.root = z elseif z.key < y.key x.left = z else x.right = z w = z while w T.nil Ricalcola-Field(w), w = w.p tempo O(log n)

Nella seconda fase RB-Insert-Fixup l unico caso che può essere ripetuto è il caso 1 che non richiede rotazioni. Negli altri casi vengono eseguite al più 2 rotazioni e ciascuna di esse richiede il ricalcolo del campo field dei due nodi ruotati e dei loro antenati. Tempo O(log n). Osservazione Nel caso del campo size non occorreva ricalcolarlo negli antenati ma questo non è sempre vero.

Left-Rotate(T, x) y = x.right x.right = y.left, y.left.p = x y.p = x.p if x.p == T.nil T.root = y elseif x == x.p.left x x.p.left = y a else x.p.right = y b x.p = y, y.left = x w = x while w T.nil Ricalcola-Field(w), w = w.p Left-Rotate(T, x) y x g a Tempo O(log n) y b g

Nella prima fase di RB-Delete viene sostituito un nodo y con un suo figlio x. Basta quindi ricalcolare il campo field di tutti gli antenati di x. Tempo O(log n). Nella seconda fase l unico caso che può essere ripetuto è il caso 2 che non effettua rotazioni. Negli altri casi vengono eseguite al più 3 rotazioni e ciascuna di esse richiede il ricalcolo del campo field dei due nodi ruotati e dei loro antenati. Tempo O(log n).

Alberi di intervalli Vogliamo aumentare gli alberi rosso-neri per ottenere una struttura dati che supporta operazioni su un insieme dinamico di intervalli [a,b] con a e b numeri reali. Oltre a Insert e Delete vogliamo una operazione Search(a,b) che ritorna un nodo dell albero il cui intervallo ha intersezione non vuota con l intervallo [a,b]. Un intervallo [a,b] si rappresenta con i due numeri reali a e b.

Dunque ogni nodo x di un albero di intervalli ha due campi x.low = a e x.high = b in cui sono memorizzati gli estremi di [a,b]. Il campo x.low è usato come chiave mentre x.high viene trattato come informazione associata. Aggiungiamo inoltre un campo x.max che mantiene il valore massimo tra gli estremi degli intervalli contenuti nel sottoalbero di radice x. Per la sentinella poniamo T.nil.max = -

[5,8] 10 [8,9] 23 [16,21] 30 [17,19] 20 [15,23] 23 [25,30] 30 [26,26] 26 low high max [0,3] 3 [6,10] 10 [19,20] 20 0 10 20 30

x.max si può calcolare in tempo O(1) come massimo tra x.high, x.left.max e x.right.max x.max = MAX(x.high, x.left.max, x.right.max) Se poniamo T.nil.max = - questa vale anche quando x.left e/o x.right sono T.nil. Dunque, per il teorema dell aumento, il campo max si può mantenere eseguendo RB-Insert e RB-Delete senza aumentare la complessità asintotica di tali operazioni.

Vediamo ora come realizzare Interval-Search(a,b) che ritorna un nodo dell albero il cui intervallo ha intersezione non vuota con l intervallo [a,b]. Dobbiamo però prima decidere come si controlla se due intervalli si intersecano.

Per gli intervalli vale la tricotomia. 1. [a,b] e [c,d] si intersecano c d c d c d c d a b a b a b a b 2. [a,b] è alla sinistra di [c,d] (b < c) a b c d 3. [a,b] è alla destra di [c,d] (d < a) c d a b

Usando questa proprietà due intervalli [a,b] e [c,d] si intersecano se e solo se i casi 2 e 3 non sono veri Quindi il test di intersezione è semplicemente: (b c) and (d a) e si esegue in tempo costante O(1)

Interval-Search(x, a, b) // cerca un nodo nel sottoalbero di radice x // il cui intervallo interseca [a,b] if x == T.nil or [a,b] interseca x return x // altrimenti o [a,b] è alla sinistra di x o // [a,b] è alla destra di x if x.left.max a // se [a,b] non interseca nessun intervallo nel // sottoalbero sinistro allora certamente non // interseca nessun intervallo nel sottoalbero destro // Posso limitarmi a cercare nel sottoalbero sinistro return Interval-Search(x.left, a, b)

else // x.left.max < a // [a,b] non interseca gli intervalli nel sottoalbero // sinistro // Posso limitarmi a cercare nel sottoalbero destro return Interval-Search(x.right, a, b) Complessità limitata dall altezza dell albero. Quindi O(log n).