Josef Troch (troch@mail.ru)
29.8.2000 (poslední změna 8.11.2000)

Algoritmy LZFG

Pojmem LZFG se označuje několik příbuzných komprimačních algoritmů, které vyvinuli Edward Fiala a Daniel Greene roku 1989. Tyto algoritmy jsou bezztrátové a adaptivní - jsou to slovníkové metody, které jsou v podstatě "kříženci" mezi metodami LZ 77 a LZ 78. Z tohoto důvodu se o těchto metodách krátce zmíním.



LZ 77 i LZ 78 jsou nazvány dle svých autorů - Abraham Lempel a Jacob Ziv a roku uveřejnění.
První (a účinnější) z těchto metod - LZ 77 - funguje na principu tzv. posuvného okna (sliding window) - toto je rozděleno na dvě části: prohlížecí okno (search buffer) a aktuální okno (look-ahead buffer). Prohlížecí okno je na začátku prázdné a v aktuálním je začátek dat, která chceme komprimovat - posuvné okno se pak posouvá "směrem doprava" - tedy prohlížecí okno se zaplňuje tím co bylo v aktuálním a do aktuálního se načítají nová data. Vlastní komprese probíhá tak, že se snažíme najít co nejdelší předponu slova v aktuálním okně, která je shodná s nějakým slovem, které začíná v prohlížecím okně. Tuto zakódujeme trojicí [i, j, k], kde i je vzdálenost začátku nalezeného slova v prohlížecím okně od začátku aktuálního okna, j je délka zmíněné předpony, a k je první znak následující za touto předponou (tedy pro triviální předponu [0, 0, k] ). Posuvné okno posuneme o j+1 znaků doprava.
Dekomprese je zde trivialní - kopíruje příslušné řetězce (po znacích - kvůli slovům, která zasahují do (ještě neznámého) aktuálního okna), přidává za ně příslušný znak k a posunuje okno.
Slovníkem je zde tedy prohlížecí okno. U této metody je bohužel dosti pomalá komprese (při klasické implementaci), narozdíl od metody LZ 78.
Metoda LZ 78 si vytváří slovník explicitně v paměti - tento je na začátku prázdný. Ze vstupu načteme nejdelší frázi f, která je již ve slovníku a jeden znak po ní následující - označme ho z. Na výstup pak zapíšeme dvojici [i ,z], kde i je ukazatel (číslo řádku) do slovníku na frázi f. Do slovníku je pak přidána fráze fz. Při zaplnění slovníku je třeba nějak ho reorganizovat - třeba smazáním celého slovníku.
Dekomprese je zde také jednoduchá - dekompresor si totiž vytváří slovník, který je v každém okamžiku stejný se slovníkem kompresoru a tak dle načtených dvojic snadno rekonstruuje původní data.
Tento algoritmus je sice rychlejší, ale v typickém případě má o něco horší kompresní poměr než LZ 77.



Všechny metody třídy LZFG jsou založeny na následující myšlence: enkoder generuje komprimovaný soubor obsahující dva druhy dat "tokeny" a ASCII kódy znaků (literály).
Tokeny mohou být dvojího druhu: literal token ("znakový") a copy token ("kopírovací").

Literal token v komprimovaném souboru znamená, že teď následuje řetězec literálů (ASCII kódů) - tedy objeví-li se v komprimovaném souboru "(literal x)" znamená to příkaz pro dekompressor - načti x znaků z komprimovaného souboru a zapiš je přímo na výstup (dekomprimovaný soubor).
Copy token ukazuje na řetězec, který už se objevil ve zpracovávaných datech - přesněji: objeví-li se v komprimovaném souboru "(copy x, y)" říká to dekompresoru, aby se vrátil o y znaků ve výstupním (dekomprimovaném) souboru a zkopíroval odtud x znaků na konec výstupního souboru.

Příklad:
Z řetězce "the_boy_on_my_right_is_the_right_boy" po zakódování může vzniknout:
"(literal 23)the_boy_on_my_right_is_(copy 4, 23)(copy 6, 13)(copy 3, 29)"

Dosažená komprese závisí mimo jiné na tom, jakým způsobem budeme zapisovat (tedy kolik místa zaberou) literal a copy tokeny.

Metoda A1

Metoda A1 je nejjednodušší metoda třídy LZFG.
Používá 8 bitů pro literal token a 16 bitů pro copy token.

Rozlišení a formát tokenů: Kompresor i dekompresor používají posuvné okno podobně jako algoritmus LZ 77 (viz výše), ovšem toto bude jinak reprezentováno pro urychlení komprese. V každém kroku se kompresor rozhoduje, zda zapíše literal token či copy token tímto způsobem: Příklad:
Algoritmus A1 tedy zakóduje text z předchozího příkladu takto:
"(literal 16)the_boy_on_my_ri(literal 7)ght_is_(copy 4 23)(copy 6 23)(copy 3 29)"

Algoritmus A1 je podobný metodě LZ 77, ale liší se mimo jiné použitím specielního literal tokenu. Empirické studie autorů algoritmu ukázaly, že pro anglický text a zdrojové soubory lze mírně zlepšit kompresní poměr zmenšením maximální délky v literal tokenu o 1 bit. Tímto ovšem přijdeme o možnost práce se souborem po bytech (literal token by byl 7 bitů).
Algoritmus A1 byl vytvořen na kompresi 8 bitového textu, ale ukazuje se jako celkem vhodný i na ostatní druhy dat.

Datové struktury pro algoritmus A1

Aby bylo možno najít nejdelší shodu (viz výše) co nejrychleji (tedy aby byl celý algoritmus rychlý) je nutno použít netriviální datové struktury.
Tedy máme posuvné okno pevné délky a sestavíme pro něj strom, obsahující všechny přípony slova v tomto okně (suffix tree).

Pro algoritmus A1 se používá modifikace struktury zvané McCreight`s suffix tree, která je sama modifikací Morrisonova PATRICIA tree, který je založen na datové struktuře zvané Trie.

Trie je stromová struktura, kde větvení nastává podle "pozic" klíče (místo dle porovnání mezi klíči) - zde "pozice" = písmeno - tedy v hloubce l ve stromu bude docházet k větvení podle l-tého písmene slov ve stromu. Každá hrana tedy odpovídá právě 1 písmenu a každý vrchol reprezentuje řetězec, který získáme na cestě z kořene do tohoto vrcholu. Příklad: budeme-li chtít sestrojit strom obsahující jediné slovo délky 20, bude mít hloubku 20 (bude to cesta).
Trie
Obrázek 1

Výše uvedený obrázek zobrazuje Trie pro 4 slova. Je zjevné, že mnoho vnitřních uzlů tohoto stromu je nadbytečných, neboť mají jen jednoho syna.

Pokud chceme vytvořit slovník pro nějaký soubor, ušetříme místo, odstraníme-li nadbytečné vnitřní uzly a použijeme-li ukazatele do souboru místo ukládání všech příslušných znaků v datové struktuře. Tímto dostáváme v podstatě Morrisonův PATRICIA tree, který je zobrazen na níže uvedeném obrázku.
PATRICIA Tree se suffix pointerem
Obrázek 2

K obrázku 2: Protože pro nás pozice odpovídají bytům, uzel ve stromě může mít až 256 synů. Jelikož 256 synů téhož listu se objeví jen zřídka, nevyhradíme na ně místo, ale raději použijeme hašování.



Pro algoritmus A1, potřebujeme vždy najít nejdelší (maximálně 16 znaků) shodu s řetězcem v aktuálním okně, která začíná na kterékoli z předchozích 4096 pozic.
Pokud všech předchozích 4096 řetězců bylo uloženo do PATRICIA tree s hloubkou d=16 (což zajistíme - viz dále), nalezení této shody je triviální (přímý průchod stromem od kořene).
Bohužel vkládání těchto řetězců může být časově náročné (Pokud jsme pro vložení řetězce začínajícího na pozici i prošli přes d pater stromu, při vkládání řetězce z pozice i+1 projdeme alespoň do hloubky d-1. Což vede k časové složitosti O(n.d) v nejhorším případě (n je délka souboru).).
Protože v dalších algoritmech bude d výrazně větší než 16, je třeba ho odstranit z časové složitosti výpočtu.

Pro vkládání řetězců v čase O(n) McCreight přidal do PATRICIA tree "suffix pointery". Tedy každý vnitřní vrchol stromu, reprezentující řetězec aX na cestě z kořene do tohoto vrcholu, má pointer na vrchol reprezentující X (tedy řetězec získaný z aX odstraněním prvního písmene).
Pokud byl v předchozím kroku přidán řetězec začínající na i-té pozici v souboru do hloubky d ve stromě, není třeba se vracet do kořene stromu pro přidání řetězce začínajícího na i+1 - místo toho použijeme suffix pointer, který nás dovede k příslušnému uzlu v hloubce d-1, odkud pokračujeme klasicky (procházením stromu).
Vytváření a používání suffix pointerů
Obrázek 3

Výše uvedený obrázek ukazuje vytváření a používání suffix pointerů.
Předpokládejme, že v předchozím kroku jsme nalezli maximální řetězec shodující se (s aktuálním řetězcem) aXY, kde a je jeden znak, X a Y jsou řetězce a b je první neshodující se znak za Y (ze vstupu).
Obrázek 3 ukazuje komplikovaný případ, kde byl do stromu přidán nový vnitřní vrchol (v1) a je třeba pro něj vytvořit suffix pointer.
Následující řetězec XYb přidáme tak, že jdeme z v1 do jeho otce (v2 - odpovídá aX), pak následujeme příslušný suffix pointer do v3 (odpovídá X), odtud sestupujeme hlouběji do stromu - nejprve znovu dle řetězce Y ("rescanning") do v4, pak z něj dolů, čímž se přidá nový řetězec. Název "rescanning" vychází z toho, že se pohybujeme dle části řetězce, která byla použita už při předchozím přidání fráze a tak není nutno porovnávat řetězce na hranách (stačí písmena přímo v dat. struktuře a údaje o délce) - což potřebujeme pro lineární časovou složitost.
"Rescanning" skončí buď v již existujícím uzlu v4 (ten má suffix pointer) nebo v uzlu v4, který musí být vytvořen kvůli vložení XYb (XYb bude odpovídat vrcholu, který bude synem v4) - pro tento nový uzel ovšem suffix pointer vytvoříme v další iteraci (přidávání další fráze).
Tedy každý uzel (kromě posledního vytvořeného) má suffix pointer.



Protože algoritmus A1 má pevnou velikost prohlížecího okna (4096 bytů), potřebujeme být schopni vymazávat části stromu, odpovídající řetězcům, které začínají dále než před 4096 byty v souboru.
Z tohoto důvodu je třeba k naší stromové struktuře něco přidat: Bohužel tím, že jsme odstranili nutnost vracet se do kořene při každém přidání fráze, mohlo by se stát, že se budeme stále pohybovat jen ve "spodních částech" určitých podstromů ("větvích") stromu a pointery ("position") v jejich částech blížších ke kořeni se stanou zastaralými (t.j. budou ukazovat na řetězce již za levým okrajem prohlížecího okna). Procházení stromu po "větvi" vždy až ke kořeni by také nebylo řešením - přišli bychom o všechny výhody suffix pointerů.
Lze udržet pointery platné bez nutnosti časově náročných updatů pomocí "častečné" aktualizace (updatu) při každém přidání fráze.
Spočítáme-li čas potřebný k updatu pointrů přes všechny nové listy, zjistíme, že na každý list vychází 2 updaty. Tyto "částečné" updaty budou mít za výsledek udržení všech pointerů na posledních 4096 bytech v souboru což je to, co jsme potřebovali. Důkazy obou výše zmíněných faktů lze nalézt ve Fialově a Greenově článku (str. 493).

Shrnutí operací jednoho kroku (přidání fráze) algoritmu (dle obrázku 3):

Jednodušší datová struktura pro A1

Vzhledem k tomu, že implementace algoritmu A1 pomocí výše uvedených datových struktur je komplikovaná a tyto datové struktury mají dosti velké paměťové nároky, uvádí Fiala a Greene ještě jednu variantu.
Tato varianta vychází z předchozích datových struktur (tedy z PATRICIA tree + cyklický řešená fronta listů), ale používá jednodušší metody na nalezení přípony (suffixu) při vkládání nové fráze a pro updatování položky "position" (tedy ukazatele do souboru) v uzlech (kvůli posunům prohlížecího okna).

Místo "částečných" updatů lze updatovat položku "position" ve všech uzlech na cestě nahoru do kořene, pokaždé, když přidáme nový list. Tímto se zbavíme nutnosti jakýchkoli aktualizací, když uzly mažeme. Zároveň můžeme odstranit update bity (už nejsou potřeba).

Alternativa k suffix pointerům už je složitější. Časová náročnost pohybu po stromě není vždy stejná - pohyb dolů vyžaduje nahlédnutí do hašovací tabulky, což trvá déle než použití ukazatele ze syna na otce. Proto rozumná cesta k nalezení potřebné přípony (z uzlu odpovídajícího aX chci nalézt uzel odpovídající X , a - znak, X - řetězec) je nalézt "suffix leaf" - tedy list reprezentující slovo, jehož nějaká předpona je hledanou příponou (X) a odtud postupovat po ukazatelích na otce směrem nahoru ke kořeni, dokud nenajdeme uzel odpovídající hledané příponě (tedy místo, kam by směřoval suffix pointer, kdyby zde byl).
Zbývá problém, jak nalézt "suffix leaf". To ovšem není tak obtížné - víme, že řetězec na pozici i v prohlížecím okně (aX) se shoduje (myslím nejdelší shodu předpon - jako výše) s řetězcem na nějaké předchozí pozici j v prohlížecím okně - tedy hledaný "suffix leaf" je položka následující za listem odpovídajícím j ve frontě listů.
Pozn.: Tato fronta musí být řešena jako pole (pro rychlý přístup) - tedy bude se zde měnit začátek této fronty (bude "cyklicky běhat" po poli) - hledaný list najdeme přičtením j+1 k začátku fronty, pak výsledek mod 4096.

Metoda A2

Velikost prohlížecího okna 4096 bytů je (zhruba) optimální pro copy a literal tokeny pevné délky (8b, 16b). Zvětšíme-li velikost prohlížecího okna, bude možno zvýšit velikost kopírovaných dat u copy tokenů (objeví se shody větší délky), ale toto vynutí zvětšení prostoru pro uložení vzdálenosti v copy tokenu.
Pro dosažení vyšší komprese je při použití většího okna vhodné použít kódování čísel s proměnnou délkou zápisu (variable-width encoding), které bude vycházet z toho, že bližší pozice v okně budou častěji používány v copy tokenech než pozice vzdálenější.
Fiala a Greene dospěli k názoru, že bude nejlépe použít vhodnou kombinaci binárního a unárního kódu a zavádějí start-step-stop kódy.

Start-step-stop kódy

Tyto kódy mají tento formát: začínají unárním kódem jakéhosi čísla n (tedy n jedniček), pak následuje jedna 0 a pak binární číslo délky start+n.step bitů. Čísla n probíhají od 0 do (stop-start)/step.
Pokud délka zmíněného binárního čísla je právě stop bitů, závěrečná 0 za unárním kódem se vynechává.

Mějme start-step-stop kód nějakého čísla x : unární kód mi tedy udává délku následujícího binárního kódu a tento binární kód po přičtení počtu všech čísel s kratším kódem (než je kód x) dává "normální" vyjádření čísla x.
Těmto kódům jsou přirozená čísla (včetně 0) přiřazena sekvenčně (tedy nejmenší čísla mají nejkratší kódy). Uvádím příklad pro (3, 2, 9) kód:

n kód rozsah
0 0xxx 0-7
1 10xxxxx 8-39
2 110xxxxxxx 40-167
3 111xxxxxxxxx 168-679


Tedy příslušný start-step-stop kód pro 7 bude 0111 a pro 10 bude 1000010.

Uvádím ještě jednoduchou funkci v programovacím jazyce C na výrobu start-step-stop kódů - tato volá 2 jednoduché funkce - zapisbit(x) (která zapíše 0, je-li x rovno 0, jinak 1) a zapbin(x, p) (která zapíše x jako binární číslo o p bitech):
void sss(int start,int step,int stop,int cislo)
       //start, step, stop: jaky kod pouzit; cislo: cislo k zakodovani
{int k=start;
 while (cislo>=(1<<k))    //(1<<k) je jiny zapis pro 2 na k
              //zde kontroluji, zda se cislo vejde uz do teto
   {zapisbit(1);          //skupiny kodu, jinak zvetsuji n
    cislo-=(1<<k);
    k+=step;
   }                      //mam prislusny unarni kod
 if (k!=stop) zapisbit(0);
 putchar(':');
 zapbin(cislo,k);         //zapisu prislusne binarni cislo
}


Algoritmus A2 je slovníková metoda, podobná algoritmu A1.
K ukládání délky shodujícího se řetězce v copy tokenu používá (2, 1, 10) start-step-stop kód, který umožňuje, aby délka tohoto řetězce byla až 2044 znaků (kódy jsou o 1 posunuty, protože nepotřebujeme kód pro délku 1; tedy 001 znamená 2 atd.).
Kód 000 je vyhrazen pro signalizaci, že jde o literal token. Pro kódování délky následujícího řetězce literálů (ASCII kódů) je použit (0, 1, 5) kód, tedy maximální délka tohoto řetězce je 63 znaků.
Pokud tedy první 3 bity tokenu nejsou 000, jde o copy token a následuje odkaz na příslušnou pozici v okně zapsaný (10,2,14) kódem, což nám umožní velikost okna až 21504 bytů.

Pro rozhodování mezi copy tokenem a literal tokenem je zvolena stejná strategie jako u algoritmu A1.

Jsou zde ještě tři drobná vylepšení, která algoritmus A2 používá pro zlepšení komprese (dle Fialy a Greena o 1 - 2 %)
  1. Protože za řetězcem literálů kratším než maximální možná délka (63) nemůže následovat ani další literal token, ani copy token délky 2, posuneme kód délky v následujícím copy tokenu o 2 dolů - tedy kódy 000, 001, 010 budou znamenat shodu v 3, 4, 5 znacích (místo literal, 2, 3). Tímto způsobem se zvětší (ve výše uvedeném případě) maximální zakódovatelná délka v copy tokenu na 2046 bytů.

  2. Jelikož pro kódování začátků souborů, či souborů krátkých nepotřebujeme velikost okna celých 21K, použijeme pro ukládání odkazu na pozici v copy tokenu kód se vzrůstající délkou, tedy (10 - x, 2, 14 - x) kód, kde x je na začátku 10 a klesá až na 0 podle toho, jak se prohlížecí okno postupně zaplňuje.
    Tedy kód, kde x = 10 (který se použije na začátku) má 21 hodnot, v okamžiku, kdy počet znaků v okně (tedy platných pozic) přesáhne 21, snížíme x na 9 atd.
    Je dobré si uvědomit, že toto vylepšení se použije jen na začátku komprese, dokud celé 21K velké okno není zaplněno - výrazné zkrácení se tedy projeví jen u malých souborů.

  3. Každý z výše uvedených kódů je schopen adresovat pevný počet pozic (od 21 pro x=10 po 21504 pro x=0). Pokud ještě okno není zcela zaplněno, či uživatel se rozhodne použít prohlížecí okno menší velikosti, nejdelší z kódů by nebyly nikdy použity (protože by to byly odkazy na neplatné pozice v prohlížecím okně - t.j. zatím nezaplněné, či při menší velikosti okna pozice za levým okrajem okna).
    Z tohoto důvodu se zde používá fázování při zápisu binární části start-step-stop kódu odkazu na pozici. Tedy místo zapsání všech hodnot k-bitově, nižší hodnoty jsou zapsány jako kratší (k-1 bitová) čísla a vyšší hodnoty jako k bitová.
    Přesněji (už jen pro binární čísla) - potřebujeme-li používat čísla 0..p-1, kde 2k-1 < p < 2k, číslo i zapíšeme takto: je-li i < 2k - p, použijeme k - 1 bitový binární kód čísla i; v opačném případě použijeme k bitový binární kód čísla i + 2k - p.
    Příklad: uživatel se rozhodne používat prohlížecí okno velikosti 16384 B, tedy je nutno použít (10, 2, 14) kód ( (9, 2, 13) kód má jen 10752 hodnot). Tento má 21504 hodnot, tedy nejdelších 5120 by nebylo nikdy použito. Poslední skupina kódů ("11" následováno 14 bitovým číslem) má 16384 různých hodnot, ze kterých by se použilo jen prvních 11264. Místo toho budeme zapisovat prvních 5120 z nich jako "11" a 13 bitové číslo a zbylých 6144 jako "11" a 14 bitové číslo.
Všechna tato 3 vylepšení povedou jen k velmi malému zvýšení komprese za cenu výrazně pomalejšího kódování.

Datové struktury pro algoritmus A2

Je možno použít obě datové struktury uvedené u algoritmu A1, ale každá z nich má své nevýhody, které zde uvádím.

První datová struktura (se suffix pointery a "částečnými updaty") by se zdála být výhodnější kvůli velké maximální délce řetězce v copy tokenu, která je zde až 2044 (2046).
Bohužel způsob aktualizace pozic (pointerů do okna) zde nezaručuje, že ve struktuře bude uložena vždy nejbližší pozice shodujícího se řetězce, proto se může občas stát, že budou použity v copy tokenech delší kódy pozice, než by bylo nezbytné. Tento problém ovšem není až tak vážný, neboť strom často nemá příliš velkou hloubku a uzly blízko kořene mají obvykle mnoho synů, takže updaty postupují stromem směrem ke kořeni celkem rychle.

Druhá (jednodušší) datová struktura, kde probíhají updaty vždy až ke kořeni, může na určitých ("nevhodných") datech fungovat až 20 krát pomaleji, ale dosáhne optimálního výsledku (pokud jde o kompresní poměr).
Autoři uvádí, že na běžných souborech komprese algoritmu užívajícího datovou strukturu se suffix pointery je asi o 0,4 % horší než u algoritmu využívajícího jednodušší datovou strukturu.

Metody B1 a B2

Algoritmus A2 má velmi rychlou dekompresi s minimálními nároky na paměť, ale komprese ačkoli probíhá v lineárním čase (vzhledem k délce souboru) je asi 5 krát pomalejší než dekomprese. Algoritmy A1 a A2 jsou tedy vhodné pro použití v případech, kdy není nutná maximální rychlost.

Rychlejší metody, označované jako B1 a B2 byly vyvinuty pro archivaci souborů, kde, dle názoru autorů, metody A1 a A2 jsou příliš pomalé.
Metody B1 a B2 vychází po řadě z metod A1 a A2 (používají stejné způsoby kódování pro copy a literal tokeny, tedy u B2 všechna 3 vylepšení uvedená u metody A2 (kód (10 - x, 2, 14 - x), fázování, posunutí po literálu kratším než 63 znaků)), ale počítají odkazy na pozice do prohlížecího okna (pro copy tokeny) odlišně.
Tyto odkazy na pozice mohou ukazovat jen na místo, odpovídající začátku řetězce, který byl zakódován nějakým předchozím copy tokenem nebo na místa, která byla zapsána pomocí literal tokenu (klidně i doprostřed řetězce literálů).
To znamená, že zde již nelze adresovat všechny pozice v prohlížecím okně.
Výše uvedeným se tyto algoritmy blíží metodě LZ 78.
Pozn.: Pozice v okně, počítané tímto způsobem jsou označovány jako "compressed displacements" (stlačená nahrazení / lépe: stlačené počáteční pozice), asi proto, že tímto dojde k výraznému zvětšení prohlížecího okna a místa, kde mohou copy tokeny začínat jsou "stlačena" k sobě, jako by okno bylo výrazně menší (tedy pro okno délky 200 KB bude třeba jen 16384 počátečních pozic - což by u A1 a A2 odpovídalo oknu velikosti 16 KB). Počáteční pozicí myslím pozici, kam může "ukazovat" copy token.
Délka v copy tokenu je stále udávána v počtu znaků (jako A1 a A2).

Zavedením tohoto způsobu číslování pozic v okně se rychlost komprimace zhruba ztrojnásobí, ovšem rychlost dekomprese a rychlost, jakou se algoritmus adaptuje na příchozí data se o něco sníží.

Datové struktury pro B1 a B2

Vzhledem k výše uvedenému způsobu číslování pozic v prohlížecím okně stávají se suffix pointery a "částečné updaty" zbytečnými, tedy vhodnou datovou strukturou je jednodušší PATRICIA tree. Nové položky jsou do slovníku vkládány jen na "hranicích" řetězců odpovídajících tokenům (copy tokeny + literály) (vstupní data jsou vlastně rozdělena na disjunktní řetězce a každý z nich se do slovníku přidává právě jednou), což lze provést jednoduše v lineárním čase tak, že každou iteraci začneme u kořene a procházíme dolů.
Je užitečné vytvořit pole "permanentních" uzlů (tedy tyto se nebudou v průběhu času rušit) pro všechny znaky (odpovídají hloubce 1 ve stromě). Protože copy token nikdy nemůže ukazovat na řetězec délky 1, nemusí nám vadit, že výše zmíněné permanentní uzly nebudou odpovídat žádné pozici v prohlížecím okně.
Tedy každá iterace začíná indexováním do tohoto pole, pak nahlédnutím do hašovací tabulky a porovnáváním se znaky na hranách stromu sestupujeme hlouběji do stromu (jako u A1). Nová pozice prohlížecího okna je zapsána do vrcholů při tomto pohybu stromem dolů, čímž je vyřešena aktualizace pozic ve stromě.

Tímto jsme se tedy zbavili všech komplikací nutných pro dosažení lineární časové složitosti u algoritmu A2.
Bohužel, jedna nová komplikace se objevila: 16384 pozic v prohlížecím okně algoritmu B2 (u B1 je tentýž problém, jen počet pozic je nižší) může odpovídat prohlížecímu oknu dlouhému miliony znaků (což nemůžeme dovolit, nemáme nekonečně velkou paměť). Autoři tedy zvolili za limit velikosti prohlížecího okna 12 . 16384 znaků. Pokud velikost okna přesáhne tento limit, listy pro nejstarší pozice v okně jsou ze stromu odstraněny (viz výše u algoritmu A1).



Dekompresor si nemusí udržovat PATRICIA tree, aby zjišťoval jakým pozicím odpovídají "stlačené pozice" které získává z komprimovaného souboru. Na toto mu stačí "přečíslovávací tabulka" - obyčejné pole, kam přistoupí pomocí "stlačené pozice" jako indexu a získá "normální" pozici do prohlížecího okna (tabulku si musí ovšem aktualizovat - po zaplnění prohlížecího okna v každé iteraci přepíše její nejstarší položku).

Výsledky metod B1 a B2

Protože se algoritmus B2 pomaleji přizpůsobuje měnícímu se charakteru vstupních dat, obvykle malé soubory komprimuje o něco hůře než algoritmus A2. Ale na textových souborech a zdrojových souborech programů dosahuje B2 asymptoticky o 6 - 8 % lepších výsledků než A2; přelom od horší komprese k lepší (porovnáváno s A2) nastává zhruba po 70000 znacích. A2 je schopen lépe zachytit blízký kontext, B2 je schopen (kvůli "compressed displacements") odkázat dále zpátky do souboru. Toto je pro B2 výhoda na souborech s "přirozenou strukturou slov", jako jsou textové soubory, a nevýhoda na souborech, kde je obzvlášť důležitý blízký kontext, jako jsou naskenované fotografie.



Fiala a Greene rovněž zkoušeli varianty, kdy jsou fráze do stromu přidávány častěji než po každém znaku zapsaném jako literál a na hranici copy tokenů. Všechny tyto metody (včetně A2) mohou být implementovány podobně jako B2, pokud se nezajímáme o rychlost komprese.

Například, o jedno procento lepší komprese lze prý dosáhnout vložením jedné počáteční pozice (tedy pozice, na kterou může ukazovat copy token) mezi každé 2 znaky reprezentované copy tokenem, odkazujícím na řetězec délky 2 (dále jen copy token délky 2). O další 0,5 % lze kompresi zlepšit přidáním počátečních pozic za každý znak, odpovídající copy tokenu délky 3.
Ale protože tyto změny zpomalují kompresi i dekompresi, žádný z algoritmů třídy LZFG je nevyužívá.

Metoda C2

Narozdíl od metod B1 a B2, metoda C2 byla vytvořena za účelem zlepšení kompresního poměru na úkor rychlosti algoritmu.
Pokud se nějaký řetězec objevuje v souboru často, žádná z výše uvedených metod nefunguje optimálně, neboť když komprimují tento řetězec, existuje několik různých pozic v okně, na které může pozice z copy tokenu ukazovat, ačkoli (při dekompresi) je třeba zkopírovat jen jeden řetězec.
Ale datové struktury, které používáme, místem neplýtvají - opakující se řetězce odpovídají stejné cestě z kořene. Založíme-li tedy kódová slova pro kopírování řetězce (copy tokeny) na datové struktuře slovníku, znatelně zlepšíme kompresní poměr. (Kódovými slovy založenými na pozicích v explicitním slovníku se tato metoda blíží metodě LZ 78, ačkoli prohlížecím oknem a explicitním udáváním délky kopírovaného řetězce připomíná metodu LZ 77.)

Metoda C2 používá pro uložení slovníku stejnou datovou strukturu jako metoda B2 (PATRICIA tree), rovněž shodně používá metodu "compressed displacements" k číslování pozic v okně.

Abychom mohli udat pozici řetězce ve slovníku, je třeba udat 2 informace: číslo uzlu a umístění na hraně mezi zmíněným uzlem a jeho otcem (protože tato hrana může odpovídat řetězci delšímu než 1 znak).
Listy a vnitřní vrcholy očíslujeme každé zvlášť. Při volbě kódového slova pro kopírování řetězce z prohlížecího okna rozlišíme 2 případy:
Pokud příslušná hrana (viz výše) končí v nějakém listě stromu, použijeme kódové slovo "LeafCopy", v opačném případě (hrana vede mezi vnitřními vrcholy stromu), použijeme kódové slovo "NodeCopy".
Tedy zmíněné řetězce opakující se v prohlížecím okně jsou zakódovány pomocí NodeCopy.

Abychom rozlišili kódová slova, musí být před každým z nich bitový příznak (flag) - 0 pro NodeCopy, 1 pro LeafCopy či literál (literal token).

V kódovém slovu pro NodeCopy je flag následován číslem příslušného (vnitřního) vrcholu (počet vnitřních vrcholů bývá zhruba 50 % počtu listů). Za číslem vrcholu následuje délka řetězce podél hrany z otce zmíněného vrcholu (tedy jak dlouhý úsek této hrany se má použít pro získání řetězce při dekompresi). Protože u NodeCopy je délka hrany často 1, údaj o délce se v tomto případě vynechává; pokud je délka hrany větší než jedna: 0 znamená, že řetězec končí přímo v uvedeném uzlu, 1 odpovídá pozici o 1 znak dolů z otce uzlu atd.
Oba výše zmíněné údaje jsou zapsány (každý zvlášť) obyčejným binárním kódem s fázováním (viz A2, 3. vylepšení); kolik bitů mám pro binární kód použít, snadno zjistím z celkového počtu vnitřních vrcholů či z délky celé použité hrany (oba tyto údaje jsou známy dekompresoru, takže ví kolikabitová čísla má číst).

Kódová slova pro LeafCopy jsou zapisována pomocí start-step-stop kódů.
Po bitovém příznaku následuje délka nejdelší shody na hraně od otce dolů do listu zapsaná (1, 1, 11) kódem, protože tato délka nemůže být 0, kód 0 znamená, že bude následovat řetězec literálů (za 0 tedy následuje jako u A2 (0, 1, 5) kód délky řetězce a pak posloupnost ASCII kódů). Kód (1, 1, 11) dovoluje zapsat délku shody až 4094 znaků.
Opět se zde používá trik známý již z metody A2: Protože po řetězci literálů kratším než maximální délka (63) nemůže následovat další řetězec literálů, následuje-li LeafCopy, délku shody posuneme o 1 dolů (tedy délka shody 1 bude zapsána jako 0 atd.).
Po délce shody následuje číslo příslušného listu (což jednoznačně odpovídá zapsání pozice (způsobem jako u metody B2) v prohlížecím okně, na které řetězec odpovídající listu začíná) zapsané (10,2,14) kódem, resp. jeho vylepšením uvedeným u metody A2 (kód (10 - x, 2, 14 - x) + fázování).

Dekompresor pro metodu C2

Narozdíl od metod A2 a B2, dekompresor pro metodu C2 si musí udržovat stejný PATRICIA tree jako kompresor mimo jiné proto, aby byl schopen zjistit, kterým pozicím v prohlížecím okně čísla uzlů odpovídají.
To ovšem není tak obtížné - změníme-li pravidlo pro rozhodování kompresoru mezi copy tokenem (tedy LeafCopy či NodeCopy) a řetězcem literálů. Tedy pokud nejdelší shoda je alespoň 2 znaky dlouhá, použijeme vždy kódové slovo pro LeafCopy či NodeCopy (i pokud jsme doteď zapisovali literal token).
Pro zrychlení budeme opět používat pole "permanentních" uzlů v hloubce 1 (jako u B2).
Zatímco kompresor prochází stromem dolů a tedy v každém uzlu musí nahlédnout do hašovací tabulky, dekompresor dostává číslo příslušného vrcholu (z něhož zjistí pozici v prohlížecím okně, ze které má kopírovat) z komprimovaného souboru a data potřebná k určení délky kopírovaného řetězce. Zkopíruje tedy příslušný řetězec, posune prohlížecí okno a pak projde stromem nahoru (až ke kořeni) a aktualizuje všechny ukazatele (v uzlech na této cestě) do prohlížecího okna na aktuální hodnotu. Protože se strom prochází jen směrem nahoru, hašovací tabulky dekompresor vůbec nepotřebuje.
S výše uvedeným rozhodovacím pravidlem mezi copy a literal tokeny lze aktualizovat (přidávat fráze) do slovníku velmi jednoduše - v případě NodeCopy či LeafCopy stačí připojit nový list k příslušné hraně (vzniká nový vnitřní vrchol) či vnitřnímu vrcholu a provést výše uvedené projití stromu; v případě literálů stačí připojit nový list k "permanentnímu" uzlu v hloubce 1 pro každý jeden znak zapsaný jako literál.



Příklad: Předpokládejme, že obrázek 4 zobrazuje PATRICIA tree pro níže uvedené prohlížecí okno. (Není to pravda! Například řetězci začínajícímu na pozici 1 by musel odpovídat nějaký list (nemohl zde být použit copy token, tedy zde byl použit literál) - ale takový ve stromě není. Nebudu se snažit vytvořit pro toto okno korektní PATRICIA tree, protože by byl moc složitý a tím i nepřehledný.)
Zároveň upozorňuji, že na obrázku není použito pole "permanentních" uzlů. Slovo uzel je zde užíváno ve významu vnitřní vrchol.
Nekorektní PATRICIA tree
Obrázek 4
Předpokládejme, že následující text k zakódování bude:
  1. "abcc..."
    Pak se použije NodeCopy, protože nejdelší shoda pro tento řetězec ve stromě je "abc" - tento řetězec končí jeden znak za uzlem 3 na hraně do uzlu 2. Tedy po bitovém identifikátoru bude zakódována dvojice (2, 1) příslušnými binárními kódy.
  2. "abcaaacb..."
    Pak bude použito LeafCopy - nejdelší shoda končí na hraně mezi uzlem 2 a listem 3, proto po bitovém identifikátoru bude zapsána (start-step-stop kódy) dvojice (2, 3).
  3. "cacbaac..."
    Pak bude následovat řetězec literálů - tedy zapíše se flag pro LeafCopy, pak příslušný (1, 1, 11) start-step-stop kód pro 0, pak (0, 1, 5) kód délky řetězce (až dokud se nenajde shoda alespoň 2 znaky), tedy zde kód pro 3 a pak následují příslušné znaky - zde "cac".




Závěr

Algoritmy LZFG dosahují jednoho z nejlepších kompresních poměrů, pokud srovnáváte jen s klasickými slovníkovými metodami (metodami třídy LZ - ne tedy třeba s metodou ACB). Jsou to dynamické proudové metody a ve srovnání s neslovníkovými proudovými metodami (dynamický Huffmanův alg, dynamické aritm. kódování...), ale i s ACB, jsou velmi rychlé.
Ovšem vhodným spojením statistické metody a slovníkové metody lze dosáhnout lepší komprese při srovnatelné rychlosti.
Algoritmy LZFG jsou dosti náročné na implementaci, především kvůli složitým datovým strukturám, a proto se v praxi příliš nepoužívají.

V roce 1990 získali Fiala a Greene patent (4,906,991) na všechny implementace algoritmu LZ 77 používající stromovou datovou strukturu. Tento patent ovšem pokrývá mnohem větší množství algoritmů, než Fiala a Greene vymysleli - konkrétně algoritmus Rodeha a Pratta z roku 1981 (Journal of the ACM, vol 28, no 1, pp 16-24), ale hlavně algoritmy použité v komprimačních programech lharc, lha a zoo (ještě před pár lety velmi oblíbených).



Použitá literatura:

  1. E.R.Fiala, D.H.Greene: Data compression with finite windows. Communications of ACM, 32(4), 490-505, April 1989.
  2. T:C.Bell, J.G.Cleary, I.H.Witten: Text Compression. Prentice Hall, Englewood CLiffs, NJ, 1992, odstavec 8.3.12.
  3. Salomon D. - Data Compression, Springer, New York 1997, odstavec 3.6.

Zhodnocení literatury

Tento referát byl vytvořen z velké většiny podle článku 1.). Tento je ovšem napsán tak, že se moc dobře nečte - v některých místech složitá angličtina, programový kód v nějakém pro mě neznámém jazyku a článek rovněž předpokládá dosti rozsáhlou znalost problematiky - především ohledně datových struktur (McCreight`s suffix tree) a operací nad nimi (zmiňuje se o nich okrajově a lze z něj pochopit (obtížně!!) jen úplné základy - ne například linearitu přidávání do suffix tree apod.). Rovněž obrázky datových struktur by si zasloužily rozsáhlejší komentář.
Obrázky 1 - 3 původně pocházejí z tohoto článku (ale některé byly samozřejmě upraveny).
V době vytváření tohoto článku, byl ovšem 1.) jediným "slušným" zdrojem informací.

Zbylé 2 knihy tomuto tématu věnují jen málo prostoru (jsou to obecné knihy o kompresi) - popisují jen vybrané metody třídy LZFG a to ještě málo podrobně.
Konkrétně 2.) se zabývá jen metodou C2 (o ostatních se ani nedozvíte, že existují) a je z ní převzat (po upravení) obrázek 4 (původně Figure 8-14). Který je sice použitelný jako příklad(nechtělo se mi kreslit korektní, dalo by to moc práce), ale je chybný !!! - není to PATRICIA tree příslušný k uvedenému textu (proč viz výše), což si autor vůbec neuvědomil!!!
Kniha 3.) se pro změnu zabývá jen metodami A1 a A2, zbytečně rozebírá triviality, ale vůbec se nezmiňuje o vhodných datových strukturách a proto pro člověka, který zná metodu LZ 77 a start-step-stop kódy, přináší jen velmi málo nového. Naopak je nepřesná ohledně toho, kdy u metody A2 (3. vylepšení) použít fázování (i při začátku komprese při obvyklé velikosti okna - dokud se zcela nezaplní) a obsahuje zásadní chybu ohledně způsobu jak fázování funguje - konkrétně u příkladu s 16K oknem a 11264 potřebnými hodnotami uvádí špatná čísla ohledně počtu kódů tvaru "11" následováno 13 bitovým číslem a "11" následováno 14 bitovým číslem. Tato chyba je ovšem velmi významná, pokud se budete snažit fázování dle článku 3.) pochopit!!! Korektní hodnoty lze nalézt výše.



Zde si můžete stáhnout celý program pro vizuální předvedení, jak fungují start-step-stop kódy. Program je napsán v programovacím jazyce C a lze ho zkompilovat třeba pomocí Borland C++ 3.1 for DOS. Program má za úkol jen předvést, jak kódování funguje, tedy není "blbuvzdorný".


Tento článek smí být používán libovolně, za předpokladu, že nebude modifikován a že takto bude učiněno se zmínkou o autorovi a odkazem na stránku http://jt.sf.cz, odkud by měla být dostupná aktuální verse tohoto článku.
Snažím se, aby informace zde uvedené byly pravdivé a pokud možno přesné, nicméně nenesu žádnou odpovědnost za to, že tomu tak opravdu je, ani za jakékoli škody, které by někomu v důsledku případných špatných či nepřesných informací z tohoto článku mohly vzniknout.


Pokud se Vám podaří tento algoritmus implementovat, zkuste mi poslat Váš výtvor - předem děkuji.


Jakékoliv dotazy či připomínky mi můžete poslat mailem.

Zpět na stránku referátů a jiných výtvorů