Si të konfiguroni telefonat inteligjentë dhe PC. Portali informativ

Algoritmi për ndërtimin e një peme kodi për një kod huffman. Kodi Huffman

  1. kodi = biti tjetër nga rryma, gjatësia = 1
  2. Kodi mirupafshim< base
    kod = kod<< 1
    kodi = kodi + biti tjetër nga transmetimi
    gjatësi = gjatësi + 1
  3. simbol=simb+baza e kodit]

Me fjalë të tjera, ne do të shtyjmë nga e majta në variablin e kodit pak nga pak nga rryma hyrëse, derisa kodi< base. При этом на каждой итерации будем увеличивать переменную length на 1 (т.е. продвигаться вниз по дереву). Цикл в (2) остановится когда мы определим длину кода (уровень в дереве, на котором находится искомый символ). Остается лишь определить какой именно символ на этом уровне нам нужен.

Supozoni se cikli në (2), pas disa përsëritjesh, është ndalur. Në këtë rast, shprehja (kodi - bazë) është numri rendor i nyjës (karakterit) të kërkuar në nivelin e gjatësisë. Nyja (simboli) i parë në nivelin e gjatësisë në pemë ndodhet në grupin e simboleve në indeks offs. Por ne nuk na intereson karakteri i parë, por karakteri nën numër (kodi - bazë). Prandaj, indeksi i simbolit të dëshiruar në grupin e simboleve është (off + (kodi - bazë)). Me fjalë të tjera, simboli i dëshiruar është simbol=simb + kod - bazë].

Le të marrim një shembull konkret. Duke përdorur algoritmin e mësipërm, ne deshifrojmë mesazhin 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 1010 10 10 10 10 10 10 1

  1. kodi = 0, gjatësia = 1
  2. kodi = 0< base = 1
    kodi = 0<< 1 = 0
    kodi = 0 + 0 = 0
    gjatësia = 1 + 1 = 2
    kodi = 0< base = 2
    kodi = 0<< 1 = 0
    kodi = 0 + 0 = 0
    gjatësia = 2 + 1 = 3
    kodi = 0< base = 2
    kodi = 0<< 1 = 0
    kodi = 0 + 1 = 1
    gjatësia = 3 + 1 = 4
    kod=1=bazë=1
  3. simbol = simbol = 2 + kod = 1 - bazë = 1] = simbol = A
  1. kodi = 1, gjatësia = 1
  2. kod=1=bazë=1
  3. simbol = simbol = 7 + kod = 1 - bazë = 1] = simbol = H
  1. kodi = 0, gjatësia = 1
  2. kodi = 0< base = 1
    kodi = 0<< 1 = 0
    kodi = 0 + 0 = 0
    gjatësia = 1 + 1 = 2
    kodi = 0< base = 2
    kodi = 0<< 1 = 0
    kodi = 0 + 0 = 0
    gjatësia = 2 + 1 = 3
    kodi = 0< base = 2
    kodi = 0<< 1 = 0
    kodi = 0 + 0 = 0
    gjatësia = 3 + 1 = 4
    kodi = 0< base = 1
    kodi = 0<< 1 = 0
    kodi = 0 + 1 = 1
    gjatësia = 4 + 1 = 5
    kodi = 1 > baza = 0
  3. simbol = simbol = 0 + kod = 1 - bazë = 0] = simbol = F

Pra, ne deshifruam 3 karakteret e para: A, H, F. Është e qartë se duke ndjekur këtë algoritëm do të marrim pikërisht mesazhin S.

Llogaritja e gjatësisë së kodit

Për të koduar një mesazh, duhet të dimë kodet e karaktereve dhe gjatësinë e tyre. Siç u përmend në seksionin e mëparshëm, kodet kanonike përcaktohen plotësisht nga gjatësia e tyre. Kështu, detyra jonë kryesore është të llogarisim gjatësinë e kodeve.

Rezulton se kjo detyrë, në shumicën dërrmuese të rasteve, nuk kërkon ndërtimin e qartë të një peme Huffman. Për më tepër, algoritmet që përdorin një paraqitje të brendshme (jo eksplicite) të pemës Huffman janë shumë më efikase për sa i përket shpejtësisë dhe kostove të kujtesës.

Deri më sot, ka shumë algoritme efikase për llogaritjen e gjatësisë së kodeve ( , ). Ne do të kufizohemi në shqyrtimin e vetëm njërit prej tyre. Ky algoritëm është mjaft i thjeshtë, por pavarësisht kësaj është shumë popullor. Përdoret në programe të tilla si zip, gzip, pkzip, bzip2 dhe shumë të tjera.

Le të kthehemi te algoritmi për ndërtimin e pemës Huffman. Në çdo përsëritje, ne kryenim një kërkim linear për dy nyjet me peshën më të vogël. Është e qartë se një radhë prioritare siç është një piramidë (minimumi) është më e përshtatshme për këtë qëllim. Nyja me peshën më të vogël do të ketë më pas prioritetin më të lartë dhe do të jetë në krye të piramidës. Ne paraqesim këtë algoritëm.

    Ne përfshijmë të gjithë personazhet e koduar në piramidë.

    Ne do të nxjerrim në mënyrë sekuenciale 2 nyje nga piramida (këto do të jenë dy nyjet me peshën më të vogël).

    Le të formojmë nyje e re dhe bashkëngjitni në të, si fëmijë, dy nyje të marra nga piramida. Në këtë rast, pesha e nyjës së formuar vendoset e barabartë me shumën e peshave të nyjeve të fëmijëve.

    Le të përfshijmë nyjen e krijuar në piramidë.

    Nëse ka më shumë se një nyje në piramidë, atëherë përsërisni 2-5.

Ne do të supozojmë se për çdo nyje ruhet një tregues për prindin e saj. Në rrënjën e pemës, ky tregues është vendosur në NULL. Tani le të zgjedhim një nyje fletë (simbol) dhe duke ndjekur treguesit e ruajtur do të ngjitemi lart në pemë derisa treguesi tjetër të bëhet NULL. Kushti i fundit do të thotë që kemi arritur në rrënjën e pemës. Është e qartë se numri i kalimeve nga niveli në nivel është i barabartë me thellësinë e nyjës së gjethes (simbolit), dhe rrjedhimisht gjatësinë e kodit të saj. Duke anashkaluar kështu të gjitha nyjet (karakteret), do të marrim gjatësitë e kodeve të tyre.

Gjatësia maksimale e kodit

Si rregull, kur kodoni, të ashtuquajturat libri i kodeve (Libri i kodeve), një strukturë e thjeshtë të dhënash, në thelb dy vargje: njëra me gjatësi, tjetra me kode. Me fjalë të tjera, kodi (si një varg bit) ruhet në një vend memorie ose regjistër me madhësi fikse (zakonisht 16, 32 ose 64). Për të shmangur një tejmbushje, duhet të jemi të sigurt që kodi do të futet në regjistër.

Rezulton se në një alfabet me karakter N, madhësia maksimale e kodit mund të arrijë (N-1) bit në gjatësi. Me fjalë të tjera, me N=256 (një variant i zakonshëm) mund të marrim një kod me gjatësi 255 bit (edhe pse për këtë skedari duhet të jetë shumë i madh: 2.292654130570773*10^53~=2^177.259)! Është e qartë se një kod i tillë nuk do të futet në regjistër dhe duhet bërë diçka me të.

Së pari, le të zbulojmë se në cilat kushte ndodh një tejmbushje. Le të jetë frekuenca e simbolit i-të e barabartë me numrin e i-të të Fibonaçit. Për shembull: A-1, B-1, C-2, D-3, E-5, F-8, G-13, H-21. Ne ndërtojmë pemën përkatëse Huffman.

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

Një pemë e tillë quhet i degjeneruar. Për ta marrë atë, frekuencat e simboleve duhet të rriten të paktën po aq sa numrat Fibonacci ose edhe më shpejt. Edhe pse në praktikë, sipas të dhënave reale, një pemë e tillë është pothuajse e pamundur të merret, është shumë e lehtë ta gjenerosh atë artificialisht. Në çdo rast, ky rrezik duhet të merret parasysh.

Ky problem mund të zgjidhet në dy mënyra të pranueshme. E para prej tyre bazohet në një nga vetitë e kodeve kanonike. Fakti është se në kodin kanonik (varg bit) jo më shumë se bitet më pak të rëndësishme mund të jenë jo zero. Me fjalë të tjera, të gjithë pjesët e tjerë mund të mos ruhen fare, pasi ato janë gjithmonë zero. Në rastin e N=256, mjafton që ne të ruajmë vetëm 8 bitët e poshtëm nga secili kod, duke supozuar se të gjithë bitat e tjerë janë të barabartë me zero. Kjo e zgjidh problemin, por vetëm pjesërisht. Kjo do të komplikojë dhe ngadalësojë shumë kodimin dhe dekodimin. Prandaj, kjo metodë përdoret rrallë në praktikë.

Mënyra e dytë është kufizimi artificial i gjatësisë së kodeve (qoftë gjatë ndërtimit ose pas). Kjo metodë është përgjithësisht e pranuar, kështu që ne do të ndalemi në të më në detaje.

Ekzistojnë dy lloje të algoritmeve kufizuese të gjatësisë së kodit. Heuristik (i përafërt) dhe optimal. Algoritmet e llojit të dytë janë mjaft të vështira për t'u zbatuar dhe, si rregull, kërkojnë më shumë kohë dhe memorie sesa ato të parat. Efikasiteti i një kodi të kufizuar heuristikisht përcaktohet nga devijimi i tij nga ai i kufizuar në mënyrë optimale. Sa më i vogël ky ndryshim, aq më mirë. Vlen të theksohet se për disa algoritme heuristike ky ndryshim është shumë i vogël ( , , ), dhe përveç kësaj, ata shumë shpesh gjenerojnë kodin optimal (edhe pse nuk garantojnë që kjo do të jetë gjithmonë kështu). Për më tepër, që nga në praktikë, tejmbushja ndodh jashtëzakonisht rrallë (përveç nëse vendoset një kufi shumë i rreptë për gjatësinë maksimale të kodit), me një alfabet të vogël, është më e përshtatshme të përdoren metoda heuristike të thjeshta dhe të shpejta.

Ne do të shqyrtojmë një algoritëm heuristik mjaft të thjeshtë dhe shumë të njohur. Ajo ka gjetur rrugën e saj në programe të tilla si zip, gzip, pkzip, bzip2 dhe shumë të tjera.

Problemi i kufizimit të gjatësisë maksimale të kodit është i barabartë me problemin e kufizimit të lartësisë së një peme Huffman. Vini re se, nga ndërtimi, çdo nyje jo gjethe e pemës Huffman ka saktësisht dy fëmijë. Në çdo përsëritje të algoritmit tonë, ne do ta ulim lartësinë e pemës me 1. Pra, le të jetë L gjatësia maksimale e kodit (lartësia e pemës) dhe kërkohet ta kufizojmë atë në L / < L. Më tej , le të jetë RN i nyja e fletës më të djathtë në nivelin i, dhe LN i - më e majta.

Le të fillojmë nga niveli L. Le ta zhvendosim nyjen RN L në vendin e prindit të saj. Sepse nyjet vijnë në çifte, ne duhet të gjejmë një vend për një nyje ngjitur me RN L. Për ta bërë këtë, gjeni nivelin j më afër L që përmban nyjet e gjetheve, të tilla që j < (L-1). Në vend të LN j, do të formojmë një nyje pa gjethe dhe do t'i bashkojmë asaj nyjen LN j dhe nyjen e mbetur pa një çift nga niveli L si fëmijë. Ne zbatojmë të njëjtin veprim për të gjitha çiftet e mbetura të nyjeve në nivelin L. Është e qartë se duke rishpërndarë nyjet në këtë mënyrë, ne kemi ulur lartësinë e pemës sonë me 1. Tani ajo është e barabartë me (L-1). Nëse tani L / < (L-1), atëherë bëni të njëjtën gjë me nivelin (L-1), etj. derisa të arrihet kufiri i kërkuar.

Le të kthehemi te shembulli ynë, ku L=5. Le të kufizojmë gjatësinë maksimale të kodit në L / =4.

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

Mund të shihet se në rastin tonë RN L = F, j=3, LN j = C. Fillimisht lëvizim nyjen RN L = F në vend të prindit të tyre.

ROOT /\ / \ / \ /\ H / \ / \ / \ / \ / \ / \ /\ /\ / \ / \ / \ / \ / \ / \ / \ / \ /\ /\ C E / \ / \ / \ / \ F A D G B(nyje e paçiftuar)

Tani në vend LN j = C formojnë një nyje pa gjethe.

ROOT /\ / \ / \ /\ H E / \ / \ / \ / \ / \ / \ F A D G ? ? B(nyje e paçiftuar) C(nyje e paçiftuar)

Lidhni dy nyje të paçiftuara në nyjen e formuar: B Dhe C.

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

Kështu, ne kemi kufizuar gjatësinë maksimale të kodit në 4. Është e qartë se duke ndryshuar gjatësitë e kodeve, kemi humbur pak në efikasitet. Pra mesazhi S, i koduar me një kod të tillë, do të ketë një madhësi prej 92 bit, d.m.th. 3 bit më shumë se kodi minimal i tepërt.

Është e qartë se sa më shumë të kufizojmë gjatësinë maksimale të kodit, aq më pak efikas do të jetë kodi. Le të zbulojmë se sa mund të kufizoni gjatësinë maksimale të kodit. Natyrisht jo më e shkurtër se pak.

Llogaritja e kodeve kanonike

Siç e kemi vënë re në mënyrë të përsëritur, gjatësia e kodeve është e mjaftueshme për të gjeneruar vetë kodet. Le të tregojmë se si mund të bëhet. Supozoni se kemi llogaritur tashmë gjatësinë e kodeve dhe kemi numëruar sa kode të secilës gjatësi kemi. Le të jetë L gjatësia maksimale e kodit dhe T i numri i kodeve me gjatësi i.

Llogarit S i - vlera fillestare kodi i gjatësisë i, për të gjitha i nga

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

Për shembullin tonë L = 5, T 1 .. 5 = (1, 0, 2 ,3, 2).

S 5 = 00000 kosh = 0 dec
S 4 = (S 5 =0 + T 5 =2) >> 1 = (00010 kosh >> 1) = 0001 kosh = 1 dhjetor
S 3 = (S 4 =1 + T 4 =3) >> 1 = (0100 koshi >> 1) = 010 koshi = 2 dhjetor
S 2 = (S 3 =2 + T 3 =2) >> 1 = (100 kosha >> 1) = 10 kosha = 2 dhjetor
S 1 = (S 2 =2 + T 2 =0) >> 1 = (10 kosh >> 1) = 1 kosh = 1 dhjetor

Mund të shihet se S 5 , S 4 , S 3 , S 1 janë pikërisht kodet e karaktereve B, A, C, H. Këto simbole i bashkon fakti se të gjitha janë të parat, secila në nivelin e vet. Me fjalë të tjera, ne kemi gjetur vlerën fillestare të kodit për çdo gjatësi (ose nivel).

Tani le të caktojmë kode për personazhet e mbetur. Kodi i karakterit të parë në nivelin i është S i, i dyti është S i + 1, i treti është S i + 2, e kështu me radhë.

Le të shkruajmë kodet e mbetura për shembullin tonë:

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

Mund të shihet se kemi marrë saktësisht të njëjtat kode sikur të kishim ndërtuar në mënyrë eksplicite pemën kanonike Huffman.

Kalimi i një peme kodi

Në mënyrë që mesazhi i koduar të deshifrohet, dekoderi duhet të ketë të njëjtën pemë koduese (në një formë ose në një tjetër) që është përdorur në kodim. Prandaj, së bashku me të dhënat e koduara, ne jemi të detyruar të ruajmë pemën përkatëse të kodit. Është e qartë se sa më i vogël të jetë, aq më mirë.

Ky problem mund të zgjidhet në disa mënyra. Shumica zgjidhje e dukshme- ruajeni pemën në mënyrë eksplicite (dmth. si një grup i renditur nyjesh dhe treguesish të një lloji ose tjetër). Kjo është ndoshta mënyra më e kotë dhe joefikase. Në praktikë, nuk përdoret.

Mund të ruani një listë të frekuencave të simboleve (p.sh. një fjalor frekuencash). Me ndihmën e tij, dekoderi mund të rindërtojë lehtësisht pemën e kodit. Edhe pse kjo metodë është më pak e kotë se ajo e mëparshme, nuk është më e mira.

Së fundi, mund të përdoret një nga vetitë e kodeve kanonike. Siç u përmend më herët, kodet kanonike përcaktohen plotësisht nga gjatësia e tyre. Me fjalë të tjera, gjithçka që i nevojitet dekoderit është një listë e gjatësive të kodit të karaktereve. Duke pasur parasysh se, mesatarisht, gjatësia e një kodi për një alfabet me karakter N mund të kodohet në bit [(log 2 (log 2 N))], marrim një algoritëm shumë efikas. Ne do të ndalemi në të më në detaje.

Supozoni se madhësia e alfabetit është N=256 dhe e ngjeshim të zakonshmen skedar teksti(ASC II). Me shumë mundësi nuk do të takojmë të gjithë N karakteret e alfabetit tonë në një skedar të tillë. Le të vendosim më pas gjatësinë e kodit të karaktereve që mungojnë zero. Në këtë rast, lista e ruajtur e gjatësisë së kodit do të përmbajë mjaftueshëm numër i madh zero (gjatësitë e kodeve të karaktereve që mungojnë) të grupuara së bashku. Secili grup i tillë mund të kompresohet duke përdorur të ashtuquajturin kodim grupor - RLE (Run - Length - Encoding). Ky algoritëm është jashtëzakonisht i thjeshtë. Në vend të një sekuence M elementësh identikë në një rresht, ne do të ruajmë elementin e parë të kësaj sekuence dhe numrin e përsëritjeve të tij, d.m.th. (M-1). Shembull: RLE("AAAABBBCDDDDDDDD")=A3 B2 C0 D6.

Për më tepër, kjo metodë mund të zgjerohet disi. Mund të aplikojmë Algoritmi RLE jo vetëm për grupet me gjatësi zero, por për të gjithë të tjerët. Kjo mënyrë e transferimit të pemës së kodit është përgjithësisht e pranuar dhe përdoret në shumicën e zbatimeve moderne.

Zbatimi: SHCODEC

Shtojca: biografia e D. Huffman

David Huffman ka lindur në vitin 1925 në Ohio, SHBA. Huffman mori diplomën e tij bachelor në inxhinieri elektrike nga Universiteti Shtetëror Ohio (Universiteti Shtetëror Ohio) në moshën 18 vjeçare. Më pas ai shërbeu në ushtri si oficer mbështetës i radarit në një shkatërrues që ndihmoi në pastrimin e minave në ujërat japoneze dhe kineze pas Luftës së Dytë Botërore. Më pas ai mori një diplomë master nga Universiteti i Ohajos dhe një doktoraturë nga Instituti i Teknologjisë në Massachusetts (MIT). Megjithëse Huffman është më i njohur për zhvillimin e një metode për ndërtimin e kodeve të tepricës minimale, ai gjithashtu dha kontribute të rëndësishme në shumë fusha të tjera (kryesisht në elektronikë). Ai kohe e gjate kryesoi Departamentin e Shkencave Kompjuterike në MIT. Në vitin 1974, tashmë profesor emeritus, dha dorëheqjen. Huffman ka marrë një sërë çmimesh të vlefshme. Në 1999 - Medalja Richard W. Hamming nga Instituti i Inxhinierëve Elektrikë dhe Elektronikë (IEEE) për kontribute të jashtëzakonshme në Teorinë e Informacionit, Medalja Louis E. Levy nga Instituti Franklin për çmimin e tij W. Wallace McDowell, IEEE Computer Society Award, IEEE Golden Jubilee Technology Innovation Award në vitin 1998. Në tetor 1999, në moshën 74-vjeçare, David Huffman vdiq nga kanceri.

R.L. Milidiu, A.A. Pessoa, E.S. Laber, "Zbatimi efikas i algoritmit të ngrohjes për ndërtimin e kodeve të prefiksave me gjatësi të kufizuar", Proc. i ALENEX (Punëtori Ndërkombëtare për Inxhinierinë dhe Eksperimentimin e Algoritmeve), pp. 1-17, Springer, Jan. 1999.

Sot, pak përdorues janë të interesuar për çështjen që lidhet me mekanizmin e kompresimit të skedarëve. Procesi i punës me Kompjuter personal shumë më e lehtë se në të kaluarën.


Sot, pothuajse çdo përdorues që punon me sistemi i skedarëve, përdor arkivat. Sidoqoftë, pak përdorues kanë menduar se si kompresohen skedarët.

Kodet Huffman u bënë opsioni i parë. Ato përdoren ende në arkivues të ndryshëm. Shumica e përdoruesve as nuk mendojnë se sa e lehtë është të kompresosh një skedar sipas kësaj skeme. NË këtë rishikim ne do të shikojmë se si kryhet kompresimi, cilat veçori ndihmojnë në përshpejtimin dhe thjeshtimin e procesit të kodimit. Ne gjithashtu do të përpiqemi të kuptojmë parimet themelore të ndërtimit të një peme kodimi.

Algoritmi: historia

Algoritmi i parë i krijuar për të kryer kodim efikas informacion elektronik, u bë kodi i propozuar nga Huffman në 1952. Është ky kod që mund të merret parasysh sot element bazë shumica e programeve të krijuara për të kompresuar informacionin. Një nga burimet më të njohura që përdorin kodi i dhënë, sot janë Arkivat RAR, ARJ, ZIP. Ky algoritëm përdoret gjithashtu për të kompresuar Imazhet JPEG Dhe objekte grafike. Gjithashtu, të gjitha fakset moderne përdorin një algoritëm kodimi që u shpik në 1952. Përkundër faktit se ka kaluar shumë kohë që nga krijimi i këtij kodi, ai përdoret në mënyrë efektive në pajisjet e stilit të vjetër, si dhe në pajisje dhe predha të reja.

Parimi i kodimit efikas

Algoritmi Huffman bazohet në një skemë që ju lejon të zëvendësoni karakteret më të mundshme dhe më të shpeshta me kode sistemi binar. Ato karaktere që janë më pak të zakonshme zëvendësohen me kode të gjata. Tranzicioni në kodet e gjata Huffman kryhet vetëm pasi sistemi përdor të gjitha vlerat minimale. Kjo teknikë bën të mundur minimizimin e gjatësisë së kodit për simbol të mesazhit origjinal. NË këtë rast E veçanta është se duhet të dihen tashmë probabilitetet e shfaqjes së shkronjave në fillim të kodimit. Mesazhi përfundimtar do të përbëhet prej tyre. Bazuar në këtë informacion, kryhet ndërtimi i pemës së kodit Huffman. Në bazë të tij do të kryhet procesi i kodimit të shkronjave në arkiv.

Kodi Huffman: shembull

Për të ilustruar algoritmin Huffman, merrni parasysh një version grafik të ndërtimit të një peme kodi. Per te perdorur këtë metodë ishte më efikase, është e nevojshme të sqarohet përkufizimi i disa vlerave që janë të nevojshme për konceptin e kësaj metode. I gjithë grupi i shumë nyjeve dhe harqeve që drejtohen nga nyja në nyje quhet graf. Vetë pema është një grafik me një grup karakteristikash specifike. Çdo nyje duhet të përmbajë jo më shumë se një nga të gjitha harqet. Një nga nyjet duhet të jetë rrënja e pemës. Kjo do të thotë që nuk duhet të përfshijë fare harqe. Nëse filloni nga rrënja e lëvizjes përgjatë harqeve, atëherë ky proces duhet t'ju lejojë të arrini në çdo nyje.

Kodet Huffman përfshijnë gjithashtu një gjë të tillë si një gjethe e një peme. Ai përfaqëson një nyje nga e cila nuk duhet të dalë asnjë hark. Nëse dy nyje janë të lidhura me një hark, atëherë njëri prej tyre është një prind dhe tjetri është një fëmijë. Nëse dy nyje kanë një nyje të përbashkët prind, atëherë ato quhen nyje si motra. Nëse, përveç gjetheve, nyjet kanë disa harqe, atëherë një pemë e tillë quhet binare. Kjo është pikërisht ajo që është pema Huffman. Një tipar i nyjeve të kësaj strukture është se pesha e secilit prind është e barabartë me shumën e peshave të fëmijëve të nyjeve.

Pema Huffman: algoritmi i ndërtimit

Ndërtimi i kodit Huffman kryhet nga shkronjat e alfabetit të hyrjes. Formohet një listë e nyjeve të lira në pemën e kodit të ardhshëm. Në këtë listë, pesha e secilës nyje duhet të jetë e njëjtë me probabilitetin e shfaqjes së letrës së mesazhit që korrespondon me nyja e dhënë. Midis disa nyjeve të lira, zgjidhet ajo që peshon më pak. Nëse në të njëjtën kohë vërehen tregues minimalë në disa nyje, atëherë mund të zgjidhni lirshëm çdo palë. Pas kësaj, krijohet nyja mëmë. Duhet të peshojë sa shuma e çiftit të dhënë të nyjeve. Më pas, prindi dërgohet në listë me nyje të lira. Fëmijët hiqen. Në këtë rast, harqet marrin treguesit përkatës, zero dhe njëshe. Ky proces përsëritet aq herë sa nevojitet për të lënë vetëm një nyje. Pas kësaj, shifrat binare shkruhen nga lart poshtë.

Si të përmirësoni efikasitetin e kompresimit

Për të rritur efikasitetin e kompresimit, gjatë ndërtimit të pemës së kodit, është e nevojshme të përdoren të gjitha të dhënat në lidhje me probabilitetin e shfaqjes së shkronjave në një skedar të caktuar që i bashkëngjitet pemës. Ato nuk duhet të lejohen të shpërndahen në një numër të madh dokumentesh tekstuale. Nëse ecni nëpër dosjen e dhënë, atëherë mund të merrni statistika se sa shpesh mund të gjenden shkronjat nga objekti që do të kompresohet.

Si të shpejtoni procesin e kompresimit

Për të shpejtuar algoritmin, përcaktimi i shkronjave duhet të kryhet jo nga treguesit e shfaqjes së shkronjave të caktuara, por nga shpeshtësia e shfaqjes së tyre. Falë kësaj, algoritmi bëhet më i thjeshtë dhe puna me të përshpejtohet ndjeshëm. Gjithashtu bën të mundur shmangien e operacioneve të ndarjes dhe me pikë lundruese. Gjithashtu, kur punoni në këtë mënyrë, algoritmi nuk mund të ndryshohet. Kjo është kryesisht për shkak të faktit se probabilitetet janë drejtpërdrejt proporcionale me frekuencat. Vlen gjithashtu t'i kushtohet vëmendje faktit që pesha përfundimtare e nyjës rrënjë do të jetë e barabartë me shumën e numrit të shkronjave në objektin që do të përpunohet.

Prodhimi

Kodet Huffman janë një algoritëm i thjeshtë dhe i krijuar prej kohësh që përdoret ende sot në shumë programe të njohura. Thjeshtësia dhe qartësia e këtij kodi ju lejon të arrini kompresim efektiv skedarë të çdo madhësie.

Një metodë relativisht e thjeshtë e kompresimit të të dhënave mund të bëhet duke krijuar të ashtuquajturat pemë Huffman në një skedar dhe të përdoret për ta kompresuar atë dhe për të dekompresuar të dhënat në të. Për shumicën e aplikacioneve, përdoren pemë binare Huffman (për shembull, secila nyje është ose një gjethe ose ka saktësisht dy nënnyje). Megjithatë, është e mundur të ndërtohen pemë Huffman me numër arbitrar nënpemë (për shembull, treshe ose, in rast i përgjithshëm, N-ic pemë).

Pema Huffman për një skedar që përmban Z personazhe të ndryshme Ajo ka Z gjethet. Rruga nga rrënja te fleta që përfaqëson një karakter të caktuar përcakton kodimin dhe çdo hap në shtegun e fletës përcakton kodimin (i cili mund të jetë 0 , 1 , ..., (N-1)). Duke vendosur karaktere që shfaqen shpesh më afër rrënjës, dhe karaktere që shfaqen më rrallë më larg rrënjës, arrihet ngjeshja e dëshiruar. Në mënyrë të rreptë, një pemë e tillë do të jetë një pemë Huffman vetëm nëse numri minimal N karaktere -ic për të koduar skedarin e dhënë.

Në këtë problem, ne do të shqyrtojmë vetëm pemët ku secila nyje është ose një nyje e brendshme ose një fletë koduese e karaktereve dhe nuk ka gjethe të izoluara që nuk kodojnë një karakter.

Figura më poshtë tregon një shembull të një peme treshe Huffman, simbolet " a"Dhe" e"E koduar me një karakter të vetëm tresh; karaktere më pak të zakonshme" s"Dhe" fq"janë të koduara duke përdorur dy karaktere treshe dhe karakteret më të rralla" x", "q"Dhe" y" janë të koduara duke përdorur tre karaktere treshe secila.

Sigurisht, nëse duam, mund ta zgjerojmë listën N karakteret -ic pastaj kthehen, është e rëndësishme të dini se cila pemë përdoret për të kompresuar të dhënat. Kjo mund të bëhet në disa mënyra. Në këtë detyrë, ne do të përdorim metodën e mëposhtme: rryma hyrëse do të paraprihet nga një kokë e përbërë nga vlera të koduara të karaktereve Z të vendosura në skedari burimor sipas rendit leksikografik.

Njohja e numrit të karaktereve hyrëse Z, kuptimi N, duke treguar " N-ariteti" i pemës Huffman dhe vetë kokës, është e nevojshme të gjendet vlera kryesore e karaktereve të koduara.

Fut te dhenat

Hyrja fillon me një numër të plotë T, i vendosur në një linjë të veçantë dhe që tregon numrin e rasteve të testimit të mëvonshëm. Më pas, secila prej T rastet e testimit, secila e vendosur në 3 -x rreshtat si më poshtë:

  • Numri i karaktereve të dallueshme në rastin e testit Z (2 Z20 );
  • Numri N, duke treguar " N-ariteti" i pemës Huffman ( 2 N10 );
  • Një varg që përfaqëson kokën e mesazhit të marrë, çdo karakter do të jetë një shifër në interval . Kjo linjë do të përmbajë më pak 200 personazhet.

Shënim : Edhe pse e rrallë, është e mundur që një titull të ketë interpretime të shumta kur transkriptohet (për shembull, për titullin " 010011101100 ", dhe vlerat Z = 5 Dhe N=2). Është e garantuar që në të gjitha rastet e testimit të propozuara në të dhënat hyrëse, ekziston një zgjidhje unike.

Prodhimi

Për secilën prej T rastet e testimit të prodhimit Z linjat që japin versionin e deshifruar të secilit Z personazhet në rend rritës. Përdorni formatin origjinal-> kodim, ku origjinale- kjo numër dhjetor në varg dhe vargun përkatës të koduar të shifrave të koduara për ato karaktere (secila shifër ≥ 0 Dhe< N).

  1. kodi = biti tjetër nga rryma, gjatësia = 1
  2. Kodi mirupafshim< base
    kod = kod<< 1
    kodi = kodi + biti tjetër nga transmetimi
    gjatësi = gjatësi + 1
  3. simbol=simb+baza e kodit]

Me fjalë të tjera, ne do të shtyjmë nga e majta në variablin e kodit pak nga pak nga rryma hyrëse, derisa kodi< base. При этом на каждой итерации будем увеличивать переменную length на 1 (т.е. продвигаться вниз по дереву). Цикл в (2) остановится когда мы определим длину кода (уровень в дереве, на котором находится искомый символ). Остается лишь определить какой именно символ на этом уровне нам нужен.

Supozoni se cikli në (2), pas disa përsëritjesh, është ndalur. Në këtë rast, shprehja (kodi - bazë) është numri rendor i nyjës (karakterit) të kërkuar në nivelin e gjatësisë. Nyja (simboli) i parë në nivelin e gjatësisë në pemë ndodhet në grupin e simboleve në indeks offs. Por ne nuk na intereson karakteri i parë, por karakteri nën numër (kodi - bazë). Prandaj, indeksi i simbolit të dëshiruar në grupin e simboleve është (off + (kodi - bazë)). Me fjalë të tjera, simboli i dëshiruar është simbol=simb + kod - bazë].

Le të marrim një shembull konkret. Duke përdorur algoritmin e mësipërm, ne deshifrojmë mesazhin 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 1010 10 10 10 10 10 10 1

  1. kodi = 0, gjatësia = 1
  2. kodi = 0< base = 1
    kodi = 0<< 1 = 0
    kodi = 0 + 0 = 0
    gjatësia = 1 + 1 = 2
    kodi = 0< base = 2
    kodi = 0<< 1 = 0
    kodi = 0 + 0 = 0
    gjatësia = 2 + 1 = 3
    kodi = 0< base = 2
    kodi = 0<< 1 = 0
    kodi = 0 + 1 = 1
    gjatësia = 3 + 1 = 4
    kod=1=bazë=1
  3. simbol = simbol = 2 + kod = 1 - bazë = 1] = simbol = A
  1. kodi = 1, gjatësia = 1
  2. kod=1=bazë=1
  3. simbol = simbol = 7 + kod = 1 - bazë = 1] = simbol = H
  1. kodi = 0, gjatësia = 1
  2. kodi = 0< base = 1
    kodi = 0<< 1 = 0
    kodi = 0 + 0 = 0
    gjatësia = 1 + 1 = 2
    kodi = 0< base = 2
    kodi = 0<< 1 = 0
    kodi = 0 + 0 = 0
    gjatësia = 2 + 1 = 3
    kodi = 0< base = 2
    kodi = 0<< 1 = 0
    kodi = 0 + 0 = 0
    gjatësia = 3 + 1 = 4
    kodi = 0< base = 1
    kodi = 0<< 1 = 0
    kodi = 0 + 1 = 1
    gjatësia = 4 + 1 = 5
    kodi = 1 > baza = 0
  3. simbol = simbol = 0 + kod = 1 - bazë = 0] = simbol = F

Pra, ne deshifruam 3 karakteret e para: A, H, F. Është e qartë se duke ndjekur këtë algoritëm do të marrim pikërisht mesazhin S.

Llogaritja e gjatësisë së kodit

Për të koduar një mesazh, duhet të dimë kodet e karaktereve dhe gjatësinë e tyre. Siç u përmend në seksionin e mëparshëm, kodet kanonike përcaktohen plotësisht nga gjatësia e tyre. Kështu, detyra jonë kryesore është të llogarisim gjatësinë e kodeve.

Rezulton se kjo detyrë, në shumicën dërrmuese të rasteve, nuk kërkon ndërtimin e qartë të një peme Huffman. Për më tepër, algoritmet që përdorin një paraqitje të brendshme (jo eksplicite) të pemës Huffman janë shumë më efikase për sa i përket shpejtësisë dhe kostove të kujtesës.

Deri më sot, ka shumë algoritme efikase për llogaritjen e gjatësisë së kodeve ( , ). Ne do të kufizohemi në shqyrtimin e vetëm njërit prej tyre. Ky algoritëm është mjaft i thjeshtë, por pavarësisht kësaj është shumë popullor. Përdoret në programe të tilla si zip, gzip, pkzip, bzip2 dhe shumë të tjera.

Le të kthehemi te algoritmi për ndërtimin e pemës Huffman. Në çdo përsëritje, ne kryenim një kërkim linear për dy nyjet me peshën më të vogël. Është e qartë se një radhë prioritare siç është një piramidë (minimumi) është më e përshtatshme për këtë qëllim. Nyja me peshën më të vogël do të ketë më pas prioritetin më të lartë dhe do të jetë në krye të piramidës. Ne paraqesim këtë algoritëm.

    Ne përfshijmë të gjithë personazhet e koduar në piramidë.

    Ne do të nxjerrim në mënyrë sekuenciale 2 nyje nga piramida (këto do të jenë dy nyjet me peshën më të vogël).

    Le të formojmë një nyje të re dhe t'i bashkojmë, si fëmijë, dy nyje të marra nga piramida. Në këtë rast, pesha e nyjës së formuar vendoset e barabartë me shumën e peshave të nyjeve të fëmijëve.

    Le të përfshijmë nyjen e krijuar në piramidë.

    Nëse ka më shumë se një nyje në piramidë, atëherë përsërisni 2-5.

Ne do të supozojmë se për çdo nyje ruhet një tregues për prindin e saj. Në rrënjën e pemës, ky tregues është vendosur në NULL. Tani le të zgjedhim një nyje fletë (simbol) dhe duke ndjekur treguesit e ruajtur do të ngjitemi lart në pemë derisa treguesi tjetër të bëhet NULL. Kushti i fundit do të thotë që kemi arritur në rrënjën e pemës. Është e qartë se numri i kalimeve nga niveli në nivel është i barabartë me thellësinë e nyjës së gjethes (simbolit), dhe rrjedhimisht gjatësinë e kodit të saj. Duke anashkaluar kështu të gjitha nyjet (karakteret), do të marrim gjatësitë e kodeve të tyre.

Gjatësia maksimale e kodit

Si rregull, kur kodoni, të ashtuquajturat libri i kodeve (Libri i kodeve), një strukturë e thjeshtë të dhënash, në thelb dy vargje: njëra me gjatësi, tjetra me kode. Me fjalë të tjera, kodi (si një varg bit) ruhet në një vend memorie ose regjistër me madhësi fikse (zakonisht 16, 32 ose 64). Për të shmangur një tejmbushje, duhet të jemi të sigurt që kodi do të futet në regjistër.

Rezulton se në një alfabet me karakter N, madhësia maksimale e kodit mund të arrijë (N-1) bit në gjatësi. Me fjalë të tjera, me N=256 (një variant i zakonshëm) mund të marrim një kod me gjatësi 255 bit (edhe pse për këtë skedari duhet të jetë shumë i madh: 2.292654130570773*10^53~=2^177.259)! Është e qartë se një kod i tillë nuk do të futet në regjistër dhe duhet bërë diçka me të.

Së pari, le të zbulojmë se në cilat kushte ndodh një tejmbushje. Le të jetë frekuenca e simbolit i-të e barabartë me numrin e i-të të Fibonaçit. Për shembull: A-1, B-1, C-2, D-3, E-5, F-8, G-13, H-21. Ne ndërtojmë pemën përkatëse Huffman.

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

Një pemë e tillë quhet i degjeneruar. Për ta marrë atë, frekuencat e simboleve duhet të rriten të paktën po aq sa numrat Fibonacci ose edhe më shpejt. Edhe pse në praktikë, sipas të dhënave reale, një pemë e tillë është pothuajse e pamundur të merret, është shumë e lehtë ta gjenerosh atë artificialisht. Në çdo rast, ky rrezik duhet të merret parasysh.

Ky problem mund të zgjidhet në dy mënyra të pranueshme. E para prej tyre bazohet në një nga vetitë e kodeve kanonike. Fakti është se në kodin kanonik (varg bit) jo më shumë se bitet më pak të rëndësishme mund të jenë jo zero. Me fjalë të tjera, të gjithë pjesët e tjerë mund të mos ruhen fare, pasi ato janë gjithmonë zero. Në rastin e N=256, mjafton që ne të ruajmë vetëm 8 bitët e poshtëm nga secili kod, duke supozuar se të gjithë bitat e tjerë janë të barabartë me zero. Kjo e zgjidh problemin, por vetëm pjesërisht. Kjo do të komplikojë dhe ngadalësojë shumë kodimin dhe dekodimin. Prandaj, kjo metodë përdoret rrallë në praktikë.

Mënyra e dytë është kufizimi artificial i gjatësisë së kodeve (qoftë gjatë ndërtimit ose pas). Kjo metodë është përgjithësisht e pranuar, kështu që ne do të ndalemi në të më në detaje.

Ekzistojnë dy lloje të algoritmeve kufizuese të gjatësisë së kodit. Heuristik (i përafërt) dhe optimal. Algoritmet e llojit të dytë janë mjaft të vështira për t'u zbatuar dhe, si rregull, kërkojnë më shumë kohë dhe memorie sesa ato të parat. Efikasiteti i një kodi të kufizuar heuristikisht përcaktohet nga devijimi i tij nga ai i kufizuar në mënyrë optimale. Sa më i vogël ky ndryshim, aq më mirë. Vlen të theksohet se për disa algoritme heuristike ky ndryshim është shumë i vogël ( , , ), dhe përveç kësaj, ata shumë shpesh gjenerojnë kodin optimal (edhe pse nuk garantojnë që kjo do të jetë gjithmonë kështu). Për më tepër, që nga në praktikë, tejmbushja ndodh jashtëzakonisht rrallë (përveç nëse vendoset një kufi shumë i rreptë për gjatësinë maksimale të kodit), me një alfabet të vogël, është më e përshtatshme të përdoren metoda heuristike të thjeshta dhe të shpejta.

Ne do të shqyrtojmë një algoritëm heuristik mjaft të thjeshtë dhe shumë të njohur. Ajo ka gjetur rrugën e saj në programe të tilla si zip, gzip, pkzip, bzip2 dhe shumë të tjera.

Problemi i kufizimit të gjatësisë maksimale të kodit është i barabartë me problemin e kufizimit të lartësisë së një peme Huffman. Vini re se, nga ndërtimi, çdo nyje jo gjethe e pemës Huffman ka saktësisht dy fëmijë. Në çdo përsëritje të algoritmit tonë, ne do ta ulim lartësinë e pemës me 1. Pra, le të jetë L gjatësia maksimale e kodit (lartësia e pemës) dhe kërkohet ta kufizojmë atë në L / < L. Më tej , le të jetë RN i nyja e fletës më të djathtë në nivelin i, dhe LN i - më e majta.

Le të fillojmë nga niveli L. Le ta zhvendosim nyjen RN L në vendin e prindit të saj. Sepse nyjet vijnë në çifte, ne duhet të gjejmë një vend për një nyje ngjitur me RN L. Për ta bërë këtë, gjeni nivelin j më afër L që përmban nyjet e gjetheve, të tilla që j < (L-1). Në vend të LN j, do të formojmë një nyje pa gjethe dhe do t'i bashkojmë asaj nyjen LN j dhe nyjen e mbetur pa një çift nga niveli L si fëmijë. Ne zbatojmë të njëjtin veprim për të gjitha çiftet e mbetura të nyjeve në nivelin L. Është e qartë se duke rishpërndarë nyjet në këtë mënyrë, ne kemi ulur lartësinë e pemës sonë me 1. Tani ajo është e barabartë me (L-1). Nëse tani L / < (L-1), atëherë bëni të njëjtën gjë me nivelin (L-1), etj. derisa të arrihet kufiri i kërkuar.

Le të kthehemi te shembulli ynë, ku L=5. Le të kufizojmë gjatësinë maksimale të kodit në L / =4.

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

Mund të shihet se në rastin tonë RN L = F, j=3, LN j = C. Fillimisht lëvizim nyjen RN L = F në vend të prindit të tyre.

ROOT /\ / \ / \ /\ H / \ / \ / \ / \ / \ / \ /\ /\ / \ / \ / \ / \ / \ / \ / \ / \ /\ /\ C E / \ / \ / \ / \ F A D G B(nyje e paçiftuar)

Tani në vend LN j = C formojnë një nyje pa gjethe.

ROOT /\ / \ / \ /\ H E / \ / \ / \ / \ / \ / \ F A D G ? ? B(nyje e paçiftuar) C(nyje e paçiftuar)

Lidhni dy nyje të paçiftuara në nyjen e formuar: B Dhe C.

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

Kështu, ne kemi kufizuar gjatësinë maksimale të kodit në 4. Është e qartë se duke ndryshuar gjatësitë e kodeve, kemi humbur pak në efikasitet. Pra mesazhi S, i koduar me një kod të tillë, do të ketë një madhësi prej 92 bit, d.m.th. 3 bit më shumë se kodi minimal i tepërt.

Është e qartë se sa më shumë të kufizojmë gjatësinë maksimale të kodit, aq më pak efikas do të jetë kodi. Le të zbulojmë se sa mund të kufizoni gjatësinë maksimale të kodit. Natyrisht jo më e shkurtër se pak.

Llogaritja e kodeve kanonike

Siç e kemi vënë re në mënyrë të përsëritur, gjatësia e kodeve është e mjaftueshme për të gjeneruar vetë kodet. Le të tregojmë se si mund të bëhet. Supozoni se kemi llogaritur tashmë gjatësinë e kodeve dhe kemi numëruar sa kode të secilës gjatësi kemi. Le të jetë L gjatësia maksimale e kodit dhe T i numri i kodeve me gjatësi i.

Llogaritni S i - vlerën fillestare të kodit të gjatësisë i, për të gjitha i nga

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

Për shembullin tonë L = 5, T 1 .. 5 = (1, 0, 2 ,3, 2).

S 5 = 00000 kosh = 0 dec
S 4 = (S 5 =0 + T 5 =2) >> 1 = (00010 kosh >> 1) = 0001 kosh = 1 dhjetor
S 3 = (S 4 =1 + T 4 =3) >> 1 = (0100 koshi >> 1) = 010 koshi = 2 dhjetor
S 2 = (S 3 =2 + T 3 =2) >> 1 = (100 kosha >> 1) = 10 kosha = 2 dhjetor
S 1 = (S 2 =2 + T 2 =0) >> 1 = (10 kosh >> 1) = 1 kosh = 1 dhjetor

Mund të shihet se S 5 , S 4 , S 3 , S 1 janë pikërisht kodet e karaktereve B, A, C, H. Këto simbole i bashkon fakti se të gjitha janë të parat, secila në nivelin e vet. Me fjalë të tjera, ne kemi gjetur vlerën fillestare të kodit për çdo gjatësi (ose nivel).

Tani le të caktojmë kode për personazhet e mbetur. Kodi i karakterit të parë në nivelin i është S i, i dyti është S i + 1, i treti është S i + 2, e kështu me radhë.

Le të shkruajmë kodet e mbetura për shembullin tonë:

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

Mund të shihet se kemi marrë saktësisht të njëjtat kode sikur të kishim ndërtuar në mënyrë eksplicite pemën kanonike Huffman.

Kalimi i një peme kodi

Në mënyrë që mesazhi i koduar të deshifrohet, dekoderi duhet të ketë të njëjtën pemë koduese (në një formë ose në një tjetër) që është përdorur në kodim. Prandaj, së bashku me të dhënat e koduara, ne jemi të detyruar të ruajmë pemën përkatëse të kodit. Është e qartë se sa më i vogël të jetë, aq më mirë.

Ky problem mund të zgjidhet në disa mënyra. Zgjidhja më e dukshme është ruajtja e pemës në mënyrë eksplicite (d.m.th. si një grup i renditur nyjesh dhe treguesish të një lloji ose tjetër). Kjo është ndoshta mënyra më e kotë dhe joefikase. Në praktikë, nuk përdoret.

Mund të ruani një listë të frekuencave të simboleve (p.sh. një fjalor frekuencash). Me ndihmën e tij, dekoderi mund të rindërtojë lehtësisht pemën e kodit. Edhe pse kjo metodë është më pak e kotë se ajo e mëparshme, nuk është më e mira.

Së fundi, mund të përdoret një nga vetitë e kodeve kanonike. Siç u përmend më herët, kodet kanonike përcaktohen plotësisht nga gjatësia e tyre. Me fjalë të tjera, gjithçka që i nevojitet dekoderit është një listë e gjatësive të kodit të karaktereve. Duke pasur parasysh se, mesatarisht, gjatësia e një kodi për një alfabet me karakter N mund të kodohet në bit [(log 2 (log 2 N))], marrim një algoritëm shumë efikas. Ne do të ndalemi në të më në detaje.

Supozoni se madhësia e alfabetit është N=256 dhe ne po kompresojmë një skedar teksti të thjeshtë (ASCII). Me shumë mundësi nuk do të takojmë të gjithë N karakteret e alfabetit tonë në një skedar të tillë. Le ta vendosim gjatësinë e kodit të karaktereve që mungojnë në zero. Në këtë rast, lista e ruajtur e gjatësisë së kodeve do të përmbajë një numër mjaft të madh zerosh (gjatësitë e kodeve të karaktereve që mungojnë) të grupuara së bashku. Secili grup i tillë mund të kompresohet duke përdorur të ashtuquajturin kodim grupor - RLE (Run - Length - Encoding). Ky algoritëm është jashtëzakonisht i thjeshtë. Në vend të një sekuence M elementësh identikë në një rresht, ne do të ruajmë elementin e parë të kësaj sekuence dhe numrin e përsëritjeve të tij, d.m.th. (M-1). Shembull: RLE("AAAABBBCDDDDDDDD")=A3 B2 C0 D6.

Për më tepër, kjo metodë mund të zgjerohet disi. Ne mund të aplikojmë algoritmin RLE jo vetëm për grupet me gjatësi zero, por për të gjithë të tjerët. Kjo mënyrë e transferimit të pemës së kodit është përgjithësisht e pranuar dhe përdoret në shumicën e zbatimeve moderne.

Zbatimi: SHCODEC

Shtojca: biografia e D. Huffman

David Huffman ka lindur në vitin 1925 në Ohio, SHBA. Huffman mori diplomën e tij bachelor në inxhinieri elektrike nga Universiteti Shtetëror i Ohajos në moshën 18-vjeçare. Më pas ai shërbeu në ushtri si oficer mbështetës i radarit në një shkatërrues që ndihmoi në pastrimin e minave në ujërat japoneze dhe kineze pas Luftës së Dytë Botërore. Më pas ai mori një diplomë master nga Universiteti i Ohajos dhe një doktoraturë nga Instituti i Teknologjisë në Massachusetts (MIT). Megjithëse Huffman është më i njohur për zhvillimin e një metode për ndërtimin e kodeve të tepricës minimale, ai gjithashtu dha kontribute të rëndësishme në shumë fusha të tjera (kryesisht në elektronikë). Ai ishte një kryetar për një kohë të gjatë i Departamentit të Shkencave Kompjuterike në MIT. Në vitin 1974, tashmë profesor emeritus, dha dorëheqjen. Huffman ka marrë një sërë çmimesh të vlefshme. Në 1999 - Medalja Richard W. Hamming nga Instituti i Inxhinierëve Elektrikë dhe Elektronikë (IEEE) për kontribute të jashtëzakonshme në Teorinë e Informacionit, Medalja Louis E. Levy nga Instituti Franklin për çmimin e tij W. Wallace McDowell, IEEE Computer Society Award, IEEE Golden Jubilee Technology Innovation Award në vitin 1998. Në tetor 1999, në moshën 74-vjeçare, David Huffman vdiq nga kanceri.

Kodimi i Huffman. Pjesa 1.
Prezantimi

Përshëndetje i dashur lexues! Ky artikull do të shqyrtojë një nga mënyrat për të kompresuar të dhënat. Kjo metodë është mjaft e përhapur dhe meriton vëmendje. Ky materialështë llogaritur në vëllim për tre artikuj, i pari prej të cilëve do t'i kushtohet algoritmit të kompresimit, i dyti - implementimi i softuerit algoritmi, dhe i treti është dekompresimi. Algoritmi i kompresimit do të shkruhet në C++, algoritmi i dekompresimit në Assembler.
Sidoqoftë, përpara se të vazhdoni me vetë algoritmin, duhet të përfshihet një teori e vogël në artikull.
Pak teori
Kompresimi (ngjeshja) është një mënyrë për të reduktuar volumin e të dhënave me qëllim të transmetimit dhe ruajtjes së mëtejshme të tyre.
Dekompresimiështë një mënyrë për të rivendosur të dhënat e kompresuara në origjinal.
Kompresimi dhe dekompresimi mund të jenë si pa humbje të cilësisë (kur informacioni i transmetuar/ruajtur në formë të ngjeshur pas dekompresimit është absolutisht identik me origjinalin), ashtu edhe me humbje të cilësisë (kur të dhënat pas dekompresimit ndryshojnë nga origjinali). Për shembull, dokumente tekstuale, bazat e të dhënave, programet mund të kompresohen vetëm në një mënyrë pa humbje të cilësisë, ndërsa fotografitë, videot dhe skedarët audio kompresohen pikërisht për shkak të humbjes së cilësisë së të dhënave burimore (shembull tipik i algoritmeve janë JPEG, MPEG, ADPCM). me një humbje ndonjëherë të padukshme të cilësisë edhe me ngjeshje 1:4 ose 1:10.
Dallohen llojet kryesore të paketimit:
  • Paketim dhjetorështë krijuar për të paketuar të dhënat e karaktereve që përbëhen vetëm nga numra. Në vend që të përdorni 8 bit për një karakter, është mjaft racionale të përdorni vetëm 4 bit për shifrat dhjetore dhe heksadecimalë, 3 bit për shifrat oktale, e kështu me radhë. Me këtë qasje, ngjeshja prej të paktën 1: 2 tashmë ndihet.
  • Kodimi relativështë kodim me humbje. Ai bazohet në faktin se elementi i ardhshëm i të dhënave ndryshon nga ai i mëparshmi për një sasi që zë më pak hapësirë ​​në memorie sesa vetë elementi. Një shembull tipikështë kompresimi audio ADPCM (Modulimi Adaptive Differential Pulse Code), i përdorur gjerësisht në telefonia dixhitale dhe ju lejon të kompresoni të dhënat audio në një raport 1: 2 me një humbje pothuajse të padukshme të cilësisë.
  • Shtypja simbolike- një metodë e ngjeshjes së informacionit në të cilën sekuenca të gjata të të dhënave identike zëvendësohen nga ato më të shkurtra.
  • Kodimi statistikor bazuar në faktin se jo të gjithë elementët e të dhënave ndodhin me të njëjtën frekuencë(ose probabilitet). Me këtë qasje, kodet zgjidhen në mënyrë që elementi që shfaqet më shpesh të korrespondojë me kodin me gjatësinë më të vogël, dhe më pak të shpeshtë - me atë më të madhin.
Për më tepër, kodet zgjidhen në atë mënyrë që gjatë dekodimit të jetë e mundur të përcaktohet në mënyrë unike elementi i të dhënave origjinale. Me këtë qasje është i mundur vetëm kodimi i orientuar në bit, në të cilin dallohen kodet e lejuara dhe të ndaluara. Nëse kodi doli të ishte i ndaluar gjatë dekodimit të sekuencës së biteve, atëherë është e nevojshme të shtoni edhe një bit të sekuencës origjinale në të dhe të përsërisni operacionin e dekodimit. Shembuj të një kodimi të tillë janë algoritmet Shannon dhe Huffman, këto të fundit do t'i shqyrtojmë.
Më konkretisht për algoritmin
Siç dihet tashmë nga nënseksioni i mëparshëm, algoritmi Huffman bazohet në kodimi statistikor. Le të hedhim një vështrim më të afërt në zbatimin e tij.
Le të ketë një burim të dhënash që transmeton karaktere (a_1, a_2, ..., a_n) me një shkallë të ndryshme probabiliteti, domethënë secila a_i korrespondon me probabilitetin (ose frekuencën) e vet P_i(a_i) , dhe ka të paktën një palë a_i dhe a_j ,i\ne j, të tilla që P_i(a_i) dhe P_j (a_j) nuk janë të barabartë. Kështu, formohet një grup frekuencash (P_1(a_1), P_2(a_2),...,P_n(a_n)), në çfarë \displaystyle \sum_(i=1)^(n) P_i(a_i)=1, meqenëse transmetuesi nuk transmeton më karaktere të tjera përveç (a_1,a_2,...,a_n).
Detyra jonë është të zgjedhim të tillë simbolet e kodit (b_1, b_2,...,b_n) me gjatësi (L_1(b_1),L_2(b_2),...,L_n(b_n)) në mënyrë që gjatësia mesatare e simbolit të kodit të mos e kalojë gjatësinë mesatare të simbolit origjinal. Në këtë rast është e nevojshme të merret parasysh kushti që nëse P_i(a_i)>P_j(a_j) dhe i\ne j, atëherë L_i(b_i)\le L_j(b_j).
Huffman propozoi të ndërtohej një pemë në të cilën nyjet me probabilitetin më të lartë janë më pak të largëta nga rrënja. Nga këtu vijon vetë metoda e ndërtimit të një peme:
1. Zgjidhni dy karaktere a_i dhe a_j, i\ne j, të tilla që P_i(a_i) dhe P_j(a_j) nga e gjithë lista (P_1(a_1),P_2,...,P_n(a_n)) janë minimale.
2. Zvogëloni degët e pemëve nga këto dy elemente në një pikë me probabilitet P=P_i(a_i)+P_j(a_j), duke shënuar një degë me zero, dhe tjetrën me një (sipas gjykimit të dikujt).
3. Përsëritni pikën 1, duke marrë parasysh pikë e re në vend të a_i dhe a_j nëse numri i pikave që rezultojnë është më i madh se një. Përndryshe, kemi arritur në rrënjën e pemës.
Tani le të përpiqemi të përdorim teorinë e marrë dhe të kodojmë informacionin e transmetuar nga burimi, duke përdorur shembullin e shtatë karaktereve.
Le të hedhim një vështrim më të afërt në ciklin e parë. Figura tregon një tabelë në të cilën çdo karakter a_i korrespondon me probabilitetin (frekuencën) e tij P_i(a_i) . Sipas pikës 1, zgjedhim dy karaktere nga tabela me probabilitetin më të ulët. Në rastin tonë, këto janë a_1 dhe a_4. Sipas paragrafit 2, ne i sjellim degët e pemës nga a_1 dhe a_4 në një pikë dhe shënojmë degën që çon në a_1 me një, dhe degën që çon në a_4 me zero. Mbi pikën e re, caktoni probabilitetin e saj (në këtë rast, 0.03). veprime të mëtejshme përsëriten tashmë duke marrë parasysh pikën e re dhe pa marrë parasysh a_1 dhe a_4 .

Pas përsëritjes shumë herë të veprimeve të mësipërme, ndërtohet pema e mëposhtme:

Sipas pemës së ndërtuar, ju mund të përcaktoni kuptimin e kodeve (b_1,b_2,...,b_n), duke zbritur nga rrënja në elementin përkatës a_i , ndërsa cakton zero ose një në sekuencën që rezulton kur kalon çdo degë (varësisht se si quhet dega specifike). Pra, tabela e kodit duket si kjo:

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

Tani le të përpiqemi të kodojmë një sekuencë karakteresh.
Le të korrespondojë simboli a_i (si shembull) me numrin i . Le të ketë një sekuencë 12672262. Ju duhet të merrni kodin binar që rezulton.
Për kodim, mund të përdorni tabelën tashmë ekzistuese të simboleve të kodit b_i, duke marrë parasysh që b_i korrespondon me simbolin a_i. Në këtë rast, kodi për numrin 1 do të jetë sekuenca 011111, për numrin 2 - 1 dhe për numrin 6 - 00. Kështu, marrim rezultatin e mëposhtëm:

Data12672262Gjatësia e koditOriginal001010110111010010110 01024 bitEncoded011111100011101100119 bit

Si rezultat i kodimit, ne fituam 5 bit dhe regjistruam sekuencën në 19 bit në vend të 24.
Megjithatë, kjo nuk jep një vlerësim të plotë të ngjeshjes së të dhënave. Le të kthehemi te matematika dhe të vlerësojmë shkallën e ngjeshjes së kodit. Kjo do të kërkojë një vlerësim të entropisë.
Entropiaështë një masë e pasigurisë së një situate (ndryshore e rastësishme) me një numër të fundëm ose çift rezultatesh. Matematikisht, entropia formulohet si shuma e produkteve të probabiliteteve të gjendjeve të ndryshme të sistemit dhe logaritmave të këtyre probabiliteteve, të marra me shenjën e kundërt:

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

Ku X është diskrete vlerë e rastësishme(në rastin tonë, një simbol kodi), dhe d është një bazë arbitrare më e madhe se një. Zgjedhja e një baze është e barabartë me zgjedhjen e një njësie entropie specifike. Meqenëse kemi të bëjmë me shifra binare, atëherë është racionale të zgjedhim d=2 si bazë.
Kështu, entropia për rastin tonë mund të përfaqësohet si:

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

Entropia ka një veti të jashtëzakonshme: është e barabartë me gjatësinë mesatare minimale të lejueshme të një simboli kodi \overline (L_(min)) në copa. E njëjta gjatësi mesatare e simbolit të kodit llogaritet me formulë

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

Duke zëvendësuar vlerat në formulat H(b) dhe \overline(L(b)), marrim rezultatin e mëposhtëm: H(b)=2.048, \overline(L(b))=2,100.
Vlerat e H(b) dhe \overline(L(b)) janë shumë afër, gjë që tregon një fitim real në zgjedhjen e algoritmit. Tani le të krahasojmë gjatësinë mesatare të simbolit burimor dhe gjatësinë mesatare të simbolit të kodit përmes raportit:

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

Kështu, kemi marrë një raport kompresimi prej 1:1.429, që është shumë i mirë.
Dhe së fundi, le të zgjidhim problemin e fundit: deshifrimin e sekuencës së biteve.
Le të ketë një sekuencë bitash për situatën tonë:

001101100001110001000111111​

Nevoja për të përcaktuar burimi, pra deshifroni këtë sekuencë.
Sigurisht, në një situatë të tillë, mund të përdorni tabelën e kodit, por kjo është mjaft e papërshtatshme, pasi gjatësia e simboleve të kodit nuk është konstante. Është shumë më e përshtatshme të zbresësh pema (duke filluar nga rrënja) sipas rregullit të mëposhtëm:
1. Pika fillestare është rrënja e pemës.
2. Lexoni rrahje e re. Nëse është zero, atëherë shkoni përgjatë degës së shënuar me zero, përndryshe, me një.
3. Nëse pika që kemi goditur është ajo përfundimtare, atëherë kemi përcaktuar simbolin e kodit që duhet të shënohet dhe të kthehemi në hapin 1. Përndryshe, hapi 2 duhet të përsëritet.
Konsideroni një shembull të deshifrimit të karakterit të parë. Jemi në një pikë me probabilitet 1.00 (rrënja e pemës), lexojmë bitin e parë të sekuencës dhe shkojmë përgjatë degës së shënuar me zero në një pikë me probabilitet 0.60. Meqenëse kjo pikë nuk është pika fundore në pemë, lexojmë bitin tjetër, i cili gjithashtu është zero, dhe shkojmë përgjatë degës së shënuar me zero në pikën a_6, që është pika e fundit. Ne kemi deshifruar simbolin - ky është numri 6. E shkruajmë dhe kthehemi te gjendjen fillestare(lëvizni në rrënjë).
Kështu sekuenca e deshifruar merr formën.

Të dhënat

001101100001110001000111111 gjatësia e koditEncobitNative6223676261233 bit

Në këtë rast, fitimi ishte 6 bit me një gjatësi mjaft të vogël sekuence.
Përfundimi sugjeron vetë: algoritmi është i thjeshtë. Megjithatë, duhet bërë një shënim: këtë algoritëm e mirë për kompresim informacion teksti(në të vërtetë, ne në fakt përdorim rreth 60 karaktere nga 256 të disponueshme kur shtypim tekstin, domethënë, probabiliteti për të takuar karaktere të tjera është afër zeros), por mjaft i keq për kompresimin e programit (pasi të gjitha karakteret në program janë pothuajse njësoj të mundshëm ). Pra, efikasiteti i algoritmit varet shumë nga lloji i të dhënave që kompresohen.
P.S
Në këtë artikull, ne shqyrtuam algoritmin e kodimit Huffman, i cili bazohet në kodim i pabarabartë. Kjo ju lejon të zvogëloni madhësinë e të dhënave të transmetuara ose të ruajtura. Algoritmi është i lehtë për t'u kuptuar dhe mund të sjellë përfitime reale. Për më tepër, ai ka një veçori tjetër të jashtëzakonshme: aftësinë për të koduar dhe deshifruar informacionin në fluturim, me kusht që probabilitetet e fjalëve të kodit të përcaktohen saktë. Edhe pse ekziston një modifikim i algoritmit që ju lejon të ndryshoni strukturën e pemës në kohë reale.
Në pjesën tjetër të artikullit, ne do të shikojmë kompresimin e skedarit të orientuar drejt bajtit duke përdorur algoritmin Huffman, të zbatuar në C ++.
Kodimi i Huffman. Pjesa 2
Prezantimi
Në pjesën e fundit, ne shqyrtuam algoritmin e kodimit, e përshkruam atë modeli matematik, kryer kodimin dhe dekodimin në shembull specifik, llogaritet gjatësia mesatare fjalë kodike, dhe gjithashtu përcaktoi raportin e kompresimit. Përveç kësaj, u nxorën përfundime në lidhje me avantazhet dhe disavantazhet e këtij algoritmi.
Megjithatë, përveç kësaj, dy çështje të tjera mbetën të pazgjidhura: zbatimi i një programi që ngjesh një skedar të dhënash dhe një program që shpaketon skedar i ngjeshur. Ky artikull i kushtohet pyetjes së parë. Prandaj, është e nevojshme të bëhet dizajn.
Dizajn
Hapi i parë është llogaritja e shpeshtësisë së shfaqjes së karaktereve në skedar. Për ta bërë këtë, ne përshkruajmë strukturën e mëposhtme:

    // Struktura për numërimin e frekuencës së simboleve

    typedef struct TFreq

    int ch;

    TTable*tabela;

    frekuenca DWORD;

    ) TFreq;

Kjo strukturë do të përshkruajë çdo karakter nga 256. chështë vetë personazhi ASCII, frekuështë numri i shfaqjeve të një karakteri në skedar. Fusha tabelaështë një tregues për një strukturë:

    // Doreza e nyjës

    typedef struct TTable

    int ch;

    TTable * majtas;

    TTable*djathtas;

    ) TTable;

Siç shihet, TT tabelaështë një përshkrues i një nyje të degëzuar me zero dhe një. Me ndihmën e këtyre strukturave, në të ardhmen do të kryhet ndërtimi i pemës së ngjeshjes. Tani le të deklarojmë për çdo karakter frekuencën dhe nyjen e vet:

    TFreq Freq[256];

Le të përpiqemi të kuptojmë se si do të ndërtohet pema. Në fazën fillestare, programi duhet të kalojë nëpër të gjithë skedarin dhe të numërojë numrin e shfaqjeve të karaktereve në të. Përveç kësaj, për çdo simbol të gjetur, programi duhet të krijojë një përshkrues të nyjeve. Pas kësaj, nga nyjet e krijuara, duke marrë parasysh frekuencën e karaktereve, programi ndërton një pemë, duke i vendosur nyjet në një renditje të caktuar dhe duke vendosur lidhje midis tyre.
Pema e ndërtuar është e mirë për dekodimin e skedarit. Por për kodimin e skedarëve, është e papërshtatshme, sepse nuk dihet se në cilin drejtim të shkohet nga rrënja për të arritur në karakterin e dëshiruar. Për ta bërë këtë, është më e përshtatshme të ndërtoni një tabelë për shndërrimin e karaktereve në kod. Pra, le të përcaktojmë një strukturë tjetër:

    // Përshkruesi i simbolit të kodit

    typedef struct TOpcode

    kodi optik DWORD;

    lente DWORD;

    ) kodi kryesor;

Këtu opcodeështë një kombinim kodi i një simboli, dhe len- gjatësia e saj (në bit). Dhe deklaroni një tabelë me 256 struktura të tilla:

    TOPCODE Opcodes [ 256 ] ;

Duke ditur karakterin e koduar, mund të përcaktoni fjalën e kodit të tij nga tabela. Tani le të kalojmë në numërimin e frekuencave të simboleve (dhe jo vetëm).
Numërimi i frekuencave të simboleve
Në parim, ky veprim nuk është i vështirë. Mjafton të hapni skedarin dhe të numëroni numrin e karaktereve në të, duke plotësuar strukturat e duhura. Le të shohim zbatimin e këtij veprimi.
Për ta bërë këtë, ne deklarojmë përshkruesit global të skedarëve:

    FILE *in, *jashtë, *montim;

— skedar nga i cili lexohen të dhënat e pakompresuara.
jashtë— një skedar në të cilin janë shkruar të dhënat e ngjeshur.
montim— një skedar në të cilin pema do të ruhet në një formë të përshtatshme për shpaketim. Meqenëse unpacker do të shkruhet në asembler, është mjaft racionale që pema të bëhet pjesë e unpacker-it, domethënë të paraqitet në formën e udhëzimeve të montimit.
Hapi i parë është inicializimi i të gjitha strukturave vlera zero:

    // Numëroni frekuencat e karaktereve

    int CountFrequency (i pavlefshëm)

    int i; // variabli i ciklit

    int count= 0 ; // variabli i ciklit të dytë

    DWORD TotalCount= 0 ; // madhësia e skedarit.

    // Inicializimi i strukturave

    për (i= 0; i< 256 ; i++ )

    Freq[i] .freq = 0 ;

    Freq[ i] .tabela = 0 ;

    Freq[i] .ch = i;

Pas kësaj, ne numërojmë numrin e shfaqjeve të një karakteri në skedar dhe madhësinë e skedarit (sigurisht, jo në mënyrën më ideale, por shembulli ka nevojë për qartësi):

    // Numërimi i frekuencave të karaktereve (karakter për karakter)

    ndërsa (!feof(in) ) // derisa të arrihet fundi i skedarit

    i= fgetc(në) ;

    nëse (i!=EOF) // nëse jo fundi i skedarit

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

    TotalCount++ ; // madhësia ++

Meqenëse kodi është i pabarabartë, do të jetë problematike që shpaketuesi të gjejë numrin e karaktereve që do të lexohen. Prandaj, është e nevojshme të rregulloni madhësinë e saj në tabelën e shpaketimit:

    // "Thuaji" shpaketuesit madhësinë e skedarit

    fprintf(asamble, "size_skedari i koduar: \n dd %8lxh\n \n", TotalCount) ;

Pas kësaj, të gjithë karakteret e përdorura zhvendosen në fillim të grupit dhe karakteret e papërdorura mbishkruhen (me permutacione).

    // zhvendosni të gjithë karakteret e papërdorura në fund

    i=0;

    numërimi=256 ;

    nderkohe une< count) // derisa të arrijmë në fund

    nëse (Freq[ i] .freq == 0 ) // nëse frekuenca është 0

    Freq[ i] = Freq[ -- numërim] ; // pastaj kopjoni regjistrimin nga fundi

    tjetër

    i++ ; // çdo gjë është në rregull - duke vazhduar.

Dhe vetëm pasi një memorie e tillë "renditëse" ndahet për nyjet (për disa kursime).

    // Alokimi i memories për nyjet

    për (i= 0; i< count; i++ )

    Freq[i] .tabela = TTable e re; // krijoni nyjen

    Freq[ i] .tabela -> majtas= 0 ; // nuk ka lidhje

    Freq[ i] .tabela -> djathtas= 0 ; // nuk ka lidhje

    Freq[ i] .tabela -> ch= Freq.ch ; // kopjoni simbolin

    Freq[i] .freq = Freq.freq ; // dhe frekuencën

    numërimi i kthimit;

Kështu, ne kemi shkruar një funksion për inicializimin fillestar të sistemit, ose, nëse shikoni algoritmin në pjesën e parë të artikullit, "ne shënuam simbolet e përdorura në një kolonë dhe u caktuam atyre probabilitete", dhe gjithashtu krijuam një "pikë fillestare" për çdo simbol - një nyje - dhe e inicializoi atë. Në fusha majtas Dhe drejtë u shkruan zero. Kështu, nëse nyja është e fundit në pemë, atëherë do të jetë e lehtë për t'u parë majtas Dhe drejtë e barabartë me zero.
Krijimi i pemës
Pra, në seksionin e mëparshëm, ne "regjistruam simbolet e përdorura në një kolonë dhe caktuam probabilitete për to." Në fakt, ne u caktuam atyre jo probabilitete, por numërues të një fraksioni (d.m.th., numrin e shfaqjeve të karaktereve në një skedar). Tani duhet të ndërtojmë një pemë. Por për ta bërë këtë, ju duhet të gjeni element minimal në listë. Për ta bërë këtë, ne prezantojmë një funksion në të cilin kalojmë dy parametra - numrin e elementeve në listë dhe elementin që do të përjashtohet (sepse do të kërkojmë në çifte dhe do të jetë shumë e pakëndshme nëse marrim të njëjtin element nga funksion dy herë):

    // kërkoni për nyjen me probabilitetin më të vogël.

    int FindLeast (int count, int index)

    int i;

    DWORD min= (indeksi== 0 ) ? 10 ; // element për t'u numëruar

    // minimale

    për (i= 1; i< count; i++ ) // qark nëpër grup

    nëse (i! = indeks) // nëse elementi nuk është i përjashtuar

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

    min=i; // më pak se minimumi - mbani mend

    kthim min; // ktheni indeksin e minimumit

Kërkimi zbatohet thjesht: së pari, zgjidhni elementin "minimum" të grupit. Nëse elementi që do të përjashtohet është 0, atëherë elementin e parë e marrim si minimum, përndryshe e konsiderojmë zeron si minimum. Ndërsa kalojmë në grup, ne krahasojmë elementin aktual me "minimumin" dhe nëse rezulton të jetë më i vogël, atëherë e shënojmë si minimum.
Tani, në fakt, vetë ndërtimi i pemës funksionon:

    // Funksioni i ndërtimit të pemëve

    void PreInit (int count)

    int ind1, ind2; // indekset e elementeve

    TTable*tabela; // treguesi te "nyja e re"

    ndërsa (numërimi > 1 ) // lak derisa të arrijmë në rrënjë

    ind1= FindLeast(count,- 1); // nyja e parë

    ind2= FindLeast(count,ind1) ; // nyja e dytë

    tabela= newTTable; // krijoni një nyje të re

    tabela->ch=-1 ; // jo përfundimtar

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

    table->right= Freq[ ind2] .tabela ; // 1 - nyja 2

    Freq[ ind1] .ch = - 1 ; // modifikoni hyrjen rreth

    Freq[ ind1] .freq + = Freq[ ind2] .freq ; // frekuenca e karaktereve

    Freq[ind1] .tabela = tabela; // dhe shkruani një nyje të re

    nëse (ind2 != (-- numëro) ) // nëse ind2 nuk është i fundit

    Freq[ind2] = Freq[ numërimi] ; // pastaj në vendin e vet

    // vendos të fundit në grup

Tabela e simboleve të kodit
Pra, ndërtuam një pemë në memorie: morëm dy nyje në çifte, krijuam një nyje të re, në të cilën u shkruan treguesit, pas së cilës nyja e dytë u hoq nga lista, dhe në vend të nyjes së parë, një e re. ishte shkruar me një degë.
Tani lind një problem tjetër: kodimi i pemës është i papërshtatshëm, sepse duhet të dini saktësisht se në cilën rrugë është një personazh i veçantë. Sidoqoftë, problemi zgjidhet mjaft thjesht: krijohet një tabelë tjetër - një tabelë e simboleve të kodit - në të dhe shkruhen kombinime bit të të gjitha simboleve të përdorura. Për ta bërë këtë, mjafton të përshkosh pemën në mënyrë rekursive një herë. Në të njëjtën kohë, për të mos e anashkaluar atë përsëri, mund të shtoni gjenerimin e një skedari assembler në funksionin e anashkalimit për dekodimin e mëtejshëm të të dhënave të ngjeshur (shih seksionin " Dizajn").
Në fakt, vetë funksioni nuk është i komplikuar. Duhet t'i caktojë 0 ose 1 kombinimit të kodit nëse nyja nuk është një nyje terminale, përndryshe shtoni simbolin e kodit në tabelë. Përveç gjithë kësaj, gjeneroni një skedar assembler. Konsideroni këtë funksion:

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

    fprintf (montim,"opcode%08lx_%04x: \n",opcode,len); // etiketa në skedar

    nëse (tbl-> ch!= - 1 ) // nyja fundore

    BYTE mod= 32 - len;

    Opcodes[ tbl-> ch] .opcode = (opcode>> mod) ; // ruaj kodin

    Opcodes[tbl->ch] .len = len; // dhe gjatësia e saj (në bit)

    // dhe krijoni etiketën e duhur

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

    tjetër // nyja nuk është fundi

    opcode>>= 1 ; // liro hapësirë ​​për një bit të ri

    len ++ ; // rrit gjatësinë e fjalës së koduar

    \n",opcode,len);

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

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

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

    // hiqni nyjen (nuk është më e nevojshme)

    fshij tbl;

Ndër të tjera, pasi kalon një nyje, funksioni e fshin atë (liron treguesin). Tani le të kuptojmë se cilat parametra i kalohen funksionit.

  • tblështë nyja që duhet anashkaluar.
  • opcodeështë fjala e kodit aktual. Pjesa më e rëndësishme duhet të jetë gjithmonë e lirë.
  • lenështë gjatësia e fjalës kodike.
Në parim, funksioni nuk është më i ndërlikuar se "faktoriali klasik" dhe nuk duhet të shkaktojë vështirësi.
dalje bit
Kështu, arritëm në pjesën jo më të këndshme të arkivuesit tonë, domethënë, në daljen e karaktereve të kodit në një skedar. Problemi është se simbolet e kodit janë me gjatësi jo uniforme dhe dalja duhet të bëhet pak nga pak. Kjo do të ndihmojë funksionin vendos kodin. Por së pari, le të deklarojmë dy variabla - numëruesin e biteve në një bajt dhe bajtin e daljes:

    // Bit counter

    int OutBits;

    // Karakteri dalës

    BYTE OutChar;

OutBits rritet me një sa herë që del një bit, OutBits==8 sinjalizon që OutChar duhet të ruhet në një skedar.

    PutCode i pavlefshëm (int ch)

    intlen;

    int outcode;

    // merrni gjatësinë e fjalës së koduar dhe vetë fjalën e koduar

    outcode= Opcodes[ ch] .opcode ; // fjala e koduar

    len= Opcodes[ch] .len ; // gjatësia (në bit)

    ndërsa (len > 0 ) // dalje pak nga pak

    // Lëviz derisa ndryshorja OutBits të mos jetë plotësisht e zënë

    ndërsa ((OutBits< 8 ) && (len> 0 ) )

    OutChar>>= 1 ; // liro hapësirë

    OutChar| = ((kodi përfundimtar& 1 )<< 7 ) ; // dhe vendosni pak në të

    outcode>>= 1 ; // pjesa tjetër e kodit

    len -- ; // zvogëloni gjatësinë

  1. OutBits ++ ; // numri i biteve është rritur

  2. }

  3. // nëse përdoren të 8 bitët, atëherë ruajini ato në një skedar

  4. nëse ( OutBits == 8 )

  5. {

  6. fputc( OutChar, jashtë ) ; // ruaj në skedar

  7. OutBits = 0 ; // rivendos

  8. nxjerr jashtë = 0 ; // parametrat

  9. }

  10. }

  11. }

Përveç kësaj, ju duhet të organizoni diçka si "flush", domethënë pas daljes së fjalës së fundit të kodit nxjerr jashtë nuk do të futet në skedarin dalës, sepse OutBits!=8. Nga këtu vjen një funksion tjetër i vogël:

  1. // "Pastro" bufferin e biteve

  2. i pavlefshëm FundPut (i pavlefshëm)

  3. {

  4. // Nëse ka bit në buffer

  5. nëse ( OutBits ! = 0 )

Artikujt kryesorë të lidhur