• Non ci sono risultati.

Implementazione di dizionari

N/A
N/A
Protected

Academic year: 2021

Condividi "Implementazione di dizionari"

Copied!
132
0
0

Testo completo

(1)

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

(2)

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.

(3)

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

(4)

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.

Inconvenienti

(5)

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

h : U ® {0..m-1}

detta funzione hash.

(6)

Siccome |U| > m esisteranno molte coppie di chiavi distinte k1 ≠ k2 tali che h(k1) = h(k2).

Diremo in questo caso che vi è una collisione tra le due chiavi k1 e k2.

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.

(7)

k1

k3 k5 k4 k2

U

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

nil

nil

nil nil nil

nil

T

nil k1

k2 nil k3 nil

h(k)

nil k3

k4

nil k2

k5

(8)

Delete(T,x)

“togli x dalla lista T[h(x.key)]”

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)]”

(9)

Delete richiede una ricerca con Search dopo di che l’eliminazione dell’elemento dalla lista si può realizzare in tempo O(1)

Search richiede tempo proporzionale alla lunghezza della lista T[h(k)]

Insert si può realizzare in tempo O(1)

(10)

Analisi di hash con liste

Valutiamo la complessità media di Search in funzione del fattore di carico α = n/m.

Siccome n è compreso tra 0 e |U| 0 ≤ α ≤ |U|/m.

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.

Supponiamo che la tavola hash T abbia m celle e che in essa siano memorizzati n elementi.

(11)

“ogni elemento in input ha la stessa

probabilità di essere mandato in una qualsiasi delle m celle”

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.

(12)

Siano n

0

,n

1

,...,n

m-1

le lunghezze delle m liste.

La lunghezza attesa di una lista è

Proprietà: Nell’ipotesi di hash uniforme semplice la ricerca di una chiave k non

presente nella tavola hash richiede tempo Q (1+α) in media.

a

=

=

= å

-

=

m

n n n m

m

i

i j

1 0

] 1

E[

(13)

Nell’ipotesi di hash uniforme semplice E[n

j

]

= α e quindi l’algoritmo richiede tempo medio Q (1+α).

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

)).

(14)

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.

(15)

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.

(16)

Nell’ipotesi di hash uniforme semplice E(X

i,j

) = 1/m.

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

î í ì

=

= ¹

) (

) (

se 1

) (

) (

se 0

,

i j

i j

j

i

h k h k

k h k

X h

(17)

Il valore atteso del numero medio di operazioni eseguite è

å å å å

= = +

= = +

+

=

+ n

i

n i j

j i n

i

n i j

j

i X

X n

n 1 1 , 1 1 (2 1E[ , ]) ]

) 2

1 ( E[

Se n ≤ cm per qualche costante positiva c allora α

= O(1) e Q(1+α) = Q(1).

å å å

=

= = +

- +

= +

= n

i n

i

n i j

i nm n

m

n 1 1 1 1 ( )

2 1 )

2 1 (

2

) 1 (

2 1 2 1

1 0

+ -

= +

=

å

-

=

n n t nm

nm

n

t

) 1

2 ( 1 2 2

2

2 + -1 = + a - = Q +a

= m m

n

(18)

Funzioni 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”

Che proprietà deve avere una una buona

funzione hash?

(19)

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.

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.

(20)

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.

(21)

Metodo della divisione

h(k) = k mod m

Una buona scelta per m è un numero primo non troppo vicino ad una potenza di 2.

Ad esempio m = 701.

Difetto: non “funziona bene” per ogni m.

Ad esempio se m = 2p è 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.

(22)

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

(23)

h(k) si calcola facilmente se si sceglie m = 2p e A = q/2w con 0 < q < 2w dove w è la lunghezza di una parola di memoria.

k q

´

= r1

w bit

h(k) p bit

r0

ë

( mod1)

û

1 2 mod

2 2 2

)

( 0

kA m

kq k r

h p w p w

=

úû ê ú

ë

ê ÷

ø ç ö

è

= æ úûú êëê

=

(24)

Randomizzazione di funzioni hash

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.

Nessuna funzione hash può evitare che un

avversario malizioso inserisca nella tavola

una sequenza di valori che vadano a finire

tutti nella stessa lista.

(25)

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.

(26)

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.

(27)

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[nh(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).

(28)

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.

(29)

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.

(30)

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”

(31)

La realizzazione di Delete è più complicata

Non possiamo infatti limitarci a porre nil nella cella!!! Perché ???

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

(32)

Delete(T, i)

T[ i ] = deleted

La Delete si limita ad assegnare alla chiave

dell’elemento da togliere un particolare valore diverso da ogni possibile chiave:

(33)

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

(34)

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”

(35)

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”

(36)

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 m2.

(37)

Ispezione lineare

La funzione hash h(k,i) si ottiene da una funzione hash ordinaria h'(k) ponendo

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.

m i

k h

i k

h ( , ) = [ ' ( ) + ] mod

(38)

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.

(39)

Ispezione quadratica

La funzione hash h(k, i) si ottiene da una funzione hash ordinaria h'(k) ponendo

I valori di m, c1 e c2 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.

dove c1 e c2 sono due costanti con c2 ≠ 0.

m i

c i

c k

h i

k

h ( , ) = [ ' ( ) +

1

+

2 2

] mod

(40)

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.

(41)

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.

(42)

Dobbiamo dimostrare che esistono due costanti c1 e c2 con c2 ≠ 0 tali che

sia una invariante del ciclo.

Per i = 0 Per i = 1 Per i = 2 Per i = 3

Calcoliamo i primi valori di h:

m i

c i

c k

h i

k h

j = ( , ) = [ '( ) + 1 + 2 2 ]mod

) ( ' k h

j =

m k

h

j = [ '( ) +1]mod

m k

h

j = [ '( ) +1+ 2]mod

m k

h

j = [ '( ) +1+ 2 + 3]mod

(43)

e in generale

Quindi

m i

i k

h

i m k i

h j

2 mod 1

2 ) 1

( '

2 mod ) 1 ) (

( '

2 úûù êëé + +

=

úûù

êëé +

+

=

(44)

Doppio hash

La funzione hash h(k,i) si ottiene da due funzione hash ordinarie h1(k) ed h2(k) ponendo

Perché la sequenza di ispezione percorra tutta la tavola il valore di h2(k) deve essere relativamente primo con m (esercizio 11.4-3 del libro).

Possiamo soddisfare questa condizione in diversi modi.

m k

ih k

h i

k

h ( , ) = [

1

( ) +

2

( )] mod

(45)

Possiamo scegliere m = 2p potenza di 2 ed h2(k) = 2 h'(k) + 1

con h'(k) funzione hash qualsiasi per una tavola di dimensione m' = m/2 = 2p-1.

Un altro modo è scegliere m primo e scegliere h2(k) che ritorna sempre un valore minore di m.

Un esempio è:

h1(k) = k mod m e h2(k) = 1 + (k mod m') dove m' è minore di m (di solito m' = m-1).

(46)

Con l’hash doppio abbiamo Q(m2) 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.

(47)

Analisi dell’indirizzamento aperto

Valutiamo la complessità media di Search in funzione del fattore di carico α = n/m.

Assumiamo l’ipotesi di hash uniforme, ossia che ogni permutazione di 0,1,..., m-1 sia ugualmente probabile come ordine di ispezione.

Notiamo che con l’indirizzamento aperto n ≤ m e quindi 0 ≤ α ≤ 1.

(48)

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.

(49)

Se α < 1 la ricerca termina con la prima cella vuota incontrata durante la sequenza di ispezione.

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.

Per l’ipotesi di hash uniforme la prima cella

ispezionata può essere con uguale probabilità una qualsiasi delle m celle.

(50)

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

In generale la probabilità che si debba ispezionare la i-esima cella della sequenza è

2

1

1 < a

-

× - m

n m

n

(51)

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

(52)

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.

(53)

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 è

2 1 2

) 1 (

1 1

1

= +

× +

å

=

=

m m

m i m

m

m i

(54)

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

n m i k

m i

m m

n

1

1 0 1

0

1 1

1 1

a

(55)

Possiamo maggiorare la sommatoria con un integrale ottenendo

a a

a a a a

= -

= -

- -

=

£ ò

å

- + -

=

1 ln 1 ln 1

1

)) ln(

1 (ln

1 1

1 1

1

n m

m

n m

m x dx k

m

n m m

n m k

(56)

Ecco una tavola dei valori di 1/α ln [1/(1-α)]

α 1/a ln [1/(1-a)]

0.3 1.19

0.9 2.56

0.95 3.15

0.7 1.72

0.5 1.39

0.99 4.65

(57)

Alberi

Alberi radicati : alberi liberi in cui un vertice è stato scelto come radice.

Alberi liberi : grafi non orientati connessi e senza cicli.

Alberi ordinati : alberi radicati con un ordine tra i figli di un nodo.

(58)

f

c h

e a

d

g b

=

f c

h e a

d

g

libero b f

c h

a e

d

g b

radicato

f

c h

a e

d

g b

1

1 2 3

1

1 2

ordinato

1 2 3

1 1 1 2

f

c h

e a

d

g b

(59)

Alberi binari

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.

(60)

c

b d

a e

posizionale

c

b d

a e

k-ario (k = 4)

f

(61)

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 Ts e Td sono alberi binari ed r è un nodo allora la terna ordinata (r, Ts ,Td ) è un albero binario.

(62)

L’albero vuoto Ø si rappresenta graficamente con quadratino nero

Per rappresentare l’albero T = (r, Ts , Td) si

disegna un nodo etichettato r e sotto di esso le due rappresentazioni dei sottoalberi Ts e Td , con Ts alla sinistra di Td

r

Ts Td

(63)

L’albero:

T = (c, (b, (d, Ø, Ø), (a, (f, Ø, Ø), Ø)), (g, (e, Ø, Ø), Ø))

si rappresenta graficamente:

c

b g

a d

f

e

(64)

Nella memoria l’albero:

T = (c, (b, (d, Ø, Ø), (a, (f, Ø, Ø), Ø)), (g, (e, Ø, Ø), Ø)) si rappresenta nel modo seguente:

p

left right

nilc

key g

nil b

enil a nil

nil fnil

nil dnil

nil

(65)

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

6 1

4

8

(66)

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)

(67)

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

(68)

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.

(69)

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.

(70)

Ricerca del minimo e del massimo:

Minimum(x) // x ≠ nil while x.left ≠ nil

x = x.left return x

Complessità O(h) dove h è l’altezza dell’albero.

Maximum(x) // x ≠ nil while x.right ≠ nil

x = x.right return x

(71)

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

Complessità O(h) dove h è l’altezza dell’albero.

Il precedente si ottiene cambiando right in left e Minimum in Maximum.

(72)

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.

(73)

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

(74)

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

(75)

// 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.

(76)

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.

(77)

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.

(78)

26

17 41

47 30

38 35 39 28

21

23 19

20 14

16

12 15

10

7 3

Esempio di albero rosso-nero:

(79)

nilc

gnil b

enil a nil

nil fnil

nil dnil

nil

(80)

26

17 41

47 30

38 35 39 28

21

23 19

20 14

16

12 15

10

7 3

E’ utile usare una sentinella al posto di nil

?

(81)

c

b g

a e

f d

?

T.nil

(82)

Proprietà: Un albero rosso-nero con n nodi interni ha altezza

h ≤ 2 log2(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) ≤ log2(n+1) ossia che n ≥ 2bh(r) - 1

(83)

Dimostriamo

n ≥ 2bh(r) – 1

per induzione sulla struttura dell’albero rosso-nero.

Se T = Ø la radice r è una foglia, bh(r) = 0 e n = 0 = 2bh(r) – 1

(84)

Sia T = (r,T1,T2) e siano r1 ed r2 le radici di T1 e T2 ed n1 ed n2 il numero di nodi interni di T1 e T2. Allora:

bh(r1) ≥ bh(r)-1 bh(r2) ≥ bh(r)-1

n = 1+ n1 + n2

1 2

1 2

1 2

1

1 2

1 2

1

1 1

2 1

-

= -

+ -

+

³

- +

- +

³

-

- ( ) ( )

) (

) ( )

(

r bh r

bh r

bh

r bh r

n

bh

Per ipotesi induttiva

(85)

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)

(86)

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.

(87)

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 x.p = y, y.left = x

b g

a g

b a

Left-Rotate(T, x)

Complessità Q(1) x

y x

y

(88)

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 y.p = x, x.right = y

b g

a g

b a

Right-Rotate(T, y)

Complessità Q(1) x

y x

y

(89)

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)

Inserimento di un nuovo elemento

(90)

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

5 9

3

7 z.p.p

y z.p

z

5 9

3

7 z.p.p

y z.p

z

(91)

else if z == z.p.right // Caso 2 z = z.p

Left-Rotate(T, z)

3 9

5 7 z.p.p

y z.p

z

5 9

3

7 z.p.p

y z

z.p

(92)

// 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

5 9

3

z.p.p 7

y z

z.p z

5 9

3

7 z.p.p

y z.p

a b

g d e

5

9

3 7

a b g

d e

(93)

Complessità.

Quindi RB-Insert ha complessità O(log n).

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).

(94)

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 Cancellazione di un elemento

(95)

// 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)

(96)

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

x 1 7

3

w a b

g d

5 9

e z

1 7

3

w x

a b

g d

5 9

e z

1

7 3

w x

a b g d 5

9 e z

(97)

// il fratello w è nero

if w.left.color == BLACK

and w.right.color == BLACK // Caso 2 w.color = RED

x = x.p

1 7

3

w x

a b

g d

5 9

e z

1 7

3

w a b

g d

5 9

e z

(98)

else if w.right.color == BLACK // Caso 3 w.left.color = BLACK

w.color = RED

Right-Rotate(T, w) w = x.p.right

1 7

3

w x

a b

g d

5 9

e z

1

7 w g

d 5

9 e z 3

x

a b

1 7

3

w x

a b

g d

5 9

e z

(99)

// 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

1 7

3

w x

a b

g d

5 9

e z

1 7

3

w x

a b

g d 5

e z

9 1

7 3

a b g d 5

9 e z

(100)

// quando termina il ciclo o x è la radice // oppure x è rosso

x.color = BLACK // Caso 0

Caso 0: x radice x

Caso 0: x rosso x 5 x 5

x 5 5

(101)

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).

(102)

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.

(103)

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.

(104)

key size

26

20

17

12

14

7

21

4

30

5

47

1

41

7

10

4

23

1

19

2

16

2

38

3

28

1

39

1

12

1

7

2

35

1

20

1

15

1

3

1

?

0

(105)

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

(106)

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)

(107)

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

(108)

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.

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)

Riferimenti

Documenti correlati

Hunt (Ed.), International Companion Encyclopedia of Children’s Literature (pp.. London and New

Tavola 33 - Spesa per consumi finali delle Amministrazioni pubbliche e delle Istituzioni senza scopo di lucro per funzione - Valori a prezzi

IMPORTANTE: Utilizzare una funzione per creare la lista, un'altra per stamparla e una terza per stampare la sottolista di

SSRI utilizzato nel trattamento della eiacu- lazione precoce: i suoi effetti avversi sono sproporzionati rispetto alla modesta efficacia e includono sfoghi aggres- sivi,

Dal Teorema di esistenza degli zeri e dallo studio della monotonia possiamo concludere che la funzione ammette esattamente due zeri per ogni α &gt; 0.. (5) La risposta corretta `

Usare funzione

• se il numero di round è pari a 2, qualsiasi rete di Feistel non può essere considerata una permutazione pseudocasuale:. • ogni rete di Feistel con numero di round pari a 3 non è

•• Determiniamo se p ed i suoi figli possono essere eliminati dall'albero. Determiniamo se p ed i suoi figli possono essere eliminati dall'albero. Se p è un nodo min, sia alfa