- codice = bit successivo dal flusso, lunghezza = 1
- Mentre il codice< base
codice = codice<< 1
codice = codice + bit successivo dal flusso
lunghezza = lunghezza + 1 - simbolo = simbolo + codice - base]
In altre parole, spingeremo da sinistra nella variabile di codice bit per bit dal flusso di input, fino a codice< base. При этом на каждой итерации будем увеличивать переменную length на 1 (т.е. продвигаться вниз по дереву). Цикл в (2) остановится когда мы определим длину кода (уровень в дереве, на котором находится искомый символ). Остается лишь определить какой именно символ на этом уровне нам нужен.
Supponiamo che il ciclo in (2), dopo diverse iterazioni, si fermi. In questo caso, l'espressione (codice - base) è il numero ordinale del nodo (carattere) richiesto a livello di lunghezza. Il primo nodo (simbolo) a livello di lunghezza nell'albero si trova nell'array symb all'indice offs. Ma non siamo interessati al primo carattere, ma al carattere sotto il numero (codice - base). Pertanto, l'indice del simbolo desiderato nell'array symb è (offs + (code - base)). In altre parole, il simbolo richiesto è simbolo = simbolo + codice - base].
Facciamo un esempio concreto. Usando l'algoritmo descritto, decodifichiamo il messaggio Z /.
Z / = "0001 1 00001 00000 1 010 011 1 011 1 010 011 0001 1 0010 010 011 011 1 1 1 010 1 1 1 0010 011 0011 1 0011 0011 011 1 010 1 1"
- codice = 0, lunghezza = 1
- codice = 0< base = 1
codice = 0<< 1 = 0
codice = 0 + 0 = 0
lunghezza = 1 + 1 = 2
codice = 0< base = 2
codice = 0<< 1 = 0
codice = 0 + 0 = 0
lunghezza = 2 + 1 = 3
codice = 0< base = 2
codice = 0<< 1 = 0
codice = 0 + 1 = 1
lunghezza = 3 + 1 = 4
codice = 1 = base = 1 - simbolo = simbolo = 2 + codice = 1 - base = 1] = simbolo = UN
- codice = 1, lunghezza = 1
- codice = 1 = base = 1
- simbolo = simbolo = 7 + codice = 1 - base = 1] = simbolo = h
- codice = 0, lunghezza = 1
- codice = 0< base = 1
codice = 0<< 1 = 0
codice = 0 + 0 = 0
lunghezza = 1 + 1 = 2
codice = 0< base = 2
codice = 0<< 1 = 0
codice = 0 + 0 = 0
lunghezza = 2 + 1 = 3
codice = 0< base = 2
codice = 0<< 1 = 0
codice = 0 + 0 = 0
lunghezza = 3 + 1 = 4
codice = 0< base = 1
codice = 0<< 1 = 0
codice = 0 + 1 = 1
lunghezza = 4 + 1 = 5
codice = 1> base = 0 - simbolo = simbolo = 0 + codice = 1 - base = 0] = simbolo = F
Quindi, abbiamo decodificato i primi 3 caratteri: UN, h, F... È chiaro che seguendo questo algoritmo otterremo esattamente il messaggio S.
Calcolo delle lunghezze dei codici
Per codificare un messaggio, abbiamo bisogno di conoscere i codici dei caratteri e la loro lunghezza. Come notato nella sezione precedente, i codici canonici sono ben definiti dalle loro lunghezze. Pertanto, il nostro compito principale è calcolare le lunghezze dei codici.
Si scopre che questo compito, nella stragrande maggioranza dei casi, non richiede la costruzione esplicita di un albero di Huffman. Inoltre, gli algoritmi che utilizzano la rappresentazione interna (implicita) dell'albero di Huffman sono molto più efficienti in termini di velocità e consumo di memoria.
Oggi esistono molti algoritmi efficienti per calcolare le lunghezze dei codici (,). Ci limiteremo a considerarne solo uno. Questo algoritmo è abbastanza semplice, ma nonostante ciò è molto popolare. Viene utilizzato in programmi come zip, gzip, pkzip, bzip2 e molti altri.
Torniamo all'algoritmo di costruzione dell'albero di Huffman. Ad ogni iterazione, abbiamo eseguito una ricerca lineare per i due nodi con il peso più basso. Chiaramente una coda prioritaria come una piramide (minima) è più appropriata a questo scopo. Il nodo con il peso più basso avrà la priorità più alta e sarà in cima alla piramide. Diamo questo algoritmo.
Includiamo tutti i caratteri codificati nella piramide.
Estraiamo in sequenza 2 nodi dalla piramide (saranno due nodi con il peso minore).
formiamo nuovo nodo e ad essa attaccate, da bambini, due nodi presi dalla piramide. In questo caso il peso del nodo formato è posto uguale alla somma dei pesi dei nodi figli.
Includiamo il nodo formato nella piramide.
Se c'è più di un nodo nella piramide, ripeti 2-5.
Assumeremo che per ogni nodo sia memorizzato un puntatore al suo genitore. Alla radice dell'albero, imposta questo puntatore su NULL. Ora selezioniamo il nodo foglia (simbolo) e seguendo i puntatori salvati saliremo sull'albero fino a quando il puntatore successivo diventa NULL. L'ultima condizione significa che abbiamo raggiunto la radice dell'albero. È chiaro che il numero di transizioni da livello a livello è pari alla profondità del nodo foglia (simbolo), e quindi alla lunghezza del suo codice. Bypassando in questo modo tutti i nodi (simboli), otteniamo le lunghezze dei loro codici.
Lunghezza massima del codice
Di norma, il cosiddetto libro dei codici, una semplice struttura dati, essenzialmente due array: uno con lunghezze, l'altro con codici. In altre parole, il codice (come stringa di bit) viene memorizzato in una locazione di memoria o registro di dimensione fissa (solitamente 16, 32 o 64). Per evitare overflow, dobbiamo essere sicuri che il codice si inserisca nel registro.
Si scopre che su un alfabeto di N caratteri, la dimensione massima del codice può essere fino a (N-1) bit di lunghezza. In altre parole, per N = 256 (variante comune) possiamo ottenere un codice di 255 bit di lunghezza (anche se per questo il file deve essere molto grande: 2.292654130570773 * 10 ^ 53 ~ = 2 ^ 177.259)! È chiaro che un tale codice non si adatta al registro e devi fare qualcosa con esso.
Innanzitutto, scopriamo in quali condizioni si verifica l'overflow. Sia la frequenza dell'i-esimo simbolo uguale all'i-esimo numero di Fibonacci. Ad esempio: UN-1, B-1, C-2, D-3, E-5, F-8, G-13, h-21. Costruiamo l'albero di Huffman corrispondente.
RADICE / \ / \ / \ / \ h / \ / \ /\ G / \ / \ /\ F / \ / \ /\ E / \ / \ /\ D / \ / \ /\ C / \ / \ UN B
Tale albero è chiamato degenerare... Per ottenerlo, le frequenze dei simboli devono crescere almeno come i numeri di Fibonacci o anche più velocemente. Sebbene in pratica, su dati reali, un tale albero sia quasi impossibile da ottenere, è molto facile generarlo artificialmente. In ogni caso, questo pericolo deve essere preso in considerazione.
Questo problema può essere risolto in due modi accettabili. La prima si basa su una delle proprietà dei codici canonici. Il punto è che nel codice canonico (stringa di bit) al massimo i bit meno significativi possono essere diversi da zero. In altre parole, tutti gli altri bit potrebbero non essere salvati affatto, poiché sono sempre zero. Nel caso di N = 256, è sufficiente salvare solo gli 8 bit meno significativi di ogni codice, assumendo che tutti gli altri bit siano uguali a zero. Questo risolve il problema, ma solo in parte. Ciò complicherà e rallenterà notevolmente sia la codifica che la decodifica. Pertanto, questo metodo è usato raramente nella pratica.
Il secondo modo è limitare artificialmente le lunghezze dei codici (durante la costruzione o dopo). Questo metodo è generalmente accettato, quindi ci soffermeremo su di esso in modo più dettagliato.
Esistono due tipi di algoritmi di codice che limitano la lunghezza. Euristico (approssimativo) e ottimale. Gli algoritmi del secondo tipo sono piuttosto complessi nell'implementazione e, di regola, richiedono più tempo e memoria rispetto ai primi. L'efficacia del codice vincolato euristicamente è determinata dalla sua deviazione dal codice vincolato in modo ottimale. Più piccola è la differenza, meglio è. Vale la pena notare che per alcuni algoritmi euristici questa differenza è molto piccola (,,), inoltre, molto spesso generano codice ottimale (sebbene non garantiscano che sarà sempre così). Inoltre, poiché in pratica, l'overflow si verifica molto raramente (a meno che non venga impostata una restrizione molto rigorosa sulla lunghezza massima del codice); con una dimensione dell'alfabeto piccola, è più opportuno utilizzare metodi euristici semplici e veloci.
Considereremo un algoritmo euristico abbastanza semplice e molto popolare. Ha trovato la sua strada in programmi come zip, gzip, pkzip, bzip2 e molti altri.
Il problema di limitare la lunghezza massima del codice è equivalente al problema di limitare l'altezza dell'albero di Huffman. Nota che, per costruzione, ogni nodo non foglia dell'albero di Huffman ha esattamente due discendenti. Ad ogni iterazione del nostro algoritmo, diminuiamo l'altezza dell'albero di 1. Quindi, sia L la lunghezza massima del codice (altezza dell'albero) ed è necessario limitarla a L / & lt L. Sia ulteriormente RN i il più a destra nodo foglia al livello i e LN i - il più a sinistra.
Iniziamo dal livello L. Sposta il nodo RN L al posto del suo genitore. Perché i nodi vanno in coppia, dobbiamo trovare un posto per un nodo adiacente a RN L. Per fare ciò, trova il livello j più vicino a L, contenente nodi foglia, tale che j & lt (L-1). Al posto di LN j, formeremo un nodo non foglio e ad esso collegheremo come figli il nodo LN j e il nodo del livello L rimasto senza coppia Applichiamo la stessa operazione a tutte le restanti coppie di nodi al livello l. È chiaro che ridistribuendo i nodi in questo modo, abbiamo ridotto l'altezza del nostro albero di 1. Ora è uguale a (L-1). Se ora L / & lt (L-1), faremo lo stesso con il livello (L-1), ecc. fino al raggiungimento del limite richiesto.
Torniamo al nostro esempio, dove L = 5. Limitiamo la lunghezza massima del codice a L / = 4.
RADICE / \ / \ / \ / \ h C E / \ / \ / \ / \ /\ UN D G / \ / \ B F
Si vede che nel nostro caso RN L = F, j = 3, LN j = C... Innanzitutto, sposta il nodo RN L = F al posto del loro genitore.
RADICE / \ / \ / \ / \ h / \ / \ / \ / \ / \ / \ /\ /\ / \ / \ / \ / \ / \ / \ / \ / \ /\ /\ C E / \ / \ / \ / \ F UN D G B(nodo non accoppiato)
Ora al posto di LN j = C Formiamo un nodo non foglia.
RADICE / \ / \ / \ / \ h E / \ / \ / \ / \ / \ / \ F UN D G ? ? B(nodo non accoppiato) C(nodo non accoppiato)
Attacciamo due spaiati al nodo formato: B e C.
RADICE / \ / \ / \ / \ h / \ / \ / \ / \ / \ / \ / \ / \ / \ /\ /\ / \ / \ / \ / \ / \ / \ / \ / \ /\ /\ /\ E / \ / \ / \ / \ / \ / \ F UN D G B C
Pertanto, abbiamo limitato la lunghezza massima del codice a 4. È chiaro che modificando le lunghezze del codice abbiamo perso un po' di efficienza. Quindi il messaggio S, codificato con tale codice, avrà una dimensione di 92 bit, ad es. 3 bit in più sulla ridondanza minima.
È chiaro che più limitiamo la lunghezza massima del codice, meno efficiente sarà il codice. Scopriamo quanto puoi limitare la lunghezza massima del codice. Ovviamente non più breve di un po'.
Calcolo dei codici canonici
Come abbiamo già notato più volte, le lunghezze dei codici sono sufficienti per generare i codici stessi. Ti mostreremo come è possibile farlo. Supponiamo di aver già calcolato le lunghezze dei codici e di aver contato quanti codici di ciascuna lunghezza abbiamo. Sia L la lunghezza massima del codice e T i il numero di codici di lunghezza i.
Calcoliamo S i - valore iniziale codice di lunghezza i, per tutti i da
S L = 0 (sempre)
S L-1 = (S L + T L) >> 1
S L-2 = (S L-1 + T L-1) >> 1
...
S 1 = 1 (sempre)
Per il nostro esempio, L = 5, T 1 .. 5 = (1, 0, 2, 3, 2).
S 5 = 00000 bin = 0 dec
S 4 = (S 5 = 0 + T 5 = 2) >> 1 = (00010 bin >> 1) = 0001 bin = 1 dec
S 3 = (S 4 = 1 + T 4 = 3) >> 1 = (0100 bin >> 1) = 010 bin = 2 dec
S 2 = (S 3 = 2 + T 3 = 2) >> 1 = (100 bin >> 1) = 10 bin = 2 dec
S 1 = (S 2 = 2 + T 2 = 0) >> 1 = (10 bin >> 1) = 1 bin = 1 dec
Si può vedere che S 5, S 4, S 3, S 1 sono esattamente i codici dei caratteri B, UN, C, h... Questi simboli sono uniti dal fatto che vengono tutti prima, ciascuno al proprio livello. In altre parole, abbiamo trovato il valore del codice iniziale per ogni lunghezza (o livello).
Ora assegniamo i codici al resto dei simboli. Il codice del primo carattere al livello i è S i, il secondo S i + 1, il terzo S i + 2, ecc.
Scriviamo i codici rimanenti per il nostro esempio:
B= S 5 = 00000 bin | UN= S 4 = 0001 scomparto | C= S 3 = 010 bin | h= S 1 = 1 bidone |
F= S 5 + 1 = 00001 bidone | D= S 4 + 1 = 0010 bin | E= S 3 + 1 = 011 bin | |
G= S 4 + 2 = 0011 bin |
Si può vedere che abbiamo ricevuto esattamente gli stessi codici come se avessimo costruito esplicitamente l'albero canonico di Huffman.
Passare un albero di codice
Affinché il messaggio codificato possa essere decodificato, il decodificatore deve avere lo stesso albero di codice (in un modo o nell'altro) utilizzato per la codifica. Pertanto, insieme ai dati codificati, siamo costretti a salvare l'albero del codice corrispondente. È chiaro che più è compatto, meglio è.
Esistono diversi modi per risolvere questo problema. Più soluzione ovvia- salvare l'albero in modo esplicito (cioè come un insieme ordinato di nodi e puntatori di un tipo o dell'altro). Questo è forse il modo più dispendioso e inefficace. In pratica non viene utilizzato.
È possibile salvare un elenco di frequenze di simboli (ad es. dizionario delle frequenze). Con il suo aiuto, il decodificatore può facilmente ricostruire l'albero del codice. Sebbene questo metodo sia meno dispendioso del precedente, non è il migliore.
Infine, può essere utilizzata una delle proprietà dei codici canonici. Come notato in precedenza, i codici canonici sono completamente determinati dalla loro lunghezza. In altre parole, tutto ciò di cui ha bisogno il decodificatore è un elenco di lunghezze di codice carattere. Considerando che, in media, la lunghezza di un codice per un alfabeto di N caratteri può essere codificata in [(log 2 (log 2 N))] bit, si ottiene un algoritmo molto efficiente. Ci soffermeremo su di esso in modo più dettagliato.
Supponiamo che la dimensione dell'alfabeto sia N = 256 e comprimiamo l'ordinario file di testo(ASCII). Molto probabilmente non troveremo tutti gli N caratteri del nostro alfabeto in un tale file. Poi mettiamo la lunghezza del codice dei caratteri mancanti uguale a zero... In questo caso, l'elenco salvato di lunghezze di codice conterrà abbastanza grande numero zeri (lunghezze dei codici carattere mancanti) raggruppati insieme. Ciascuno di questi gruppi può essere compresso utilizzando la cosiddetta codifica di gruppo - RLE (Run - Length - Encoding). Questo algoritmo è estremamente semplice. Invece di una sequenza di M elementi identici in una riga, salveremo il primo elemento di questa sequenza e il numero delle sue ripetizioni, ad es. (M-1). Esempio: RLE ("AAAABBBCDDDDDDD") = A3 B2 C0 D6.
Inoltre, questo metodo può essere alquanto esteso. Possiamo applicare Algoritmo RLE non solo ai gruppi di lunghezza zero, ma a tutto il resto. Questo modo di passare un albero di codice è comune e viene utilizzato nella maggior parte delle implementazioni moderne.
Attuazione: SHCODEC
Appendice: biografia di D. Huffman
David Huffman è nato nel 1925 nell'Ohio, negli Stati Uniti. Huffman ha conseguito la laurea in ingegneria elettrica da Università Statale Ohio (Ohio State University) all'età di 18 anni. Ha poi servito nell'esercito come ufficiale di supporto radar su un cacciatorpediniere che ha contribuito a disinnescare le mine nelle acque giapponesi e cinesi dopo la seconda guerra mondiale. Successivamente, ha conseguito un master presso la Ohio University e un dottorato presso il Massachusetts Institute of Technology (MIT). Sebbene Huffman sia meglio conosciuto per aver sviluppato un metodo per costruire codici minimamente ridondanti, ha anche dato importanti contributi in molti altri campi (principalmente nell'elettronica). Lui per molto tempo dirigeva il Dipartimento di Informatica del MIT. Nel 1974, già professore emerito, si dimise. Huffman ha ricevuto numerosi premi preziosi. 1999 - Richard W. Hamming Medal dell'Institute of Electrical and Electronics Engineers (IEEE) per contributi eccezionali alla teoria dell'informazione, Louis E. Levy Medal del Franklin Institute per la sua tesi di dottorato sui circuiti sequenziali, W. Wallace McDowell Award, IEEE Computer Society Award, IEEE Gold Jubilee Technology Innovation Award nel 1998. Nell'ottobre 1999, all'età di 74 anni, David Huffman morì di cancro. R.L. Milidiu, A.A. Pessoa, E.S. Laber, "Implementazione efficiente dell'algoritmo di warm-up per la costruzione di codici di prefisso ristretti in lunghezza", Proc. di Alenex (International Workshop on Algorithm Engineering and Experimentation), pp. 1-17, Springer, gennaio 1999. |
Oggi, pochi utenti sono interessati alla domanda relativa al meccanismo di compressione dei file. Il processo di lavoro con personal computer rispetto al passato, è diventato molto più facile da implementare.
Oggi, quasi tutti gli utenti con cui lavora file system utilizza gli archivi. Tuttavia, pochi utenti hanno pensato a come vengono compressi i file.
I codici di Huffman erano la prima opzione. Sono ancora utilizzati in vari archivi. La maggior parte degli utenti non pensa nemmeno a quanto sia facile comprimere un file usando questo schema. V questa recensione vedremo come viene eseguita la compressione, quali caratteristiche aiutano ad accelerare e semplificare il processo di codifica. Cercheremo anche di comprendere i principi di base della costruzione di un albero di codifica.
Algoritmo: storia
Il primo algoritmo progettato per eseguire una codifica efficiente informazioni elettroniche, divenne il codice proposto da Huffman nel 1952. È questo codice che può essere considerato oggi elemento di base la maggior parte dei programmi progettati per comprimere le informazioni. Alcune delle fonti più popolari che utilizzano codice dato, oggi sono Archivi RAR, ARJ, ZIP. Questo algoritmo viene utilizzato anche per la compressione Immagini JPEG e oggetti grafici... Inoltre, tutti i fax moderni utilizzano un algoritmo di codifica inventato nel 1952. Nonostante sia passato molto tempo dalla creazione di questo codice, è effettivamente utilizzato in apparecchiature del vecchio tipo, nonché in nuove apparecchiature e proiettili.
Il principio della codifica efficiente
Al centro dell'algoritmo di Huffman c'è uno schema che permette di sostituire i caratteri più probabili e più comuni con dei codici sistema binario... I caratteri meno comuni vengono sostituiti con codici lunghi. Transizione a codici lunghi Huffman viene eseguito solo dopo che il sistema utilizza tutti i valori minimi. Questa tecnica consente di ridurre al minimo la lunghezza del codice per carattere del messaggio originale. V in questo caso la particolarità risiede nel fatto che devono essere già note le probabilità di comparsa delle lettere all'inizio della codifica. Il messaggio finale sarà composto da loro. Sulla base di queste informazioni, viene costruito un albero di codifica di Huffman. Sulla base di esso, verrà eseguito il processo di codifica delle lettere nell'archivio.
Codice Huffman: esempio
Per illustrare l'algoritmo di Huffman, si consideri una versione grafica della creazione di un albero del codice. Usare questo metodo era più efficace, è necessario chiarire la definizione di alcuni dei valori necessari per il concetto di questo metodo. L'intera raccolta di un insieme di nodi e archi diretti da un nodo all'altro è chiamata grafo. L'albero stesso è un grafico con un insieme di proprietà specifiche. Ciascun nodo non deve includere più di uno di tutti gli archi. Uno dei nodi deve essere la radice dell'albero. Ciò significa che gli archi non dovrebbero entrarci affatto. Se inizi dalla radice dello spostamento lungo gli archi, questo processo dovrebbe consentirti di arrivare a qualsiasi nodo.
I codici di Huffman includono anche un concetto come una foglia di un albero. Rappresenta un nodo dal quale nessun arco deve uscire. Se due nodi sono collegati da un arco, uno di essi è un genitore e l'altro è un figlio. Se due nodi hanno un nodo padre comune, vengono chiamati nodi di pari livello. Se, oltre alle foglie, i nodi hanno diversi archi, un tale albero viene chiamato binario. Questo è esattamente ciò che è l'albero di Huffman. Una caratteristica dei nodi di questa struttura è che il peso di ciascun genitore è uguale alla somma del peso dei figli nodali.
Albero di Huffman: algoritmo di costruzione
La costruzione del codice di Huffman viene eseguita dalle lettere dell'alfabeto di input. Viene formato un elenco di nodi liberi nell'albero del codice futuro. In questa lista, il peso di ogni nodo dovrebbe essere uguale alla probabilità di occorrenza della lettera del messaggio che corrisponde a questo nodo... Tra i vari nodi liberi, viene selezionato quello che pesa di meno. Se, allo stesso tempo, si osservano indicatori minimi in più nodi, è possibile scegliere liberamente qualsiasi coppia. Successivamente, viene creato il nodo padre. Dovrebbe pesare tanto quanto pesa la somma della coppia di nodi data. Il genitore viene quindi inviato alla lista con nodi liberi. I bambini vengono rimossi. In questo caso, gli archi ricevono gli indicatori corrispondenti, zero e uno. Questo processo si ripete tante volte quante sono necessarie per lasciare un solo nodo. Successivamente, le cifre binarie vengono scritte dall'alto verso il basso.
Come migliorare l'efficienza della compressione
Per aumentare l'efficienza della compressione, durante la costruzione dell'albero del codice, è necessario utilizzare tutti i dati relativi alla probabilità di comparsa delle lettere in un particolare file allegato all'albero. Non dovrebbero essere sparsi su un gran numero di documenti di testo. Se attraversi il questa vita, quindi puoi ottenere statistiche sulla frequenza con cui vengono trovate lettere dall'oggetto che deve essere compresso.
Come accelerare il processo di compressione
Per accelerare il funzionamento dell'algoritmo, l'identificazione delle lettere dovrebbe essere effettuata non dagli indicatori dell'aspetto di determinate lettere, ma dalla frequenza della loro occorrenza. Grazie a ciò, l'algoritmo diventa più semplice e il lavoro con esso è notevolmente accelerato. Consente inoltre di evitare operazioni di divisione e virgola mobile. Inoltre, quando si lavora in questa modalità, l'algoritmo non può essere modificato. Ciò è dovuto principalmente al fatto che le probabilità sono direttamente proporzionali alle frequenze. Vale anche la pena prestare attenzione al fatto che il peso finale del nodo radice sarà uguale alla somma del numero di lettere nell'oggetto da elaborare.
Conclusione
I codici di Huffman sono un algoritmo semplice e consolidato che viene ancora utilizzato oggi in molti programmi popolari... La semplicità e la chiarezza di questo codice permette di raggiungere compressione effettiva file di qualsiasi dimensione.
Un metodo relativamente semplice di compressione dei dati può essere realizzato creando i cosiddetti alberi di Huffman per un file e utilizzati per comprimerlo e decomprimere i dati in esso contenuti. La maggior parte delle applicazioni utilizza alberi binari di Huffman (ad esempio, ogni nodo è una foglia o ha esattamente due sottonodi). È possibile, tuttavia, costruire alberi di Huffman con un numero arbitrario sottoalberi (ad esempio, ternario o, in caso generale, n-come alberi).
Albero di Huffman per file che contengono Z personaggi diversi Esso ha Z le foglie. Il percorso dalla radice alla foglia che rappresenta un particolare carattere determina la codifica, e ogni passo lungo il percorso alla foglia determina la codifica (che può essere 0 , 1 , ..., (N-1)). Posizionando i caratteri comuni più vicino alla radice e i caratteri meno comuni più lontano dalla radice, si ottiene la compressione desiderata. A rigor di termini, un tale albero sarà un albero di Huffman solo se, per effetto della codifica, il numero minimo n-ari caratteri per codificare il file specificato.
In questo problema, considereremo solo alberi, dove ogni nodo è un nodo interno o una foglia che codifica caratteri e non ci sono foglie isolate che non codificano un carattere.
La figura seguente mostra un esempio di albero ternario di Huffman, simboli " un" e " e"codificato con un singolo carattere ternario; caratteri meno comuni" S" e " P"sono codificati utilizzando due caratteri ternari e i caratteri più rari" X", "Q" e " sì"sono codificati utilizzando tre caratteri ternari ciascuno.
Certo, se vogliamo ampliare la lista n-ary poi indietro, è importante sapere quale albero viene utilizzato per comprimere i dati. Questo può essere fatto in diversi modi. In questo compito useremo metodo successivo: il flusso di dati di input sarà preceduto da un'intestazione composta da valori di caratteri codificati Z situata in file sorgente in ordine lessicografico.
Conoscere il numero di caratteri inseriti Z, senso n indicando " n-arity" dell'albero di Huffman e dell'intestazione stessa, è necessario trovare il valore primario dei caratteri codificati.
Dati in ingresso
I dati di input iniziano con un numero intero T, che si trova su una riga separata e che indica il numero di casi di test successivi. Successivamente, ciascuno dei T casi di test, ognuno dei quali si trova in 3 -esima riga come segue:
- Numero di caratteri distinti nel caso di test Z (2 ≤ Z ≤ 20 );
- Numero n indicando " n-arità dell'"albero di Huffman ( 2 ≤ n ≤ 10 );
- Una stringa che rappresenta l'intestazione del messaggio ricevuto, ogni carattere sarà una cifra nell'intervallo ... Questa riga conterrà meno 200 caratteri.
Nota: Sebbene raro, è possibile che un'intestazione abbia più interpretazioni durante la decodifica (ad esempio, per un'intestazione " 010011101100 ", e valori Z = 5 e N = 2). È garantito che in tutti i casi di test proposti nei dati di input, c'è una soluzione unica.
Produzione
Per ciascuno di T output dei casi di test Z righe che forniscono una versione decodificata di ciascuno di Z caratteri in ordine crescente. Usa il formato originale-> codifica, dove originale- esso numero decimale nell'intervallo e la stringa di cifre codificate corrispondente per quei caratteri (ogni cifra ≥ 0 e< n).
- codice = bit successivo dal flusso, lunghezza = 1
- Mentre il codice< base
codice = codice<< 1
codice = codice + bit successivo dal flusso
lunghezza = lunghezza + 1 - simbolo = simbolo + codice - base]
In altre parole, spingeremo da sinistra nella variabile di codice bit per bit dal flusso di input, fino a codice< base. При этом на каждой итерации будем увеличивать переменную length на 1 (т.е. продвигаться вниз по дереву). Цикл в (2) остановится когда мы определим длину кода (уровень в дереве, на котором находится искомый символ). Остается лишь определить какой именно символ на этом уровне нам нужен.
Supponiamo che il ciclo in (2), dopo diverse iterazioni, si fermi. In questo caso, l'espressione (codice - base) è il numero ordinale del nodo (carattere) richiesto a livello di lunghezza. Il primo nodo (simbolo) a livello di lunghezza nell'albero si trova nell'array symb all'indice offs. Ma non siamo interessati al primo carattere, ma al carattere sotto il numero (codice - base). Pertanto, l'indice del simbolo desiderato nell'array symb è (offs + (code - base)). In altre parole, il simbolo richiesto è simbolo = simbolo + codice - base].
Facciamo un esempio concreto. Usando l'algoritmo descritto, decodifichiamo il messaggio Z /.
Z / = "0001 1 00001 00000 1 010 011 1 011 1 010 011 0001 1 0010 010 011 011 1 1 1 010 1 1 1 0010 011 0011 1 0011 0011 011 1 010 1 1"
- codice = 0, lunghezza = 1
- codice = 0< base = 1
codice = 0<< 1 = 0
codice = 0 + 0 = 0
lunghezza = 1 + 1 = 2
codice = 0< base = 2
codice = 0<< 1 = 0
codice = 0 + 0 = 0
lunghezza = 2 + 1 = 3
codice = 0< base = 2
codice = 0<< 1 = 0
codice = 0 + 1 = 1
lunghezza = 3 + 1 = 4
codice = 1 = base = 1 - simbolo = simbolo = 2 + codice = 1 - base = 1] = simbolo = UN
- codice = 1, lunghezza = 1
- codice = 1 = base = 1
- simbolo = simbolo = 7 + codice = 1 - base = 1] = simbolo = h
- codice = 0, lunghezza = 1
- codice = 0< base = 1
codice = 0<< 1 = 0
codice = 0 + 0 = 0
lunghezza = 1 + 1 = 2
codice = 0< base = 2
codice = 0<< 1 = 0
codice = 0 + 0 = 0
lunghezza = 2 + 1 = 3
codice = 0< base = 2
codice = 0<< 1 = 0
codice = 0 + 0 = 0
lunghezza = 3 + 1 = 4
codice = 0< base = 1
codice = 0<< 1 = 0
codice = 0 + 1 = 1
lunghezza = 4 + 1 = 5
codice = 1> base = 0 - simbolo = simbolo = 0 + codice = 1 - base = 0] = simbolo = F
Quindi, abbiamo decodificato i primi 3 caratteri: UN, h, F... È chiaro che seguendo questo algoritmo otterremo esattamente il messaggio S.
Calcolo delle lunghezze dei codici
Per codificare un messaggio, abbiamo bisogno di conoscere i codici dei caratteri e la loro lunghezza. Come notato nella sezione precedente, i codici canonici sono ben definiti dalle loro lunghezze. Pertanto, il nostro compito principale è calcolare le lunghezze dei codici.
Si scopre che questo compito, nella stragrande maggioranza dei casi, non richiede la costruzione esplicita di un albero di Huffman. Inoltre, gli algoritmi che utilizzano la rappresentazione interna (implicita) dell'albero di Huffman sono molto più efficienti in termini di velocità e consumo di memoria.
Oggi esistono molti algoritmi efficienti per calcolare le lunghezze dei codici (,). Ci limiteremo a considerarne solo uno. Questo algoritmo è abbastanza semplice, ma nonostante ciò è molto popolare. Viene utilizzato in programmi come zip, gzip, pkzip, bzip2 e molti altri.
Torniamo all'algoritmo di costruzione dell'albero di Huffman. Ad ogni iterazione, abbiamo eseguito una ricerca lineare per i due nodi con il peso più basso. Chiaramente una coda prioritaria come una piramide (minima) è più appropriata a questo scopo. Il nodo con il peso più basso avrà la priorità più alta e sarà in cima alla piramide. Diamo questo algoritmo.
Includiamo tutti i caratteri codificati nella piramide.
Estraiamo in sequenza 2 nodi dalla piramide (saranno due nodi con il peso minore).
Formiamo un nuovo nodo e ad esso colleghiamo, da bambini, due nodi presi dalla piramide. In questo caso il peso del nodo formato è posto uguale alla somma dei pesi dei nodi figli.
Includiamo il nodo formato nella piramide.
Se c'è più di un nodo nella piramide, ripeti 2-5.
Assumeremo che per ogni nodo sia memorizzato un puntatore al suo genitore. Alla radice dell'albero, imposta questo puntatore su NULL. Ora selezioniamo il nodo foglia (simbolo) e seguendo i puntatori salvati saliremo sull'albero fino a quando il puntatore successivo diventa NULL. L'ultima condizione significa che abbiamo raggiunto la radice dell'albero. È chiaro che il numero di transizioni da livello a livello è pari alla profondità del nodo foglia (simbolo), e quindi alla lunghezza del suo codice. Bypassando in questo modo tutti i nodi (simboli), otteniamo le lunghezze dei loro codici.
Lunghezza massima del codice
Di norma, il cosiddetto libro dei codici, una semplice struttura dati, essenzialmente due array: uno con lunghezze, l'altro con codici. In altre parole, il codice (come stringa di bit) viene memorizzato in una locazione di memoria o registro di dimensione fissa (solitamente 16, 32 o 64). Per evitare overflow, dobbiamo essere sicuri che il codice si inserisca nel registro.
Si scopre che su un alfabeto di N caratteri, la dimensione massima del codice può essere fino a (N-1) bit di lunghezza. In altre parole, per N = 256 (variante comune) possiamo ottenere un codice di 255 bit di lunghezza (anche se per questo il file deve essere molto grande: 2.292654130570773 * 10 ^ 53 ~ = 2 ^ 177.259)! È chiaro che un tale codice non si adatta al registro e devi fare qualcosa con esso.
Innanzitutto, scopriamo in quali condizioni si verifica l'overflow. Sia la frequenza dell'i-esimo simbolo uguale all'i-esimo numero di Fibonacci. Ad esempio: UN-1, B-1, C-2, D-3, E-5, F-8, G-13, h-21. Costruiamo l'albero di Huffman corrispondente.
RADICE / \ / \ / \ / \ h / \ / \ /\ G / \ / \ /\ F / \ / \ /\ E / \ / \ /\ D / \ / \ /\ C / \ / \ UN B
Tale albero è chiamato degenerare... Per ottenerlo, le frequenze dei simboli devono crescere almeno come i numeri di Fibonacci o anche più velocemente. Sebbene in pratica, su dati reali, un tale albero sia quasi impossibile da ottenere, è molto facile generarlo artificialmente. In ogni caso, questo pericolo deve essere preso in considerazione.
Questo problema può essere risolto in due modi accettabili. La prima si basa su una delle proprietà dei codici canonici. Il punto è che nel codice canonico (stringa di bit) al massimo i bit meno significativi possono essere diversi da zero. In altre parole, tutti gli altri bit potrebbero non essere salvati affatto, poiché sono sempre zero. Nel caso di N = 256, è sufficiente salvare solo gli 8 bit meno significativi di ogni codice, assumendo che tutti gli altri bit siano uguali a zero. Questo risolve il problema, ma solo in parte. Ciò complicherà e rallenterà notevolmente sia la codifica che la decodifica. Pertanto, questo metodo è usato raramente nella pratica.
Il secondo modo è limitare artificialmente le lunghezze dei codici (durante la costruzione o dopo). Questo metodo è generalmente accettato, quindi ci soffermeremo su di esso in modo più dettagliato.
Esistono due tipi di algoritmi di codice che limitano la lunghezza. Euristico (approssimativo) e ottimale. Gli algoritmi del secondo tipo sono piuttosto complessi nell'implementazione e, di regola, richiedono più tempo e memoria rispetto ai primi. L'efficacia del codice vincolato euristicamente è determinata dalla sua deviazione dal codice vincolato in modo ottimale. Più piccola è la differenza, meglio è. Vale la pena notare che per alcuni algoritmi euristici questa differenza è molto piccola (,,), inoltre, molto spesso generano codice ottimale (sebbene non garantiscano che sarà sempre così). Inoltre, poiché in pratica, l'overflow si verifica molto raramente (a meno che non venga impostata una restrizione molto rigorosa sulla lunghezza massima del codice); con una dimensione dell'alfabeto piccola, è più opportuno utilizzare metodi euristici semplici e veloci.
Considereremo un algoritmo euristico abbastanza semplice e molto popolare. Ha trovato la sua strada in programmi come zip, gzip, pkzip, bzip2 e molti altri.
Il problema di limitare la lunghezza massima del codice è equivalente al problema di limitare l'altezza dell'albero di Huffman. Nota che, per costruzione, ogni nodo non foglia dell'albero di Huffman ha esattamente due discendenti. Ad ogni iterazione del nostro algoritmo, diminuiamo l'altezza dell'albero di 1. Quindi, sia L la lunghezza massima del codice (altezza dell'albero) ed è necessario limitarla a L / & lt L. Sia ulteriormente RN i il più a destra nodo foglia al livello i e LN i - il più a sinistra.
Iniziamo dal livello L. Sposta il nodo RN L al posto del suo genitore. Perché i nodi vanno in coppia, dobbiamo trovare un posto per un nodo adiacente a RN L. Per fare ciò, trova il livello j più vicino a L, contenente nodi foglia, tale che j & lt (L-1). Al posto di LN j, formeremo un nodo non foglio e ad esso collegheremo come figli il nodo LN j e il nodo del livello L rimasto senza coppia Applichiamo la stessa operazione a tutte le restanti coppie di nodi al livello l. È chiaro che ridistribuendo i nodi in questo modo, abbiamo ridotto l'altezza del nostro albero di 1. Ora è uguale a (L-1). Se ora L / & lt (L-1), faremo lo stesso con il livello (L-1), ecc. fino al raggiungimento del limite richiesto.
Torniamo al nostro esempio, dove L = 5. Limitiamo la lunghezza massima del codice a L / = 4.
RADICE / \ / \ / \ / \ h C E / \ / \ / \ / \ /\ UN D G / \ / \ B F
Si vede che nel nostro caso RN L = F, j = 3, LN j = C... Innanzitutto, sposta il nodo RN L = F al posto del loro genitore.
RADICE / \ / \ / \ / \ h / \ / \ / \ / \ / \ / \ /\ /\ / \ / \ / \ / \ / \ / \ / \ / \ /\ /\ C E / \ / \ / \ / \ F UN D G B(nodo non accoppiato)
Ora al posto di LN j = C Formiamo un nodo non foglia.
RADICE / \ / \ / \ / \ h E / \ / \ / \ / \ / \ / \ F UN D G ? ? B(nodo non accoppiato) C(nodo non accoppiato)
Attacciamo due spaiati al nodo formato: B e C.
RADICE / \ / \ / \ / \ h / \ / \ / \ / \ / \ / \ / \ / \ / \ /\ /\ / \ / \ / \ / \ / \ / \ / \ / \ /\ /\ /\ E / \ / \ / \ / \ / \ / \ F UN D G B C
Pertanto, abbiamo limitato la lunghezza massima del codice a 4. È chiaro che modificando le lunghezze del codice abbiamo perso un po' di efficienza. Quindi il messaggio S, codificato con tale codice, avrà una dimensione di 92 bit, ad es. 3 bit in più sulla ridondanza minima.
È chiaro che più limitiamo la lunghezza massima del codice, meno efficiente sarà il codice. Scopriamo quanto puoi limitare la lunghezza massima del codice. Ovviamente non più breve di un po'.
Calcolo dei codici canonici
Come abbiamo già notato più volte, le lunghezze dei codici sono sufficienti per generare i codici stessi. Ti mostreremo come è possibile farlo. Supponiamo di aver già calcolato le lunghezze dei codici e di aver contato quanti codici di ciascuna lunghezza abbiamo. Sia L la lunghezza massima del codice e T i il numero di codici di lunghezza i.
Calcoliamo S i - il valore iniziale del codice di lunghezza i, per tutti i da
S L = 0 (sempre)
S L-1 = (S L + T L) >> 1
S L-2 = (S L-1 + T L-1) >> 1
...
S 1 = 1 (sempre)
Per il nostro esempio, L = 5, T 1 .. 5 = (1, 0, 2, 3, 2).
S 5 = 00000 bin = 0 dec
S 4 = (S 5 = 0 + T 5 = 2) >> 1 = (00010 bin >> 1) = 0001 bin = 1 dec
S 3 = (S 4 = 1 + T 4 = 3) >> 1 = (0100 bin >> 1) = 010 bin = 2 dec
S 2 = (S 3 = 2 + T 3 = 2) >> 1 = (100 bin >> 1) = 10 bin = 2 dec
S 1 = (S 2 = 2 + T 2 = 0) >> 1 = (10 bin >> 1) = 1 bin = 1 dec
Si può vedere che S 5, S 4, S 3, S 1 sono esattamente i codici dei caratteri B, UN, C, h... Questi simboli sono uniti dal fatto che vengono tutti prima, ciascuno al proprio livello. In altre parole, abbiamo trovato il valore del codice iniziale per ogni lunghezza (o livello).
Ora assegniamo i codici al resto dei simboli. Il codice del primo carattere al livello i è S i, il secondo S i + 1, il terzo S i + 2, ecc.
Scriviamo i codici rimanenti per il nostro esempio:
B= S 5 = 00000 bin | UN= S 4 = 0001 scomparto | C= S 3 = 010 bin | h= S 1 = 1 bidone |
F= S 5 + 1 = 00001 bidone | D= S 4 + 1 = 0010 bin | E= S 3 + 1 = 011 bin | |
G= S 4 + 2 = 0011 bin |
Si può vedere che abbiamo ricevuto esattamente gli stessi codici come se avessimo costruito esplicitamente l'albero canonico di Huffman.
Passare un albero di codice
Affinché il messaggio codificato possa essere decodificato, il decodificatore deve avere lo stesso albero di codice (in un modo o nell'altro) utilizzato per la codifica. Pertanto, insieme ai dati codificati, siamo costretti a salvare l'albero del codice corrispondente. È chiaro che più è compatto, meglio è.
Esistono diversi modi per risolvere questo problema. La soluzione più ovvia è memorizzare l'albero in modo esplicito (cioè come un insieme ordinato di nodi e puntatori di un tipo o dell'altro). Questo è forse il modo più dispendioso e inefficace. In pratica non viene utilizzato.
È possibile salvare un elenco di frequenze di simboli (ad es. dizionario delle frequenze). Con il suo aiuto, il decodificatore può facilmente ricostruire l'albero del codice. Sebbene questo metodo sia meno dispendioso del precedente, non è il migliore.
Infine, può essere utilizzata una delle proprietà dei codici canonici. Come notato in precedenza, i codici canonici sono completamente determinati dalla loro lunghezza. In altre parole, tutto ciò di cui ha bisogno il decodificatore è un elenco di lunghezze di codice carattere. Considerando che, in media, la lunghezza di un codice per un alfabeto di N caratteri può essere codificata in [(log 2 (log 2 N))] bit, si ottiene un algoritmo molto efficiente. Ci soffermeremo su di esso in modo più dettagliato.
Supponiamo che la dimensione dell'alfabeto sia N = 256 e comprimiamo un file di testo normale (ASCII). Molto probabilmente non troveremo tutti gli N caratteri del nostro alfabeto in un tale file. Poniamo quindi uguale a zero la lunghezza del codice dei caratteri mancanti. In questo caso, l'elenco salvato di lunghezze di codice conterrà un numero sufficientemente grande di zeri (lunghezze di codice di caratteri mancanti) raggruppati insieme. Ciascuno di questi gruppi può essere compresso utilizzando la cosiddetta codifica di gruppo - RLE (Run - Length - Encoding). Questo algoritmo è estremamente semplice. Invece di una sequenza di M elementi identici in una riga, salveremo il primo elemento di questa sequenza e il numero delle sue ripetizioni, ad es. (M-1). Esempio: RLE ("AAAABBBCDDDDDDD") = A3 B2 C0 D6.
Inoltre, questo metodo può essere alquanto esteso. Possiamo applicare l'algoritmo RLE non solo a gruppi di lunghezza zero, ma a tutti gli altri. Questo modo di passare un albero di codice è comune e viene utilizzato nella maggior parte delle implementazioni moderne.
Attuazione: SHCODEC
Appendice: biografia di D. Huffman
David Huffman è nato nel 1925 nell'Ohio, negli Stati Uniti. Huffman ha conseguito la laurea in Ingegneria Elettrica presso la Ohio State University all'età di 18 anni. Ha poi servito nell'esercito come ufficiale di supporto radar su un cacciatorpediniere che ha contribuito a disinnescare le mine nelle acque giapponesi e cinesi dopo la seconda guerra mondiale. Successivamente, ha conseguito un master presso la Ohio University e un dottorato presso il Massachusetts Institute of Technology (MIT). Sebbene Huffman sia meglio conosciuto per aver sviluppato un metodo per costruire codici minimamente ridondanti, ha anche dato importanti contributi in molti altri campi (principalmente nell'elettronica). È stato a lungo capo del dipartimento di informatica del MIT. Nel 1974, già professore emerito, si dimise. Huffman ha ricevuto numerosi premi preziosi. 1999 - Richard W. Hamming Medal dell'Institute of Electrical and Electronics Engineers (IEEE) per contributi eccezionali alla teoria dell'informazione, Louis E. Levy Medal del Franklin Institute per la sua tesi di dottorato sui circuiti sequenziali, W. Wallace McDowell Award, IEEE Computer Society Award, IEEE Gold Jubilee Technology Innovation Award nel 1998. Nell'ottobre 1999, all'età di 74 anni, David Huffman morì di cancro. |