HW5: Huffmanovo Kódovanie

Odevzdávání části A je ukončeno.
Autor zadání Radoslav Sabol
Úprava Radoslav Sabol
Odevzdávané soubory src/*
Konec odevzdání části A 2026-04-21 24:00
Konec odevzdání části B 2026-05-18 24:00
Opravy v zadání 2026-04-16 21:41 2
Kostru k úkolu nelze stáhnout

Predstavenie úlohy

V kontexte kompresie v informačnej teórii uvažujme o konečnej, neprázdnej množine $\Sigma$ (alebo inač povedané, abecede) a pravdepodobnostnom rozdelení $P: \Sigma \rightarrow [0,1]$. Vašou úlohou je skonštruovať prefixové kódovanie $C: \Sigma \rightarrow \{0,1\}^*$. Kódovanie musí byť také, aby očakávaná dĺžka kódu $L(C) = \sum_{s \in \Sigma} P(s) \cdot\bigl|C(s)\bigr|$ bola asymptoticky minimalizovaná v súlade so Shannonovou entropiou $H(P) = −\sum_{s\in\Sigma} P(s) \log_2 P(s)$, pričom je rešpektovaná štandardizovaná forma kladená kanonickou verziou Huffmanového algoritmu.

Skvelé, nie? A teraz menej formálne. Vašou úlohou je implementovať Huffmanovo kódovanie, teda algoritmus pre bezstrátovú kompresiu dát, ktorý prevedie znaky vstupného súboru (odteraz "symboly") na postupnosti bitov (odteraz "kódové slová") také, že vo výsledku minimalizujete veľkosť výstupného súboru. Intuitívne – symbolom, ktoré sa vo vstupnom súbore vyskytujú najčastejšie, priradíte kratšie kódové slová, zatiaľ čo najviac zriedkavým symbolom priradíte tie najdlhšie.

Huffmanov kód má v praxi široké využitie ako súčasť moderných kompresných algoritmov. Konkrétne je kódovanie súčasťou komplexnejších algoritmov (DEFLATE) a má využitie v bežne používaných formátoch ako ZIP, gzip, a JPEG.

Prvá časť (A)

Vašou úlohou bude implementovať dve funkcie:

int huffman_encode(const char *input_path, const char *output_path);
int huffman_decode(const char *input_path, const char *output_path);

Parameter input_path obsahuje cestu ku vstupnému binárnemu súboru. V prípade, že je cesta nullptr, alebo operačný systém nedokáže otvoriť súbor z ľubovoľného dôvodu, vypíšte na štandardný chybový výstup vhodnú hlášku a program ukončite.

Podobné pravidlá platia pre cestu ku výstupnému súboru output_path s jednou výnimkou. V prípade, že je output_path nullptr, vypíšte výstup na stdout.

Obe funkcie vrátia 0 v prípade úspechu a ľubovoľné iné číslo v prípade chyby. Program tak isto skončí s chybou v prípade, že je vstupný súbor prázdny.

Algoritmus pre huffman_encode

Uvažujme, že symbol má dĺžku 1 bajt.

Postup je nasledovný:
  1. Pre každý znak vo vstupnom súbore spočítame jeho absolútnu frekvenciu, t.j. jeho počet výskytov.

  2. Vypočítajte optimálnu dĺžku kódu pre každý symbol vyskytujúci sa v súbore. Podľa algoritmu je potrebné vybudovať binárny strom odspodu nahor, kde listy obsahujú samotné symboly a ich frekvencie. Ku konštrukcii sa vám hodí vaša obľúbená implementácia prioritnej fronty, a mala by prebiehať nasledovne:

    1. Do prioritnej fronty uložte všetky listy stromu (teda symboly a ich frekvencie)

    2. Keď má prioritná fronta len jeden prvok, tak váš strom je hotový a onen prvok sa stáva koreňom stromu

    3. Vyberte 2 najmenšie prvky z prioritnej fronty. Vytvorte nový uzol – jeho potomkovia budú práve vybrané prvky a jeho frekvencia bude súčet frekvencií potomkov. Vložte nový uzol do prioritnej fronty.

    4. Vráťte sa na krok b.

      • Strom máte hotový. Dĺžka optimálneho kódovania pre daný symbol v bitoch je dĺžka cesty od koreňa stromu ku listu dotyčného symbolu. V momente, ako si tieto dĺžky uchováte, strom už nebudete naďalej potrebovať.

  3. Priraďte symbolom konkrétne kódové slová podľa kanonického Huffmanovho kódovania:

    1. Zoradte symboly vzostupne: primárne podľa dĺžky kódu, sekundárne podľa hodnoty symbolu (0–255)

    2. Prvému symbolu v zoradenom zozname priraďte kód = 0

    3. Pre každý ďalší symbol v zozname:

      • Ak má rovnakú dĺžku ako predchádzajúci symbol: kód = predchádzajúci_kód + 1

      • Ak je dlhší o k bitov: kód = (predchádzajúci_kód + 1) << k

  4. Huffmanov kód je hotový, môžete ho použiť na zakódovanie vstupného súboru.

Variant, ktorý v tejto úlohe implementujeme, je takzvaný "Kanonický Huffmanov kód". Od klasického algoritmu sa líši predovšetkým v kroku 3, ktorý nám zabezpečuje deterministickejšie správanie v prípadoch, že niektoré symboly majú identické frekvencie.
Vstupný súbor môže byť ľubovoľnej dĺžky – v žiadnom prípade ho nenačítajte do pamäte celý, aj keď to pre vás bude znamenať viacero priechodov

Požiadavky na výstup

V tejto sekcii popíšeme, ako by mal vyzerať obsah výstupného súboru pre huffman_encode. Bohužiaľ nestačí zapísať len zakódovaný bitový prúd – navyše potrebujeme aj určitú nápovedu, aby bol súbor deterministicky dekódovateľný. Preto bude výstupný súbor rozdelený na 3 časti:

+------------------+------------------+------------------+
| number_of_codes  |  code_lengths    |  encoded_input   |
|   (8 bytes)      |  (256 bytes)     |  (variable)      |
|   uint64_t LE    |  256× uint8_t    |  bitový prúd     |
+------------------+------------------+------------------+
  • number_of_codes – hodnota typu uint64_t v little-endian formáte, obsahuje počet zakódovaných symbolov. Tento parameter vám pri dekódovaní pomôže rozoznať rozdiel medzi validnými kódovými slovami a zarovnaním v poslednom bajte.

  • code_lengths – pole 256 hodnôt typu uint8_t, kde code_lengths[i] obsahuje dĺžku kódu pre symbol s hodnotou i (0–255). V prípade, že sa znak vo vstupe nevyskytuje, ponechajte túto hodnotu na nule.

  • encoded_input – sekvencia bitov zodpovedajúca kanonickému Huffmanovmu kódovaniu. V prípade, že posledný bajt nie je dokonale zarovnaný, doplníme ho nulami.

Príklad

Máme vstupný súbor s nasledujúcim obsahom: "eebbeecdebeeebecceeeddeb bbeceedebeeddeeeeccee eedeeedeeebeedeceedeb eeedeceeedebe"

Výsledné kódovanie máte v tabuľke nižšie. Pri výpočte kódov sa používa kanonické zoradenie: najprv podľa dĺžky kódu, potom podľa hodnoty symbolu.

Symbol Frekvencia Dĺžka kódu Kódovanie

'e'

48

1

0

'd'

12

2

10

'b'

11

3

110

medzera

3

4

1110

'c'

8

4

1111

Pre lepšie pochopenie kroku 3 (priradenie kódov) uvádzame detailný výpočet:

Symbol Dĺžka Výpočet kódu Kód (binárne)

'e'

1

0

0

'd'

2

(0 + 1) << 1 = 2

10

'b'

3

(2 + 1) << 1 = 6

110

medzera

4

(6 + 1) << 1 = 14

1110

'c'

4

14 + 1 = 15

1111

Dĺžky kódovaní boli odvodené zo stromu, ktorý nájdete nižšie:

        (82)
       /    \
    (34)     e(48)
   /    \
d(12)   (22)
       /    \
    (11)     b(11)
   /    \
space(3) c(8)

Po zakódovaní bude výstupný súbor v hexadecimálnom zápise vyzerať následovne:

00000000: 5200 0000 0000 0000 0000 0000 0000 0000  R...............
00000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000020: 0000 0000 0000 0000 0400 0000 0000 0000  ................
00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000060: 0000 0000 0000 0000 0000 0304 0201 0000  ................
00000070: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000080: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000090: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000100: 0000 0000 0000 0000 363e 619f e29b b679  ........6>a....y
00000110: 3141 fe71 0862 7937 09e2 60              1A.q.by7..`
  1. 5200 0000 0000 0000 (little-endian) je number_of_codes, ktorý odpovedá číslu 82

  2. Následuje dlhá séria núl, keďže valná väčšina znakov je nevyužitých. Každopádne, môžete vidieť, ako prvá 04 odpovedá dĺžke kódovania pre medzeru (ASCII 32), a 0304 0201 sú dĺžky kódovania pre znaky 'c', 'b', 'd', 'e'

  3. od 363e ďalej je samotný obsah súboru. V binárnej podobe pre lepšie porovnanie s tabulkou vyzerá obsah následovne:

    00000108: 00110110 00111110 01100001 10011111 11100010 10011011  6>a...
    0000010e: 10110110 01111001 00110001 01000001 11111110 01110001  .y1A.q
    00000114: 00001000 01100010 01111001 00110111 00001001 11100010  .by7..
    0000011a: 01100000

Algoritmus pre huffman_decode

Dekódovanie vstupného súboru by malo prebiehať nasledovne:
  1. Načítajte hlavičku súboru, ktorá obsahuje počet symbolov v kóde a dĺžky prefixových kódov pre každý symbol.

  2. Keďže máte k dispozícii dĺžky kódov podobne ako pri zakódovaní, zopakujte 3. krok z algoritmu kódovania, aby ste získali konkrétne kódové slová.

  3. Kódové slová použite pri dekódovaní súboru. Pre vyhľadávanie správneho symbolu použite vhodnú dátovú štruktúru.

Do výstupného súboru zapisujte už len samotný obsah dekódovaného súboru. Intuitívne, výstup pre encode a následný decode by mal byť totožný so vstupným súborom. Navyše, keď nájdete akýkoľvek problém s konzistenciou (počet symbolov v hlavičke nesúhlasí s počtom symbolov v obsahu, obsah obsahuje znaky, ktoré nie sú Huffmanovým kódom…​), program okamžite ukončite s vhodnou chybovou hláškou.

Testovanie

V adresári test/ máte okrem základných testov dodanú malú vzorku súborov v test/test_files - členenie do podadresárov je následovné:
  1. input/ - vstupné súbory

  2. output/ - očakávané výstupy

  3. test_outputs/ - adresár do ktorého sa zapisujú výstupy vášho riešenia - môžete ich použiť na ďalšie ladenie

Keďže testy majú cesty k súborom zadané na pevno, budete ich musieť spúšťať z adresára hw5, a to následovne:

./build/test/src-test

Poznámky

  • Dbajte na korektnú prácu so súbormi a dynamicky alokovanou pamäťou.

  • Ošetrite všetky chybové stavy a vypíšte zmysluplné chybové hlášky.

Opravy v zadání

Reformat the new block about testing

1fd3be3 2026-04-16 21:41 Roman Lacko
 -179,2 +179,3  Po zakódovaní bude výstupný súbor v hexadecimálnom zápise vyzerať násle
 === Algoritmus pre huffman_decode
+
 .Dekódovanie vstupného súboru by malo prebiehať nasledovne:
 -190,2 +191,3  Navyše, keď nájdete akýkoľvek problém s konzistenciou (počet symbolov v h
 === Testovanie
+
 .V adresári `test/` máte okrem základných testov dodanú malú vzorku súborov v `test/test_files` - členenie do podadresárov je následovné:
 -194,2 +196,3  Navyše, keď nájdete akýkoľvek problém s konzistenciou (počet symbolov v h
 . `test_outputs/` - adresár do ktorého sa zapisujú výstupy vášho riešenia - môžete ich použiť na ďalšie ladenie
+
 Keďže testy majú cesty k súborom zadané na pevno, budete ich musieť spúšťať z adresára `hw5`, a to následovne:

Clarify student testing

3b7f0b4 2026-04-16 16:49 Radoslav Sabol
 -189,2 +189,10  Navyše, keď nájdete akýkoľvek problém s konzistenciou (počet symbolov v h
 
+=== Testovanie
+.V adresári `test/` máte okrem základných testov dodanú malú vzorku súborov v `test/test_files` - členenie do podadresárov je následovné:
+. `input/` - vstupné súbory
+. `output/` - očakávané výstupy
+. `test_outputs/` - adresár do ktorého sa zapisujú výstupy vášho riešenia - môžete ich použiť na ďalšie ladenie
+Keďže testy majú cesty k súborom zadané na pevno, budete ich musieť spúšťať z adresára `hw5`, a to následovne:
+
+    ./build/test/src-test
 
vygenerováno 2026-05-09 11:01 upravit