Sei sulla pagina 1di 40

LA COMPRESSIONE

Dispensa per il Corso di


Tecnologie Informatiche

di Chiara Epifanio

Attenzione: si tratta di una copia preliminare, la copia definitiva


verrà messa quanto prima online.
Indice
1 La compressione 1

2 Algoritmi elementari di codifica 9


2.1 Eliminazione di ripetizioni . . . . . . . . . . . . . . . . . . . . 9
2.2 Codifica differenziale . . . . . . . . . . . . . . . . . . . . . . . 10
2.3 Rappresentazione topografica . . . . . . . . . . . . . . . . . . 11
2.4 Codifica a blocchi . . . . . . . . . . . . . . . . . . . . . . . . . 12

3 Compressione statistica, o probabilistica 12


3.1 Algoritmo di Huffman . . . . . . . . . . . . . . . . . . . . . . 12
3.2 Codifica di Shannon-Fano . . . . . . . . . . . . . . . . . . . . 17
3.3 Codici aritmetici . . . . . . . . . . . . . . . . . . . . . . . . . 20

4 I compressori linguistici 22
4.1 LZ77: una compressione sliding window . . . . . . . . . . . . . 23
4.2 Problemi derivanti dall’uso di LZ77 . . . . . . . . . . . . . . . 25
4.3 LZ78: un nuovo algoritmo . . . . . . . . . . . . . . . . . . . . 27
4.4 LZW . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
1 La compressione
Principale obiettivo di questa breve dispensa è quello di approfondire nozioni
e tecniche legate alla problematica della compressione dati.
Da un punto di vista intuitivo “comprimere” un testo, un’immagine, un
suono o più in generale un oggetto significa ridurne le dimensioni. Lo scopo
della compressione è, quindi, quello di fornire una rappresentazione dei dati
in una forma ridotta senza che il significato degli stessi venga cambiato. In
tal modo sarà possibile ricostruire in un momento successivo i dati originali
o una loro approssimazione. Diamo adesso due definizioni utili per potere
approfondire meglio questo nuovo concetto.

Definizione 1.1 : Si definisce Lossy data compression un qualsiasi algo-


ritmo che permetta di ottenere, tramite la decodifica della codifica dell’input,
non i dati iniziali, bensı̀ una loro approssimazione.

Definizione 1.2 : Si definisce Lossless data compression un qualsiasi al-


goritmo che permetta di ottenere, tramite la decodifica della codifica dell’input,
esattamente i dati iniziali.

Gli algoritmi di cui alla definizione 1.1 sono utili in casi nei quali non sia
necessario riottenere esattamente l’input dato, ma ne basta una ricostruzione
abbastanza fedele. Un esempio si può avere nel caso della compressione di
immagini, in cui basta ottenere, tramite decodifica, un’immagine che dif-
ferisca dall’input in qualche pixel, in modo che l’occhio umano non si possa
accorgere della differenza.

Gli algoritmi di cui alla definizione 1.2, invece, sono più appropriati per
comprimere testi (in una qualsiasi lingua o codice), casi in cui, quindi, non è

1
consentito perdere neanche un bit dell’informazione di partenza. Nonostante
ciò, esistono alcuni algoritmi lossless utilizzati per comprimere immagini.

Supponendo che l’alfabeto sorgente sia A e che il codominio della nostra


funzione sia 2 = {0, 1} (considerando le codifiche all’interno di un qualsiasi
altro alfabeto otterremmo gli stessi risultati) e chiamando A∗ l’insieme di
tutte le stringhe ottenute, tramite concatenazione, a partire da elementi di
A, diamo la seguente definizione.

Definizione 1.3 : Si definisce compressore, o compressione, una qual-


siasi funzione parziale α : A∗ → 2∗ (la “parzialità” è richiesta per poter
raggiungere coefficienti di compressione migliori, dal momento che il com-
pressore può cosı̀ tralasciare parole che, per il nostro scopo, sono inessen-
ziali). Se α è una funzione iniettiva, allora l’algoritmo dato sarà lossless.
Infatti, in tal caso l’operazione di codifica sarà tale che in fase di decodifica
si avrà un’unica ricostruzione dell’input, quella originale.

Assumeremo d’ora in poi di considerare solo “lossless data compression”.


Per far ciò sarà necessario, come detto, che la compressione sia una funzione
iniettiva.

Alla fine degli anni ’40, i primi anni della teoria dell’informazione, ci
si cominciò a porre il problema di creare nuovi algoritmi di codifica che
si basassero sui concetti di ridondanza e di quantità di informazione. Ci
si rese conto che, conoscendo le probabilità dei simboli di un testo qual-
siasi, sarebbe stato possibile limitare la quantità di memoria necessaria per
lo stesso. I primi algoritmi effettivi di compressione dati sono stati introdotti
nel 1950 e sono quelli di Shannon-Fano e di Huffman, che verranno esa-
minati nel prossimo paragrafo, che si basavano proprio su concetti stati-
stici. In quegli anni, però, non ci si poteva avvalere certamente di processori

2
molto efficienti, per cui, ad una attenta analisi, il rapporto costi-benefici tra
i costi computazionali per i processi di codifica e di decodifica ed i bene-
fici ottenuti per far ciò non risultava molto conveniente. Con l’avvento di
microprocessori economici, invece, la compressione ha assunto un ruolo più
rilevante ed è diventata una componente essenziale nel campo della teoria
dell’informazione e nello studio delle sue applicazioni all’immagazzinamento
di dati e alla trasmissione. Questi sono, infatti, i campi in cui viene maggior-
mente sfruttato l’uso di compressori. Esaminiamo brevemente la situazione
in entrambi i casi.

Per quanto riguarda l’immagazzinamento dei dati, essi vengono compressi


prima di essere archiviati in un supporto di memoria in modo che esso possa
contenere il più possibile dei dati, che poi verranno decompressi all’atto della
lettura.

USER ENCODER STORAGE DECODER USER


DEVICE

Nonostante l’esistenza ormai sul mercato di supporti di memorizzazione


capaci di contenere al loro interno una gran quantità di dati, gli algoritmi
di compressione mantengono la loro notevole importanza: per esempio sono
molto interessanti per la creazione di archivi o, anche, per l’applicazione a
tipi particolari di dati, quali le immagini.

Per quanto riguarda la trasmissione, invece, sarà necessario che colui che
trasmette i dati li comprima prima di farlo e che chi li riceve li decomprima
prima di leggerli.

3
SENDER ENCODER DECODER RECEIVER
Trasmissione

Per esempio, consideriamo un’azienda che abbia 20 linee telefoniche de-


dicate che collegano Palermo con Roma e che trasmetta, attraverso queste
linee, dati grazie a dei modem. Se i dati possono essere compressi di un
fattore 2, cioè viene impiegata la metà dei bits necessari per trasmettere
l’input per intero, allora la velocità con cui verranno trasmessi viene rad-
doppiata (indipendentemente dalla velocità dei modems). L’azienda potrà,
dunque, decidere se inviare e ricevere il doppio dei dati ogni giorno, ovvero
se mantenere solo 10 linee telefoniche dedicate.

La codifica consiste nella trasformazione di un insieme di simboli emessi


da una sorgente in un nuovo insieme che passa poi per un canale di trasmis-
sione. All’uscita dal canale, l’insieme di simboli viene trasformato mediante
un decoder, che permette al receiver di ottenere l’input dato. Il canale può
essere un supporto fisico, una linea telefonica o altro. Durante la codifica e
la decodifica, un insieme può dovere soddisfare certe proprietà, o vincoli, e
può subire alcune alterazioni (in questo caso l’insieme verrà modificato e si
dirà che è un “canale con rumore”, o “noisy channel”).

Il processo di codifica-decodifica dovrà rispondere alle esigenze di chi lo


applica, cioè alle prestazioni richieste dall’applicazione specifica. Nel nostro
caso, cioè la compressione, lo scopo sarà ovviamente quello di ridurre la di-
mensione dell’informazione trasmessa o immagazzinata. Vi sono altri casi di
processi di codifica-decodifica: ad esempio quando parliamo di crittografia lo
scopo primo è che i dati codificati siano accessibili solo alle persone autoriz-
zate. Il codice correttore di errori, invece, cercherà di “correggere gli errori”
apportati da un noisy channel ad un insieme dato. In altri casi, l’insieme
verrà modificato in modo da potere soddisfare a certi vincoli che riguardano

4
la suddivisione dei simboli dell’insieme che dovrà passare per il canale. Tutti
questi generi di codifica appena esaminati possono essere racchiusi in un unico
schema (cf. fig. seguente), in cui l’ordine con il quale si presentano i vari
processi di codifica è essenziale. Per esempio la decodifica di compressione,
come d’altronde anche quella di crittografia, dovrà avere a che fare solo con
canali senza rumore.

Definizione 1.4 : Definiamo Canale senza rumore un canale che non


“sporchi” il segnale emesso (colui che riceve i dati ottiene esattamente quelli
trasmessi, senza alcuna “sbavatura”).

CANALE SENZA RUMORE

CANALE CON RUMORE

SOURCE ENCODER ENCODER ENCODER CHANNEL DECODER DECODER DECODER RECEIVER

COMPRESSIONE CORRETTORE VINCOLI VINCOLI CORRETTORE COMPRESSIONE


DI ERRORI DI ERRORI
RUMORE

D’ora in poi considereremo solo canali senza rumore, in modo da ottenere


risultati che dipendano esclusivamente dall’algoritmo di compressione ado-
perato. Questa assunzione non costituirà una limitazione dal momento che,
come abbiamo detto, grazie ai codici correttori di errori potremo sempre
ottenere canali senza rumore o buone approssimazioni di essi.

5
Definizione 1.5 : Diciamo che il nostro algoritmo di compressione-decom-
pressione lavora in tempo reale se esiste una costante k tale che in ogni k
unità di tempo esattamente un nuovo carattere viene letto dall’encoder e uno
viene scritto dal decoder. L’unica eccezione a ciò è che noi possiamo avere un
certo ritardo tra l’encoder e il decoder. Quindi, esiste un’altra costante l tale
che durante le prime l unità di tempo il decoder può non produrre caratteri
e, nel caso in cui l’input sia finito, possono trascorrere l unità di tempo tra
quando l’encoder ha finito di leggere e quando il decoder ha finito di restituire
caratteri.

Definizione 1.6 : Definiamo on-line un algoritmo che lavora man mano


che riceve i dati, off-line, invece, uno che esamina tutti i dati prima di
produrre l’output.

Se gli algoritmi considerati sono on-line, allora l’immagazzinamento di


dati può essere visto come un caso particolare di trasmissione, caso in cui
il sender ed il receiver coincidono. E’ da notare, comunque, che algoritmi
on-line lavorano anche off-line, ma non vale il viceversa.

Adesso dobbiamo però stabilire come valutare se un algoritmo di com-


pressione è “buono”, e quindi avere dei criteri su cui basarci per scegliere se
in un dato caso è più conveniente adoperare un tipo di codifica o un altro.
Per far ciò diamo la seguente definizione.

Definizione 1.7 : Dato un compressore α : A∗ → 2∗ definiamo coeffi-


ciente di compressione della parola w ∈ A∗

|w|
τ (w) =
|α(w)|

6
In generale, risulterà ovviamente che |α(w)| < |w| e che, quindi, τ (w) >
1 . È ovvio, inoltre, in base a quanto detto ed alle definizioni date, che
un alto coefficiente di compressione rappresenta una codifica più efficiente.
Secondo l’input da comprimere, possono, infatti, esistere compressori più o
meno adatti. Possono esistere, però, anche input per i quali un compressore
produca un output più voluminoso!

Osservazione 1.1 Alcuni testi presentano il coefficiente di compressione


come
|α(w)|
.
|w|
Seguendo una tale definizione, ovviamente, le codifiche più efficienti avranno
coefficienti di compressione più bassi.

Un’altra grandezza molto importante è l’entropia di una sorgente. Esi-


stono due tipi fondamentali di entropia: l’entropia statistica e l’entropia
topologica di una sorgente. Nella teoria dell’informazione - e in rapporto alla
teoria dei segnali - l’entropia misura la quantità di incertezza o informazione
presente in un segnale aleatorio. Da un altro punto di vista l’entropia è la
minima complessità descrittiva di una variabile aleatoria, ovvero il limite in-
feriore della compressione dei dati. Essa rappresenta un lower bound sulla
possibile compressione lossless: il minore numero di bit necessari per codifi-
care un messaggio.
Si deve a Claude Shannon lo studio dell’entropia in tale contesto. Nel
primo teorema di Shannon, o teorema di Shannon sulla codifica di sorgente,
egli dimostrò che una sorgente casuale d’informazione non può essere rap-
presentata con un numero di bit inferiore alla sua entropia, cioè alla sua
autoinformazione media.
L’entropia di una variabile aleatoria X è la media dell’autoinformazione
I(xi ) delle realizzazioni della variabile stessa (x1 , x2 , . . . , xn ):

7
n
X n
X 1
H(X) = E [I (xi )] = I(xi ) · P (xi ) = P (xi ) · log2 .
i=1 i=1 P (xi )

La base del logaritmo originariamente utilizzata da Shannon fu quella


naturale, tuttavia è oggi di uso comune la base 2 in quanto consente di
ottenere dei risultati più chiari (in particolare, il valore di entropia ottenuta
è misurato in bit). Si nota molto facilmente che H(X) ≥ 0.
L’entropia di una sorgente binaria con probabilità p ed 1 − p è:

1 1
H(X) = p log2 + (1 − p) log2 .
p 1−p
Vale quindi 1 bit in caso di equiprobabilità dei risultati, e 0 bit nel caso
in cui la sorgente sia completamente prevedibile (e cioè emetta sempre zeri o
sempre uni). Tale risultato è ragionevole in quanto nel primo caso si afferma
che è necessario un bit d’informazione per ogni messaggio emesso dalla sor-
gente, mentre nel secondo caso non è necessario alcun bit in quanto si conosce
a priori il valore di tutti i messaggi e quindi la sorgente è del tutto inutile.
Si dimostra che per l’entropia vale sempre la relazione:

H(X) ≤ log2 n.
dove n la cardinalità dell’alfabeto di messaggi considerati (nel caso della
moneta, n = 2).
In analoga maniera si può parlare di entropia topologica di una sorgente,
concetto sul quale non ci dilungheremo, ma che ha la seguente definizione.

Definizione 1.8 Si definisce entropia topologica della sorgente X la


quantità

1
H(X) = lim sup log(sn )
n→∞ n

8
dove sn = |X ∩ An |, essendo An l’insieme delle stringhe di lunghezza n
sull’alfabeto A (per convenzione assumiamo che log(0) = 0).

Descriviamo adesso alcuni particolari algoritmi di compressione dati.

2 Algoritmi elementari di codifica


Esaminiamo adesso alcuni algoritmi elementari di codifica che possono essere
utili ai fini della compressione dati.

2.1 Eliminazione di ripetizioni


Supponiamo di avere un testo s ad elementi in A che contenga un certo nu-
mero di ripetizioni di caratteri. All’interno di esso sostituiamo ogni sequenza
di n lettere α con

&αn

dove & è un nuovo carattere, che ovviamente verrà omesso nel caso in cui
|A| = 1, che sta ad indicare il fatto che la lettera che lo segue è ripetuta. In
termini matematici ciò corrisponde a scrivere αn .

Questo algoritmo è molto utile in quanto vengono eliminate molte ri-


dondanze e quindi viene risparmiato spazio superfluo. Non sarà conveniente
se la stringa considerata contiene poche ripetizioni, dal momento che occu-
peremmo 3 unità di memoria (&αn, una per ogni simbolo), ottenendo un
output più lungo dell’input dato. In questo caso, quindi, non si potrebbe
certo parlare di compressione.

9
2.2 Codifica differenziale
La codifica differenziale viene adoperata comunemente per creare archivi di
successive versioni di uno stesso file. Il file iniziale viene interamente me-
morizzato sul disco e per ogni versione successiva vengono registrate solo le
differenze con le precedenti.

Supponiamo di avere una sequenza (u1 , u2 , ..., un ) di dati, essa può essere
rappresentata dalla sequenza di differenze (u1 , u2 − u1 , ..., un − un−1 ) dove “-”
deve essere un’operazione appropriata.

Un esempio può essere quello della memorizzazione di successive foto di


uno stesso panorama. Lo spostamento delle foglie causato dal vento potrebbe
essere l’unica differenza tra due di esse. Sarebbe, pertanto, inutile memoriz-
zare in entrambi i file tutti i particolari delle foto, lo si potrebbe fare solo in
uno di essi e nel secondo conservare le informazioni riguardo la differenza tra
i due panorami.

Un altro esempio di codifica differenziale può essere dato dall’esame delle


righe di una pagina. Una prima codifica viene effettuata sulle righe consecu-
tive: se la codifica binaria della n-esima di esse è

0111001 0101101 1010101 1010100 1010110 1010000

e quella della (n + 1)-esima è

0101001 0101101 0111010 1101010 1101110 0001000

la riga che deve essere trasmessa sarà

0010000 0000000 1101111 0111110 0111000 1011000.

10
Se in quest’ultima le sequenze di “1” vengono codificate tramite la loro
posizione e la loro lunghezza all’interno della riga, otteniamo:

(3, 1), (12, 2), (2, 4), (2, 5), (3, 3), (4, 1), (2, 2)...
(il che significa che al 3◦ posto c’è un 1, successivamente dal 12◦ posto in poi
ci sono due 1 successivi, ...). Pagine con righe diverse tra di loro porteranno,
ovviamente, a coefficienti di compressione molto bassi; i migliori coefficienti
verranno raggiunti nel caso di pagine interamente bianche (o nere).

2.3 Rappresentazione topografica


Un altro algoritmo elementare è la rappresentazione topografica. A tal fine
si utilizza uno sfondo uniforme e si codificano le posizioni di alcuni caratteri
predominanti nel testo e diversi dal carattere che riempie lo sfondo. As-
sumiamo, ad esempio, che ad ogni carattere venga associata la sua codifica
ASCII e che lo sfondo sia composto da “-” (cioè 00101101), allora la sequenza

“ − e − e − −ee”,
cioè

00101101 01100101 00101101 01100101 00101101 00101101 01100101 01100101

viene trasformata in

01010011 01100101,
il che corrisponde a “Se” nel codice ASCII. La stringa 01010011 (“S”) de-
termina la posizione della “e” all’interno dell’input dato, infatti troviamo un
“1” nella posizione di ogni carattere diverso da “-”; in tal modo sarà molto
semplice la decodifica.

11
2.4 Codifica a blocchi
Gli algoritmi più comuni per comprimere un testo sfruttano la codifica di
ogni simbolo in esso presente tramite una stringa di lunghezza dlogk (n)e (in
cui n rappresenta la cardinalità dell’insieme sorgente e k quella del nuovo
alfabeto). Questo algoritmo viene denominato “codifica a blocchi k-aria”.

Un esempio si ha nel caso del codice ASCII, che trasforma ognuno dei 256
caratteri ASCII in una stringa binaria costituita da 8 cifre successive facenti
parte dell’alfabeto {0, 1}; questo è, ovviamente, un esempio di codifica binaria
a blocchi.

3 Compressione statistica, o probabilistica


Agli algoritmi di codifica presentati nella sezione precedente si affiancano
nella letteratura classica due grosse classi di compressori: gli algoritmi sta-
tistici si basano sulla probabilità dei singoli simboli dell’input di presentarsi
all’interno dello stesso per poterlo comprimere; quelli linguistici, invece, com-
primono una sequenza sostituendo al suo interno sottosequenze della stessa
con puntatori a strutture dati chiamate dizionari. La ricerca nel campo della
compressione sta facendo, però, notevoli passi avanti, il che ha portato a
nuovi tipi di algoritmi di compressione dati, tra i quali quelli che fanno uso
di “antidizionari”. In questa sezione approfondiremo gli algoritmi statistici.

3.1 Algoritmo di Huffman


La Codifica di Huffman è un algoritmo greedy (un algoritmo greedy permette
di ottenere una soluzione ottima di un problema prendendo in ogni punto
di scelta dell’algoritmo la decisione che, al momento, appare la migliore -
localmente ottima) di codifica usato per la compressione lossless di dati. Il

12
suo inventore è David A. Huffman, che la presentò nell’articolo A Method
for the Construction of Minimum-Redundancy Codes, pubblicato nel 1952.
Nel 1951 a David Huffman, ai tempi dottorando in Teoria dell’Informazione
al Massachusetts Institute of Technology (MIT), fu assegnata da Robert M.
Fano una tesi sul problema di trovare il codice binario più efficiente per
rappresentare una sorgente probabilistica. A Huffman venne l’idea di usare
un albero binario ordinato in base alle frequenze dei singoli simboli emessi
dalla sorgente e cosı̀ inventò un nuovo algoritmo di codifica che creava un
codice a lunghezza variabile.

Supponiamo di avere una stringa w ad elementi in un alfabeto Σ e di


esaminarla carattere per carattere (o equivalentemente una sorgente prob-
abilistica che emette simboli ognuno con una certa probabilità). Immagi-
niamo, inoltre, di conoscere per ogni carattere si di w la probabilità che esso
compaia nella stringa w. Il criterio su cui si basa questo algoritmo è che
ai simboli più probabili vengono associate le parole più brevi (foglie meno
profonde nell’albero che verrà in seguito descritto) e a quelli meno probabili
quelle più lunghe (foglie più profonde). Dunque, le codifiche ottenute avranno
lunghezza variabile, esattamente ognuna di esse richiederà un numero intero
di bits. Cosı̀ facendo, la lunghezza del testo codificato sarà ridotta al minimo
e il codice ottenuto sarà prefisso, il che permetterà di ottenere la decodifica
esatta (dal momento che nessuna codifica può essere prefisso di nessun’altra).

Ad ogni carattere si dell’input w sull’alfabeto Σ viene associata una parola


Huff(si ) (dove con Huff abbiamo indicato la funzione di codifica), sull’alfabeto
{0, 1}, del codice prefisso da determinare. Se riscriviamo le parole Huff(si ) su
un albero binario A, le foglie sono in corrispondenza biunivoca con le lettere.

13
Definizione 3.1 : Se nx è il numero di occorrenze del simbolo x all’interno
di w (informazione che ci fornisce la probabilità che esso si presenti all’interno
del testo), definiamo peso di A, la
X
nx d(fx )
dove d(fx ) rappresenta la distanza dalla radice della foglia associata alla let-
tera x.

Il nostro scopo è, dunque, quello di determinare, dato un testo w, l’albero


A di peso minimo.

Algoritmo di Huffman (costruzione dell’albero di peso minimo)


In tale algoritmo ricopre un ruolo fondamentale l’insieme

N = {(wi , pi )/wi ∈ Σ∗ }

di coppie costituite da sottostringhe di w e pesi delle sottostringhe. Analiz-


ziamo, dunque, l’algoritmo

1. Inizializziamo N = {(si , nsi )/si ∈ Σ, si ∈ w} con le lettere di w e i


relativi numeri di occorrenze in w e ordiniamo i suoi elementi in ordine
decrescente secondo i pesi/occorrenze;

2. Ripetiamo iterativamente fino a che la cardinalità di N non diventi 1


le seguenti operazioni:

• consideriamo i due elementi di N di peso minimo e attribuiamo


ai nodi corrispondenti uno stesso padre il cui peso sarà la somma
di quelli dei nodi dati;
• eliminiamo da N i due nodi del punto precedente e inseriamo un
elemento rappresentante il padre (di peso la somma dei due pesi
degli elementi eliminati) .

14
Dal momento che esiste la possibilità, dato un nodo, di scegliere arbitra-
riamente le codifiche dei suoi figli (0 al sinistro e 1 al destro o viceversa) e, a
parità di peso, quale altro nodo scegliere per renderlo suo fratello, la codifica
non sarà unica.

Esempio: Sia w = abracadabra. Le occorrenze delle lettere sono: na =


5, nb = 2, nc = 1, nd = 1, nr = 2. Un albero creato dall’algoritmo è
RADICE

0 1

5 6

0 1

5 2 4

0 1

5 2 2 2

0 1

5 2 2 1 1

a b r c d

15
Le parole di {0, 1}∗ associate alle lettere a, b, c, d, r sono, dunque:

Huff(a) = 0, Huff(b) = 10, Huff(c) = 1110, Huff(d) = 1111, Huff(r) = 110

e quindi, la codifica di w sull’alfabeto {0, 1} sarà:

Huff(w) = 0 10 110 0 1110 0 1111 0 10 110 0


a b r a c a d a b r a
di lunghezza 23.

Casi limite della codifica di Huffman sono collegati alla sequenza di Fi-
bonacci. Infatti se le frequenze dei caratteri coincidono con i termini della
successione di Fibonacci, l’albero risultante appare bilanciato tutto da una
parte, quindi la lunghezza dei codici per ogni carattere, a partire da quello
più frequente che sarà codificato con un bit, aumenterà di un bit per ogni
carattere man mano che si scende nell’albero. Solo i due caratteri meno
frequenti avranno un codice della stessa lunghezza, differente soltanto per
l’ultimo bit.

Vediamo adesso la decodifica. L’algoritmo di Huffman è lossless, quindi


è caratterizzato da una funzione iniettiva. Il fatto che il codice creato è
prefisso fa sı̀ che la suddivisione di Huff(w) in codifiche di simboli, e dunque
la decodifica, sia unica.

Il principale difetto dell’algoritmo di Huffman è che il testo in input deve


essere letto due volte: la prima per calcolare le frequenze delle lettere, e
la seconda per codificare il testo. Si potrebbe risolvere il problema qualora
si considerassero delle frequenze medie; in questo modo, però, l’algoritmo
non assicurerebbe, ovviamente, la lunghezza minima del testo codificato.
Un’alternativa per risolvere il problema è data dall’algoritmo adattativo di
Huffman che costruisce l’albero e lo modifica man mano che legge il testo.

16
3.2 Codifica di Shannon-Fano
Il primo algoritmo famoso di codifica di simboli è stato introdotto alla fine
degli anni ’40 contemporaneamente da Shannon nei laboratori Bell e da Fano
al M.I.T. Analogamente a quanto visto per la codifica di Huffman, questo
è un algoritmo statistico che si basa sulla probabilità di ogni simbolo di
presentarsi all’interno del messaggio. Le codifiche ottenute saranno tali che:

• la codifica di ogni simbolo sarà differente da quella di ogni altro, dunque


la decodifica sarà unica;

• codifiche di simboli con bassa probabilità occuperanno più bits, vice-


versa codifiche di simboli con alta probabilità richiederanno un numero
minore di bits.

Algoritmo di Shannon-Fano (costruzione dell’albero di Shannon-Fano)


L’algoritmo di Shannon-Fano e quello di Huffman sono molto simili: dif-
feriscono solo nella costruzione dell’albero. Infatti, mentre l’albero di Huff-
man viene costruito a partire dalle foglie per poi risalire alla radice (“bottom-
up”), quello di Shannon-Fano viene, invece, costruito dall’alto verso il basso
(“top-down”). Nonostante siano simili, però, l’algoritmo di Huffman è preferi-
bile perché porta a risultati migliori.
Descriviamo allora questo nuovo algoritmo:

• Data una lista di simboli, costruiamo una tabella delle probabilità per
ognuno di essi contando le loro occorrenze nel testo;

• ordiniamo in maniera decrescente secondo le probabilità i simboli di cui


sopra;

• creiamo un albero costituito dalla sola radice, associato alla tabella


intera;

17
• ripetiamo iterativamente, finché ad ogni simbolo non corrisponda una
foglia dell’albero associato alla tabella, le tre operazioni seguenti:

i) suddividiamo la tabella data in due sottotabelle in modo tale che la


somma delle occorrenze dei simboli della prima sia il più possibile
vicina a quella della seconda;
ii) concateniamo alle codifiche dei simboli della prima sottotabella
la codifica binaria 0, a quelli del secondo 1 e in corrispondenza
creiamo due figli del nodo associato alla tabella, il figlio sinistro
associato alla prima sottotabella, il figlio destro alla seconda;
iii) applichiamo la i) alle nuove tabelle.

Dal momento che abbiamo per ben due volte la possibilità di fare delle
scelte arbitrarie, nella suddivisione di ogni tabella e nell’assegnazione delle
etichette ai rami (0 al sinistro e 1 al destro o viceversa), la codifica di
Shannon-Fano non porta ad un unico risultato.

Esempio: Consideriamo lo stesso esempio di sopra, cioè

s = abracadabra.
Esaminando le occorrenze, e quindi le probabilità, delle singole lettere di
s, ed ordinandole decrescentemente, otteniamo

Simboli Probabilita‘

a 5
b 2
r 2
c 1
d 1

Consideriamo adesso la i) e la ii): applicandole una prima volta otteniamo

18
Simboli Probabilita‘

a 5 0

Prima b 2 1
Suddivisione
r 2 1
c 1 1
d 1 1

ed iterativamente, tramite la iii), la tabella seguente.

Simboli Probabilita‘ Codifiche

a 5 0
Prima
Suddivisione
b 2 100
Terza
Suddivisione
r 2 101
Seconda
Suddivisione
c 1 110
Quarta
Suddivisione
d 1 111

L’albero relativo alla codifica di Shannon-Fano appena effettuata di s =


abracadabra è

19
RADICE

0 1

0 1

0 1 0 1

b r c d

Le parole di {0, 1}∗ associate alle lettere a, b, c, d, r sono:

SF (a) = 0, SF (b) = 100, SF (c) = 110, SF (d) = 111, SF (r) = 101.

La codifica di s sull’alfabeto {0, 1} è, dunque:

SF (s) = 0 100 101 0 110 0 111 0 100 101 0


a b r a c a d a b r a
di lunghezza 23.
In generale, Huffman porta a risultati di gran lunga migliori di quelli
ottenuti con l’applicazione di Shannon-Fano.

3.3 Codici aritmetici


Negli anni ’80 sono stati introdotti nuovi tipi di algoritmi statistici, i codici
aritmetici, che hanno ovviato ad alcuni problemi che si presentavano con

20
l’algoritmo di Huffman. Infatti, come vedremo, i codici aritmetici codificano
simboli, o gruppi di simboli, con numeri decimali, e quindi permettono di
adoperare per codifica numeri frazionari di bits. Il codice di Huffman, invece,
associa un intero ad ogni elemento della sorgente, il che porta ad un dispendio
eccessivo in termini di memoria. Se esaminiamo un solo simbolo, ovviamente,
i risultati ottenuti con i due diversi algoritmi saranno molto simili; ma una
volta che esaminiamo un testo lungo, la somma degli arrotondamenti (tali
si possono considerare i bits adoperati dalla codifica di Huffman rispetto a
quelli dei codici aritmetici) per i singoli simboli porta ad una differenza netta
con i risultati ottenuti con i codici aritmetici.
Descriviamo adesso questi nuovi algoritmi; essi si basano sul fatto che,
una volta note le probabilità dei singoli simboli di un alfabeto A di comparire
in una stringa data, si può suddividere l’intervallo [0, 1) in tanti suoi sottoin-
tervalli, in modo tale che ad ogni stringa di elementi dell’alfabeto sia associato
uno di essi. Grazie a questo algoritmo sia l’encoder che il decoder lavorano
on-line. Infatti, esaminando la stringa l’encoder può direttamente consid-
erare carattere per carattere ed associare delle codifiche ad ogni gruppo di
simboli esaminati; il decoder a sua volta, riuscirà a risalire immediatamente
alla decodifica. Per chiarire meglio il concetto facciamo un esempio.

Esempio: Supponiamo di non considerare la stringa, già più volte esa-


minata, abracadabra, ma per semplicità una sua sottostringa

s = abracadabr,
in modo da potere suddividere più facilmente l’intervallo [0, 1) e da rendere
cosı̀ maggiormente comprensibile l’esempio stesso. Le probabilità delle sin-
gole lettere saranno:

na = 4, nb = 2, nc = 1, nd = 1, nr = 2.

21
Considerando solo alcune delle possibili concatenazioni di lettere, otte-
niamo la seguente tabella, nella quale è stato dato un particolare ordine ai
caratteri, ma bisogna sottolineare che questo può essere cambiato in qualsiasi
altra maniera.

Sottostringhe Probabilità
a [0,.4)
b [.4,.6)
c [.6,.7)
d [.7,.8)
r [.8,1)
aa [0,.16)
ab [.16,.24)
ac [.24,.28)
ad [.28,.32)
ar [.32,.4)

4 I compressori linguistici
Gli algoritmi di Lempel e Ziv si inquadrano tra quegli algoritmi di compres-
sione che analizzano le proprietà “strutturali” o “linguistiche” del testo (cioè
il fatto che certe concatenazioni di caratteri possano comparire, mentre altre
non sono presenti) per potere ridurre le dimensioni dello stesso.
Questi tipi di codifica che introdurremo ora si basano sulla possibilità di
effettuare sostituzioni all’interno della stringa. Il principio su cui ci si basano
è che possano esistere delle sottostringhe dell’input che si ripetono, e quindi
che si possa sostituire ognuna di esse con un puntatore (chiamato codeword)
alla posizione in cui questa stessa si è presentata per la prima volta all’interno
del testo. L’output dell’algoritmo descritto, quindi, sarà una successione di
caratteri dell’alfabeto sorgente, intervallati da alcune codewords. Dal mo-
mento che si potrebbe presentare anche la possibilità di avere come input

22
un singolo carattere, dobbiamo essere in grado di potere rappresentare anche
questo con un puntatore, quello al carattere stesso. Generalizzando, possiamo
assumere che l’output di una qualsiasi stringa sia una successione di code-
words (si presenteranno due diversi tipi di puntatori: quelli a stringhe e quelli
ai caratteri stessi da cui sono richiamati). Un altro modo di rappresentare
l’output è quello di creare una lista di stringhe, o dizionario, rappresentante
i singoli fattori in cui viene suddiviso l’input dato.
Tra gli algoritmi che sfruttano le sostituzioni, l’algoritmo di Lempel e Ziv
è, senza dubbio, uno dei più importanti nel campo della compressione ed ha
riscosso successo soprattutto per le sue applicazioni. Più precisamente le sue
numerose varianti costituiscono probabilmente l’algoritmo in uso più diffuso
(tra di esse vi è, per esempio, la utility “compress” del sistema UNIX).
Esistono due algoritmi di Lempel e Ziv, uno risalente al 1977, LZ77, ed
uno al 1978, LZ78. A queste ne va affiancato un terzo, LZW, che rappresenta
l’implementazione da parte di Terry Welch di LZ78. Va, infine, tenuto conto
delle altre versioni introdotte da numerosi studiosi per migliorare sempre più
i risultati ottenuti.

4.1 LZ77: una compressione sliding window


Nel 1977 Lempel e Ziv pubblicarono un articolo, A Universal Algorithm for
Sequential Data Compression nel quale introducevano un nuovo algoritmo di
compressione dati, LZ77.
La struttura principale di questo algoritmo è una “finestra di testo” (chia-
mata sliding window per i motivi che adesso vedremo) di piccole dimensioni,
suddivisa in due parti. La prima di esse rappresenta il corpo principale ed
è costituita da un insieme, la cui cardinalità dipenderà dall’ampiezza della
sliding window, di elementi di s recentemente codificati. La seconda, invece,
di dimensioni notevolmente minori, è un look-ahead buffer, cioè una fine-

23
stra contenente alcuni caratteri non ancora esaminati di s, immediatamente
successivi a quelli del corpo principale, per la codifica dei quali è necessario
“guardare indietro” nella sliding window. In genere la prima contiene al suo
interno alcune migliaia di caratteri, il look-ahead buffer, invece, da 10 a 100.
Scopo dell’algoritmo è quello di codificare s facendo scorrere la sliding
window all’interno di esso. Più precisamente, noi esaminiamo il testo in
input poco per volta, considerando la finestra di testo che man mano si sposta
lungo lo stesso, e cerchiamo l’esistenza nel corpo principale, che rappresenta
il dizionario, di una stringa che combaci con almeno una parte di quella
contenuta nel look-ahead buffer. In tal modo otteniamo una codifica data
dalla tripla:

a) posizione all’interno del corpo principale del primo carattere della più
lunga sottostringa comune (essa potrà avere anche lunghezza 0);

b) lunghezza della sottostringa di cui al punto a);

c) carattere ad essa successivo nel look-ahead buffer.

Esempio:
Supponiamo di considerare

s = abracadabra abracadabra abracadabra,

e che ad un certo istante t la finestra sia quella descritta dalla figura seguente.

abracadabra abracadabra abra

corpo principale Look-Ahead Buffer

24
Notiamo che la stringa ’ abra’ è interamente contenuta nel dizionario e la
codifichiamo tramite la tripla (12,5,”). Infatti la posizione nel corpo princi-
pale del carattere iniziale della prima occorrenza della stringa data è 12, la
sua lunghezza è 5 ed infine non abbiamo caratteri aggiuntivi, in quanto il
look-ahead buffer contiene solo questa sottostringa di s.
Dopo avere effettuato questa codifica la finestra si sposta esattamente di
tanti posti quanto è la lunghezza della stringa codificata (nel nostro caso
5) e viene esaminata una nuova stringa rispetto ad un nuovo dizionario.
Otteniamo, dunque,

adabra abracadabra abra cadab

corpo principale Look-Ahead Buffer

4.2 Problemi derivanti dall’uso di LZ77


L’algoritmo LZ77, però, andava incontro a vari problemi derivanti dalla li-
mitatezza della sliding window. Più esattamente, l’avere imposto che il corpo
principale, cioè il dizionario, fosse limitato faceva sı̀ che una parola ripetuta
spesso all’interno di un testo molto lungo, come può essere nel nostro caso il
termine “compressione”, poteva essere richiamata raramente proprio perché
raro era il caso in cui si potevano trovare due occorrenze della stessa vicine
tra di loro. Un altro problema era correlato all’ampiezza limitata del look-
ahead buffer; per questo motivo non si potevano avere puntatori a parole
lunghe, anche quando se ne fosse presentata l’occasione.

Esempio

25
Supponiamo di riconsiderare l’esempio del paragrafo precedente

abracadabra abracadabra abracadabra

e di aggiungere dopo il look-ahead buffer qualche altro carattere successivo


di s.

abracadabra abracadabra abra cadabra

corpo principale Look-Ahead Buffer

In questo caso, anche se fosse possibile richiamare l’intera parola abracadabra,


dovremmo considerare solo la sua sottostringa abra, il che limita di gran lunga
i risultati ottenuti e porta a coefficienti non del tutto esaurienti. (Ovviamente
in caso di testi all’interno dei quali abbiamo poche ripetizioni LZ77 conserva
ancora tutta la sua efficienza).
Si potrebbe pensare di risolvere il problema aumentando le dimensioni
della finestra, sia del dizionario che del buffer, ma anche questo porta delle
difficoltà. Se noi aumentiamo l’ampiezza del dizionario, per esempio, da
4K a 64K, abbiamo bisogno di 4 bits suppletivi per codificare la posizione
della stringa richiamata; esattamente invece di 12 bits dovremo utilizzarne 16
(ricordiamo che 212 = 4096 e 216 = 65536). Inoltre, l’aumento di dimensione
del buffer da 16 bits a 1K comporta l’uso non più di 4 ma di 10 bits per
codificare la lunghezza di una frase (24 = 16, 210 = 1024). Tutto ciò porta,
quindi, ad un incremento del costo necessario per codificare una stringa da 17
a 27 bits, il che provoca un notevole aumento del coefficiente di compressione,
e quindi una corrispondente diminuzione dell’efficienza dell’algoritmo dato.

26
4.3 LZ78: un nuovo algoritmo
Di tutti i problemi derivanti da LZ77 si resero conto gli stessi Lempel e Ziv
che nel 1978 pubblicarono un articolo, Compression of Individual Sequences
via Variable-Rate Coding, nel quale illustravano un nuovo algoritmo, LZ78,
molto simile a LZ77 ma differente da esso nella descrizione del dizionario
(questa versione rappresenta una compressione universale ottimale su sor-
genti fattoriali).
LZ78 abbandona il concetto della finestra di testo: se LZ77 teneva conto di
un dizionario di stringhe definite all’interno di una finestra fissata e finita, con
LZ78 il dizionario diventa un insieme potenzialmente illimitato di stringhe.
Questo nuovo algoritmo somiglia a quello precedente in molti suoi aspetti:
per esempio LZ77 dava in output un insieme di triple (la posizione della
precedente occorrenza della stringa, la lunghezza della stessa e il carattere
ad essa seguente); un medesimo output viene dato da LZ78, facendo eccezione
per la lunghezza dell’espressione che non è necessaria dato che il decoder già
la conosce.
Le uniche differenze tra i due algoritmi si riscontrano proprio in quei
punti essenziali a far sı̀ che LZ78 sia molto più efficiente di LZ77, cioè che
dia risultati migliori. Infatti, invece che una sliding window di dimensione
prefissata, LZ78 utilizza un dizionario la cui dimensione cresce man mano che
viene esaminato il testo. Le stringhe del dizionario saranno richiamate ogni
qualvolta sarà necessario ed inoltre in qualsiasi momento, anche dopo avere
esaminato milioni di ulteriori caratteri (contrariamente a quanto avveniva
per LZ77 con il quale una stringa veniva richiamata solo se ad una piccola
distanza dal look-ahead buffer).

Esaminiamo adesso più da vicino questo nuovo algoritmo. La funzione


totale LZ : A∗ → 2∗ lavora nel modo seguente: considerata una qualsiasi
parola w ∈ A∗ , per essa troviamo una fattorizzazione unica definita da:

27
w = w0 w1 · · · wn n ∈ IN + ,

in modo che:

• w0 sia la parola vuota;

• per ogni i tale che 1 ≤ i ≤ (n − 1), wi è il più lungo prefisso di wi · · · wn


che sia possibile scrivere nella forma wi = wji ai , con ai ∈ A e ji < i;

• wn = wjn , con jn < n.

Dunque il primo fattore deve essere vuoto ed ogni altro, tranne l’ultimo,
deve essere costituito da un fattore precedentemente incontrato seguito da
un elemento di A. È ovvio che bisogna scegliere una giusta struttura dati per
rappresentare tale dizionario in maniera tale che l’algoritmo risulti efficiente.
Tra le possibili rappresentazioni in memoria del dizionario, si può utilizzare
un albero A(w) in cui vengono immesse le parole wi . In tale albero i rami
sono etichettati da elementi di A tali che figli dello stesso padre siano collegati
ad esso tramite rami aventi labels diverse. In particolare per la codifica del
dizionario di LZ78 possiamo utilizzare un patricia tree, avente al più 256
(quanti sono i caratteri ASCII) figli per ogni nodo. Codificare il testo nel
suo corrispondente binario prima di comprimerlo può essere utile non solo
per riuscire ad ottenere una quantità maggiore di ripetizioni, ma anche per
potere semplificare la struttura dati utilizzata per rappresentare l’albero, che
cosı̀ diventa binario.
Ogni fattore wi corrisponde ad un nodo di A(w) e, labellandolo con i, si
può facilmente vedere che la condizione di iniettività richiesta ad ogni lossless
data compression viene verificata (basta considerare l’applicazione

w → (A(w), jn )).

28
Per effettuare l’operazione di decodifica, bisogna ricostruire A(w) cosı̀ come
lo si costruisce in fase di codifica e da questo si ottengono i fattori wi di
w. Dunque l’output deve essere tale da potere effettuare una ricostruzione
sincronizzata dell’albero dato.

Descriviamo adesso l’output del nostro algoritmo.


Ogni fattore wi , 1 ≤ i ≤ (n − 1), viene trasmesso tramite la coppia
(ji , ai ), mentre wn tramite jn . Dal momento che per trasmettere ji abbiamo
bisogno di dlog(i)e bits (necessari e sufficienti per specificare uno qualsiasi
tra 0, 1, ..., i − 1), e che il carattere ai può trasmettere se stesso, allora la
lunghezza dell’output dipenderà esclusivamente dal numero (n − 1) di rami
di A(w). Otterremo quindi, per un’opportuna costante K:
n
X
| LZ(w) |= dlog(i)e + K(n − 1).
i=1

Tenuto conto che dlog(i)e ≤ 1 + log(n − 1)1 , otteniamo:

| LZ(w) |≤ n[1 + log(n − 1)] + K(n − 1) ≤ (n − 1) log(n − 1) + K 0 (n − 1).

con K 0 costante opportuna dipendente da A, costante che può essere anche


negativa. Dunque, comunque sia fatto questo albero A(w), l’output dipende
solamente da n − 1 numeri di lunghezza predeterminata (al più dlog(n − 1)e)
e da n − 1 caratteri di A. Tenuto conto, però, che il termine dominante nella
somma è il primo, notiamo che K 0 non è influente nel calcolo del coefficiente
di compressione.
1
La diseguaglianza dlog(i)e ≤ 1 + log(n − 1) vale perché:

• per ogni i ≤ (n − 1) risulta dlog(i)e ≤ 1 + log(i) ≤ 1 + log(n − 1),


• dlog(n)e ≤ 1 + log(n − 1)

29
Analizziamo adesso la lunghezza dell’input w in funzione dell’albero co-
struito a partire da esso. Ogni fattore wi influisce su | w | esattamente in
misura pari alla profondità del vertice i, denotata con height(i), nell’albero
dato. Otterremo, pertanto:

n−1
X
| w |= height(jn ) + height(i).
i=1
P
Il termine dominante della somma soprascritta, n−1 i=1 height(i), dipende
esclusivamente dalla forma dell’albero e viene denominato internal path length
dell’albero dato.

LZ78, inoltre, rappresenta una compressione continua.


Per definire rigorosamente la continuità conviene collocarsi nell’ambito
delle parole infinite. Denotiamo con Aω l’insieme delle parole infinite su A.
La metrica rispetto a cui la compressione risulta continua è quella del “più
lungo prefisso comune”. Definiamo questa metrica d nel seguente modo: date
due parole x e y appartenenti ad Aω diremo che:

• d(x, y) = 0, se le due parole sono uguali;

• d(x, y) = 21a , se x coincide con y solo nei primi a caratteri, ovvero se


esistono x0 prefisso di x e y 0 prefisso di y, tali che | x0 |=| y 0 |= a, con
x0 e y 0 i più lunghi prefissi rispettivamente di x e di y soddisfacenti tale
proprietà.

30
x’

y’

Esempi:

1. Se x e y sono due parole infinite uguali, allora d(x, y) = 0;

2. se x e y, invece, coincidono per intero tranne che nel primo carattere,


allora a = 0 e quindi d(x, y) = 1.

Proviamo adesso che la funzione LZ è continua rispetto alla distanza


appena definita. Ciò equivale a dimostrare che:

lim LZ(x) = LZ(y)


x→y

Ma che significa che x → y? Significa che d(x, y) → 0, cioè che, se x0 è il


più lungo prefisso comune a x e y, 2|x1 0 | → 0, quindi che | x0 |→ ∞, cioè ancora
che x e y coincidono tranne che in al più un numero finito di caratteri finali,
quindi gli alberi LZ(x) e LZ(y) coincidono tranne che in al più un numero
finito di rami, da cui LZ(x) → LZ(y). Dunque, otteniamo la seguente:

Proposizione 4.1 LZ : A∗ → 2∗ è una compressione continua.

31
Sia w ∈ A∗ tale che A(w) abbia a rami, sia I l’internal path length di A(w)
e sia τ il coefficiente di compressione2 di LZ, allora:

alog(a) + Ka
τ (w) ≤ ,
I
con K costante dipendente da A.

Riassumiamo adesso il procedimento adottato da LZ78 per potere fare lo


stesso esempio fatto per LZ77 e fare, quindi, i debiti paragoni. Il dizionario
viene inizializzato con la stringa vuota ²; man mano che si va avanti nel
testo viene aggiornato con nuove stringhe, date dalla concatenazione di una
“frase” precedente (la più lunga stringa comune al testo ed al dizionario, essa
potrebbe essere anche la prima stringa, quella vuota) con un nuovo carattere.
Il processo di codifica, ovviamente, termina non appena il cursore che si
muove all’interno del testo arriva alla fine dello stesso. Questo è ciò che dà
in output anche LZ77: la posizione all’interno della finestra della precedente
occorrenza dell’espressione data e il carattere aggiuntivo. LZ78 fa un ulteriore
passo, aggiunge questa nuova stringa al dizionario. Talvolta, però, LZ77 può
risultare più efficiente; è il caso in cui abbiamo un testo molto corto con
ripetizioni di frammenti dello stesso molto vicine tra di loro. È il caso, però,
anche di un testo lungo all’interno del quale non si abbiano ridondanze, o
comunque se ne presentino poche. In quest’ultima situazione LZ78 adopera
un dizionario di notevoli dimensioni senza che esso sia utilizzabile, ma solo
sciupando una notevole quantità di memoria; LZ77, al contrario, fa uso,
come dizionario, di una piccola finestra che scorre all’interno del testo e per
la quale, quindi, non si fa un uso spropositato di memoria. Si riscontra,
pertanto, una notevole differenza tra i risultati ottenuti, dal momento che
il dizionario di LZ78 non viene utilizzato quasi mai, ma apporta solo un
2 |LZ(w)|
coefficiente definito come τ (w) = |w|

32
dispendio in termini di memoria e di tempo di calcolo (infatti dobbiamo
ricordare che la parte di testo da codificare deve essere confrontata con ogni
singola stringa del dizionario).

ESEMPIO: Facciamo lo stesso esempio di sopra:


abracadabra abracadabra abracadabra
Nel caso di compressione mediante l’utilizzo di LZ78 otteniamo la tabella
seguente, rappresentante per ogni stringa del dizionario il modo in cui la si
è ottenuta.

POSIZIONE NEL DIZIONARIO CARATTERE DIZIONARIO


DELLA STRINGA RICHIAMATA SUPPLETIVO POS. STR.

0 ε
0 a 1 ’a’
0 b 2 ’b’
0 r 3 ’r’
1 c 4 ’ac’
1 d 5 ’ad’
1 b 6 ’ab’
3 a 7 ’ra’
0 ’ ’ 8 ’ ’
6 r 9 ’abr’
4 a 10 ’aca’
0 d 11 ’d’
9 a 12 ’abra’
8 a 13 ’ a’
2 r 14 ’br’
10 d 15 ’acad’
12 ε

33
Per quanto riguarda, invece, l’albero della figura seguente, ho voluto
mostrare qualche fase della costruzione del dizionario, evidenziando quella
di inizializzazione (I) ed alcune di aggiornamento (II dopo un passo, III dopo
due, IV dopo 6).

a a b a r
b

c d b

I II III IV

Alla fine dell’algoritmo l’albero rappresentante il dizionario sarà

34
a b ’’ d
r

c b r a a
d

a r

d a

4.4 LZW
Nel 1984 Terry Welch pubblicò un articolo, A Technique for High-Performance
Data Compression, nel quale illustrava un nuovo algoritmo, LZW, basato su
LZ78.
Questo algoritmo si basa sulla costruzione di una tabella, dizionario, che
contiene stringhe e trasforma sequenze di caratteri dell’input dato in cod-
ici di lunghezza fissata. Questa tabella, che viene costruita man mano che
il testo viene esaminato, ha una proprietà molto importante: il prefisso di
ogni stringa in essa contenuta fa parte ancora di essa. Ciò significa che se nel
dizionario troviamo wa, con w stringa ed a carattere aggiuntivo, allora vi sarà
anche w. Per fare ciò il dizionario deve essere inizializzato con tutti i sim-
boli dell’alfabeto. Ovviamente grazie a questo algoritmo si risparmia molto

35
tempo, però si presenta il problema per cui alcuni simboli poco diffusi occu-
pano memoria senza essere necessari e il patricia tree assume, cosı̀, dimen-
sioni notevoli senza motivo. Se consideriamo tutti i caratteri ASCII, infatti,
il dizionario contiene, già prima di iniziare la compressione, ben 256 stringhe
e la prima posizione possibile all’interno dello stesso sarà la 257-esima. Si
potrebbe ovviare a questo nuovo problema eliminando dal dizionario originale
esattamente quei simboli che non appariranno all’interno del testo in input.
Ricordiamo, inoltre, che se un dizionario di 256 stringhe comporta l’uso di
8 bits, uno che ne contenga un numero compreso tra 257 e 512 impiegherà
9 bits e se facciamo un esame in termini di bytes, basta avere anche una
sola stringa in più, cioè 257, per avere bisogno non più di un byte, ma di
due, spreco eccessivo soprattutto per un algoritmo di compressione. Questo
è forse l’unico difetto di LZW.

Esempio: Consideriamo sempre lo stesso esempio:

abracadabra abracadabra abracadabra.

Le nuove stringhe possono essere racchiuse nella tabella seguente.

36
NUOVE STRINGHE DEL DIZIONARIO

256 = ’ab’
257 = ’br’
258 = ’ra’
259 = ’ac’
260 = ’ca’
261 = ’ad’
262 = ’da’
263 =’abr’
264 = ’ra’
265 = ’ a’
266 =’abra’
267 = ’aca’
268 = ’ada’
269 =’abra’

Ricordo, però, che questa non coincide con il dizionario; infatti, per ottenere il
risultato finale dobbiamo aggiungere al risultato ottenuto le stringhe iniziali,
cioè quelle coincidenti con i singoli caratteri. Saranno perciò necessari 9 bits.

La scoperta di Welch sta nell’avere limitato la quantità di bits da usare per


codificare un qualsiasi testo. Lempel e Ziv, infatti, da “buoni matematici”
avevano ipotizzato di potere usare un numero infinito di bits, cosa impossibile
dato che il computer è una macchina fisica e quindi finita. Welch, invece,
prova che sono sufficienti 13 bits. Infatti, non appena il dizionario contiene
8191 (213 = 8192) stringhe procediamo nel seguente modo: eliminiamo le
codifiche non adoperate e shiftiamo quelle rimaste in modo da lasciare spazio
libero alla fine della tabella; se cosı̀ non può essere, cioè tutte le codifiche
sono state utilizzate, consideriamo quelle richiamate più recentemente e più

37
frequentemente ed eliminiamo le altre. In questo modo otterremo il risultato
di potere utilizzare una quantità finita di bits, esattamente al più 13, per
codificare un testo di dimensioni qualsiasi, paradossalmente anche infinite.

38