Cum se configurează smartphone-uri și PC-uri. Portal informativ
  • Acasă
  • Recenzii
  • Algoritm pentru construirea unui arbore de cod Huffman. Codul Huffman

Algoritm pentru construirea unui arbore de cod Huffman. Codul Huffman

  1. cod = următorul bit din flux, lungime = 1
  2. În timp ce codul< base
    cod = cod<< 1
    cod = cod + următorul bit din flux
    lungime = lungime + 1
  3. simbol = simbol + cod - bază]

Cu alte cuvinte, vom împinge din stânga în variabila cod bit cu bit din fluxul de intrare, până la cod< base. При этом на каждой итерации будем увеличивать переменную length на 1 (т.е. продвигаться вниз по дереву). Цикл в (2) остановится когда мы определим длину кода (уровень в дереве, на котором находится искомый символ). Остается лишь определить какой именно символ на этом уровне нам нужен.

Să presupunem că bucla din (2), după mai multe iterații, se oprește. În acest caz, expresia (cod - bază) este numărul ordinal al nodului (caracterului) necesar la nivelul lungimii. Primul nod (simbol) la nivelul lungimii din arbore este situat în matricea de simboluri la indexul offs. Dar nu ne interesează primul caracter, ci caracterul de sub număr (cod - bază). Prin urmare, indexul simbolului dorit în matricea symb este (offs + (cod - bază)). Cu alte cuvinte, simbolul necesar este simbol = simbol + cod - bază].

Să dăm un exemplu concret. Folosind algoritmul descris, decodăm mesajul 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 1 0010 1 1 1 0010 1 0 1 1 0 1 0 1 0 1 0 1 1 0

  1. cod = 0, lungime = 1
  2. cod = 0< base = 1
    cod = 0<< 1 = 0
    cod = 0 + 0 = 0
    lungime = 1 + 1 = 2
    cod = 0< base = 2
    cod = 0<< 1 = 0
    cod = 0 + 0 = 0
    lungime = 2 + 1 = 3
    cod = 0< base = 2
    cod = 0<< 1 = 0
    cod = 0 + 1 = 1
    lungime = 3 + 1 = 4
    cod = 1 = baza = 1
  3. simbol = simbol = 2 + cod = 1 - bază = 1] = simbol = A
  1. cod = 1, lungime = 1
  2. cod = 1 = baza = 1
  3. simbol = simbol = 7 + cod = 1 - bază = 1] = simbol = H
  1. cod = 0, lungime = 1
  2. cod = 0< base = 1
    cod = 0<< 1 = 0
    cod = 0 + 0 = 0
    lungime = 1 + 1 = 2
    cod = 0< base = 2
    cod = 0<< 1 = 0
    cod = 0 + 0 = 0
    lungime = 2 + 1 = 3
    cod = 0< base = 2
    cod = 0<< 1 = 0
    cod = 0 + 0 = 0
    lungime = 3 + 1 = 4
    cod = 0< base = 1
    cod = 0<< 1 = 0
    cod = 0 + 1 = 1
    lungime = 4 + 1 = 5
    cod = 1> baza = 0
  3. simbol = simbol = 0 + cod = 1 - bază = 0] = simbol = F

Deci, am decodat primele 3 caractere: A, H, F... Este clar că urmând acest algoritm vom primi exact mesajul S.

Calcularea lungimii codului

Pentru a codifica un mesaj, trebuie să cunoaștem codurile caracterelor și lungimile acestora. După cum sa menționat în secțiunea anterioară, codurile canonice sunt bine definite prin lungimile lor. Astfel, sarcina noastră principală este să calculăm lungimile codurilor.

Se pare că această sarcină, în majoritatea covârșitoare a cazurilor, nu necesită construirea explicită a unui arbore Huffman. Mai mult, algoritmii care folosesc reprezentarea internă (implicita) a arborelui Huffman sunt mult mai eficienți în ceea ce privește viteza și consumul de memorie.

Astăzi există mulți algoritmi eficienți pentru calcularea lungimii codurilor (,). Ne vom restrânge să luăm în considerare doar una dintre ele. Acest algoritm este destul de simplu, dar, în ciuda acestui fapt, este foarte popular. Este folosit în programe precum zip, gzip, pkzip, bzip2 și multe altele.

Să revenim la algoritmul de construcție a arborelui Huffman. La fiecare iterație, am efectuat o căutare liniară pentru cele două noduri cu cea mai mică pondere. În mod clar, o coadă cu prioritate, cum ar fi o piramidă (minim), este mai potrivită în acest scop. Nodul cu cea mai mică greutate va avea cea mai mare prioritate și se va afla în vârful piramidei. Să dăm acest algoritm.

    Să includem toate caracterele codificate în piramidă.

    Să extragem secvenţial 2 noduri din piramidă (acestea vor fi două noduri cu cea mai mică greutate).

    Să ne formăm nod nouși atașează-i, în copilărie, două noduri luate din piramidă. În acest caz, greutatea nodului format este setată egală cu suma greutăților nodurilor copil.

    Să includem nodul format în piramidă.

    Dacă există mai mult de un nod în piramidă, repetați 2-5.

Vom presupune că un pointer către părintele său este stocat pentru fiecare nod. Setăm acest indicator egal cu NULL la rădăcina arborelui. Acum haideți să selectăm nodul frunză (simbol) și urmând pointerii salvati vom urca în arbore până când următorul pointer devine NULL. Ultima condiție înseamnă că am ajuns la rădăcina copacului. Este clar că numărul de tranziții de la nivel la nivel este egal cu adâncimea nodului frunză (simbol) și, prin urmare, cu lungimea codului său. Ocolind în acest fel toate nodurile (simbolurile), obținem lungimile codurilor lor.

Lungimea maximă a codului

De regulă, așa-numitul cartea de coduri, o structură de date simplă, în esență două tablouri: unul cu lungimi, celălalt cu coduri. Cu alte cuvinte, codul (ca șir de biți) este stocat într-o locație de memorie sau într-un registru de dimensiune fixă ​​(de obicei 16, 32 sau 64). Pentru a evita debordarea, trebuie să fim siguri că codul se va încadra în registru.

Se pare că pe un alfabet cu N caractere, dimensiunea maximă a codului poate fi de până la (N-1) biți în lungime. Cu alte cuvinte, pentru N = 256 (o variantă comună) putem obține un cod de 255 de biți lungime (deși pentru aceasta fișierul trebuie să fie foarte mare: 2.292654130570773 * 10 ^ 53 ~ = 2 ^ 177.259)! Este clar că un astfel de cod nu se va încadra în registru și trebuie să faci ceva cu el.

Mai întâi, să aflăm în ce condiții are loc revărsarea. Fie frecvența simbolului i-lea să fie egală cu al-lea număr Fibonacci. De exemplu: A-1, B-1, C-2, D-3, E-5, F-8, G-13, H-21. Să construim arborele Huffman corespunzător.

ROOT / \ / \ / \ / \ H / \ / \ /\ G / \ / \ /\ F / \ / \ /\ E / \ / \ /\ D / \ / \ /\ C / \ / \ A B

Un astfel de copac se numește degenerat... Pentru a-l obține, frecvențele simbolurilor trebuie să crească cel puțin ca numere Fibonacci sau chiar mai repede. Deși în practică, pe date reale, un astfel de arbore este aproape imposibil de obținut, este foarte ușor să îl generezi artificial. În orice caz, acest pericol trebuie luat în considerare.

Această problemă poate fi rezolvată în două moduri acceptabile. Primul se bazează pe una dintre proprietățile codurilor canonice. Ideea este că în codul canonic (șir de biți) cel puțin biții semnificativi pot fi non-zero. Cu alte cuvinte, este posibil ca toți ceilalți biți să nu fie salvați deloc, deoarece sunt întotdeauna zero. În cazul lui N = 256, este suficient să salvăm doar cei 8 biți mai puțin semnificativi din fiecare cod, presupunând că toți ceilalți biți sunt egali cu zero. Acest lucru rezolvă problema, dar numai parțial. Acest lucru va complica foarte mult și va încetini atât codarea, cât și decodarea. Prin urmare, această metodă este rar folosită în practică.

A doua modalitate este de a limita artificial lungimile codurilor (fie în timpul construcției, fie după). Această metodă este în general acceptată, așa că ne vom opri mai detaliat asupra ei.

Există două tipuri de algoritmi de cod de limitare a lungimii. Euristică (aproximativă) și optimă. Algoritmii de al doilea tip sunt destul de complexi în implementare și, de regulă, necesită mai mult timp și memorie decât primii. Eficacitatea codului constrâns euristic este determinată de abaterea sa de la codul constrâns optim. Cu cât diferența este mai mică, cu atât mai bine. Este de remarcat faptul că pentru unii algoritmi euristici această diferență este foarte mică (,,), în plus, ei generează foarte des cod optim (deși nu garantează că așa va fi întotdeauna). Mai mult, din moment ce în practică, depășirea apare extrem de rar (cu excepția cazului în care este stabilită o restricție foarte strictă asupra lungimii maxime a codului); cu o dimensiune alfabetică mică, este mai oportun să se utilizeze metode euristice simple și rapide.

Vom lua în considerare un algoritm euristic destul de simplu și foarte popular. Și-a găsit drumul în programe precum zip, gzip, pkzip, bzip2 și multe altele.

Problema limitării lungimii maxime a codului este echivalentă cu problema limitării înălțimii arborelui Huffman. Rețineți că, prin construcție, orice nod fără frunză al arborelui Huffman are exact doi descendenți. La fiecare iterație a algoritmului nostru, vom scădea înălțimea arborelui cu 1. Deci, fie L lungimea maximă a codului (înălțimea arborelui) și este necesar să o limităm la L / & lt L. Fie mai departe RN i cea mai dreaptă nodul frunză la nivelul i, iar LN i - cel mai din stânga.

Să începem de la nivelul L. Mută ​​nodul RN L la locul părintelui său. pentru că nodurile merg în perechi, trebuie să găsim un loc pentru un nod adiacent RN L. Pentru a face acest lucru, găsiți nivelul j cel mai apropiat de L, care conține noduri de frunze, astfel încât j & lt (L-1). În locul lui LN j, formăm un nod fără foaie și îi atașăm ca copii nodul LN j și nodul neîmperecheat de la nivelul L. Aplicam aceeași operație tuturor perechilor de noduri rămase la nivelul L. Este clar că redistribuind nodurile în acest fel, am redus înălțimea arborelui nostru cu 1. Acum este egal cu (L-1). Dacă acum L / & lt (L-1), atunci vom face același lucru cu nivelul (L-1), etc. până la atingerea limitei cerute.

Să revenim la exemplul nostru, unde L = 5. Să limităm lungimea maximă a codului la L / = 4.

ROOT / \ / \ / \ / \ H C E / \ / \ / \ / \ /\ A D G / \ / \ B F

Se vede ca in cazul nostru RN L = F, j = 3, LN j = C... Mai întâi, mutați nodul RN L = Fîn locul părintelui lor.

ROOT / \ / \ / \ / \ H / \ / \ / \ / \ / \ / \ /\ /\ / \ / \ / \ / \ / \ / \ / \ / \ /\ /\ C E / \ / \ / \ / \ F A D G B(nod neîmperecheat)

Acum, în locul lui LN j = C Să formăm un nod fără frunză.

ROOT / \ / \ / \ / \ H E / \ / \ / \ / \ / \ / \ F A D G ? ? B(nod neîmperecheat) C(nod neîmperecheat)

Să atașăm două nepereche la nodul format: Bși C.

ROOT / \ / \ / \ / \ H / \ / \ / \ / \ / \ / \ / \ / \ / \ /\ /\ / \ / \ / \ / \ / \ / \ / \ / \ /\ /\ /\ E / \ / \ / \ / \ / \ / \ F A D G B C

Astfel, am limitat lungimea maximă a codului la 4. Este clar că prin modificarea lungimii codului am pierdut puțin în eficiență. Deci mesajul S, codificat cu un astfel de cod, va avea o dimensiune de 92 de biți, adică. Încă 3 biți peste Redundanța Minimă.

Este clar că cu cât limităm mai mult lungimea maximă a codului, cu atât codul va fi mai puțin eficient. Să aflăm cât de mult poți limita lungimea maximă a codului. Evident, nu mai scurt decât puțin.

Calculul Codurilor Canonice

După cum am observat deja de multe ori, lungimile codurilor sunt suficiente pentru a genera codurile în sine. Vă vom arăta cum se poate face acest lucru. Să presupunem că am calculat deja lungimile codurilor și am numărat câte coduri din fiecare lungime avem. Fie L lungimea maximă a codului și T i numărul de coduri de lungime i.

Calculăm S i - valoarea initiala codul lungimii i, pentru tot i de la

S L = 0 (întotdeauna)
S L-1 = (S L + T L) >> 1
S L-2 = (S L-1 + T L-1) >> 1
...
S 1 = 1 (întotdeauna)

Pentru exemplul nostru, 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

Se poate observa că S 5, S 4, S 3, S 1 sunt exact codurile de caractere B, A, C, H... Aceste simboluri sunt unite de faptul că toate vin pe primul loc, fiecare la nivelul său. Cu alte cuvinte, am găsit valoarea inițială a codului pentru fiecare lungime (sau nivel).

Acum să atribuim coduri restului simbolurilor. Codul primului caracter la nivelul i este S i, al doilea S i + 1, al treilea S i + 2 etc.

Să scriem codurile rămase pentru exemplul nostru:

B= S 5 = 00000 bin A= S 4 = 0001 bin C= S 3 = 010 bin H= S 1 = 1 bin
F= S 5 + 1 = 00001 bin D= S 4 + 1 = 0010 bin E= S 3 + 1 = 011 bin
G= S 4 + 2 = 0011 bin

Se poate observa că am primit exact aceleași coduri ca și cum am fi construit în mod explicit arborele canonic Huffman.

Trecerea unui arbore de cod

Pentru ca mesajul codificat să fie decodat, decodorul trebuie să aibă același arbore de cod (într-o formă sau alta) care a fost folosit pentru codificare. Prin urmare, împreună cu datele codificate, suntem forțați să salvăm arborele de cod corespunzător. Este clar că cu cât este mai compact, cu atât mai bine.

Există mai multe modalități de a rezolva această problemă. Cel mai solutie evidenta- salvați arborele în mod explicit (adică ca un set ordonat de noduri și pointeri de un fel sau altul). Acesta este poate cel mai risipitor și ineficient mod. În practică, nu este folosit.

Poate fi salvată o listă de frecvențe simbol (adică dicționar de frecvență). Cu ajutorul său, decodorul poate reconstrui cu ușurință arborele de cod. Deși această metodă este mai puțin risipitoare decât cea anterioară, nu este cea mai bună.

În cele din urmă, poate fi folosită una dintre proprietățile codurilor canonice. După cum sa menționat mai devreme, codurile canonice sunt complet determinate de lungimile lor. Cu alte cuvinte, tot ce are nevoie decodorul este o listă de lungimi de cod de caractere. Având în vedere că, în medie, lungimea unui cod pentru un alfabet cu N caractere poate fi codificată în [(log 2 (log 2 N))] biți, obținem un algoritm foarte eficient. Ne vom opri asupra ei mai detaliat.

Să presupunem că dimensiunea alfabetului este N = 256 și comprimăm cea obișnuită fisier text(ASCII). Cel mai probabil nu vom găsi toate N caracterele alfabetului nostru într-un astfel de fișier. Apoi punem lungimea codului caracterelor lipsă egal cu zero... În acest caz, lista salvată de lungimi de cod va conține suficient număr mare zerouri (lungimile codurilor de caractere lipsă) grupate împreună. Fiecare astfel de grup poate fi comprimat folosind așa-numita codificare de grup - RLE (Run - Length - Encoding). Acest algoritm este extrem de simplu. În loc de o secvență de M elemente identice într-un rând, vom salva primul element din această secvență și numărul repetărilor sale, adică. (M-1). Exemplu: RLE ("AAAABBCDDDDDDD") = A3 B2 C0 D6.

Mai mult, această metodă poate fi oarecum extinsă. Putem aplica Algoritmul RLE nu numai la grupuri de lungimi zero, ci la toate celelalte. Acest mod de a trece un arbore de cod este comun și este folosit în majoritatea implementărilor moderne.

Implementare: SHCODEC

Anexă: biografia lui D. Huffman

David Huffman s-a născut în 1925 în Ohio, SUA. Huffman a primit licența în Inginerie Electrică de la universitate de stat Ohio (Ohio State University) la vârsta de 18 ani. Apoi a servit în armată ca ofițer de sprijin radar pe un distrugător care a ajutat la dezamorsarea minelor în apele japoneze și chineze după al Doilea Război Mondial. Ulterior, a primit un master de la Universitatea Ohio și un doctorat de la Massachusetts Institute of Technology (MIT). Deși Huffman este cel mai bine cunoscut pentru dezvoltarea unei metode de construire a codurilor minim redundante, el a adus, de asemenea, contribuții importante în multe alte domenii (mai ales în electronică). El pentru mult timp a condus Departamentul de Informatică de la MIT. În 1974, deja profesor emerit, a demisionat. Huffman a primit o serie de premii valoroase. 1999 - Medalia Richard W. Hamming de la Institutul de Ingineri Electrici și Electronici (IEEE) pentru contribuții excepționale la teoria informației, Medalia Louis E. Levy de la Institutul Franklin pentru teza sa de doctorat privind circuitele secvențiale, Premiul W. Wallace McDowell, IEEE Computer Society Award, IEEE Gold Jubilee Technology Innovation Award în 1998. În octombrie 1999, la vârsta de 74 de ani, David Huffman a murit de cancer.

R.L. Milidiu, A.A. Pessoa, E.S. Laber, „Implementarea eficientă a algoritmului de încălzire pentru construirea codurilor de prefix cu restricții de lungime”, Proc. de ALENEX (Atelier internațional de inginerie și experimentare a algoritmilor), pp. 1-17, Springer, ian. 1999.

Astăzi, puțini dintre utilizatori sunt interesați de întrebarea legată de mecanismul de comprimare a fișierelor. Procesul de lucru cu calculator personalîn comparație cu trecutul, a devenit mult mai ușor de implementat.


Astăzi, aproape orice utilizator cu care lucrează Sistemul de fișiere folosește arhive. Cu toate acestea, puțini utilizatori s-au gândit la modul în care fișierele sunt comprimate.

Codurile Huffman au fost prima opțiune. Ele sunt încă folosite în diferite arhivare. Majoritatea utilizatorilor nici măcar nu se gândesc cât de ușor este să comprimați un fișier folosind această schemă. V această recenzie ne vom uita la modul în care se realizează compresia, ce caracteristici ajută la accelerarea și simplificarea procesului de codificare. Vom încerca, de asemenea, să înțelegem principiile de bază ale construirii unui arbore de codare.

Algoritm: istorie

Primul algoritm conceput pentru a realiza o codificare eficientă informatii electronice, a devenit codul propus de Huffman în 1952. Acest cod poate fi luat în considerare astăzi element de bază majoritatea programelor concepute pentru a comprima informații. Unele dintre cele mai populare surse care folosesc codul dat, astăzi sunt arhivele RAR, ARJ, ZIP. Acest algoritm este folosit și pentru compresie Imagini JPEGși obiecte grafice... De asemenea, toate faxurile moderne folosesc un algoritm de codare care a fost inventat încă din 1952. În ciuda faptului că a trecut mult timp de la crearea acestui cod, acesta este utilizat eficient în echipamente de tip vechi, precum și în echipamente și carcase noi.

Principiul codificării eficiente

În centrul algoritmului Huffman se află o schemă care vă permite să înlocuiți cele mai probabile și mai comune caractere cu coduri sistem binar... Acele caractere care sunt mai puțin comune sunt înlocuite cu coduri lungi. Trecerea la coduri lungi Huffman se efectuează numai după ce sistemul utilizează toate valorile minime. Această tehnică face posibilă reducerea la minimum a lungimii codului pe caracter al mesajului original. V în acest caz particularitatea constă în faptul că probabilitățile de apariție a literelor la începutul codificării trebuie deja cunoscute. Mesajul final va fi compus din ele. Pe baza acestor informații, se construiește un arbore de codificare Huffman. Pe baza acestuia, se va desfășura procesul de codificare a literelor în arhivă.

Cod Huffman: exemplu

Pentru a ilustra algoritmul Huffman, luați în considerare o versiune grafică a construirii unui arbore de cod. A folosi aceasta metoda a fost mai eficient, este necesar să se clarifice definiția unora dintre valorile care sunt necesare pentru conceptul acestei metode. Întreaga colecție a unui set de noduri și arce care sunt direcționate de la nod la nod se numește grafic. Arborele în sine este un grafic cu un set de proprietăți specifice. Fiecare nod ar trebui să includă nu mai mult de unul dintre toate arcurile. Unul dintre noduri trebuie să fie rădăcina arborelui. Aceasta înseamnă că arcuri nu ar trebui să intre deloc în el. Dacă începeți de la rădăcina deplasării de-a lungul arcurilor, atunci acest proces ar trebui să vă permită să ajungeți la orice nod.

Codurile Huffman includ, de asemenea, un astfel de concept precum o frunză de copac. Reprezintă un nod din care nu ar trebui să iasă niciun arc. Dacă două noduri sunt conectate printr-un arc, atunci unul dintre ele este părinte, iar celălalt este copil. Dacă două noduri au un nod părinte comun, atunci ele se numesc noduri frați. Dacă, pe lângă frunze, nodurile au mai multe arce, atunci un astfel de arbore se numește binar. Acesta este exact ceea ce este arborele Huffman. O caracteristică a nodurilor acestei structuri este că greutatea fiecărui părinte este egală cu suma greutății copiilor nodali.

Arborele Huffman: algoritm de construcție

Construcția codului Huffman se realizează din literele alfabetului introdus. Se formează o listă de noduri care sunt libere în viitorul arbore de cod. În această listă, ponderea fiecărui nod ar trebui să fie aceeași cu probabilitatea de apariție a literei de mesaj care corespunde cu acest nod... Dintre mai multe noduri libere, este selectat cel care cântărește cel mai puțin. Dacă, în același timp, se observă indicatorii minimi în mai multe noduri, atunci puteți alege liber orice pereche. După aceea, este creat nodul părinte. Ar trebui să cântărească atât cât cântărește suma perechii date de noduri. Părintele este apoi trimis la lista cu noduri libere. Copiii sunt îndepărtați. În acest caz, arcurile primesc indicatorii corespunzători, zerouri și unu. Acest proces se repetă de câte ori este nevoie pentru a lăsa un singur nod. După aceea, cifrele binare sunt scrise de sus în jos.

Cum să îmbunătățiți eficiența compresiei

Pentru a crește eficiența compresiei, în timpul construcției arborelui de cod, este necesar să se utilizeze toate datele referitoare la probabilitatea apariției literelor într-un anumit fișier care este atașat arborelui. Ele nu ar trebui să fie împrăștiate pe un număr mare de documente text. Dacă treci prin acest fișier, apoi puteți obține statistici despre cât de des sunt găsite litere de la obiectul care urmează să fie comprimat.

Cum să accelerezi procesul de compresie

Pentru a accelera funcționarea algoritmului, identificarea literelor ar trebui să fie efectuată nu de indicatorii apariției anumitor litere, ci de frecvența apariției lor. Datorită acestui lucru, algoritmul devine mai simplu, iar lucrul cu acesta este semnificativ accelerat. De asemenea, face posibilă evitarea operațiunilor de divizare și virgulă mobilă. De asemenea, atunci când lucrați în acest mod, algoritmul nu poate fi schimbat. Acest lucru se datorează în principal faptului că probabilitățile sunt direct proporționale cu frecvențele. De asemenea, merită să acordați atenție faptului că greutatea finală a nodului rădăcină va fi egală cu suma numărului de litere din obiectul care urmează să fie procesat.

Concluzie

Codurile Huffman sunt un algoritm simplu și de lungă durată care este folosit și astăzi în mulți programe populare... Simplitatea și claritatea acestui cod vă permit să realizați compresie eficientă fișiere de orice dimensiune.

O metodă relativ simplă de comprimare a datelor poate fi realizată prin crearea așa-numiților arbori Huffman pentru un fișier și folosit pentru a-l comprima și a decomprima datele din acesta. Majoritatea aplicațiilor folosesc arbori Huffman binari (de exemplu, fiecare nod este fie o frunză, fie are exact două subnoduri). Este posibil, totuși, să construiți copaci Huffman cu un număr arbitrar subarbori (de exemplu, ternari sau, în caz general, N-ca copacii).

Arborele Huffman pentru fișierul care conține Z personaje diferite Are Z frunze. Calea de la rădăcină la frunză care reprezintă un anumit caracter determină codificarea, iar fiecare pas de-a lungul căii către frunză determină codarea (care poate fi 0 , 1 , ..., (N-1)). Prin plasarea caracterelor comune mai aproape de rădăcină și a caracterelor mai puțin comune mai departe de rădăcină, se obține compresia dorită. Strict vorbind, un astfel de arbore va fi un arbore Huffman numai dacă, ca urmare a codificării, numărul minim N caractere -ary pentru a codifica fișierul specificat.

În această problemă, vom lua în considerare doar arbori, în care fiecare nod este fie un nod intern, fie o frunză de codificare a caracterelor și nu există frunze izolate care să nu codifice un caracter.

Figura de mai jos prezintă un exemplu de arbore ternar Huffman, simboluri " A" și " e„codificat cu un singur caracter ternar; caractere mai puțin comune” s" și " p„sunt codificate folosind două caractere ternare și cele mai rare caractere” X", "q" și " y„sunt codificați folosind trei caractere ternare fiecare.

Desigur, dacă vrem să extindem lista N-are caractere apoi înapoi, este important să știți ce arbore este folosit pentru a comprima datele. Acest lucru se poate face în mai multe moduri. În această sarcină vom folosi următoarea metodă: fluxul de date de intrare va fi precedat de un antet format din valori de caractere codificate Z situat în fișier sursăîn ordine lexicografică.

Cunoașterea numărului de caractere introduse Z, sens N indicând " N-aritatea "arborelui Huffman și a antetului în sine, este necesar să se găsească valoarea primară a caracterelor codificate.

Date de intrare

Datele de intrare încep cu un număr întreg T, situat pe o linie separată și indicând numărul cazurilor de testare ulterioare. În continuare, fiecare dintre T cazuri de testare, fiecare dintre acestea fiind situat în 3 -lele rânduri, după cum urmează:

  • Numărul de caractere distincte în cazul de testare Z (2 Z20 );
  • Număr N indicând " N-aritatea arborelui Huffman ( 2 N10 );
  • Un șir reprezentând antetul mesajului primit, fiecare caracter va fi o cifră în interval ... Această linie va conține mai puțin 200 personaje.

Notă: Deși rar, este posibil ca un antet să aibă mai multe interpretări la decodificare (de exemplu, pentru un antet " 010011101100 ", și valori Z = 5și N = 2). Este garantat că în toate cazurile de testare propuse în datele de intrare, există o soluție unică.

Ieșire

Pentru fiecare dintre T ieșirea cazurilor de testare Z linii care oferă o versiune decodificată a fiecăruia dintre Z caractere în ordine crescătoare. Utilizați formatul original-> codificare, Unde original- aceasta numar decimalîn intervalul și șirul de cifre codificate corespunzător pentru acele caractere (fiecare cifră ≥ 0 și< N).

  1. cod = următorul bit din flux, lungime = 1
  2. În timp ce codul< base
    cod = cod<< 1
    cod = cod + următorul bit din flux
    lungime = lungime + 1
  3. simbol = simbol + cod - bază]

Cu alte cuvinte, vom împinge din stânga în variabila cod bit cu bit din fluxul de intrare, până la cod< base. При этом на каждой итерации будем увеличивать переменную length на 1 (т.е. продвигаться вниз по дереву). Цикл в (2) остановится когда мы определим длину кода (уровень в дереве, на котором находится искомый символ). Остается лишь определить какой именно символ на этом уровне нам нужен.

Să presupunem că bucla din (2), după mai multe iterații, se oprește. În acest caz, expresia (cod - bază) este numărul ordinal al nodului (caracterului) necesar la nivelul lungimii. Primul nod (simbol) la nivelul lungimii din arbore este situat în matricea de simboluri la indexul offs. Dar nu ne interesează primul caracter, ci caracterul de sub număr (cod - bază). Prin urmare, indexul simbolului dorit în matricea symb este (offs + (cod - bază)). Cu alte cuvinte, simbolul necesar este simbol = simbol + cod - bază].

Să dăm un exemplu concret. Folosind algoritmul descris, decodăm mesajul 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 1 0010 1 1 1 0010 1 0 1 1 0 1 0 1 0 1 0 1 1 0

  1. cod = 0, lungime = 1
  2. cod = 0< base = 1
    cod = 0<< 1 = 0
    cod = 0 + 0 = 0
    lungime = 1 + 1 = 2
    cod = 0< base = 2
    cod = 0<< 1 = 0
    cod = 0 + 0 = 0
    lungime = 2 + 1 = 3
    cod = 0< base = 2
    cod = 0<< 1 = 0
    cod = 0 + 1 = 1
    lungime = 3 + 1 = 4
    cod = 1 = baza = 1
  3. simbol = simbol = 2 + cod = 1 - bază = 1] = simbol = A
  1. cod = 1, lungime = 1
  2. cod = 1 = baza = 1
  3. simbol = simbol = 7 + cod = 1 - bază = 1] = simbol = H
  1. cod = 0, lungime = 1
  2. cod = 0< base = 1
    cod = 0<< 1 = 0
    cod = 0 + 0 = 0
    lungime = 1 + 1 = 2
    cod = 0< base = 2
    cod = 0<< 1 = 0
    cod = 0 + 0 = 0
    lungime = 2 + 1 = 3
    cod = 0< base = 2
    cod = 0<< 1 = 0
    cod = 0 + 0 = 0
    lungime = 3 + 1 = 4
    cod = 0< base = 1
    cod = 0<< 1 = 0
    cod = 0 + 1 = 1
    lungime = 4 + 1 = 5
    cod = 1> baza = 0
  3. simbol = simbol = 0 + cod = 1 - bază = 0] = simbol = F

Deci, am decodat primele 3 caractere: A, H, F... Este clar că urmând acest algoritm vom primi exact mesajul S.

Calcularea lungimii codului

Pentru a codifica un mesaj, trebuie să cunoaștem codurile caracterelor și lungimile acestora. După cum sa menționat în secțiunea anterioară, codurile canonice sunt bine definite prin lungimile lor. Astfel, sarcina noastră principală este să calculăm lungimile codurilor.

Se pare că această sarcină, în majoritatea covârșitoare a cazurilor, nu necesită construirea explicită a unui arbore Huffman. Mai mult, algoritmii care folosesc reprezentarea internă (implicita) a arborelui Huffman sunt mult mai eficienți în ceea ce privește viteza și consumul de memorie.

Astăzi există mulți algoritmi eficienți pentru calcularea lungimii codurilor (,). Ne vom restrânge să luăm în considerare doar una dintre ele. Acest algoritm este destul de simplu, dar, în ciuda acestui fapt, este foarte popular. Este folosit în programe precum zip, gzip, pkzip, bzip2 și multe altele.

Să revenim la algoritmul de construcție a arborelui Huffman. La fiecare iterație, am efectuat o căutare liniară pentru cele două noduri cu cea mai mică pondere. În mod clar, o coadă cu prioritate, cum ar fi o piramidă (minim), este mai potrivită în acest scop. Nodul cu cea mai mică greutate va avea cea mai mare prioritate și se va afla în vârful piramidei. Să dăm acest algoritm.

    Să includem toate caracterele codificate în piramidă.

    Să extragem secvenţial 2 noduri din piramidă (acestea vor fi două noduri cu cea mai mică greutate).

    Să formăm un nou nod și să-i atașăm, ca copii, două noduri luate din piramidă. În acest caz, greutatea nodului format este setată egală cu suma greutăților nodurilor copil.

    Să includem nodul format în piramidă.

    Dacă există mai mult de un nod în piramidă, repetați 2-5.

Vom presupune că un pointer către părintele său este stocat pentru fiecare nod. Setăm acest indicator egal cu NULL la rădăcina arborelui. Acum haideți să selectăm nodul frunză (simbol) și urmând pointerii salvati vom urca în arbore până când următorul pointer devine NULL. Ultima condiție înseamnă că am ajuns la rădăcina copacului. Este clar că numărul de tranziții de la nivel la nivel este egal cu adâncimea nodului frunză (simbol) și, prin urmare, cu lungimea codului său. Ocolind în acest fel toate nodurile (simbolurile), obținem lungimile codurilor lor.

Lungimea maximă a codului

De regulă, așa-numitul cartea de coduri, o structură de date simplă, în esență două tablouri: unul cu lungimi, celălalt cu coduri. Cu alte cuvinte, codul (ca șir de biți) este stocat într-o locație de memorie sau într-un registru de dimensiune fixă ​​(de obicei 16, 32 sau 64). Pentru a evita debordarea, trebuie să fim siguri că codul se va încadra în registru.

Se pare că pe un alfabet cu N caractere, dimensiunea maximă a codului poate fi de până la (N-1) biți în lungime. Cu alte cuvinte, pentru N = 256 (o variantă comună) putem obține un cod de 255 de biți lungime (deși pentru aceasta fișierul trebuie să fie foarte mare: 2.292654130570773 * 10 ^ 53 ~ = 2 ^ 177.259)! Este clar că un astfel de cod nu se va încadra în registru și trebuie să faci ceva cu el.

Mai întâi, să aflăm în ce condiții are loc revărsarea. Fie frecvența simbolului i-lea să fie egală cu al-lea număr Fibonacci. De exemplu: A-1, B-1, C-2, D-3, E-5, F-8, G-13, H-21. Să construim arborele Huffman corespunzător.

ROOT / \ / \ / \ / \ H / \ / \ /\ G / \ / \ /\ F / \ / \ /\ E / \ / \ /\ D / \ / \ /\ C / \ / \ A B

Un astfel de copac se numește degenerat... Pentru a-l obține, frecvențele simbolurilor trebuie să crească cel puțin ca numere Fibonacci sau chiar mai repede. Deși în practică, pe date reale, un astfel de arbore este aproape imposibil de obținut, este foarte ușor să îl generezi artificial. În orice caz, acest pericol trebuie luat în considerare.

Această problemă poate fi rezolvată în două moduri acceptabile. Primul se bazează pe una dintre proprietățile codurilor canonice. Ideea este că în codul canonic (șir de biți) cel puțin biții semnificativi pot fi non-zero. Cu alte cuvinte, este posibil ca toți ceilalți biți să nu fie salvați deloc, deoarece sunt întotdeauna zero. În cazul lui N = 256, este suficient să salvăm doar cei 8 biți mai puțin semnificativi din fiecare cod, presupunând că toți ceilalți biți sunt egali cu zero. Acest lucru rezolvă problema, dar numai parțial. Acest lucru va complica foarte mult și va încetini atât codarea, cât și decodarea. Prin urmare, această metodă este rar folosită în practică.

A doua modalitate este de a limita artificial lungimile codurilor (fie în timpul construcției, fie după). Această metodă este în general acceptată, așa că ne vom opri mai detaliat asupra ei.

Există două tipuri de algoritmi de cod de limitare a lungimii. Euristică (aproximativă) și optimă. Algoritmii de al doilea tip sunt destul de complexi în implementare și, de regulă, necesită mai mult timp și memorie decât primii. Eficacitatea codului constrâns euristic este determinată de abaterea sa de la codul constrâns optim. Cu cât diferența este mai mică, cu atât mai bine. Este de remarcat faptul că pentru unii algoritmi euristici această diferență este foarte mică (,,), în plus, ei generează foarte des cod optim (deși nu garantează că așa va fi întotdeauna). Mai mult, din moment ce în practică, depășirea apare extrem de rar (cu excepția cazului în care este stabilită o restricție foarte strictă asupra lungimii maxime a codului); cu o dimensiune alfabetică mică, este mai oportun să se utilizeze metode euristice simple și rapide.

Vom lua în considerare un algoritm euristic destul de simplu și foarte popular. Și-a găsit drumul în programe precum zip, gzip, pkzip, bzip2 și multe altele.

Problema limitării lungimii maxime a codului este echivalentă cu problema limitării înălțimii arborelui Huffman. Rețineți că, prin construcție, orice nod fără frunză al arborelui Huffman are exact doi descendenți. La fiecare iterație a algoritmului nostru, vom scădea înălțimea arborelui cu 1. Deci, fie L lungimea maximă a codului (înălțimea arborelui) și este necesar să o limităm la L / & lt L. Fie mai departe RN i cea mai dreaptă nodul frunză la nivelul i, iar LN i - cel mai din stânga.

Să începem de la nivelul L. Mută ​​nodul RN L la locul părintelui său. pentru că nodurile merg în perechi, trebuie să găsim un loc pentru un nod adiacent RN L. Pentru a face acest lucru, găsiți nivelul j cel mai apropiat de L, care conține noduri de frunze, astfel încât j & lt (L-1). În locul lui LN j, formăm un nod fără foaie și îi atașăm ca copii nodul LN j și nodul neîmperecheat de la nivelul L. Aplicam aceeași operație tuturor perechilor de noduri rămase la nivelul L. Este clar că redistribuind nodurile în acest fel, am redus înălțimea arborelui nostru cu 1. Acum este egal cu (L-1). Dacă acum L / & lt (L-1), atunci vom face același lucru cu nivelul (L-1), etc. până la atingerea limitei cerute.

Să revenim la exemplul nostru, unde L = 5. Să limităm lungimea maximă a codului la L / = 4.

ROOT / \ / \ / \ / \ H C E / \ / \ / \ / \ /\ A D G / \ / \ B F

Se vede ca in cazul nostru RN L = F, j = 3, LN j = C... Mai întâi, mutați nodul RN L = Fîn locul părintelui lor.

ROOT / \ / \ / \ / \ H / \ / \ / \ / \ / \ / \ /\ /\ / \ / \ / \ / \ / \ / \ / \ / \ /\ /\ C E / \ / \ / \ / \ F A D G B(nod neîmperecheat)

Acum, în locul lui LN j = C Să formăm un nod fără frunză.

ROOT / \ / \ / \ / \ H E / \ / \ / \ / \ / \ / \ F A D G ? ? B(nod neîmperecheat) C(nod neîmperecheat)

Să atașăm două nepereche la nodul format: Bși C.

ROOT / \ / \ / \ / \ H / \ / \ / \ / \ / \ / \ / \ / \ / \ /\ /\ / \ / \ / \ / \ / \ / \ / \ / \ /\ /\ /\ E / \ / \ / \ / \ / \ / \ F A D G B C

Astfel, am limitat lungimea maximă a codului la 4. Este clar că prin modificarea lungimii codului am pierdut puțin în eficiență. Deci mesajul S, codificat cu un astfel de cod, va avea o dimensiune de 92 de biți, adică. Încă 3 biți peste Redundanța Minimă.

Este clar că cu cât limităm mai mult lungimea maximă a codului, cu atât codul va fi mai puțin eficient. Să aflăm cât de mult poți limita lungimea maximă a codului. Evident, nu mai scurt decât puțin.

Calculul Codurilor Canonice

După cum am observat deja de multe ori, lungimile codurilor sunt suficiente pentru a genera codurile în sine. Vă vom arăta cum se poate face acest lucru. Să presupunem că am calculat deja lungimile codurilor și am numărat câte coduri din fiecare lungime avem. Fie L lungimea maximă a codului și T i numărul de coduri de lungime i.

Se calculează S i - valoarea inițială a codului lungimii i, pentru tot i din

S L = 0 (întotdeauna)
S L-1 = (S L + T L) >> 1
S L-2 = (S L-1 + T L-1) >> 1
...
S 1 = 1 (întotdeauna)

Pentru exemplul nostru, 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

Se poate observa că S 5, S 4, S 3, S 1 sunt exact codurile de caractere B, A, C, H... Aceste simboluri sunt unite de faptul că toate vin pe primul loc, fiecare la nivelul său. Cu alte cuvinte, am găsit valoarea inițială a codului pentru fiecare lungime (sau nivel).

Acum să atribuim coduri restului simbolurilor. Codul primului caracter la nivelul i este S i, al doilea S i + 1, al treilea S i + 2 etc.

Să scriem codurile rămase pentru exemplul nostru:

B= S 5 = 00000 bin A= S 4 = 0001 bin C= S 3 = 010 bin H= S 1 = 1 bin
F= S 5 + 1 = 00001 bin D= S 4 + 1 = 0010 bin E= S 3 + 1 = 011 bin
G= S 4 + 2 = 0011 bin

Se poate observa că am primit exact aceleași coduri ca și cum am fi construit în mod explicit arborele canonic Huffman.

Trecerea unui arbore de cod

Pentru ca mesajul codificat să fie decodat, decodorul trebuie să aibă același arbore de cod (într-o formă sau alta) care a fost folosit pentru codificare. Prin urmare, împreună cu datele codificate, suntem forțați să salvăm arborele de cod corespunzător. Este clar că cu cât este mai compact, cu atât mai bine.

Există mai multe modalități de a rezolva această problemă. Cea mai evidentă soluție este să stocați arborele în mod explicit (adică ca un set ordonat de noduri și pointeri de un fel sau altul). Acesta este poate cel mai risipitor și ineficient mod. În practică, nu este folosit.

Poate fi salvată o listă de frecvențe simbol (adică dicționar de frecvență). Cu ajutorul său, decodorul poate reconstrui cu ușurință arborele de cod. Deși această metodă este mai puțin risipitoare decât cea anterioară, nu este cea mai bună.

În cele din urmă, poate fi folosită una dintre proprietățile codurilor canonice. După cum sa menționat mai devreme, codurile canonice sunt complet determinate de lungimile lor. Cu alte cuvinte, tot ce are nevoie decodorul este o listă de lungimi de cod de caractere. Având în vedere că, în medie, lungimea unui cod pentru un alfabet cu N caractere poate fi codificată în [(log 2 (log 2 N))] biți, obținem un algoritm foarte eficient. Ne vom opri asupra ei mai detaliat.

Să presupunem că dimensiunea alfabetului este N = 256 și comprimăm un fișier text simplu (ASCII). Cel mai probabil nu vom găsi toate N caracterele alfabetului nostru într-un astfel de fișier. Să setăm apoi lungimea codului caracterelor lipsă egală cu zero. În acest caz, lista salvată de lungimi de cod va conține un număr suficient de mare de zerouri (lungimi de cod ale caracterelor lipsă) grupate împreună. Fiecare astfel de grup poate fi comprimat folosind așa-numita codificare de grup - RLE (Run - Length - Encoding). Acest algoritm este extrem de simplu. În loc de o secvență de M elemente identice într-un rând, vom salva primul element din această secvență și numărul repetărilor sale, adică. (M-1). Exemplu: RLE ("AAAABBCDDDDDDD") = A3 B2 C0 D6.

Mai mult, această metodă poate fi oarecum extinsă. Putem aplica algoritmul RLE nu numai grupurilor de lungimi zero, ci și tuturor celorlalți. Acest mod de a trece un arbore de cod este comun și este folosit în majoritatea implementărilor moderne.

Implementare: SHCODEC

Anexă: biografia lui D. Huffman

David Huffman s-a născut în 1925 în Ohio, SUA. Huffman și-a luat licența în Inginerie Electrică de la Ohio State University la vârsta de 18 ani. Apoi a servit în armată ca ofițer de sprijin radar pe un distrugător care a ajutat la dezamorsarea minelor în apele japoneze și chineze după al Doilea Război Mondial. Ulterior, a primit un master de la Universitatea Ohio și un doctorat de la Massachusetts Institute of Technology (MIT). Deși Huffman este cel mai bine cunoscut pentru dezvoltarea unei metode de construire a codurilor minim redundante, el a adus, de asemenea, contribuții importante în multe alte domenii (mai ales în electronică). A fost de mult timp șef al Departamentului de Informatică de la MIT. În 1974, deja profesor emerit, a demisionat. Huffman a primit o serie de premii valoroase. 1999 - Medalia Richard W. Hamming de la Institutul de Ingineri Electrici și Electronici (IEEE) pentru contribuții excepționale la teoria informației, Medalia Louis E. Levy de la Institutul Franklin pentru teza sa de doctorat privind circuitele secvențiale, Premiul W. Wallace McDowell, IEEE Computer Society Award, IEEE Gold Jubilee Technology Innovation Award în 1998. În octombrie 1999, la vârsta de 74 de ani, David Huffman a murit de cancer.

Codare Huffman. Partea 1.
Introducere

Bună dragă cititor! Acest articol va discuta una dintre modalitățile de comprimare a datelor. Această metodă este destul de răspândită și merită o anumită atenție. Acest material calculat în volum pentru trei articole, primul dintre care va fi dedicat algoritmului de compresie, al doilea - implementare software algoritm, iar al treilea este decompresia. Algoritmul de compresie va fi scris în C++, algoritmul de decompresie în Assembler.
Cu toate acestea, înainte de a continua cu algoritmul în sine, o mică teorie ar trebui inclusă în articol.
Un pic de teorie
Comprimare (comprimare) - o modalitate de a reduce cantitatea de date în scopul transmiterii și stocării lor ulterioare.
Decompresie Este o modalitate de a restabili datele comprimate la datele originale.
Comprimarea și decompresia pot fi fie fără pierdere de calitate (când informația transmisă/ stocată în formă comprimată după decompresie este absolut identică cu cea originală), fie cu pierdere de calitate (când datele de după decompresie diferă de original). De exemplu, documente text, bazele de date, programele pot fi comprimate doar într-un mod fără pierderi de calitate, în timp ce imaginile, videoclipurile și fișierele audio sunt comprimate tocmai din cauza pierderii calității datelor originale (un exemplu tipic de algoritmi - JPEG, MPEG, ADPCM), cu uneori o pierdere imperceptibilă a calității chiar și la compresie 1:4 sau 1:10.
Se disting principalele tipuri de ambalaje:
  • Ambalare zecimală este destinat pentru împachetarea datelor de caractere care constau numai din numere. În loc să folosiți 8 biți pentru un caracter, este destul de rațional să folosiți doar 4 biți pentru cifre zecimale și hexazecimale, 3 biți pentru octal și așa mai departe. Cu această abordare, se simte deja o compresie de cel puțin 1: 2.
  • Codificare relativă este o codare cu pierderi. Se bazează pe faptul că elementul de date ulterior diferă de cel anterior printr-o cantitate care ocupă mai puțin spațiu în memorie decât elementul în sine. Un exemplu tipic este compresia audio ADPCM (Adaptive Differential Pulse Code Modulation), utilizată pe scară largă în telefonie digitalăși vă permite să comprimați datele audio într-un raport de 1: 2 cu o pierdere aproape imperceptibilă a calității.
  • Suprimarea simbolică- o metodă de comprimare a informațiilor, în care secvențele lungi de date identice sunt înlocuite cu altele mai scurte.
  • Codificare statistică pe baza faptului că nu toate elementele de date se întâlnesc cu aceeasi frecventa(sau probabilitate). Cu această abordare, codurile sunt alese astfel încât elementul cel mai frecvent să corespundă codului cu lungimea cea mai scurtă, iar cel mai puțin frecvent - cu cea mai mare.
În plus, codurile sunt selectate în așa fel încât în ​​timpul decodării a fost posibil să se determine fără ambiguitate elementul datelor originale. Cu această abordare, este posibilă doar codarea orientată pe biți, în care se disting codurile permise și cele interzise. Dacă, la decodarea unei secvențe de biți, codul s-a dovedit a fi interzis, atunci trebuie adăugat încă un bit din secvența originală și operația de decodare trebuie repetată. Exemple de astfel de codificare sunt algoritmii Shannon și Huffman, pe cei din urmă pe care îi vom lua în considerare.
Mai precis despre algoritm
După cum se știe deja din subsecțiunea anterioară, algoritmul lui Huffman se bazează pe codificare statistică... Să aruncăm o privire mai atentă asupra implementării sale.
Să existe o sursă de date care transferă caractere (a_1, a_2, ..., a_n) cu grade diferite de probabilitate, adică fiecare a_i are propria sa probabilitate (sau frecvență) P_i (a_i), și există cel puțin o pereche a_i și a_j, i \ ne j, astfel încât P_i (a_i) și P_j (a_j) ) nu sunt sunt egale. Astfel, se formează un set de frecvențe (P_1 (a_1), P_2 (a_2), ..., P_n (a_n)), ce legătură are \ stil de afișare \ sum_ (i = 1) ^ (n) P_i (a_i) = 1, deoarece transmițătorul nu transmite alte caractere decât (a_1, a_2, ..., a_n).
Sarcina noastră este să găsim așa ceva caractere de cod (b_1, b_2, ..., b_n) cu lungimi (L_1 (b_1), L_2 (b_2), ..., L_n (b_n)) astfel încât lungimea medie a simbolului codului să nu depășească lungimea medie a simbolului original. În acest caz, este necesar să se țină cont de condiția ca dacă P_i (a_i)> P_j (a_j)și i \ ne j, atunci L_i (b_i) \ le L_j (b_j).
Huffman a propus să construiască un copac în care nodurile sunt cel mai probabil cel mai puțin îndepărtate de rădăcină. Prin urmare, însăși metoda de construire a unui arbore urmează:
1. Selectați două simboluri a_i și a_j, i \ ne j, astfel încât P_i (a_i) și P_j (a_j) din întreaga listă (P_1 (a_1), P_2, ..., P_n (a_n)) sunt minime.
2. Reduceți ramurile copacilor de la aceste două elemente la un punct cu probabilitate P = P_i (a_i) + P_j (a_j) prin marcarea unei ramuri cu zero și cealaltă cu una (la discreție).
3. Repetați punctul 1 ținând cont punct nouîn loc de a_i și a_j, dacă numărul punctelor rezultate este mai mare decât unu. Altfel, am ajuns la rădăcina copacului.
Acum să încercăm să folosim teoria obținută și să codificăm informațiile transmise de sursă, folosind exemplul de șapte caractere.
Să aruncăm o privire mai atentă la primul ciclu. Figura prezintă un tabel în care fiecare simbol a_i are propria probabilitate (frecvență) P_i (a_i). Conform punctului 1, selectăm două simboluri din tabel cu cea mai mică probabilitate. În cazul nostru, acestea sunt a_1 și a_4. Conform punctului 2, reducem ramurile copacilor de la a_1 și a_4 la un punct și notăm cu unu ramura care duce la a_1, iar ramura care duce la a_4 cu zero. Deasupra noului punct, îi atribuim probabilitatea (în acest caz - 0,03) V acțiune ulterioară se repetă ținând cont deja de punctul nou și fără a ține cont de a_1 și a_4.

După repetarea repetată a acțiunilor de mai sus, se construiește următorul arbore:

Prin arborele construit, puteți determina valoarea codurilor (b_1, b_2, ..., b_n), coborând de la rădăcină la elementul corespunzător a_i, în timp ce se atribuie zero sau unu secvenței rezultate la trecerea fiecărei ramuri (în funcție de modul în care este numită ramura particulară). Astfel, tabelul de coduri arată astfel:

ib iL i (b i) 1 011111 62 1 13 0110 44 011110 65 010 36 00 27 01110 5

Acum să încercăm să codificăm o secvență de caractere.
Fie simbolul a_i să corespundă (de exemplu) numărului i. Să existe o secvență 12672262. Trebuie să obțineți codul binar rezultat.
Pentru codificare, puteți utiliza tabelul de simboluri de cod deja existent b_i, ținând cont că b_i corespunde simbolului a_i. În acest caz, codul pentru cifra 1 va fi secvența 011111, pentru cifra 2 - 1 și pentru cifra 6 - 00. Astfel, obținem următorul rezultat:

Data12672262 Lungime cod Nativ 001010110111010010110 01024 biți Cod 011111100011101100119 biți

Ca rezultat al codificării, am câștigat 5 biți și am scris secvența în 19 biți în loc de 24.
Cu toate acestea, aceasta nu oferă o estimare completă a compresiei datelor. Să ne întoarcem la matematică și să estimăm raportul de compresie al codului. Aceasta necesită o estimare a entropiei.
Entropie- o măsură a incertitudinii unei situații (variabilă aleatoare) cu un număr finit sau par de rezultate. Matematic, entropia este formulată ca suma produselor probabilităților diferitelor stări ale sistemului prin logaritmii acestor probabilități, luate cu semnul opus:

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

Unde X este discret valoare aleatorie(în cazul nostru, un caracter de cod) și d este o rază arbitrară mai mare decât unu. Alegerea bazei echivalează cu alegerea unei anumite unități de măsură a entropiei. Din moment ce avem de-a face cu cifre binare, atunci este rațional să alegeți d = 2 ca bază.
Astfel, entropia pentru cazul nostru poate fi reprezentată ca:

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

Entropia are o proprietate remarcabilă: este egală cu lungimea medie minimă admisă a unui simbol de cod \ overline (L_ (min))în biți. Aceeași lungime medie a simbolului codului este calculată prin formulă

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

Înlocuind valorile în formulele H (b) și \ overline (L (b)), obținem următorul rezultat: H (b) = 2,048, \ overline (L (b)) = 2.100.
Valorile lui H (b) și \ overline (L (b)) sunt foarte apropiate, ceea ce indică un câștig real în alegerea algoritmului. Acum să comparăm lungimea medie a simbolului original și lungimea medie a simbolului codului prin raport:

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

Astfel, am obținut un raport de compresie de 1: 1.429, ceea ce este foarte bun.
Și, în sfârșit, să rezolvăm ultima problemă: decriptarea secvenței de biți.
Să existe o secvență de biți pentru situația noastră:

001101100001110001000111111​

Este necesar să se definească sursă, adică decodați această secvență.
Desigur, într-o astfel de situație, puteți utiliza tabelul de coduri, dar acest lucru este destul de incomod, deoarece lungimea simbolurilor codului nu este constantă. Este mult mai convenabil să coborâți copacul (începând de la rădăcină) conform următoarei reguli:
1. Punctul de plecare este rădăcina arborelui.
2. Citiți noua bataie... Dacă este zero, mergeți de-a lungul ramurii marcate cu zero, în caz contrar - cu unul.
3. Dacă punctul în care lovim este cel final, atunci am determinat caracterul codului, care trebuie notat și revenit la punctul 1. În caz contrar, punctul 2 ar trebui repetat.
Să luăm în considerare un exemplu de decodare a primului simbol. Ne aflăm într-un punct cu o probabilitate de 1,00 (rădăcina arborelui), citim primul bit al secvenței și mergem de-a lungul ramului marcat cu zero până la un punct cu probabilitatea de 0,60. Deoarece acest punct nu este punctul final din arbore, citim următorul bit, care este tot zero, și mergem de-a lungul ramurilor marcate cu zero până la punctul a_6, care este cel final. Am decodat simbolul - acesta este numărul 6. Îl notăm și revenim la starea initiala(mutați la rădăcină).
Astfel, secvența decodificată ia forma.

Date

001101100001110001000111111 Lungime cod Cod 00110110000111000100011111127 biți Original 6223676261233 biți

În acest caz, câștigul a fost de 6 biți cu o lungime a secvenței destul de scurtă.
Concluzia sugerează de la sine: algoritmul este simplu. Cu toate acestea, trebuie făcută o notă: acest algoritm bun pentru compresie informații text(într-adevăr, folosim aproximativ 60 de caractere din cele 256 disponibile la tastare, adică probabilitatea de a întâlni alte caractere este aproape de zero), dar este suficient de rău pentru comprimarea programelor (deoarece toate caracterele din program sunt aproape la fel de probabile). ). Deci eficiența algoritmului depinde foarte mult de tipul de date care sunt comprimate.
P.S
În acest articol, am examinat algoritmul de codare Huffman, care se bazează pe codificare neuniformă... Vă permite să reduceți dimensiunea datelor transmise sau stocate. Algoritmul este ușor de înțeles și poate oferi câștiguri reale. În plus, are încă o proprietate remarcabilă: capacitatea de a codifica și decoda informații din mers, cu condiția ca probabilitățile cuvintelor de cod să fie corect determinate. Deși există o modificare a algoritmului care vă permite să schimbați structura arborelui în timp real.
În următoarea parte a articolului, ne vom uita la compresia fișierelor orientată pe octeți folosind algoritmul Huffman, implementat în C++.
Codare Huffman. Partea 2
Introducere
În ultima parte, am examinat algoritmul de codare, l-am descris model matematic, a efectuat codificarea și decodarea pe exemplu concret, a calculat lungimea medie cuvânt codși, de asemenea, a determinat raportul de compresie. În plus, s-au tras concluzii despre avantajele și dezavantajele acestui algoritm.
Cu toate acestea, pe lângă aceasta, încă două întrebări au rămas nerezolvate: implementarea programului care comprimă fișierul de date și programul care despachetează fișier comprimat... Acest articol este dedicat primei întrebări. Prin urmare, ar trebui să începeți să proiectați.
Proiecta
Primul pas este de a calcula frecvența de apariție a caracterelor în fișier. Pentru a face acest lucru, descriem următoarea structură:

    // Structură pentru calcularea frecvenței caracterului

    typedef struct TFreq

    int ch;

    TTtable * tabel;

    frecvență DWORD;

    ) TFreq;

Această structură va descrie fiecare caracter din 256. cap- caracterul ASCII în sine, frecvență- numărul de apariții ale simbolului în fișier. Camp masa- indicator spre structura:

    // descriptor de nod

    typedef struct TTable

    int ch;

    TTtable * stânga;

    TTtable * dreapta;

    ) TTtable;

Așa cum se vede, Tabelul Este un descriptor de nod care se bifurcă în zero și unu. Cu ajutorul acestor structuri se va realiza pe viitor constructia arborelui de compresie. Acum să declarăm frecvența noastră și nodul nostru pentru fiecare simbol:

    TFreq Freq [256];

Să încercăm să ne dăm seama cum va fi construit copacul. În etapa inițială, programul trebuie să parcurgă întregul fișier și să numere numărul de apariții ale caracterelor din acesta. În plus, programul trebuie să creeze un descriptor de nod pentru fiecare simbol pe care îl găsește. După aceea, din nodurile create, ținând cont de frecvența simbolurilor, programul construiește un arbore, plasând nodurile într-o anumită ordine și stabilind legături între ele.
Arborele construit este bun pentru decodarea fișierului. Dar pentru codificarea unui fișier, este incomod, deoarece nu se știe în ce direcție să mergi de la rădăcină pentru a ajunge la caracterul necesar. Pentru aceasta, este mai convenabil să construiți un tabel de conversie de la caracter la cod. Prin urmare, vom defini încă o structură:

    // Descriptor de caractere de cod

    typedef struct TOpcode

    cod operațional DWORD;

    DWORD len;

    ) TOPcode;

Aici opcode Este combinația de cod a caracterului și len- lungimea acestuia (în biți). Și să declarăm un tabel cu 256 de astfel de structuri:

    TOPcode Opcodes [256];

Cunoscând caracterul care trebuie codificat, puteți determina cuvântul de cod al acestuia din tabel. Acum să trecem direct la calcularea frecvenței simbolurilor (și nu numai).
Numărarea frecvențelor simbolului
În principiu, această acțiune nu este dificilă. Este suficient să deschideți fișierul și să numărați numărul de caractere din acesta, completând structurile corespunzătoare. Să vedem implementarea acestei acțiuni.
Pentru a face acest lucru, vom declara descriptori de fișiere globale:

    DOSAR * in, * out, * asamblare;

în- fisierul din care se citesc datele necomprimate.
afară- fișierul în care sunt scrise datele comprimate.
asamblare- un fișier în care arborele va fi salvat într-o formă convenabilă pentru despachetare. Deoarece unpacker-ul va fi scris în assembler, este destul de rațional să faceți arborele parte din unpacker, adică să-l reprezentăm sub formă de instrucțiuni în Assembler.
Primul pas este inițializarea tuturor structurilor valori zero:

    // Numărarea frecvenței simbolurilor

    int CountFrequency (void)

    int i; // variabilă buclă

    număr int = 0; // a doua variabilă a buclei

    DWORD TotalCount = 0; // mărime fișier.

    // Inițializați structurile

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

    Frecventa [i] .frecventa = 0;

    Frecventa [i] .tabel = 0;

    Frecventa [i] .ch = i;

După aceea, numărăm numărul de apariții ale simbolului în fișier și dimensiunea fișierului (desigur, nu în cel mai ideal mod, dar exemplul are nevoie de claritate):

    // Numărarea frecvenței simbolurilor (simbolic)

    în timp ce (! feof (în)) // până când se ajunge la sfârșitul fișierului

    i = fgetc (in);

    dacă (i! = EOF) // dacă nu sfârșitul fișierului

    Frecventa [i] .frecventa ++; // frecvența ++

    TotalCount ++; // dimensiune ++

Deoarece codul este neuniform, va fi dificil pentru dezambalător să afle numărul de caractere care trebuie citite. Prin urmare, este necesar să-i fixați dimensiunea în tabelul de despachetare:

    // „Spune” dispozitivului de despachetare dimensiunea fișierului

    fprintf (asamblare, "cod_file_size: \ n dd% 8lxh \ n \ n ", Numărătoare totală);

După aceea, toate caracterele folosite sunt mutate la începutul matricei, iar cele neutilizate sunt suprascrise (prin permutări).

    // mută toate caracterele nefolosite până la sfârșit

    i = 0;

    număr = 256;

    in timp ce eu< count) // încă nu am ajuns la final

    if (Freq [i] .freq == 0) // dacă frecvența este 0

    Frecventa [i] = Frecventa [- count]; // apoi copiați intrarea de la sfârșit

    altfel

    i ++; // totul este OK - mergi mai departe.

Și numai după o astfel de „sortare” este alocată memorie pentru noduri (pentru o anumită economie).

    // Alocați memorie pentru noduri

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

    Freq [i] .table = nou TTtable; // creează un nod

    Frecventa [i] .table -> stânga = 0; // fără conexiuni

    Frecventa [i] .tabel -> dreapta = 0; // fără conexiuni

    Freq [i] .table -> ch = Freq.ch; // copiază simbolul

    Frecventa [i] .freq = Freq.freq; // și frecvența

    număr de returnări;

Astfel, am scris o funcție pentru inițializarea inițială a sistemului sau, dacă te uiți la algoritmul din prima parte a articolului, „am notat simbolurile folosite într-o coloană și le-am atribuit probabilități”, precum și pentru fiecare simbolul a creat un „punct de plecare” - un nod - și l-a inițializat... În câmpuri stângași dreapta a notat zerouri. Astfel, dacă nodul este ultimul din arbore, atunci va fi ușor de văzut stângași dreapta egal cu zero.
Crearea arborilor
Deci, în secțiunea anterioară, am „notat simbolurile folosite într-o coloană și le-am atribuit probabilități”. De fapt, nu le-am atribuit probabilități, ci numărătorii fracției (adică numărul de apariții ale caracterelor din fișier). Acum trebuie să construim un copac. Dar pentru a face acest lucru, trebuie să găsiți element minim pe listă. Pentru a face acest lucru, introducem o functie in care trecem doi parametri - numarul de elemente din lista si elementul de exclus (pentru ca vom cauta in perechi, si va fi foarte neplacut daca primim acelasi element de doua ori de la functia):

    // găsiți nodul cu cea mai mică probabilitate.

    int FindLeast (număr int, index int)

    int i;

    DWORD min = (index == 0)? 10; // elementul de numărat

    // minim

    pentru (i = 1; i< count; i++ ) // buclă prin matrice

    dacă (i! = index) // dacă elementul nu este exclus

    dacă (Frecv [i] .frec< Freq[ min] .freq ) // сравниваем

    min = i; // mai puțin decât minimul - amintiți-vă

    retur min; // returnează indexul minim

Căutarea nu este dificilă: în primul rând, selectăm elementul „minim” al matricei. Dacă elementul exclus este 0, atunci luăm primul element ca minim, în caz contrar considerăm zero ca minim. Pe măsură ce parcurgem matricea, comparăm elementul curent cu cel „minim”, iar dacă se dovedește a fi mai puțin, atunci îl marchem ca fiind minim.
Acum, de fapt, construcția copacului funcționează în sine:

    // Funcție pentru construirea unui arbore

    void PreInit (număr int)

    int ind1, ind2; // indici de elemente

    TTtable * tabel; // pointer către „nod nou”

    în timp ce (număr > 1) // buclă până ajungem la rădăcină

    ind1 = FindLeast (număr, - 1); // primul nod

    ind2 = FindLeast (număr, ind1); // al doilea nod

    tabel = nou TTtable; // creează un nou nod

    table-> ch = - 1; // nu este definitivă

    table-> left = Freq [ind1] .table; // 0 - nodul 1

    table-> dreapta = Freq [ind2] .table; // 1 - nodul 2

    Frecventa [ind1] .ch = - 1; // modifică înregistrarea despre

    Frecventa [ind1] .frecventa + = Frecventa [ind2] .frecventa; // frecvență pentru simbol

    Frecventa [ind1] .table = tabel; // și scrie un nou nod

    dacă (ind2! = (- număr)) // dacă ind2 nu este ultimul

    Frecvență [ind2] = Frecvență [număr]; // apoi la locul lui

    // pune ultimul în tablou

Tabelul simbolurilor codurilor
Deci, am construit un arbore în memorie: am luat două noduri în perechi, am creat un nou nod, în care am scris pointeri către ele, după care al doilea nod a fost eliminat din listă și în locul primului nod am scris un nou nod. unul cu furculita.
Acum apare o altă problemă: este incomod să codificați într-un arbore, deoarece trebuie să știți exact pe ce cale se află un anumit simbol. Cu toate acestea, problema este rezolvată destul de simplu: este creat un alt tabel - un tabel de simboluri de cod - și combinațiile de biți ale tuturor simbolurilor utilizate sunt scrise în el. Pentru a face acest lucru, este suficient să traversați recursiv arborele o dată. În același timp, pentru a nu-l ocoli din nou, puteți adăuga generarea unui fișier de asamblare la funcția de ocolire pentru decodarea ulterioară a datelor comprimate (consultați secțiunea " Proiecta").
De fapt, funcția în sine nu este complicată. Ar trebui să atribuie 0 sau 1 cuvântului de cod dacă nodul nu este final, altfel adăugați caracterul codului în tabel. Pe lângă toate acestea, generați un fișier de asamblare. Luați în considerare această funcție:

    void RecurseMake (TTable * tbl, DWORD opcode, int len)

    fprintf (asamblare, "opcode% 08lx_% 04x: \ n ", opcode, len); // etichetă la fișier

    if (tbl-> ch! = - 1) // nod final

    BYTE mod = 32 - len;

    Opcodes [tbl-> ch] .opcode = (opcode >> mod); // salvează codul

    Opcodes [tbl-> ch] .len = len; // și lungimea sa (în biți)

    // și creează eticheta corespunzătoare

    fprintf (asamblare, „db% 03xh, 0ffh, 0ffh, 0ffh \ n \ n”, tbl-> ch);

    altfel // nodul nu este final

    opcode >> = 1; // eliberează spațiu pentru un nou bit

    len ++; // crește lungimea cuvântului de cod

    \ n ", opcode, len);

    fprintf (asamblare, "dw opcode% 08lx_% 04x \ n \ n ", opcode | 0x80000000, len);

    RecurseMake (tbl-> stânga, opcode, len);

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

    // eliminați nodul (nu mai este necesar)

    șterge tbl;

Printre altele, după parcurgerea unui nod, funcția îl șterge (eliberează indicatorul). Acum să ne dăm seama ce parametri sunt trecuți funcției.

  • tbl- nodul care trebuie ocolit.
  • opcode- cuvântul de cod curent. Cel mai semnificativ bit trebuie să fie întotdeauna gratuit.
  • len- lungimea cuvântului cod.
În principiu, funcția nu este mai complicată decât „factorialul clasic” și nu ar trebui să provoace dificultăți.
Ieșire de biți
Așa că am ajuns la partea nu foarte plăcută a arhivatorului nostru, și anume, la ieșirea simbolurilor de cod într-un fișier. Problema este că simbolurile codului sunt neuniforme în lungime și ieșirea trebuie făcută bit cu bit. Acest lucru va ajuta funcția PutCode... Dar mai întâi, să declarăm două variabile - contorul de biți din octet și octetul de ieșire:

    // Contor de biți

    int OutBits;

    // Caracter afișat

    BYTE OutChar;

OutBits este incrementat cu unu de fiecare dată când un bit este scos, OutBits == 8 semnalează că OutChar ar trebui să fie salvat într-un fișier.

    void PutCode (int ch)

    int len;

    int outcode;

    // obțineți lungimea cuvântului de cod și a cuvântului de cod în sine

    outcode = Opcodes [ch] .opcode; // cuvânt de cod

    len = Opcodes [ch] .len; // lungime (în biți)

    în timp ce (len> 0) // iesire bit cu bit

    // Buclă în timp ce variabila OutBits nu este utilizată pe deplin

    în timp ce ((OutBits< 8 ) && (len> 0 ) )

    OutChar >> = 1; // eliberează spațiu

    OutChar | = ((cod de ieșire și 1)<< 7 ) ; // și pune puțin în el

    outcode >> = 1; // următorul bit de cod

    len -- ; // scade lungimea

  1. OutBits ++ ; // numărul de biți a crescut

  2. }

  3. // dacă toți cei 8 biți sunt folosiți, salvați-i într-un fișier

  4. dacă ( OutBits == 8 )

  5. {

  6. fputc( OutChar, afară ) ; // salvează în fișier

  7. OutBits = 0 ; // setat la zero

  8. OutChar = 0 ; // parametrii

  9. }

  10. }

  11. }

În plus, trebuie să organizați ceva de genul „fflush”, adică după rezultatul ultimului cuvânt de cod OutChar nu se va potrivi în fișierul de ieșire deoarece OutBits! = 8. De aici vine o altă mică funcție:

  1. // „Ștergeți” tamponul de biți

  2. vid EndPut (vid)

  3. {

  4. // Dacă există biți în buffer

  5. dacă ( OutBits ! = 0 )

Top articole similare