Kako postaviti pametne telefone i računala. Informativni portal
  • Dom
  • Recenzije
  • Algoritam za konstruiranje Huffmanovog kodnog stabla. Huffmanov kod

Algoritam za konstruiranje Huffmanovog kodnog stabla. Huffmanov kod

  1. kod = sljedeći bit iz toka, duljina = 1
  2. Dok kod< base
    kod = kod<< 1
    kod = kod + sljedeći bit iz toka
    duljina = duljina + 1
  3. simbol = simbol + kod - baza]

Drugim riječima, gurat ćemo s lijeve strane u varijablu koda bit po bit od ulaznog toka, do koda< base. При этом на каждой итерации будем увеличивать переменную length на 1 (т.е. продвигаться вниз по дереву). Цикл в (2) остановится когда мы определим длину кода (уровень в дереве, на котором находится искомый символ). Остается лишь определить какой именно символ на этом уровне нам нужен.

Pretpostavimo da se petlja u (2), nakon nekoliko iteracija, zaustavlja. U ovom slučaju izraz (kod - baza) je redni broj traženog čvora (znaka) na razini duljine. Prvi čvor (simbol) na razini duljine u stablu nalazi se u nizu simbola na indeksu isključenja. No, ne zanima nas prvi znak, već znak ispod broja (šifra - baza). Stoga je indeks željenog simbola u nizu simbola (offs + (code - base)). Drugim riječima, traženi simbol je simbol = simbol + kod - baza].

Navedimo konkretan primjer. Koristeći opisani algoritam dekodiramo poruku 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 11 0 1 0 1 0 1 0 1

  1. kod = 0, duljina = 1
  2. kod = 0< base = 1
    kod = 0<< 1 = 0
    kod = 0 + 0 = 0
    duljina = 1 + 1 = 2
    kod = 0< base = 2
    kod = 0<< 1 = 0
    kod = 0 + 0 = 0
    duljina = 2 + 1 = 3
    kod = 0< base = 2
    kod = 0<< 1 = 0
    kod = 0 + 1 = 1
    duljina = 3 + 1 = 4
    kod = 1 = baza = 1
  3. simbol = symb = 2 + kod = 1 - baza = 1] = symb = A
  1. kod = 1, dužina = 1
  2. kod = 1 = baza = 1
  3. simbol = symb = 7 + kod = 1 - baza = 1] = symb = H
  1. kod = 0, duljina = 1
  2. kod = 0< base = 1
    kod = 0<< 1 = 0
    kod = 0 + 0 = 0
    duljina = 1 + 1 = 2
    kod = 0< base = 2
    kod = 0<< 1 = 0
    kod = 0 + 0 = 0
    duljina = 2 + 1 = 3
    kod = 0< base = 2
    kod = 0<< 1 = 0
    kod = 0 + 0 = 0
    duljina = 3 + 1 = 4
    kod = 0< base = 1
    kod = 0<< 1 = 0
    kod = 0 + 1 = 1
    duljina = 4 + 1 = 5
    kod = 1> baza = 0
  3. simbol = symb = 0 + kod = 1 - baza = 0] = symb = F

Dakle, dekodirali smo prva 3 znaka: A, H, F... Jasno je da ćemo slijedeći ovaj algoritam dobiti upravo poruku S.

Izračunavanje duljine koda

Da bismo kodirali poruku, moramo znati kodove znakova i njihove duljine. Kao što je navedeno u prethodnom odjeljku, kanonski kodovi dobro su definirani svojim duljinama. Stoga je naš glavni zadatak izračunati duljine kodova.

Ispada da ovaj zadatak, u ogromnoj većini slučajeva, ne zahtijeva eksplicitnu konstrukciju Huffmanovog stabla. Štoviše, algoritmi koji koriste interni (implicitni) prikaz Huffmanovog stabla mnogo su učinkovitiji u smislu brzine i potrošnje memorije.

Danas postoji mnogo učinkovitih algoritama za izračun duljine kodova (,). Ograničit ćemo se na razmatranje samo jednog od njih. Ovaj algoritam je prilično jednostavan, ali unatoč tome vrlo je popularan. Koristi se u programima kao što su zip, gzip, pkzip, bzip2 i mnogi drugi.

Vratimo se algoritmu konstrukcije Huffmanovog stabla. Na svakoj iteraciji izvršili smo linearnu pretragu za dva čvora s najmanjom težinom. Jasno je da je prioritetni red poput piramide (minimum) prikladniji za ovu svrhu. Čvor s najmanjom težinom imat će najveći prioritet i bit će na vrhu piramide. Dajemo ovaj algoritam.

    Uključimo sve kodirane znakove u piramidu.

    Izdvojmo uzastopno 2 čvora iz piramide (to će biti dva čvora s najmanjom težinom).

    Formirajmo se novi čvor i pričvrstite na njega, kao djeca, dva čvora uzeta iz piramide. U ovom slučaju, težina formiranog čvora je postavljena jednaka zbroju težina podređenih čvorova.

    Uključimo formirani čvor u piramidu.

    Ako u piramidi ima više čvorova, ponovite 2-5.

Pretpostavit ćemo da je pointer na njegov roditelj pohranjen za svaki čvor. Postavljamo ovaj pokazivač jednak NULL u korijenu stabla. Sada odaberimo lisni čvor (simbol) i slijedeći spremljene pokazivače popeti ćemo se na stablo sve dok sljedeći pokazivač ne postane NULL. Posljednji uvjet znači da smo došli do korijena stabla. Jasno je da je broj prijelaza s razine na razinu jednak dubini lisnog čvora (simbola), a time i duljini njegovog koda. Zaobilazeći na ovaj način sve čvorove (simbole), dobivamo duljine njihovih kodova.

Maksimalna duljina koda

U pravilu, tzv šifrarnik, jednostavna struktura podataka, u biti dva niza: jedan s duljinama, drugi s kodovima. Drugim riječima, kod (kao bitni niz) je pohranjen na memorijskoj lokaciji ili registru fiksne veličine (obično 16, 32 ili 64). Kako bismo izbjegli prelijevanje, moramo biti sigurni da će kod stati u registar.

Ispada da na abecedi N znakova maksimalna veličina koda može biti duljine do (N-1) bita. Drugim riječima, za N = 256 (uobičajena varijanta) možemo dobiti kod od 255 bita (iako za to datoteka mora biti vrlo velika: 2,292654130570773 * 10 ^ 53 ~ = 2 ^ 177,259)! Jasno je da takva šifra neće stati u registar i s njom morate nešto poduzeti.

Prvo, otkrijmo pod kojim uvjetima dolazi do preljeva. Neka je frekvencija i-tog simbola jednaka i-tom Fibonaccijevom broju. Na primjer: A-1, B-1, C-2, D-3, E-5, F-8, G-13, H-21. Konstruirajmo odgovarajuće Huffmanovo stablo.

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

Takvo stablo se zove degenerirati... Da bi ga dobili, frekvencije simbola moraju rasti barem kao Fibonaccijevi brojevi ili čak brže. Iako je u praksi, na stvarnim podacima, takvo stablo gotovo nemoguće dobiti, vrlo ga je lako generirati umjetno. U svakom slučaju, tu opasnost treba uzeti u obzir.

Ovaj se problem može riješiti na dva prihvatljiva načina. Prvi se temelji na jednom od svojstava kanonskih kodova. Poanta je u tome da u kanoničkom kodu (bit string) najmanje bitni bitovi mogu biti različiti od nule. Drugim riječima, svi ostali bitovi možda uopće neće biti spremljeni, budući da uvijek su nula. U slučaju N = 256, dovoljno nam je da iz svakog koda spremimo samo najmanje značajnih 8 bitova, uz pretpostavku da su svi ostali bitovi jednaki nuli. Ovo rješava problem, ali samo djelomično. To će uvelike zakomplicirati i usporiti i kodiranje i dekodiranje. Stoga se ova metoda rijetko koristi u praksi.

Drugi način je umjetno ograničavanje duljine kodova (bilo tijekom izgradnje ili poslije). Ova metoda je općeprihvaćena, pa ćemo se na njoj detaljnije zadržati.

Postoje dvije vrste algoritama koda za ograničavanje duljine. Heuristički (približan) i optimalan. Algoritmi drugog tipa prilično su složeni u implementaciji i u pravilu zahtijevaju više vremena i memorije od prvih. Učinkovitost heuristički ograničenog koda određena je njegovim odstupanjem od optimalno ograničenog koda. Što je razlika manja, to bolje. Vrijedi napomenuti da je za neke heurističke algoritme ta razlika vrlo mala (,,), štoviše, oni vrlo često generiraju optimalan kod (iako ne jamče da će to uvijek biti tako). Štoviše, budući da u praksi se prelijevanje događa iznimno rijetko (osim ako nije postavljeno vrlo strogo ograničenje maksimalne duljine koda); s malom veličinom abecede, svrsishodnije je koristiti jednostavne i brze heurističke metode.

Razmotrit ćemo jedan prilično jednostavan i vrlo popularan heuristički algoritam. Pronašao je put u programe kao što su zip, gzip, pkzip, bzip2 i mnogi drugi.

Problem ograničavanja maksimalne duljine koda je ekvivalentan problemu ograničavanja visine Huffmanovog stabla. Imajte na umu da, po konstrukciji, svaki nelisni čvor Huffmanovog stabla ima točno dva potomka. Pri svakoj iteraciji našeg algoritma smanjit ćemo visinu stabla za 1. Dakle, neka je L maksimalna duljina koda (visina stabla) i potrebno ju je ograničiti na L / & lt L. Neka je daljnji RN i krajnji desni lisni čvor na razini i, a LN i - krajnji lijevi.

Počnimo od razine L. Pomaknite RN L čvor na mjesto njegovog roditelja. Jer čvorovi idu u paru, moramo pronaći mjesto za čvor uz RN L. Da biste to učinili, pronađite razinu j najbližu L, koja sadrži čvorove lista, tako da je j & lt (L-1). Umjesto LN j formiramo čvor koji nije u obliku lista i kao podrijetli mu pripajamo čvor LN j i nespareni čvor s razine L. Istu operaciju primjenjujemo na sve preostale parove čvorova na razini L. Jasno je da smo preraspodjelom čvorova na ovaj način smanjili visinu našeg stabla za 1. Sada je jednako (L-1). Ako sada L / & lt (L-1), onda ćemo isto učiniti s razinom (L-1) itd. dok se ne dosegne traženo ograničenje.

Vratimo se na naš primjer, gdje je L = 5. Ograničimo maksimalnu duljinu koda na L / = 4.

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

Vidi se da je u našem slučaju RN L = F, j = 3, LN j = C... Prvo pomaknite čvor RN L = F umjesto svog roditelja.

KORIJEN / \ / \ / \ / \ H / \ / \ / \ / \ / \ / \ /\ /\ / \ / \ / \ / \ / \ / \ / \ / \ /\ /\ C E / \ / \ / \ / \ F A D G B(nespareni čvor)

Sada na mjestu LN j = C Formiramo nelisni čvor.

KORIJEN / \ / \ / \ / \ H E / \ / \ / \ / \ / \ / \ F A D G ? ? B(nespareni čvor) C(nespareni čvor)

Priložimo dva nesparena na formirani čvor: B i C.

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

Stoga smo maksimalnu duljinu koda ograničili na 4. Jasno je da smo promjenom duljine koda malo izgubili na učinkovitosti. Dakle, poruka S, kodirana takvim kodom, bit će veličine 92 bita, t.j. Još 3 bita iznad minimalne redundance.

Jasno je da što više ograničimo maksimalnu duljinu koda, to će kod biti manje učinkovit. Hajde da saznamo koliko možete ograničiti maksimalnu duljinu koda. Očito ne kraće od malo.

Izračun kanonskih kodova

Kao što smo već mnogo puta primijetili, duljine kodova dovoljne su za generiranje samih kodova. Pokazat ćemo vam kako se to može učiniti. Pretpostavimo da smo već izračunali duljine kodova i izbrojali koliko kodova svake duljine imamo. Neka je L maksimalna duljina koda, a T i broj kodova duljine i.

Računamo S i - početna vrijednost kod duljine i, za sve i od

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

Za naš primjer, 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

Može se vidjeti da su S 5, S 4, S 3, S 1 upravo znakovni kodovi B, A, C, H... Ove simbole ujedinjuje činjenica da su svi na prvom mjestu, svaki na svojoj razini. Drugim riječima, pronašli smo početnu vrijednost koda za svaku duljinu (ili razinu).

Sada dodijelimo kodove ostalim simbolima. Šifra prvog znaka na razini i je S i, drugog S i + 1, trećeg S i + 2, itd.

Napišimo preostale kodove za naš primjer:

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

Vidi se da smo dobili potpuno iste kodove kao da smo eksplicitno izgradili kanonsko Huffmanovo stablo.

Prenošenje kodnog stabla

Da bi se kodirana poruka mogla dekodirati, dekoder mora imati isto stablo koda (u ovom ili onom obliku) koje je korišteno za kodiranje. Stoga smo zajedno s kodiranim podacima prisiljeni spremiti odgovarajuće stablo koda. Jasno je da što je kompaktniji, to bolje.

Postoji nekoliko načina za rješavanje ovog problema. Najviše očito rješenje- eksplicitno spremite stablo (tj. kao uređeni skup čvorova i pokazivača ove ili one vrste). Ovo je možda najrasipniji i najneučinkovitiji način. U praksi se ne koristi.

Popis frekvencija simbola (tj. frekvencijski rječnik) može se spremiti. Uz njegovu pomoć, dekoder može lako rekonstruirati kodno stablo. Iako je ova metoda manje rasipna od prethodne, nije najbolja.

Konačno, može se koristiti jedno od svojstava kanonskih kodova. Kao što je ranije navedeno, kanonski kodovi su potpuno određeni svojim duljinama. Drugim riječima, sve što dekoder treba je popis duljina znakovnog koda. Uzimajući u obzir da se u prosjeku duljina jednog koda za abecedu N znakova može kodirati u [(log 2 (log 2 N))] bitovima, dobivamo vrlo učinkovit algoritam. Zadržat ćemo se na tome detaljnije.

Pretpostavimo da je veličina abecede N = 256, a mi komprimiramo običnu tekstualnu datoteku(ASCII). Najvjerojatnije u takvoj datoteci nećemo pronaći svih N znakova naše abecede. Zatim stavljamo duljinu koda znakova koji nedostaju jednaka nuli... U tom slučaju će spremljeni popis duljina kodova sadržavati dovoljno veliki broj nule (duljine znakovnih kodova koji nedostaju) grupirane zajedno. Svaka takva grupa može se komprimirati pomoću tzv. grupnog kodiranja - RLE (Run - Length - Encoding). Ovaj algoritam je izuzetno jednostavan. Umjesto niza od M identičnih elemenata u nizu, spremit ćemo prvi element ovog niza i broj njegovih ponavljanja, t.j. (M-1). Primjer: RLE ("AAABBBCDDDDDDD") = A3 B2 C0 D6.

Štoviše, ova se metoda može donekle proširiti. Možemo se prijaviti RLE algoritam ne samo na skupine nulte duljine, već na sve ostale. Ovaj način prosljeđivanja kodnog stabla je uobičajen i koristi se u većini modernih implementacija.

Implementacija: SHCODEC

Dodatak: biografija D. Huffmana

David Huffman rođen je 1925. godine u Ohiju, SAD. Huffman je diplomirao elektrotehniku ​​od državno sveučilište Ohio (Ohio State University) u dobi od 18 godina. Potom je služio u vojsci kao časnik radarske potpore na razaraču koji je pomogao u deaktiviranju mina u japanskim i kineskim vodama nakon Drugog svjetskog rata. Nakon toga je magistrirao na Sveučilištu Ohio i doktorirao na Massachusetts Institute of Technology (MIT). Iako je Huffman najpoznatiji po razvoju metode za konstruiranje minimalno redundantnih kodova, također je dao važan doprinos mnogim drugim poljima (uglavnom u elektronici). On dugo vremena vodio je Odjel za informatiku na MIT-u. Godine 1974. već kao profesor emeritus dao je ostavku. Huffman je dobio niz vrijednih nagrada. 1999. - Medalja Richarda W. Hamminga s Instituta inženjera elektrotehnike i elektronike (IEEE) za izniman doprinos teoriji informacija, medalja Louisa E. Levyja s Instituta Franklin za njegovu doktorsku tezu o sekvencijalnim krugovima, nagrada W. Wallacea McDowella, IEEE Computer Nagrada društva, IEEE Gold Jubilee Technology Innovation Award 1998. U listopadu 1999., u dobi od 74 godine, David Huffman je umro od raka.

R.L. Milidiu, A.A. Pessoa, E.S. Laber, "Učinkovita implementacija algoritma zagrijavanja za konstrukciju prefiksnih kodova s ​​ograničenom duljinom", Proc. ALENEX-a (Međunarodna radionica o algoritamskom inženjerstvu i eksperimentiranju), pp. 1-17, Springer, siječanj. 1999.

Danas je malo korisnika zainteresirano za pitanje vezano uz mehanizam kompresije datoteka. Proces rada sa osobno računalo u usporedbi s prošlošću, postalo je mnogo lakše implementirati.


Danas gotovo svaki korisnik koji radi s sustav datoteka koristi arhive. Međutim, malo je korisnika razmišljalo o tome kako se datoteke komprimiraju.

Huffmanovi kodovi bili su prva opcija. Još uvijek se koriste u raznim arhivima. Većina korisnika niti ne razmišlja o tome kako je jednostavno komprimirati datoteku pomoću ove sheme. V ovu recenziju pogledat ćemo kako se kompresija provodi, koje značajke pomažu ubrzati i pojednostaviti proces kodiranja. Također ćemo pokušati razumjeti osnovne principe izgradnje stabla kodiranja.

Algoritam: povijest

Prvi algoritam dizajniran za provođenje učinkovitog kodiranja elektroničke informacije, postao je kod koji je predložio Huffman 1952. godine. To je taj kod koji se danas može razmatrati osnovni element većina programa dizajniranih za komprimiranje informacija. Neki od popularnijih izvora koji koriste dati kod, danas su RAR arhive, ARJ, ZIP. Ovaj algoritam se također koristi za kompresiju JPEG slike i grafički objekti... Također, svi moderni faksovi koriste algoritam kodiranja koji je izmišljen davne 1952. godine. Unatoč činjenici da je prošlo puno vremena od stvaranja ovog koda, on se učinkovito koristi u opremi starog tipa, kao iu novoj opremi i školjkama.

Načelo učinkovitog kodiranja

U srcu Huffmanovog algoritma je shema koja vam omogućuje zamjenu najvjerojatnijih i najčešćih znakova kodovima binarni sustav... Oni znakovi koji su rjeđi zamjenjuju se dugim kodovima. Prijelaz na dugi kodovi Huffman se provodi tek nakon što sustav koristi sve minimalne vrijednosti. Ova tehnika omogućuje minimiziranje duljine koda po znaku izvorne poruke. V u ovom slučaju posebnost je u tome što se već moraju znati vjerojatnosti pojave slova na početku kodiranja. Konačna poruka bit će sastavljena od njih. Na temelju ovih informacija konstruira se Huffmanovo stablo kodiranja. Na temelju toga će se provesti proces kodiranja slova u arhivi.

Huffmanov kod: primjer

Kako biste ilustrirali Huffmanov algoritam, razmotrite grafičku verziju izgradnje kodnog stabla. Koristiti ovu metodu bila učinkovitija, potrebno je pojasniti definiciju nekih od vrijednosti koje su neophodne za koncept ove metode. Cijela zbirka skupa čvorova i lukova koji su usmjereni od čvora do čvora naziva se graf. Samo stablo je graf sa skupom specifičnih svojstava. Svaki čvor ne smije uključivati ​​više od jednog od svih lukova. Jedan od čvorova mora biti korijen stabla. To znači da lukovi uopće ne bi trebali ulaziti u njega. Ako krenete od korijena kretanja duž lukova, tada bi vam ovaj proces trebao omogućiti da dođete do bilo kojeg čvora.

Huffmanovi kodovi također uključuju takav koncept kao što je list drveta. Predstavlja čvor iz kojeg ne smije izlaziti luk. Ako su dva čvora povezana lukom, onda je jedan od njih roditelj, a drugi dijete. Ako dva čvora imaju zajednički roditeljski čvor, onda se nazivaju bratski i sestrinski čvorovi. Ako uz lišće čvorovi imaju nekoliko lukova, tada se takvo stablo naziva binarnim. To je upravo ono što je Huffmanovo stablo. Značajka čvorova ove strukture je da je težina svakog roditelja jednaka zbroju težine djece čvorova.

Huffmanovo stablo: konstrukcijski algoritam

Konstrukcija Huffmanovog koda izvodi se od slova ulazne abecede. Formira se popis čvorova koji su slobodni u budućem kodnom stablu. Na ovom popisu težina svakog čvora treba biti jednaka vjerojatnosti pojavljivanja slova poruke koje odgovara ovaj čvor... Među nekoliko slobodnih čvorova odabire se onaj koji ima najmanju težinu. Ako se istodobno promatraju minimalni pokazatelji u nekoliko čvorova, tada možete slobodno odabrati bilo koji par. Nakon toga se kreira roditeljski čvor. Trebao bi težiti onoliko koliko teži zbroj zadanog para čvorova. Roditelj se zatim šalje na popis sa slobodnim čvorovima. Djeca se uklanjaju. U tom slučaju lukovi dobivaju odgovarajuće indikatore, nule i jedinice. Ovaj proces ponavlja onoliko puta koliko je potrebno da se ostavi samo jedan čvor. Nakon toga se binarne znamenke zapisuju od vrha prema dolje.

Kako poboljšati učinkovitost kompresije

Kako bi se povećala učinkovitost kompresije, prilikom izrade kodnog stabla potrebno je koristiti sve podatke o vjerojatnosti pojave slova u pojedinoj datoteci koja je priložena stablu. Ne smije se dopustiti da budu razbacani po velikom broju tekstualnih dokumenata. Ako hodate kroz ovu datoteku, tada možete dobiti statistiku o tome koliko se često slova nalaze u objektu koji treba komprimirati.

Kako ubrzati proces kompresije

Da bi se ubrzao rad algoritma, identifikaciju slova treba provoditi ne pokazateljima izgleda određenih slova, već učestalošću njihovog pojavljivanja. Zahvaljujući tome, algoritam postaje jednostavniji, a rad s njim značajno se ubrzava. Također omogućuje izbjegavanje operacija dijeljenja i s pomičnim zarezom. Također, kada radite u ovom načinu rada, algoritam se ne može promijeniti. To je uglavnom zbog činjenice da su vjerojatnosti izravno proporcionalne frekvencijama. Također je vrijedno obratiti pozornost na činjenicu da će konačna težina korijenskog čvora biti jednaka zbroju broja slova u objektu koji se obrađuje.

Zaključak

Huffmanovi kodovi su jednostavan i dugo uspostavljen algoritam koji se i danas koristi u mnogima popularni programi... Jednostavnost i jasnoća ovog koda omogućuje vam postizanje učinkovita kompresija datoteke bilo koje veličine.

Relativno jednostavna metoda sažimanja podataka može se postići stvaranjem takozvanih Huffmanovih stabala za datoteku koja se koriste za njihovo komprimiranje i dekomprimiranje podataka u njoj. Većina aplikacija koristi binarna Huffmanova stabla (na primjer, svaki čvor je ili list ili ima točno dva podčvora). Međutim, moguće je konstruirati Huffmanova stabla proizvoljan broj podstabla (na primjer, ternarna ili, in opći slučaj, N-kao drveće).

Huffmanovo stablo za datoteku koja sadrži Z različiti likovi Ima Z lišće. Put od korijena do lista koji predstavlja određeni znak određuje kodiranje, a svaki korak duž puta do lista određuje kodiranje (koje može biti 0 , 1 , ..., (N-1)). Postavljanjem uobičajenih znakova bliže korijenu, a manje uobičajenih znakova dalje od korijena, postiže se željena kompresija. Strogo govoreći, takvo će stablo biti Huffmanovo stablo samo ako, kao rezultat kodiranja, minimalni broj N-arnih znakova za kodiranje navedene datoteke.

U ovom problemu razmatrat ćemo samo stabla, gdje je svaki čvor ili unutarnji čvor ili list za kodiranje znakova i nema izoliranih listova koji ne kodiraju znak.

Slika ispod prikazuje primjer Huffmanovog ternarnog stabla, simboli " a"i" e"kodirano s jednim ternarnim znakom; manje uobičajeni znakovi" s"i" str"kodirani su pomoću dva ternarna znaka i najrjeđih znakova" x", "q"i" y"kodirani su korištenjem po tri ternarna znaka.

Naravno, ako želimo proširiti popis N-ary znakove pa natrag, važno je znati koje se stablo koristi za komprimiranje podataka. To se može učiniti na nekoliko načina. U ovom zadatku koristit ćemo se sljedeća metoda: toku ulaznih podataka prethodit će zaglavlje koje se sastoji od vrijednosti kodiranih znakova Z nalazi se u izvorna datoteka leksikografskim redom.

Poznavanje broja ulaznih znakova Z, značenje N označavajući " N-arity" Huffmanovog stabla i samog zaglavlja, potrebno je pronaći primarnu vrijednost kodiranih znakova.

Ulazni podaci

Ulazni podaci počinju cijelim brojem T, koji se nalazi u zasebnom retku i označava broj sljedećih testnih slučajeva. Zatim, svaki od T test slučajevima, od kojih se svaki nalazi u 3 -ti redovi kako slijedi:

  • Broj različitih znakova u testnom slučaju Z (2 Z20 );
  • Broj N označavajući " N-arnost "Huffmanovog stabla ( 2 N10 );
  • Niz koji predstavlja zaglavlje primljene poruke, svaki znak će biti znamenka u rasponu ... Ovaj red će sadržavati manje 200 likovima.

Bilješka: Iako rijetko, moguće je da zaglavlje ima više tumačenja prilikom dekodiranja (na primjer, za zaglavlje " 010011101100 “, i vrijednosti Z = 5 i N = 2). Zajamčeno je da u svim testnim slučajevima predloženim u ulaznim podacima postoji jedinstveno rješenje.

Izlaz

Za svaku od T izlaz testnih slučajeva Z linije koje daju dekodiranu verziju svakog od Z znakova u rastućem redoslijedu. Koristite format original-> kodiranje, gdje izvornik- to decimalni broj u rasponu i odgovarajući niz kodiranih znamenki za te znakove (svaka znamenka ≥ 0 i< N).

  1. kod = sljedeći bit iz toka, duljina = 1
  2. Dok kod< base
    kod = kod<< 1
    kod = kod + sljedeći bit iz toka
    duljina = duljina + 1
  3. simbol = simbol + kod - baza]

Drugim riječima, gurat ćemo s lijeve strane u varijablu koda bit po bit od ulaznog toka, do koda< base. При этом на каждой итерации будем увеличивать переменную length на 1 (т.е. продвигаться вниз по дереву). Цикл в (2) остановится когда мы определим длину кода (уровень в дереве, на котором находится искомый символ). Остается лишь определить какой именно символ на этом уровне нам нужен.

Pretpostavimo da se petlja u (2), nakon nekoliko iteracija, zaustavlja. U ovom slučaju izraz (kod - baza) je redni broj traženog čvora (znaka) na razini duljine. Prvi čvor (simbol) na razini duljine u stablu nalazi se u nizu simbola na indeksu isključenja. No, ne zanima nas prvi znak, već znak ispod broja (šifra - baza). Stoga je indeks željenog simbola u nizu simbola (offs + (code - base)). Drugim riječima, traženi simbol je simbol = simbol + kod - baza].

Navedimo konkretan primjer. Koristeći opisani algoritam dekodiramo poruku 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 11 0 1 0 1 0 1 0 1

  1. kod = 0, duljina = 1
  2. kod = 0< base = 1
    kod = 0<< 1 = 0
    kod = 0 + 0 = 0
    duljina = 1 + 1 = 2
    kod = 0< base = 2
    kod = 0<< 1 = 0
    kod = 0 + 0 = 0
    duljina = 2 + 1 = 3
    kod = 0< base = 2
    kod = 0<< 1 = 0
    kod = 0 + 1 = 1
    duljina = 3 + 1 = 4
    kod = 1 = baza = 1
  3. simbol = symb = 2 + kod = 1 - baza = 1] = symb = A
  1. kod = 1, dužina = 1
  2. kod = 1 = baza = 1
  3. simbol = symb = 7 + kod = 1 - baza = 1] = symb = H
  1. kod = 0, duljina = 1
  2. kod = 0< base = 1
    kod = 0<< 1 = 0
    kod = 0 + 0 = 0
    duljina = 1 + 1 = 2
    kod = 0< base = 2
    kod = 0<< 1 = 0
    kod = 0 + 0 = 0
    duljina = 2 + 1 = 3
    kod = 0< base = 2
    kod = 0<< 1 = 0
    kod = 0 + 0 = 0
    duljina = 3 + 1 = 4
    kod = 0< base = 1
    kod = 0<< 1 = 0
    kod = 0 + 1 = 1
    duljina = 4 + 1 = 5
    kod = 1> baza = 0
  3. simbol = symb = 0 + kod = 1 - baza = 0] = symb = F

Dakle, dekodirali smo prva 3 znaka: A, H, F... Jasno je da ćemo slijedeći ovaj algoritam dobiti upravo poruku S.

Izračunavanje duljine koda

Da bismo kodirali poruku, moramo znati kodove znakova i njihove duljine. Kao što je navedeno u prethodnom odjeljku, kanonski kodovi dobro su definirani svojim duljinama. Stoga je naš glavni zadatak izračunati duljine kodova.

Ispada da ovaj zadatak, u ogromnoj većini slučajeva, ne zahtijeva eksplicitnu konstrukciju Huffmanovog stabla. Štoviše, algoritmi koji koriste interni (implicitni) prikaz Huffmanovog stabla mnogo su učinkovitiji u smislu brzine i potrošnje memorije.

Danas postoji mnogo učinkovitih algoritama za izračun duljine kodova (,). Ograničit ćemo se na razmatranje samo jednog od njih. Ovaj algoritam je prilično jednostavan, ali unatoč tome vrlo je popularan. Koristi se u programima kao što su zip, gzip, pkzip, bzip2 i mnogi drugi.

Vratimo se algoritmu konstrukcije Huffmanovog stabla. Na svakoj iteraciji izvršili smo linearnu pretragu za dva čvora s najmanjom težinom. Jasno je da je prioritetni red poput piramide (minimum) prikladniji za ovu svrhu. Čvor s najmanjom težinom imat će najveći prioritet i bit će na vrhu piramide. Dajemo ovaj algoritam.

    Uključimo sve kodirane znakove u piramidu.

    Izdvojmo uzastopno 2 čvora iz piramide (to će biti dva čvora s najmanjom težinom).

    Formirajmo novi čvor i priložimo mu, kao djeca, dva čvora preuzeta iz piramide. U ovom slučaju, težina formiranog čvora je postavljena jednaka zbroju težina podređenih čvorova.

    Uključimo formirani čvor u piramidu.

    Ako u piramidi ima više čvorova, ponovite 2-5.

Pretpostavit ćemo da je pointer na njegov roditelj pohranjen za svaki čvor. Postavljamo ovaj pokazivač jednak NULL u korijenu stabla. Sada odaberimo lisni čvor (simbol) i slijedeći spremljene pokazivače popeti ćemo se na stablo sve dok sljedeći pokazivač ne postane NULL. Posljednji uvjet znači da smo došli do korijena stabla. Jasno je da je broj prijelaza s razine na razinu jednak dubini lisnog čvora (simbola), a time i duljini njegovog koda. Zaobilazeći na ovaj način sve čvorove (simbole), dobivamo duljine njihovih kodova.

Maksimalna duljina koda

U pravilu, tzv šifrarnik, jednostavna struktura podataka, u biti dva niza: jedan s duljinama, drugi s kodovima. Drugim riječima, kod (kao bitni niz) je pohranjen na memorijskoj lokaciji ili registru fiksne veličine (obično 16, 32 ili 64). Kako bismo izbjegli prelijevanje, moramo biti sigurni da će kod stati u registar.

Ispada da na abecedi N znakova maksimalna veličina koda može biti duljine do (N-1) bita. Drugim riječima, za N = 256 (uobičajena varijanta) možemo dobiti kod od 255 bita (iako za to datoteka mora biti vrlo velika: 2,292654130570773 * 10 ^ 53 ~ = 2 ^ 177,259)! Jasno je da takva šifra neće stati u registar i s njom morate nešto poduzeti.

Prvo, otkrijmo pod kojim uvjetima dolazi do preljeva. Neka je frekvencija i-tog simbola jednaka i-tom Fibonaccijevom broju. Na primjer: A-1, B-1, C-2, D-3, E-5, F-8, G-13, H-21. Konstruirajmo odgovarajuće Huffmanovo stablo.

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

Takvo stablo se zove degenerirati... Da bi ga dobili, frekvencije simbola moraju rasti barem kao Fibonaccijevi brojevi ili čak brže. Iako je u praksi, na stvarnim podacima, takvo stablo gotovo nemoguće dobiti, vrlo ga je lako generirati umjetno. U svakom slučaju, tu opasnost treba uzeti u obzir.

Ovaj se problem može riješiti na dva prihvatljiva načina. Prvi se temelji na jednom od svojstava kanonskih kodova. Poanta je u tome da u kanoničkom kodu (bit string) najmanje bitni bitovi mogu biti različiti od nule. Drugim riječima, svi ostali bitovi možda uopće neće biti spremljeni, budući da uvijek su nula. U slučaju N = 256, dovoljno nam je da iz svakog koda spremimo samo najmanje značajnih 8 bitova, uz pretpostavku da su svi ostali bitovi jednaki nuli. Ovo rješava problem, ali samo djelomično. To će uvelike zakomplicirati i usporiti i kodiranje i dekodiranje. Stoga se ova metoda rijetko koristi u praksi.

Drugi način je umjetno ograničavanje duljine kodova (bilo tijekom izgradnje ili poslije). Ova metoda je općeprihvaćena, pa ćemo se na njoj detaljnije zadržati.

Postoje dvije vrste algoritama koda za ograničavanje duljine. Heuristički (približan) i optimalan. Algoritmi drugog tipa prilično su složeni u implementaciji i u pravilu zahtijevaju više vremena i memorije od prvih. Učinkovitost heuristički ograničenog koda određena je njegovim odstupanjem od optimalno ograničenog koda. Što je razlika manja, to bolje. Vrijedi napomenuti da je za neke heurističke algoritme ta razlika vrlo mala (,,), štoviše, oni vrlo često generiraju optimalan kod (iako ne jamče da će to uvijek biti tako). Štoviše, budući da u praksi se prelijevanje događa iznimno rijetko (osim ako nije postavljeno vrlo strogo ograničenje maksimalne duljine koda); s malom veličinom abecede, svrsishodnije je koristiti jednostavne i brze heurističke metode.

Razmotrit ćemo jedan prilično jednostavan i vrlo popularan heuristički algoritam. Pronašao je put u programe kao što su zip, gzip, pkzip, bzip2 i mnogi drugi.

Problem ograničavanja maksimalne duljine koda je ekvivalentan problemu ograničavanja visine Huffmanovog stabla. Imajte na umu da, po konstrukciji, svaki nelisni čvor Huffmanovog stabla ima točno dva potomka. Pri svakoj iteraciji našeg algoritma smanjit ćemo visinu stabla za 1. Dakle, neka je L maksimalna duljina koda (visina stabla) i potrebno ju je ograničiti na L / & lt L. Neka je daljnji RN i krajnji desni lisni čvor na razini i, a LN i - krajnji lijevi.

Počnimo od razine L. Pomaknite RN L čvor na mjesto njegovog roditelja. Jer čvorovi idu u paru, moramo pronaći mjesto za čvor uz RN L. Da biste to učinili, pronađite razinu j najbližu L, koja sadrži čvorove lista, tako da je j & lt (L-1). Umjesto LN j formiramo čvor koji nije u obliku lista i kao podrijetli mu pripajamo čvor LN j i nespareni čvor s razine L. Istu operaciju primjenjujemo na sve preostale parove čvorova na razini L. Jasno je da smo preraspodjelom čvorova na ovaj način smanjili visinu našeg stabla za 1. Sada je jednako (L-1). Ako sada L / & lt (L-1), onda ćemo isto učiniti s razinom (L-1) itd. dok se ne dosegne traženo ograničenje.

Vratimo se na naš primjer, gdje je L = 5. Ograničimo maksimalnu duljinu koda na L / = 4.

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

Vidi se da je u našem slučaju RN L = F, j = 3, LN j = C... Prvo pomaknite čvor RN L = F umjesto svog roditelja.

KORIJEN / \ / \ / \ / \ H / \ / \ / \ / \ / \ / \ /\ /\ / \ / \ / \ / \ / \ / \ / \ / \ /\ /\ C E / \ / \ / \ / \ F A D G B(nespareni čvor)

Sada na mjestu LN j = C Formiramo nelisni čvor.

KORIJEN / \ / \ / \ / \ H E / \ / \ / \ / \ / \ / \ F A D G ? ? B(nespareni čvor) C(nespareni čvor)

Priložimo dva nesparena na formirani čvor: B i C.

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

Stoga smo maksimalnu duljinu koda ograničili na 4. Jasno je da smo promjenom duljine koda malo izgubili na učinkovitosti. Dakle, poruka S, kodirana takvim kodom, bit će veličine 92 bita, t.j. Još 3 bita iznad minimalne redundance.

Jasno je da što više ograničimo maksimalnu duljinu koda, to će kod biti manje učinkovit. Hajde da saznamo koliko možete ograničiti maksimalnu duljinu koda. Očito ne kraće od malo.

Izračun kanonskih kodova

Kao što smo već mnogo puta primijetili, duljine kodova dovoljne su za generiranje samih kodova. Pokazat ćemo vam kako se to može učiniti. Pretpostavimo da smo već izračunali duljine kodova i izbrojali koliko kodova svake duljine imamo. Neka je L maksimalna duljina koda, a T i broj kodova duljine i.

Izračunavamo S i - početnu vrijednost koda duljine i, za sve i iz

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

Za naš primjer, 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

Može se vidjeti da su S 5, S 4, S 3, S 1 upravo znakovni kodovi B, A, C, H... Ove simbole ujedinjuje činjenica da su svi na prvom mjestu, svaki na svojoj razini. Drugim riječima, pronašli smo početnu vrijednost koda za svaku duljinu (ili razinu).

Sada dodijelimo kodove ostalim simbolima. Šifra prvog znaka na razini i je S i, drugog S i + 1, trećeg S i + 2, itd.

Napišimo preostale kodove za naš primjer:

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

Vidi se da smo dobili potpuno iste kodove kao da smo eksplicitno izgradili kanonsko Huffmanovo stablo.

Prenošenje kodnog stabla

Da bi se kodirana poruka mogla dekodirati, dekoder mora imati isto stablo koda (u ovom ili onom obliku) koje je korišteno za kodiranje. Stoga smo zajedno s kodiranim podacima prisiljeni spremiti odgovarajuće stablo koda. Jasno je da što je kompaktniji, to bolje.

Postoji nekoliko načina za rješavanje ovog problema. Najočitije rješenje je eksplicitno pohraniti stablo (tj. kao uređeni skup čvorova i pokazivača ove ili one vrste). Ovo je možda najrasipniji i najneučinkovitiji način. U praksi se ne koristi.

Popis frekvencija simbola (tj. frekvencijski rječnik) može se spremiti. Uz njegovu pomoć, dekoder može lako rekonstruirati kodno stablo. Iako je ova metoda manje rasipna od prethodne, nije najbolja.

Konačno, može se koristiti jedno od svojstava kanonskih kodova. Kao što je ranije navedeno, kanonski kodovi su potpuno određeni svojim duljinama. Drugim riječima, sve što dekoder treba je popis duljina znakovnog koda. Uzimajući u obzir da se u prosjeku duljina jednog koda za abecedu N znakova može kodirati u [(log 2 (log 2 N))] bitovima, dobivamo vrlo učinkovit algoritam. Zadržat ćemo se na tome detaljnije.

Pretpostavimo da je veličina abecede N = 256 i da komprimiramo običnu tekstualnu datoteku (ASCII). Najvjerojatnije u takvoj datoteci nećemo pronaći svih N znakova naše abecede. Postavimo zatim duljinu koda znakova koji nedostaju jednaku nuli. U tom slučaju, spremljeni popis duljina kodova sadržavat će dovoljno velik broj nula (duljine kodova znakova koji nedostaju) grupiranih zajedno. Svaka takva grupa može se komprimirati pomoću tzv. grupnog kodiranja - RLE (Run - Length - Encoding). Ovaj algoritam je izuzetno jednostavan. Umjesto niza od M identičnih elemenata u nizu, spremit ćemo prvi element ovog niza i broj njegovih ponavljanja, t.j. (M-1). Primjer: RLE ("AAABBBCDDDDDDD") = A3 B2 C0 D6.

Štoviše, ova se metoda može donekle proširiti. RLE algoritam možemo primijeniti ne samo na grupe nulte duljine, već i na sve ostale. Ovaj način prosljeđivanja kodnog stabla je uobičajen i koristi se u većini modernih implementacija.

Implementacija: SHCODEC

Dodatak: biografija D. Huffmana

David Huffman rođen je 1925. godine u Ohiju, SAD. Huffman je diplomirao elektrotehniku ​​na Državnom sveučilištu Ohio u dobi od 18 godina. Potom je služio u vojsci kao časnik radarske potpore na razaraču koji je pomogao u deaktiviranju mina u japanskim i kineskim vodama nakon Drugog svjetskog rata. Nakon toga je magistrirao na Sveučilištu Ohio i doktorirao na Massachusetts Institute of Technology (MIT). Iako je Huffman najpoznatiji po razvoju metode za konstruiranje minimalno redundantnih kodova, također je dao važan doprinos mnogim drugim poljima (uglavnom u elektronici). Bio je dugogodišnji voditelj Odjela za informatiku na MIT-u. Godine 1974. već kao profesor emeritus dao je ostavku. Huffman je dobio niz vrijednih nagrada. 1999. - Medalja Richarda W. Hamminga s Instituta inženjera elektrotehnike i elektronike (IEEE) za izniman doprinos teoriji informacija, medalja Louisa E. Levyja s Instituta Franklin za njegovu doktorsku tezu o sekvencijalnim krugovima, nagrada W. Wallacea McDowella, IEEE Computer Nagrada društva, IEEE Gold Jubilee Technology Innovation Award 1998. U listopadu 1999., u dobi od 74 godine, David Huffman je umro od raka.

Huffmanovo kodiranje. 1. dio.
Uvod

Pozdrav dragi čitatelju! Ovaj će članak raspravljati o jednom od načina komprimiranja podataka. Ova metoda je prilično raširena i zaslužuje pažnju. Ovaj materijal izračunat u volumenu za tri članka, od kojih će prvi biti posvećen algoritmu kompresije, drugi - implementacija softvera algoritam, a treći je dekompresija. Algoritam kompresije bit će napisan u C++, algoritam dekompresije u Assembleru.
Međutim, prije nego što nastavite sa samim algoritmom, u članak treba uključiti malo teorije.
Malo teorije
Kompresija (kompresija) - način smanjenja količine podataka u svrhu njihovog daljnjeg prijenosa i pohrane.
Dekompresija To je način vraćanja komprimiranih podataka na izvorne podatke.
Kompresija i dekompresija mogu biti bez gubitka kvalitete (kada je prenesena/pohranjena informacija u komprimiranom obliku nakon dekompresije potpuno identična izvornoj) i s gubitkom kvalitete (kada se podaci nakon dekompresije razlikuju od izvornika). Na primjer, tekstualni dokumenti, baze podataka, programi mogu se komprimirati samo na način bez gubitka kvalitete, dok se slike, video i audio datoteke komprimiraju upravo zbog gubitka kvalitete izvornih podataka (tipičan primjer algoritama - JPEG, MPEG, ADPCM), uz ponekad neprimjetan gubitak kvalitete čak i uz kompresiju 1:4 ili 1:10.
Razlikuju se glavne vrste pakiranja:
  • Decimalno pakiranje namijenjen je pakiranju znakovnih podataka koji se sastoje samo od brojeva. Umjesto korištenja 8 bita za znak, sasvim je racionalno koristiti samo 4 bita za decimalne i heksadecimalne znamenke, 3 bita za oktalne i tako dalje. Ovim pristupom već se osjeća kompresija od najmanje 1: 2.
  • Relativno kodiranje je kodiranje s gubitkom. Temelji se na činjenici da se sljedeći element podataka razlikuje od prethodnog po količini koja zauzima manje prostora u memoriji od samog elementa. Tipičan primjer je ADPCM (Adaptive Differencial Pulse Code Modulation) audio kompresija, koja se široko koristi u digitalna telefonija i omogućuje komprimiranje audio podataka u omjeru 1: 2 uz gotovo neprimjetan gubitak kvalitete.
  • Simboličko potiskivanje- metoda kompresije informacija, u kojoj se dugi nizovi identičnih podataka zamjenjuju kraćima.
  • Statističko kodiranje na temelju činjenice da se sve stavke podataka ne susreću s istu frekvenciju(ili vjerojatnost). Ovim pristupom kodovi se biraju tako da najčešći element odgovara kodu s najkraćom duljinom, a najrjeđi - s najvećom.
Osim toga, kodovi su odabrani na takav način da je tijekom dekodiranja bilo moguće nedvosmisleno odrediti element izvornih podataka. Ovim pristupom moguće je samo bitno orijentirano kodiranje u kojem se razlikuju dopušteni i zabranjeni kodovi. Ako se kod dekodiranja bitnog niza ispostavi da je kod zabranjen, tada mu se mora dodati još jedan bit izvornog niza i operacija dekodiranja se mora ponoviti. Primjeri takvog kodiranja su algoritmi Shannon i Huffman, od kojih ćemo potonje razmotriti.
Točnije o algoritmu
Kao što je već poznato iz prethodnog pododjeljka, Huffmanov algoritam se temelji na statističko kodiranje... Pogledajmo pobliže njegovu provedbu.
Neka postoji izvor podataka koji prenosi znakove (a_1, a_2, ..., a_n) s različitim stupnjevima vjerojatnosti, to jest, svaki a_i ima svoju vjerojatnost (ili frekvenciju) P_i (a_i), i postoji barem jedan par a_i i a_j, i \ ne j, tako da P_i (a_i) i P_j (a_j ) nisu jednaki. Tako se formira skup frekvencija (P_1 (a_1), P_2 (a_2), ..., P_n (a_n)), kakve to veze ima \ displaystyle \ zbroj_ (i = 1) ^ (n) P_i (a_i) = 1, budući da odašiljač ne prenosi više znakova osim (a_1, a_2, ..., a_n).
Naš zadatak je pronaći takve kodni znakovi (b_1, b_2, ..., b_n) s duljinama (L_1 (b_1), L_2 (b_2), ..., L_n (b_n)) tako da prosječna duljina kodnog simbola ne prelazi prosječnu duljinu izvornog simbola. U ovom slučaju potrebno je uzeti u obzir uvjet da ako P_i (a_i)> P_j (a_j) i i \ ne j, onda L_i (b_i) \ le L_j (b_j).
Huffman je predložio izgradnju stabla u kojem su čvorovi najvjerojatnije najmanje udaljeni od korijena. Stoga sama metoda izgradnje stabla slijedi:
1. Odaberite dva simbola a_i i a_j, i \ ne j, tako da P_i (a_i) i P_j (a_j) s cijelog popisa (P_1 (a_1), P_2, ..., P_n (a_n)) su minimalne.
2. Smanjite grane stabala s ova dva elementa na jednu točku s vjerojatnošću P = P_i (a_i) + P_j (a_j) tako da jednu granu označite nulom, a drugu jednom (po vlastitom nahođenju).
3. Ponovite točku 1 uzimajući u obzir nova točka umjesto a_i i a_j, ako je broj rezultirajućih točaka veći od jedan. Inače, došli smo do korijena stabla.
Sada pokušajmo upotrijebiti dobivenu teoriju i kodirati informacije koje prenosi izvor, koristeći primjer od sedam znakova.
Pogledajmo pobliže prvi ciklus. Slika prikazuje tablicu u kojoj svaki simbol a_i ima svoju vjerojatnost (frekvenciju) P_i (a_i). Prema točki 1. iz tablice biramo dva simbola s najmanjom vjerojatnošću. U našem slučaju to su a_1 i a_4. Prema točki 2, grane stabla s a_1 i a_4 svedemo na jednu točku i granu koja vodi do a_1 označavamo s jedan, a granu koja vodi do a_4 s nulom. Iznad nove točke dodjeljujemo njezinu vjerojatnost (u ovom slučaju - 0,03) V daljnje djelovanje ponavljaju se već uzimajući u obzir novu točku i bez uzimanja u obzir a_1 i a_4.

Nakon višestrukog ponavljanja gornjih radnji, gradi se sljedeće stablo:

Po izgrađenom stablu možete odrediti vrijednost kodova (b_1, b_2, ..., b_n), spuštajući se od korijena do odgovarajućeg elementa a_i, dok dodjeljujući nulu ili jedan rezultirajućem nizu prilikom prolaska svake grane (ovisno o tome kako je određena grana imenovana). Dakle, tablica kodova izgleda ovako:

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

Pokušajmo sada kodirati niz znakova.
Neka simbol a_i odgovara (kao primjer) broju i. Neka postoji niz 12672262. Trebate dobiti rezultirajući binarni kod.
Za kodiranje možete koristiti već postojeću tablicu simbola koda b_i, uzimajući u obzir da b_i odgovara simbolu a_i. U ovom slučaju, kod za znamenku 1 bit će niz 011111, za znamenku 2 - 1, a za znamenku 6 - 00. Tako dobivamo sljedeći rezultat:

Data12672262 Dužina koda Izvorno 001010110111010010110 01024 bit Kodirano 011111100011101100119 bit

Kao rezultat kodiranja, osvojili smo 5 bita i napisali slijed u 19 bita umjesto u 24.
Međutim, to ne daje potpunu procjenu kompresije podataka. Vratimo se matematici i procijenimo omjer kompresije koda. To zahtijeva procjenu entropije.
Entropija- mjera neizvjesnosti situacije (slučajna varijabla) s konačnim ili parnim brojem ishoda. Matematički, entropija se formulira kao zbroj umnožaka vjerojatnosti različitih stanja sustava logaritmima tih vjerojatnosti, uzetih s suprotnim predznakom:

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

Gdje je X diskretan slučajna vrijednost(u našem slučaju, kodni znak), a d je proizvoljni radiks veći od jedan. Izbor baze jednak je izboru određene mjerne jedinice entropije. Budući da imamo posla s binarne znamenke, tada je za bazu racionalno odabrati d = 2.
Dakle, entropija za naš slučaj može se predstaviti kao:

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

Entropija ima izvanredno svojstvo: jednaka je minimalnoj dopuštenoj prosječnoj duljini kodnog simbola \ nadcrt (L_ (min)) u bitovima. Ista prosječna duljina simbola koda izračunava se po formuli

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

Zamjenom vrijednosti u formule H (b) i \ overline (L (b)), dobivamo sljedeći rezultat: H (b) = 2,048, \ nadcrt (L (b)) = 2.100.
Vrijednosti H (b) i \ overline (L (b)) su vrlo blizu, što ukazuje na stvarnu dobit u izboru algoritma. Sada usporedimo prosječnu duljinu izvornog simbola i prosječnu duljinu simbola koda kroz omjer:

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

Tako smo dobili omjer kompresije 1:1,429, što je vrlo dobro.
I na kraju, riješimo posljednji problem: dešifriranje slijeda bitova.
Neka postoji niz bitova za našu situaciju:

001101100001110001000111111​

Potrebno je definirati izvor, odnosno dekodirati ovaj niz.
Naravno, u takvoj situaciji možete koristiti tablicu kodova, ali to je prilično nezgodno, jer duljina simbola koda nije konstantna. Mnogo je prikladnije spustiti se stablom (počevši od korijena) prema sljedećem pravilu:
1. Polazna točka je korijen stabla.
2. Pročitajte novi ritam... Ako je nula, onda idite duž grane označene nulom, inače - s jednom.
3. Ako je točka u koju smo pogodili konačna, tada smo odredili kodni znak koji treba zapisati i vratiti na točku 1. U suprotnom, točku 2 treba ponoviti.
Razmotrimo primjer dekodiranja prvog simbola. Nalazimo se u točki s vjerojatnošću 1.00 (korijen stabla), čitamo prvi bit niza i idemo duž grane označene nulom do točke s vjerojatnošću 0.60. Budući da ova točka nije konačna točka u stablu, čitamo sljedeći bit, koji je također nula, i idemo duž grane označene nulom do točke a_6, koja je konačna. Dekodirali smo simbol - ovo je broj 6. Zapisujemo ga i vraćamo se početno stanje(premjesti u korijen).
Dakle, dekodirani slijed poprima oblik.

Podaci

001101100001110001000111111 Duljina koda Kodirano 00110110000111000100011111127 bita Original 6223676261233 bita

U ovom slučaju, dobitak je bio 6 bita s prilično kratkom duljinom sekvence.
Zaključak se nameće sam od sebe: algoritam je jednostavan. Međutim, treba napomenuti: ovaj algoritam dobro za kompresiju tekstualne informacije(zaista, mi zapravo koristimo oko 60 znakova od dostupnih 256 prilikom tipkanja, odnosno vjerojatnost susreta s drugim znakovima je blizu nuli), ali je dovoljno loša za komprimiranje programa (budući da su svi znakovi u programu gotovo jednako vjerojatni ). Dakle, učinkovitost algoritma uvelike ovisi o vrsti podataka koji se komprimiraju.
P.S
U ovom članku smo ispitali Huffmanov algoritam kodiranja koji se temelji na neravnomjerno kodiranje... Omogućuje vam smanjenje veličine prenesenih ili pohranjenih podataka. Algoritam je jednostavan za razumijevanje i može dati stvarne dobitke. Osim toga, ima još jedno izvanredno svojstvo: sposobnost kodiranja i dekodiranja informacija u hodu, pod uvjetom da su vjerojatnosti kodnih riječi ispravno određene. Iako postoji izmjena algoritma koja vam omogućuje promjenu strukture stabla u stvarnom vremenu.
U sljedećem dijelu članka pogledat ćemo bajt-orijentiranu kompresiju datoteka pomoću Huffmanovog algoritma, implementiranog u C++.
Huffmanovo kodiranje. 2. dio
Uvod
U posljednjem dijelu ispitali smo algoritam kodiranja, opisali ga matematički model, izvršio kodiranje i dekodiranje na konkretan primjer, izračunao je prosječnu duljinu kodna riječ a također je odredio omjer kompresije. Osim toga, doneseni su zaključci o prednostima i nedostacima ovog algoritma.
No, osim ovoga, ostala su neriješena još dva pitanja: implementacija programa koji komprimira podatkovnu datoteku i programa koji raspakira komprimirana datoteka... Ovaj je članak posvećen prvom pitanju. Stoga biste trebali početi dizajnirati.
Oblikovati
Prvi korak je izračunavanje učestalosti pojavljivanja znakova u datoteci. Da bismo to učinili, opisujemo sljedeću strukturu:

    // Struktura za izračun učestalosti znaka

    typedef struct TFreq

    int ch;

    TTtable * tablica;

    DWORD frekvencija;

    ) TFreq;

Ova struktura će opisati svaki znak od 256. CH- sam ASCII znak, frekv- broj pojavljivanja simbola u datoteci. Polje stol- pokazivač na strukturu:

    // deskriptor čvora

    typedef struct TTable

    int ch;

    TTtable * lijevo;

    TTtable * desno;

    ) TTtable;

kao što se vidi, Tablica Je deskriptor čvora koji se račva na nulu i jedan. Uz pomoć ovih konstrukcija, u budućnosti će se izvoditi konstrukcija kompresijskog stabla. Sada deklarirajmo našu frekvenciju i naš čvor za svaki simbol:

    TFreq Freq [256];

Pokušajmo shvatiti kako će stablo biti izgrađeno. U početnoj fazi, program mora proći kroz cijelu datoteku i prebrojati broj pojavljivanja znakova u njoj. Osim toga, program mora stvoriti deskriptor čvora za svaki simbol koji pronađe. Nakon toga, od kreiranih čvorova, uzimajući u obzir učestalost simbola, program gradi stablo, postavljajući čvorove određenim redoslijedom i uspostavljajući veze između njih.
Konstruirano stablo je dobro za dekodiranje datoteke. Ali za kodiranje datoteke to je nezgodno, jer se ne zna u kojem smjeru ići od korijena da bi se došlo do traženog znaka. Za to je prikladnije izraditi tablicu pretvorbe znakova u kod. Stoga ćemo definirati još jednu strukturu:

    // Deskriptor znakova koda

    typedef struktura TOPcode

    DWORD opcode;

    DWORD len;

    ) TOP kod;

Ovdje opcode Je li kodna kombinacija znaka i len- njegova duljina (u bitovima). I deklarirajmo tablicu od 256 takvih struktura:

    TOPcode Opcodes [256];

Poznavajući znak koji treba kodirati, možete odrediti njegovu kodnu riječ iz tablice. Prijeđimo sada izravno na izračun učestalosti simbola (i ne samo).
Brojanje frekvencija simbola
U principu, ova akcija nije teška. Dovoljno je otvoriti datoteku i prebrojati broj znakova u njoj, ispunjavajući odgovarajuće strukture. Pogledajmo provedbu ove akcije.
Da bismo to učinili, deklarirati ćemo globalne deskriptore datoteke:

    FILE * unutra, * van, * sklop;

u- datoteka iz koje se čitaju nekomprimirani podaci.
van- datoteka u koju se upisuju komprimirani podaci.
skup- datoteka u koju će stablo biti spremljeno u obliku prikladnom za raspakiranje. Budući da će raspakivač biti napisan u asembleru, sasvim je racionalno stablo učiniti dijelom raspakivača, odnosno predstaviti ga u obliku instrukcija u Assembleru.
Prvi korak je inicijalizacija svih struktura nulte vrijednosti:

    // Brojanje učestalosti simbola

    int CountFrequency (void)

    int i; // varijabla petlje

    int broj = 0; // druga varijabla petlje

    DWORD TotalCount = 0; // veličina datoteke.

    // Inicijaliziranje struktura

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

    Frekv [i] .freq = 0;

    Frekv [i] .tablica = 0;

    Frekv [i] .ch = i;

Nakon toga brojimo broj pojavljivanja simbola u datoteci i veličinu datoteke (naravno, ne na najidealniji način, ali primjer treba pojasniti):

    // Brojanje učestalosti simbola (simbolično)

    dok (! feof (in)) // dok se ne dostigne kraj datoteke

    i = fgetc (in);

    ako (i! = EOF) // ako nije kraj datoteke

    Frekv [i] .freq ++; // frekvencija ++

    TotalCount ++; // veličina ++

Budući da je kod neravnomjeran, raspakivaču će biti teško otkriti broj znakova za čitanje. Stoga je potrebno popraviti njegovu veličinu u tablici za raspakiranje:

    // "Recite" alatu za raspakivanje veličinu datoteke

    fprintf (assemb, "coded_file_size: \ n dd% 8lxh \ n \ n ", TotalCount);

Nakon toga svi korišteni znakovi se pomiču na početak niza, a neiskorišteni se prepisuju (permutacijama).

    // pomiče sve neiskorištene znakove na kraj

    i = 0;

    broj = 256;

    dok ja< count) // još nisu stigli do kraja

    if (Freq [i] .freq == 0) // ako je frekvencija 0

    Frekv [i] = Frekv [- broj]; // zatim kopirajte unos s kraja

    drugo

    ja ++; // sve je OK - kreni dalje.

I tek nakon takvog "razvrstavanja" memorija se dodjeljuje čvorovima (za neku ekonomiju).

    // Dodijeli memoriju za čvorove

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

    Frekv [i] .tablica = nova TTtablica; // stvoriti čvor

    Frekv [i] .tablica -> lijevo = 0; // nema veza

    Frekv [i] .tablica -> desno = 0; // nema veza

    Frekv [i] .tablica -> ch = Frekv.ch; // kopiraj simbol

    Frekv [i] .freq = Frekv.frekv; // i učestalost

    povratni broj;

Tako smo napisali funkciju za početnu inicijalizaciju sustava ili, ako pogledate algoritam u prvom dijelu članka, "zapisali simbole koji se koriste u stupcu i dodijelili im vjerojatnosti", a također i za svaki simbol je stvorio "početnu točku" - čvor - i inicijalizirao ga ... U polja lijevo i pravo zapisao nule. Dakle, ako je čvor posljednji u stablu, onda će ga biti lako vidjeti lijevo i pravo jednaka nuli.
Stvaranje stabla
Dakle, u prethodnom odjeljku, "zapisali smo simbole korištene u stupcu i dodijelili im vjerojatnosti." Zapravo, nismo im dodijelili vjerojatnosti, već brojnike razlomka (to jest, broj pojavljivanja znakova u datoteci). Sada moramo izgraditi drvo. Ali da biste to učinili, morate pronaći minimalni element na listi. Da bismo to učinili, uvodimo funkciju u koju prosljeđujemo dva parametra - broj elemenata na popisu i element koji treba isključiti (jer ćemo tražiti u parovima, a bit će vrlo neugodno ako dobijemo isti element dvaput od funkcija):

    // pronaći čvor s najmanjom vjerojatnošću.

    int FindLeast (int count, int index)

    int i;

    DWORD min = (indeks == 0)? 10 ; // element koji se broji

    // minimalno

    za (i = 1; i< count; i++ ) // petlja kroz niz

    ako (i! = indeks) // ako element nije isključen

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

    min = i; // manje od minimuma - zapamti

    povratak min; // vraća minimalni indeks

Pretraživanje nije teško: prvo odabiremo "minimalni" element niza. Ako je isključeni element 0, tada uzimamo prvi element kao minimum, u protivnom nulu smatramo minimumom. Dok prolazimo kroz niz, uspoređujemo trenutni element s onim "minimalnim", a ako se pokaže da je manji, onda ga označavamo kao minimalni.
Sada, zapravo, sama funkcija izgradnje stabla:

    // Funkcija za građenje stabla

    void PreInit (int count)

    int ind1, ind2; // indeksi elemenata

    TTtable * tablica; // pokazivač na "novi čvor"

    dok (broj> 1) // petlja dok ne dođemo do korijena

    ind1 = FindLeast (broj, - 1); // prvi čvor

    ind2 = FindLeast (broj, ind1); // drugi čvor

    tablica = nova TT tablica; // stvoriti novi čvor

    tablica-> ch = - 1; // nije konačno

    tablica-> lijevo = Frekv [ind1] .tablica; // 0 - čvor 1

    tablica-> desno = Freq [ind2] .tablica; // 1 - čvor 2

    Frekv [ind1] .ch = - 1; // modificirati zapis o

    Frekv [ind1] .freq + = Frekv [ind2] .frekv; // frekvencija za simbol

    Freq [ind1] .table = tablica; // i napiši novi čvor

    ako (ind2! = (- broji)) // ako ind2 nije posljednji

    Frekv [ind2] = Frekv [broj]; // onda na svoje mjesto

    // stavi zadnji u niz

Tablica simbola koda
Dakle, izgradili smo stablo u memoriji: uzeli smo dva čvora u paru, kreirali novi čvor, u koji smo napisali pokazivače na njih, nakon čega je drugi čvor uklonjen s liste, a umjesto prvog čvora napisali smo novi jedan s vilicom.
Sada se javlja još jedan problem: nezgodno je kodirati u stablu, jer morate točno znati na kojoj se stazi nalazi određeni simbol. Međutim, problem je riješen prilično jednostavno: kreira se još jedna tablica - tablica kodnih simbola - i u nju se upisuju kombinacije bitova svih korištenih simbola. Da biste to učinili, dovoljno je jednom rekurzivno prijeći stablo. Istodobno, kako ga ne biste ponovno zaobišli, možete dodati generiranje asemblerske datoteke funkciji zaobilaženja za daljnje dekodiranje komprimiranih podataka (pogledajte odjeljak " Oblikovati").
Zapravo, sama funkcija nije komplicirana. Trebao bi dodijeliti 0 ili 1 kodnoj riječi ako čvor nije konačan, u suprotnom dodajte kodni znak u tablicu. Osim svega ovoga, generirajte datoteku montaže. Razmotrite ovu funkciju:

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

    fprintf (sklop, "opcode% 08lx_% 04x: \ n ", opcode, len); // oznaka u datoteku

    if (tbl-> ch! = - 1) // završni čvor

    BYTE mod = 32 - len;

    Opkodovi [tbl-> ch] .opcode = (opcode >> mod); // spremi kod

    Opkodovi [tbl-> ch] .len = len; // i njegova duljina (u bitovima)

    // i kreirajte odgovarajuću oznaku

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

    drugo // čvor nije konačan

    opcode >> = 1; // osloboditi prostor za novi bit

    len ++; // povećati duljinu kodne riječi

    \ n ", opcode, len);

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

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

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

    // ukloniti čvor (više nije potreban)

    izbrisati tbl;

Između ostalog, nakon prelaska čvora, funkcija ga briše (oslobađa pokazivač). Sada ćemo shvatiti koji se parametri prosljeđuju funkciji.

  • tbl- čvor koji treba zaobići.
  • opcode- trenutnu kodnu riječ. Najznačajniji bit uvijek mora biti slobodan.
  • len- duljina kodne riječi.
U principu, funkcija nije kompliciranija od "klasičnog faktorijala" i ne bi trebala uzrokovati poteškoće.
Bitni izlaz
Tako smo došli do ne baš ugodnog dijela našeg arhivatora, odnosno do izlaza kodnih simbola u datoteku. Problem je u tome što su kodni simboli neujednačeni po duljini i izlaz se mora izvoditi malo po bit. To će pomoći funkciji PutCode... Ali prvo, deklarirajmo dvije varijable - brojač bitova u bajtu i izlazni bajt:

    // Brojač bitova

    int OutBits;

    // Prikazani znak

    BYTE OutChar;

OutBits se povećava za jedan svaki put kada se izlazi bit, OutBits == 8 signalizira da OutChar treba biti spremljen u datoteku.

    void PutCode (int ch)

    int len;

    int outcode;

    // dobiti duljinu kodne riječi i samu kodnu riječ

    outcode = Opkodovi [ch] .opcode; // kodna riječ

    len = Opkodovi [ch] .len; // duljina (u bitovima)

    dok (len> 0) // izlaz bit po bit

    // Petlja dok se varijabla OutBits ne koristi u potpunosti

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

    Izlazni znak >> = 1; // osloboditi prostor

    OutChar | = ((izlazni kod & 1)<< 7 ) ; // i stavite malo u njega

    outcode >> = 1; // sljedeći bit koda

    len -- ; // smanjiti duljinu

  1. OutBits ++ ; // povećao se broj bitova

  2. }

  3. // ako se koristi svih 8 bitova, onda ih spremite u datoteku

  4. ako ( OutBits == 8 )

  5. {

  6. fputc( OutChar, van ) ; // spremi u datoteku

  7. OutBits = 0 ; // postavljen na nulu

  8. OutChar = 0 ; // parametri

  9. }

  10. }

  11. }

Osim toga, trebate organizirati nešto poput "fflush", odnosno nakon izlaza posljednje kodne riječi OutChar neće stati u izlaznu datoteku jer OutBits! = 8. Odavde dolazi još jedna mala funkcija:

  1. // "Izbriši" bitni međuspremnik

  2. poništiti EndPut (poništiti)

  3. {

  4. // Ako postoje bitovi u međuspremniku

  5. ako ( OutBits ! = 0 )

Vrhunski povezani članci