Come configurare smartphone e PC. Portale informativo
  • casa
  • Recensioni
  • Algoritmo per la costruzione di un albero del codice di Huffman. Codice di Huffman

Algoritmo per la costruzione di un albero del codice di Huffman. Codice di Huffman

  1. codice = bit successivo dal flusso, lunghezza = 1
  2. Mentre il codice< base
    codice = codice<< 1
    codice = codice + bit successivo dal flusso
    lunghezza = lunghezza + 1
  3. 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"

  1. codice = 0, lunghezza = 1
  2. 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
  3. simbolo = simbolo = 2 + codice = 1 - base = 1] = simbolo = UN
  1. codice = 1, lunghezza = 1
  2. codice = 1 = base = 1
  3. simbolo = simbolo = 7 + codice = 1 - base = 1] = simbolo = h
  1. codice = 0, lunghezza = 1
  2. 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
  3. 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 " "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 Z20 );
  • Numero n indicando " n-arità dell'"albero di Huffman ( 2 n10 );
  • 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).

  1. codice = bit successivo dal flusso, lunghezza = 1
  2. Mentre il codice< base
    codice = codice<< 1
    codice = codice + bit successivo dal flusso
    lunghezza = lunghezza + 1
  3. 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"

  1. codice = 0, lunghezza = 1
  2. 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
  3. simbolo = simbolo = 2 + codice = 1 - base = 1] = simbolo = UN
  1. codice = 1, lunghezza = 1
  2. codice = 1 = base = 1
  3. simbolo = simbolo = 7 + codice = 1 - base = 1] = simbolo = h
  1. codice = 0, lunghezza = 1
  2. 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
  3. 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.

Codifica di Huffman. Parte 1.
introduzione

Ciao caro lettore! Questo articolo discuterà uno dei modi per comprimere i dati. Questo metodo è abbastanza diffuso e merita un po' di attenzione. Questo materiale calcolato in volume per tre articoli, il primo dei quali sarà dedicato all'algoritmo di compressione, il secondo - implementazione del software algoritmo, e il terzo è la decompressione. L'algoritmo di compressione sarà scritto in C++, l'algoritmo di decompressione in Assembler.
Tuttavia, prima di procedere con l'algoritmo stesso, nell'articolo dovrebbe essere inclusa una piccola teoria.
Un po' di teoria
Compressione (compressione) - un modo per ridurre la quantità di dati ai fini della loro ulteriore trasmissione e conservazione.
DecompressioneÈ un modo per ripristinare i dati compressi ai dati originali.
La compressione e la decompressione possono essere sia senza perdita di qualità (quando le informazioni trasmesse/memorizzate in forma compressa dopo la decompressione sono assolutamente identiche a quelle originali), sia con perdita di qualità (quando i dati dopo la decompressione differiscono dall'originale). Ad esempio, documenti di testo, database, programmi possono essere compressi solo in un modo senza perdita di qualità, mentre immagini, video e file audio vengono compressi proprio a causa della perdita di qualità dei dati originali (un tipico esempio di algoritmi - JPEG, MPEG, ADPCM), con talvolta impercettibile perdita di qualità anche con compressione 1:4 o 1:10.
Si distinguono le principali tipologie di confezionamento:
  • Imballaggio decimaleè inteso per il confezionamento di dati di carattere costituiti solo da numeri. Invece di usare 8 bit per un carattere, è abbastanza razionale usare solo 4 bit per cifre decimali ed esadecimali, 3 bit per ottale e così via. Con questo approccio, si avverte già una compressione di almeno 1: 2.
  • Codifica relativaè una codifica con perdita. Si basa sul fatto che l'elemento di dati successivo differisce dal precedente per una quantità che occupa meno spazio in memoria rispetto all'elemento stesso. Un tipico esempioè la compressione audio ADPCM (Adaptive Differencial Pulse Code Modulation), ampiamente utilizzata in telefonia digitale e permette di comprimere i dati audio in un rapporto di 1:2 con perdita di qualità quasi impercettibile.
  • Soppressione simbolica- un metodo di compressione delle informazioni, in cui lunghe sequenze di dati identici sono sostituite da altre più brevi.
  • Codifica statistica in base al fatto che non tutti i dati si incontrano con la stessa frequenza(o probabilità). Con questo approccio, i codici vengono scelti in modo tale che l'elemento più frequente corrisponda al codice con la lunghezza più breve e il meno frequente con il più grande.
Inoltre, i codici sono selezionati in modo tale che durante la decodifica sia stato possibile determinare in modo univoco l'elemento dei dati originali. Con questo approccio è possibile solo la codifica orientata ai bit, in cui si distinguono i codici consentiti e quelli vietati. Se, durante la decodifica di una sequenza di bit, il codice risultasse vietato, è necessario aggiungere un altro bit della sequenza originale e ripetere l'operazione di decodifica. Esempi di tale codifica sono gli algoritmi di Shannon e Huffman, di cui prenderemo in considerazione l'ultimo.
Più specificamente sull'algoritmo
Come già noto dalla sottosezione precedente, l'algoritmo di Huffman si basa su codifica statistica... Diamo un'occhiata più da vicino alla sua implementazione.
Lascia che ci sia un'origine dati che trasferisce i caratteri (a_1, a_2, ..., a_n) con vari gradi di probabilità, cioè ogni a_i ha la sua probabilità (o frequenza) P_i (a_i), ed esiste almeno una coppia a_i e a_j, i \ ne j, tale che P_i (a_i) e P_j (a_j ) non sono uguali. Quindi, si forma un insieme di frequenze (P_1 (a_1), P_2 (a_2), ..., P_n (a_n)), cosa ha a che fare con \ stile di visualizzazione \ somma_ (i = 1) ^ (n) P_i (a_i) = 1, poiché il trasmettitore non trasmette altri caratteri oltre a (a_1, a_2, ..., a_n).
Il nostro compito è trovare tale caratteri in codice (b_1, b_2, ..., b_n) con lunghezze (L_1 (b_1), L_2 (b_2), ..., L_n (b_n)) in modo che la lunghezza media del simbolo del codice non superi la lunghezza media del simbolo originale. In questo caso, è necessario tener conto della condizione che se P_i (a_i)> P_j (a_j) e io \ ne j, allora L_i (b_i) \ le L_j (b_j).
Huffman ha proposto di costruire un albero in cui i nodi sono molto probabilmente i meno distanti dalla radice. Quindi il metodo stesso di costruzione di un albero segue:
1. Seleziona due simboli a_i e a_j, i \ ne j, tali che P_i (a_i) e P_j (a_j) dall'intero elenco (P_1 (a_1), P_2, ..., P_n (a_n)) sono minimi.
2. Riduci i rami degli alberi da questi due elementi a un punto con probabilità P = P_i (a_i) + P_j (a_j) contrassegnando un ramo con zero e l'altro con uno (a propria discrezione).
3. Ripetere il punto 1 tenendo conto nuovo punto invece di a_i e a_j, se il numero dei punti risultanti è maggiore di uno. Altrimenti, abbiamo raggiunto la radice dell'albero.
Proviamo ora ad utilizzare la teoria ottenuta ea codificare le informazioni trasmesse dalla sorgente, utilizzando l'esempio dei sette caratteri.
Diamo un'occhiata più da vicino al primo ciclo. La figura mostra una tabella in cui ogni simbolo a_i ha una propria probabilità (frequenza) P_i (a_i). Secondo il punto 1, selezioniamo due simboli dalla tabella con la probabilità più bassa. Nel nostro caso, questi sono a_1 e a_4. Secondo il punto 2, riduciamo i rami dell'albero da a_1 e a_4 a un punto e contrassegniamo il ramo che porta ad a_1 con uno e il ramo che porta ad a_4 con zero. Sopra il nuovo punto, assegniamo la sua probabilità (in questo caso - 0,03) V ulteriore azione vengono ripetute tenendo già conto del nuovo punto e senza tener conto di a_1 e a_4.

Dopo ripetute ripetizioni delle azioni di cui sopra, viene costruito il seguente albero:

Dall'albero costruito, puoi determinare il valore dei codici (b_1, b_2, ..., b_n), discendente dalla radice all'elemento corrispondente a_i, mentre assegna zero o uno alla sequenza risultante quando si passa ciascun ramo (a seconda di come viene chiamato il particolare ramo). Pertanto, la tabella dei codici si presenta così:

iob ioio (b io) 1 011111 62 1 13 0110 44 011110 65 010 36 00 27 01110 5

Ora proviamo a codificare una sequenza di caratteri.
Lascia che il simbolo a_i corrisponda (a titolo di esempio) al numero i. Lascia che ci sia una sequenza 12672262. Devi ottenere il codice binario risultante.
Per la codifica si può utilizzare la tabella dei simboli dei codici già esistente b_i, tenendo conto che b_i corrisponde al simbolo a_i. In questo caso, il codice per la cifra 1 sarà la sequenza 011111, per la cifra 2 - 1 e per la cifra 6 - 00. Quindi, otteniamo il seguente risultato:

Dati12672262 Lunghezza codice Nativo 001010110111010010110 01024 bit Codificato 011111100011101100119 bit

Come risultato della codifica, abbiamo vinto 5 bit e scritto la sequenza in 19 bit invece di 24.
Tuttavia, questo non fornisce una stima completa della compressione dei dati. Torniamo ai calcoli e stimiamo il rapporto di compressione del codice. Ciò richiede una stima dell'entropia.
entropia- una misura dell'incertezza di una situazione (variabile casuale) con un numero finito o pari di esiti. Matematicamente, l'entropia è formulata come la somma dei prodotti delle probabilità dei vari stati del sistema per i logaritmi di queste probabilità, presi con il segno opposto:

H (X) = - \ displaystyle \ sum_ (i = 1) ^ (n) P_i \ cdot log_d (P_i).​

Dove X è discreto valore casuale(nel nostro caso, un carattere di codice) e d è una radice arbitraria maggiore di uno. La scelta della base equivale alla scelta di una certa unità di misura dell'entropia. Visto che abbiamo a che fare con cifre binarie, allora è razionale scegliere d = 2 come base.
Pertanto, l'entropia per il nostro caso può essere rappresentata come:

H (b) = - \ displaystyle \ sum_ (i = 1) ^ (n) P_i (a_i) \ cdot log_2 (P_i (a_i)).​

L'entropia ha una proprietà notevole: è uguale alla lunghezza media minima consentita di un simbolo di codice \ sopralineato (L_ (min)) a bit. La stessa lunghezza media del simbolo del codice è calcolata dalla formula

\ overline (L (b)) = \ displaystyle \ sum_ (i = 1) ^ (n) P_i (a_i) \ cdot L_i (b_i).​

Sostituendo i valori nelle formule H (b) e \ overline (L (b)), otteniamo il seguente risultato: H (b) = 2.048, \ sopralinea (L (b)) = 2.100.
I valori di H (b) e \ overline (L (b)) sono molto vicini, il che indica un guadagno reale nella scelta dell'algoritmo. Ora confrontiamo la lunghezza media del simbolo originale e la lunghezza media del simbolo del codice attraverso il rapporto:

\ frac (\ overline (L_ (src))) (L (b)) = \ frac (3) (2,1) = 1.429.​

Quindi, abbiamo ottenuto un rapporto di compressione di 1: 1,429, che è molto buono.
E infine, risolviamo l'ultimo problema: decifrare la sequenza di bit.
Lascia che ci sia una sequenza di bit per la nostra situazione:

001101100001110001000111111​

È necessario definire fonte, ovvero decodificare questa sequenza.
Ovviamente, in una situazione del genere, puoi usare la tabella dei codici, ma questo è abbastanza scomodo, poiché la lunghezza dei simboli del codice non è costante. È molto più conveniente discendere l'albero (a partire dalla radice) secondo la seguente regola:
1. Il punto di partenza è la radice dell'albero.
2. Leggi nuovo ritmo... Se è zero, vai lungo il ramo contrassegnato da zero, altrimenti - con uno.
3. Se il punto in cui colpiamo è quello finale, allora abbiamo determinato il carattere del codice, che dovrebbe essere scritto e tornare al punto 1. Altrimenti, il punto 2 dovrebbe essere ripetuto.
Consideriamo un esempio di decodifica del primo simbolo. Siamo in un punto con probabilità 1,00 (la radice dell'albero), leggiamo il primo bit della sequenza e percorriamo il ramo segnato con zero fino a un punto con probabilità 0,60. Poiché questo punto non è il punto finale dell'albero, leggiamo il bit successivo, che è anche zero, e percorriamo il ramo contrassegnato da zero fino al punto a_6, che è l'ultimo. Abbiamo decodificato il simbolo: questo è il numero 6. Lo scriviamo e torniamo a lo stato iniziale(passa alla radice).
Pertanto, la sequenza decodificata assume la forma.

Dati

001101100001110001000111111 Lunghezza codice Codificato 00110110000111000100011111127 bit Originale 6223676261233 bit

In questo caso, il guadagno era di 6 bit con una lunghezza di sequenza piuttosto breve.
La conclusione suggerisce se stessa: l'algoritmo è semplice. Tuttavia, una nota dovrebbe essere fatta: questo algoritmo buono per la compressione informazioni di testo(in effetti, durante la digitazione utilizziamo effettivamente circa 60 caratteri dai 256 disponibili, ovvero la probabilità di incontrare altri caratteri è vicina allo zero), ma è già abbastanza grave per comprimere i programmi (poiché tutti i caratteri nel programma sono quasi ugualmente probabili ). Quindi l'efficienza dell'algoritmo dipende molto dal tipo di dati che vengono compressi.
P.S
In questo articolo, abbiamo esaminato l'algoritmo di codifica di Huffman, che si basa su codifica irregolare... Consente di ridurre la dimensione dei dati trasmessi o memorizzati. L'algoritmo è facile da capire e può dare guadagni reali. Inoltre, ha un'altra proprietà notevole: la capacità di codificare e decodificare le informazioni al volo, a condizione che le probabilità delle parole in codice siano determinate correttamente. Anche se c'è una modifica dell'algoritmo che permette di cambiare la struttura ad albero in tempo reale.
Nella parte successiva dell'articolo, esamineremo la compressione dei file orientata ai byte utilizzando l'algoritmo di Huffman, implementato in C++.
Codifica di Huffman. Parte 2
introduzione
Nell'ultima parte, abbiamo esaminato l'algoritmo di codifica, descritto modello matematico, ha eseguito la codifica e la decodifica su esempio specifico, calcolata la lunghezza media parola in codice e anche determinato il rapporto di compressione. Inoltre, sono state tratte conclusioni sui vantaggi e gli svantaggi di questo algoritmo.
Tuttavia, oltre a questo, restavano irrisolte altre due domande: l'implementazione del programma che comprime il file di dati e il programma che decomprime file compresso... Questo articolo è dedicato alla prima domanda. Pertanto, dovresti iniziare a progettare.
Design
Il primo passaggio consiste nel calcolare la frequenza di occorrenza dei caratteri nel file. Per fare ciò, descriviamo la seguente struttura:

    // Struttura per il calcolo della frequenza del carattere

    typedef struct TFreq

    int ch;

    TTable * tabella;

    DWORD freq;

    ) TFreq;

Questa struttura descriverà ogni carattere su 256. ch- il carattere ASCII stesso, frequenza- il numero di occorrenze del simbolo nel file. Campo tavolo- puntatore alla struttura:

    // descrittore del nodo

    typedef struct TTable

    int ch;

    Tavolo * sinistra;

    TTable * destra;

    ) TTable;

Come visto, tabellaÈ un descrittore di nodo che si biforca in zero e uno. Con l'aiuto di queste strutture, in futuro, verrà eseguita la costruzione dell'albero di compressione. Ora dichiariamo la nostra frequenza e il nostro nodo per ogni simbolo:

    Freq TFreq [256];

Proviamo a capire come sarà costruito l'albero. Nella fase iniziale, il programma deve esaminare l'intero file e contare il numero di occorrenze dei caratteri in esso contenuti. Inoltre, il programma deve creare un descrittore di nodo per ogni simbolo che trova. Successivamente, dai nodi creati, tenendo conto della frequenza dei simboli, il programma costruisce un albero, disponendo i nodi in un certo ordine e stabilendo collegamenti tra loro.
L'albero costruito è buono per decodificare il file. Ma per codificare un file, è scomodo, perché non si sa in quale direzione andare dalla radice per arrivare al carattere richiesto. Per questo, è più conveniente creare una tabella di conversione da carattere a codice. Pertanto, definiremo un'altra struttura:

    // Descrittore del carattere del codice

    typedef struct TOPcode

    codice operativo DWORD;

    DWORD len;

    ) Topcode;

Qui codice operativoÈ la combinazione di codice del carattere, e len- la sua lunghezza (in bit). E dichiariamo una tabella di 256 tali strutture:

    Codici operativi TOPcode [256];

Conoscendo il carattere da codificare, è possibile determinarne il codice dalla tabella. Passiamo ora direttamente al calcolo della frequenza dei simboli (e non solo).
Conteggio delle frequenze dei simboli
In linea di principio, questa azione non è difficile. È sufficiente aprire il file e contare il numero di caratteri in esso contenuti, compilando le strutture appropriate. Vediamo l'implementazione di questa azione.
Per fare ciò, dichiareremo i descrittori di file globali:

    FILE * in, * out, * assemb;

in- il file da cui vengono letti i dati non compressi.
fuori- il file in cui sono scritti i dati compressi.
assemblare- un file in cui verrà salvato l'albero in una forma conveniente per l'estrazione. Poiché l'unpacker sarà scritto in assembler, è abbastanza razionale rendere l'albero parte dell'unpacker, ovvero rappresentarlo sotto forma di istruzioni in Assembler.
Il primo passo è inizializzare tutte le strutture zero valori:

    // Conteggio della frequenza dei simboli

    int CountFrequency (void)

    int io; // variabile loop

    int conteggio = 0; // seconda variabile del ciclo

    DWORD TotalCount = 0; // dimensione del file.

    // Inizializza le strutture

    per (i = 0; i< 256 ; i++ )

    Freq [i] .freq = 0;

    Freq [i] .tabella = 0;

    Freq [i] .ch = i;

Successivamente, contiamo il numero di occorrenze del simbolo nel file e la dimensione del file (ovviamente, non nel modo più ideale, ma l'esempio ha bisogno di chiarezza):

    // Conteggio della frequenza dei simboli (simbolicamente)

    mentre (! feof (in)) // fino alla fine del file

    i = fgetc (in);

    se (i! = EOF) // se non la fine del file

    Freq[i] .freq++; // frequenza ++

    Conteggio totale ++; // taglia ++

Poiché il codice non è uniforme, sarà difficile per l'unpacker scoprire il numero di caratteri da leggere. Pertanto, è necessario fissarne le dimensioni nella tabella di disimballaggio:

    // "Comunica" all'unpacker la dimensione del file

    fprintf (assemb, "coded_file_size: \ n gg% 8lxh \ n \ n ", Conteggio totale);

Successivamente, tutti i caratteri utilizzati vengono spostati all'inizio dell'array e quelli non utilizzati vengono sovrascritti (tramite permutazioni).

    // sposta alla fine tutti i caratteri non utilizzati

    io = 0;

    conteggio = 256;

    mentre io< count) // non sono ancora arrivato alla fine

    if (Freq [i] .freq == 0) // se la frequenza è 0

    Freq [i] = Freq [- conteggio]; // quindi copia la voce dalla fine

    altro

    io++; // va tutto bene - vai avanti.

E solo dopo tale "ordinamento" viene allocata la memoria per i nodi (per un po' di economia).

    // Alloca memoria per i nodi

    per (i = 0; i< count; i++ )

    Freq [i] .table = nuovo TTable; // crea un nodo

    Freq [i] .table -> sinistra = 0; // nessuna connessione

    Freq [i] .table -> destra = 0; // nessuna connessione

    Freq [i] .table -> ch = Freq.ch; // copia il simbolo

    Freq [i] .freq = Freq.freq; // e frequenza

    conteggio dei resi;

Quindi, abbiamo scritto una funzione per l'inizializzazione iniziale del sistema, o, se guardi l'algoritmo nella prima parte dell'articolo, "annotato i simboli usati in una colonna e assegnato loro le probabilità", e anche per ogni symbol ha creato un "punto di partenza" - un nodo - e lo ha inizializzato ... nei campi sinistra e Giusto annotato gli zeri. Quindi, se il nodo è l'ultimo nell'albero, allora sarà facile da vedere da sinistra e Giusto uguale a zero.
Creazione dell'albero
Quindi, nella sezione precedente, abbiamo "scritto i simboli utilizzati in una colonna e assegnato loro le probabilità". Infatti non abbiamo assegnato loro delle probabilità, ma i numeratori della frazione (cioè il numero di occorrenze dei caratteri nel file). Ora dobbiamo costruire un albero. Ma per fare questo, devi trovare elemento minimo sulla lista. Per fare ciò, introduciamo una funzione in cui passiamo due parametri: il numero di elementi nell'elenco e l'elemento da escludere (perché cercheremo in coppia e sarà molto spiacevole se riceviamo lo stesso elemento due volte da la funzione):

    // trova il nodo con la probabilità più bassa.

    int FindLeast (int count, int index)

    int io;

    DWORD min = (indice == 0)? 10; // l'elemento da contare

    // minimo

    per (i = 1; i< count; i++ ) // scorre l'array

    if (i! = indice) // se l'elemento non è escluso

    if (Freq [i] .freq< Freq[ min] .freq ) // сравниваем

    min = io; // meno del minimo - ricorda

    ritorno minimo; // restituisce l'indice minimo

La ricerca non è difficile: per prima cosa selezioniamo l'elemento "minimo" dell'array. Se l'elemento escluso è 0, allora prendiamo il primo elemento come minimo, altrimenti consideriamo zero come minimo. Mentre esaminiamo l'array, confrontiamo l'elemento corrente con quello "minimo" e, se risulta essere inferiore, lo contrassegniamo come minimo.
Ora, infatti, la stessa funzione di costruzione dell'albero:

    // Funzione per costruire un albero

    void PreInit (int count)

    int ind1, ind2; // indici degli elementi

    TTable * tabella; // puntatore a "nuovo nodo"

    mentre (conta > 1) // loop fino a raggiungere la radice

    ind1 = FindLeast (count, - 1); // primo nodo

    ind2 = FindLeast (count, ind1); // secondo nodo

    tabella = nuova TTable; // crea un nuovo nodo

    tabella-> ch = - 1; // non definitivo

    tabella-> sinistra = Freq [ind1] .table; // 0 - nodo 1

    tabella-> destra = Freq [ind2] .table; // 1 - nodo 2

    Freq [ind1] .ch = - 1; // modifica il record su

    Freq [ind1] .freq + = Freq [ind2] .freq; // frequenza per il simbolo

    Freq [ind1] .table = tabella; // e scrivi un nuovo nodo

    if (ind2! = (- count)) // se ind2 non è l'ultimo

    Freq [ind2] = Freq [count]; // poi al suo posto

    // metto l'ultimo nell'array

Codice Simboli Tabella
Quindi, abbiamo costruito un albero in memoria: abbiamo preso due nodi in coppia, creato un nuovo nodo, in cui abbiamo scritto dei puntatori ad essi, dopodiché il secondo nodo è stato rimosso dall'elenco e invece del primo nodo abbiamo scritto un nuovo uno con una forchetta.
Ora sorge un altro problema: è scomodo codificare in un albero, perché è necessario sapere esattamente su quale percorso si trova un particolare simbolo. Tuttavia, il problema è risolto in modo abbastanza semplice: viene creata un'altra tabella - una tabella di simboli di codice - e in essa vengono scritte le combinazioni di bit di tutti i simboli utilizzati. Per fare ciò, è sufficiente attraversare ricorsivamente l'albero una volta. Allo stesso tempo, per non bypassarlo nuovamente, è possibile aggiungere alla funzione di bypass la generazione di un file assembler per un'ulteriore decodifica dei dati compressi (vedere la sezione " Design").
In realtà, la funzione in sé non è complicata. Dovrebbe assegnare 0 o 1 alla parola di codice se il nodo non è definitivo, altrimenti aggiungere il carattere di codice alla tabella. Oltre a tutto questo, genera un file assembly. Considera questa funzione:

    void RecurseMake (TTable * tbl, codice operativo DWORD, int len)

    fprintf (assemb, "opcode% 08lx_% 04x: \ n ", codice operativo, len); // etichetta su file

    if (tbl-> ch! = - 1) // nodo finale

    BYTE mod = 32 - lente;

    Opcodes [tbl-> ch] .opcode = (opcode >> mod); // salva il codice

    Codici operativi [tbl-> ch] .len = len; // e la sua lunghezza (in bit)

    // e crea l'etichetta corrispondente

    fprintf (assemblare, "db% 03xh, 0ffh, 0ffh, 0ffh \ n \ n", tbl-> ch);

    altro // il nodo non è definitivo

    codice operativo >> = 1; // libera spazio per un nuovo bit

    len++; // aumenta la lunghezza del codice

    \ n ", codice operativo, len);

    fprintf (assemb, "dw opcode% 08lx_% 04x \n\n", codice operativo | 0x80000000, lente);

    RecurseMake (tbl-> left, opcode, len);

    RecurseMake (tbl-> right, opcode | 0x80000000, len);

    // rimuove il nodo (non è più necessario)

    eliminare tbl;

Tra l'altro, dopo aver attraversato un nodo, la funzione lo cancella (libera il puntatore). Ora cerchiamo di capire quali parametri vengono passati alla funzione.

  • tbl- il nodo da bypassare.
  • codice operativo- il codice corrente. Il bit più significativo deve essere sempre libero.
  • len- la lunghezza della parola in codice.
In linea di principio, la funzione non è più complicata del "fattoriale classico" e non dovrebbe causare difficoltà.
Uscita bit
Quindi siamo arrivati ​​alla parte non molto piacevole del nostro archiviatore, vale a dire l'output dei simboli di codice su un file. Il problema è che i simboli del codice sono di lunghezza non uniforme e l'output deve essere eseguito bit per bit. Questo aiuterà la funzione MettiCodice... Ma prima, dichiariamo due variabili: il contatore dei bit nel byte e il byte di output:

    // Contatore di bit

    int OutBits;

    // Carattere visualizzato

    BYTE OutChar;

OutBits viene incrementato di uno ogni volta che viene emesso un bit, OutBits == 8 segnala che OutChar deve essere salvato in un file.

    void PutCode (int ch)

    lente d'ingrandimento;

    int outcode;

    // ottengo la lunghezza della parola in codice e la parola in codice stessa

    outcode = Opcodes [ch] .opcode; // parola in codice

    len = Codici operativi [ch] .len; // lunghezza (in bit)

    mentre (len> 0) // emette bit per bit

    // Loop mentre la variabile OutBits non è completamente utilizzata

    mentre ((OutBits< 8 ) && (len> 0 ) )

    OutChar >> = 1; // libera spazio

    OutChar | = ((codifica & 1)<< 7 ) ; // e mettici un po' dentro

    outcode >> = 1; // prossimo bit di codice

    len -- ; // diminuisce la lunghezza

  1. OutBits ++ ; // il numero di bit è aumentato

  2. }

  3. // se vengono utilizzati tutti gli 8 bit, salvali in un file

  4. Se ( OutBits == 8 )

  5. {

  6. fputc( OutChar, fuori ) ; // salva su file

  7. OutBits = 0 ; // imposta a zero

  8. OutChar = 0 ; // parametri

  9. }

  10. }

  11. }

Inoltre, devi organizzare qualcosa come "fflush", cioè dopo l'output dell'ultima parola in codice OutChar non entrerà nel file di output perché OutBits! = 8. Da qui nasce un'altra piccola funzione:

  1. // "Cancella" il bit buffer

  2. vuoto EndPut (vuoto)

  3. {

  4. // Se ci sono bit nel buffer

  5. Se ( OutBits ! = 0 )

Principali articoli correlati