Huffman Canonico: approfondimento
Come abbiamo visto, Huffman canonico ci permette di ottenere una decompressione più veloce e con un uso più efficiente della memoria Ma... fino ad adesso abbiamo supposto che le lunghezze di parola siano date. Per provare la fattibilità dell'algoritmo di codifica di Huffman canonico bisogna fare vedere un modo per calcolare tali lunghezze Huffman canonico
Calcolo delle lunghezze di parola - I L'efficienza con la quale vengono calcolate influenza soltanto le performance di codifica, e abbiamo detto che la codifica è meno importante della decodifica Nonostante questo, quando si usano parole come simboli sorgente, è possibile avere un alfabeto di varie migliaia di simboli (parole). In questi casi avere una soluzione efficiente può veramente fare la differenza rispetto ad utilizzare il classico albero di Huffman
Il problema Dati in input un file di n numeri interi, di cui l'i-simo è il numero di volte in cui il simbolo i compare nel testo Se c i è l'i-simo numero intero nel file, la probabilità del simbolo i- simo è... se abbiamo un file che ha direttamente le probabilità la procedura è praticamente la stessa Dati in uscita: le lunghezze di parola di Huffman
L'idea chiave Utilizzare uno heap E' facile trovare il valore più piccolo, perché è nel nodo radice C'è un modo semplice ed elegante di memorizzare l'albero binario in un semplice vettore
Un'occhiata allo heap - I Come abbiamo già detto il figlio di sinistra del nodo in posizione i è in posizione 2i, mentre il figlio destro è memorizzato nella posizione 2i+1. Questo implica che il padre di un nodo in posizione k sia in posizione k/2 (intero inferiore) Qual è la profondità di uno heap con n elementi? SOL. log n (intero superiore)
Un'occhiata allo heap - II Come viene memorizzato in un array?
Un'occhiata allo heap - III Rimozione dell'elemento più piccolo e riorganizzazione 2 * intero_sup(log n) in tempo, 2 confronti per ogni livello dell'albero < > 8 8 < > 8 11 < >
Costruzione dello heap E' possibile dimostrare che il costo di costruire uno heap da una lista disordinata di n elementi richiede circa O(n) confronti, quindi ha complessità lineare A confronto, quanto costa, nel caso peggiore, ordinare la lista con un algoritmo di ordinamento? SOL. O(n log n)
Calcolo delle lunghezze di parola - II L'algoritmo inizia costruendo un array iniziale con 2n posizioni le ultime n posizioni memorizzano le frequenze dei simboli la prima metà dell'array contiene uno heap per trovare in modo efficiente il simbolo con la frequenza più bassa Mano a mano che gli elementi sono rimossi dallo heap, lo spazio liberato è utilizzato per memorizzare i rami dell'albero in costruzione
Calcolo delle lunghezze di parola - III Anche le frequenze sono sovrascritte per fare posto ai puntatori che costituiscono le connessioni dell'albero Alla fine l'array contiene uno heap con un solo elemento e l'albero di Huffman heap foglie (frequenze) 1n2n heap foglie & puntatori dell'albero 1h2n puntatori dell'albero h=12n
Calcolo lunghezze: fase 1 Si leggono le frequenze dal file e si memorizzano nelle ultime n posizioni dell'array (che chiameremo A) Ogni posizione i, 1 ≤ i ≤ n punta alla frequenza corrispondente A[n+i] Quindi la prima metà di A è ordinata in uno heap secondo quanto visto prima In pratica bisogna garantire che Alla fine A[1] memorizza m 1 : A[m 1 ]=min(A[n+1],..., A[2n])
Calcolo lunghezze: fase 2 h = n while h > 1 { m 1 = A[1] -- prendi la radice dello heap h = h la posizione A[h] non è più parte dello heap “riordina lo heap” m 2 =A[1] -- ora m 1, m 2 puntano alle due freq. più piccole A[h+1]=A[m 1 ]+A[m 2 ] -- il nuovo elemento va in posizione h+1 A[1]=h+1 -- il nuovo elemento è reinserito nello heap A[m 1 ]=A[m 2 ]=h+1 -- le frequenze più piccole sono sovrascritte da puntatori dell'albero “riordina lo heap” }
Esempio 457 1hm1m h+1m1m1 m2m2 97 1m1m1 m2m h m1m1 m2m2
Calcolo lunghezze: fase 3 Dopo n -1 iterazioni rimane nello the heap un unico elemento in posizione 2 and A[1]=2, perché appunto è l'unico elemento rimasto Per trovare la profondità nell'albero di una determinata foglia possiamo semplicemente partire da essa e seguire i puntatori fino a raggiungere il posto 2. Il numero di puntatori seguiti è la lunghezza cercata for n + 1 ≤ i ≤ 2n { d =0; r = i; -- d è il contatore, r l'elemento corrente while r > 2 d = d + 1; r = A[r]; -- segui i puntatori fino al posto 2 A[i] = d -- cioè A[i] è la lunghezza della parola i }
Il costo dell'algoritmo prima fase O(n) seconda fase O(n log n) uno heap di n elementi è riordinato circa 2n volte ogni riordinamento costa d log h e · d log n e iterazioni, ciascuna della quali ha costo costante terza fase O(n 2 ) nel caso peggiore c'è una iterazione per ogni bit di ciascuna parola Quanti sono i bit in totale? distribuzione uniforme → n log n bit caso peggiore: il simbolo i ha bisogno di i bit →
Terza fase rivista - I Notate che i nodi sono aggiunti all'albero dalla posizione h verso la posizione 2 perciò tutti i puntatori dell'albero vanno “da destra verso sinistra” Quindi se partiamo dalla posizione 2 verso la posizione 2n, per ogni nodo incontrato abbiamo già di sicuro incontrato il suo nodo padre!
Terza fase rivista - II Perciò un algoritmo molto efficiente - O(n) - per calcolare le lunghezze di parola consiste nel partire dalla posizione 2 che ha lunghezza 0 e precedere verso la posizione 2n etichettando ogni posizione con la lunghezza associata con il suo padre, aumentata di 1 La terza fase quindi diventa A[2]=0 for i = 3 to 2n A[i]=A[A[i]]+1 -- A[A[i]] è il padre di A[i]