Miskolci Egyetem Informatikai Intézet Általános Informatikai Tanszé k Pance Miklós Adatstruktúrák, algoritmusok előadásvázlat Miskolc, 2004 Technikai közreműködő: Imre Mihály, műszaki informatikus hallgató
Tömörítés: LZ 77 LZ 77 Sliding Window compression: Alapja: Jacob Ziv, Abraham Lampel: „A Universal Algorithm for Sequental Data Compression” IEEE Transactions on Information Theory. Az LZ 77 tömörítő szótárként az előzőleg látott szöveget használja. Az input szöveg kifejezéseit a szótárra mutató pointerekkel helyettesíti. A tömörítés foka függ a szótár kifejezések hosszától, az előzőleg látott szövegre nyíló ablak nagyságától, és a forrás szövegnek a modellre vonatkozó entrópiájától. A szöveg ablak két részre osztott. Az első a jelenleg dekódolt szöveg nagy blokkja, a második általában sokkal kisebb előrenéző buffer. Az előrenéző bufferben az inputszövegáramból olvasott karakterek vannak, amit még nem kódoltunk be. 2
Tömörítés: LZ 77 A szöveg ablak szokásos mérete általában néhány ezer karakter. Az előrenéző buffer általában sokkal kisebb, tíz – száz karakter. 3 for(i = 0; i <MAX–1; i++) \r for(j =i+1;j<MAX ;j++) \r szöveg ablak előrenéző puffer A szöveg ablak 64 karakter, ebből 16-ot használ az előrenéző puffer. Az LZ 77 eredetileg token sorozatot ad ki, melyek mindegyike három adatot tartalmaz az aktuális előrenéző puffer, változó hosszúságú kifejezésére: •mutató egy szöveg ablakbeli kifejezésre, •a kifejezés hossza, •a kifejezést követő első karakter az előrenéző pufferben. A példában az előrenéző puffer tartalma: ’. Így a token: 14, 4,
Tömörítés: LZ 77 Ezután a tömörítő program a szöveg ablakot 5 karakterrel eltolja, ami az éppen elkódolt (encode) kifejezés szélessége. Ezután 5 új jelet olvas az előrenéző pufferbe és az eljárás ismétlődik. 4 Ezután a ’;j+’ kifejezést kódolja be : 40, 2, ’+’ Ha nincs megfelelés, akkor 0 hosszúságú kifejezést ad ki, pl.: 0, 0, ’*’. Ez nem hatékony, de így bármilyen szöveg bekódolható. Egy durva implementáció (brute force): megkeresi a leghosszabb egyezést, bekódol, eltol. = 0; i <MAX –1; i++) \r for(j =i+1;j<MAX;j++) \r a[i] szöveg ablak előrenéző puffer
Tömörítés: LZ 77 Betömörítés: int window_cmp(char *w, int i, int j, int length) { int count = 0; while(length--) { if(w[i++] == w[j++]) count++; else return(count); } return(count); } 5
Tömörítés: LZ 77 match_poz = 0; match_len = 0; for(i = 0; i < ( Winsize - elonezsize ); i++) { len = window_cmp( win, i, elonez, elonezsize); if(len > match_len) { match_poz = i; match_len = len; } encode(match_poz, match_len, win[ elonez + match_len]); memmove(win, win + match_len +1, Winsize - match_len); for(i = 0; i < match_len + 1; i++) win [ Winsize - match_len + i] = getc(input); 6
Tömörítés: LZ 77 Kitömörítés (decompression): Nincs összehasonlítás. Beolvassa a tokent, kiírja a kifejezést, kiírja a követő karaktert, eltol, ismétel végig. decode(&match_poz, &match_len; &charac); fwrite(win+ match_poz, 1, match_len, output); putc(charac, output); for(i = 0; i < match_len; i++) win[elonez + i] = win[match_poz + i]; win[elonez + i] = charac; memmove(win, win + match_len + 1, Winsize - match_len); 7
Tömörítés: LZ 77 Ennek a kitömörítő eljárásnak egy érdekes mellékhatása, hogy használhat olyan kifejezést is egy létező kifejezés bekódolására, amit még nem enkódolt. Pl. egy fájl, ami 100 A betűt tartalmaz egymásután: Az első A enkódja: (0,0,’A’) 8 Ezután a következő 9 A betű kódolható így is: (38,9,’A’). Bár mi láthatjuk a kifejezést az előrenéző pufferben (az A karaktereket), de a dekóder erre nem képes. Amikor a dekóder megkapja a (38,9,’A’) tokent, akkor a puffere: AA A A A A A match_poz elonez_puff
Tömörítés: LZ 77 De a decompress algoritmus ezt meg tudja oldani: a ciklusban a match_poz –ból másol az elonez pufferbe: 9 AA match_poz +i elonez_puff + i végül Ez az LZ 77 tömörítés gyors alkalmazkodását bizonyítja. Bekódolt 10 karakteres sorozatot, amikor a szótárában még csak egyetlen karakter volt belőle. AA A A A A match_poz +i elonez_puff + i
Tömörítés: LZ 77 Problémák az LZ 77-tel A fenti implementáció az algoritmusnak egy laza interpretációja. Nyilvánvaló a teljesítmény szűk keresztmetszete (bottleneck), a string összehasonlítás: a szöveg ablak minden pozícióján összevet az előrenéző pufferrel. Ez még csak romlik, ha a teljesítmény (tömörítés foka) fokozására növeljük az ablak méretét, azaz a szótár méretét. A dekódolást ez nem befolyásolja. A másik probléma a csúszó ablak kezelésének módja, kényelmességből itt a csúszó ablakot úgy kezeltük mintha ez valóban végig csúszna a szövegen. Helyette a kezdő és vég pointereket csúsztatjuk a puffer (a teljes szöveg) mentén. 10
Tömörítés: LZ 77 De ekkor a modulo indexet kell használnunk: int window_cmp(char* w, int i, int j, int len) { count = 0; while(len--) { if (w[i] == w[j]) count++; else return(count); i = ++i % winsize; j = ++j % winsize; } return(count); } 11
Tömörítés: LZ 77 Egy enkód probléma Ha nem talál egyező kifejezést, akkor az egyetlen karakter bekódolására is a három komponensű tokent használja. Pl. használjunk egy 4096 bájtos ablakot, 16 bájtos előrenéző pufferrel. Ehhez 20 bit az ablak pozíció, 4 bit a kifejezés hossz = 24, egyetlen 8 bites jel bekódolására. 12
Tömörítés: LZ SS 1. változtatás: a kifejezés tárolása Az LZ 77-ben a kifejezések folytonos szövegblokként tárolódnak, minden szervezettség nélkül. Az LZ SS bináris kereső fa szerkezetet használ a kifejezések tárolására. Így a leghosszabb megegyező kifejezés megtalálása a korábbi winsize* kifejezésméret helyett annak logaritmusával arányos. Ez bátoríthat a nagyobb ablakokkal való kísérletezésre. Pl. az ablak megduplázása az összehasonlítási időt csak 1 egységgel növeli, míg az LZ 77-nél ez duplája. 2. változtatás: a token kialakítása LZ 77 : a token 3 részből áll LZ SS megengedi a pointerek és karakterek szabad keveredését. A beindulásnál csupa ismeretlen kifejezés jön... Az LZ SS a tokenek elé egybites jelzőt tesz az offset/hossz páros illetve az egyetlen karakter jelzésére az outputban. Ennél kisebb gondot okoz, hogy a követő karaktert is kiírja. 13
Tömörítés: LZ Az alkalmazott adatszerkezetek: 1.unsigned char win[winsize]; nem az ablak csúszik, hanem a pointerek, ekkor az (i+1) mod winsize művelet hatékonyabban végezhető, ha a winsize 2 hatványa 2.a kifejezések tárolására bináris kereső fát használunk: struct{ int parent; int smaller_child; int larger_child; } tree[winsize + 1]; 14
Tömörítés: LZ A tree[Winsize] elem a fa gyökerét jelöli ki, ehhez nem tartozik kifejezés, nincs kisebb, nagyobb gyereke, a nagyobb gyerek indexe magára a fa gyökerére mutat. Ez csökkenti a feldolgozási időt és egyszerűsíti a kódot. Pl. törlésnél ilyen kódrészlet: tree[tree[i].parent].child = tree[i].child mivel a gyökérre mutató pointert ugyanabban a fában tároljuk, ezért nem kell külön ellenőrzés, arra, hogy az a gyökér-e. Mégha i a gyökér csomópont, akkor is a tree[i].parent még érvényes csomópontra mutat a fában. Egy további szokatlan jellemző, hogy az LZ SS egy speciális kódot használ a tömörített adat vége elérésének jelzésére. Ebben az esetben a zérus ablak index az adatáram végét jelzi. Így ez nem használható érvényes kifejezésként. 15
Tömörítés: LZ A 0 kifejezést nem használjuk, így a 0 csomópontot speciális UNUSED indexként használva kódot takaríthatunk meg. Pl. a törlés kódrészleténél: if (tree[i].smaller_child != UNUSED) tree[tree[i].smaller_child].parent = tree[i].parent; if (tree[i].larger_child != UNUSED) tree[tree[i].larger_child].parent = tree[i].parent; De ha az UNUSED index egy megengedett tárolóterületre mutat, akkor az érvényességi vizsgálat elhagyható. tree[tree[i].smaller_child].parent = tree[i].parent; tree[tree[i].larger_child].parent = tree[i].parent; Mivel a tree[0] értéket sohasem használjuk navigálásra, hibát nem okoz, és jelentős CPU időt takarít meg. 16
Tömörítés: LZ Kiegyensúlyozás A kereső fa könnyen láncolt listává alakulhat, mivel a fájlokban gyakran előfordulnak csökkenő vagy növekvő kifejezések. Ezek gyakran megesnek, de a csúszó ablak természete folytán gyorsan ki is mennek a fából. Ezért fa kiegyensúlyozást általában nem építenek be. „Greedy” vagy a „lehető legjobb” Az LZ 77 és az LZ SS is greedy algoritmus, mivel nem néznek előre az input áramba, hogy azt analizálják az indexek és karakterek legjobb kombinációja érdekében. A gyakorlatban néhány % megtakarítás mutatkozik, a feldolgozási idő pedig jelentősen nő. Néhány jó heurisztikát szoktak mindössze használni és ez a greedy algoritmus határozottan jó. 17
Tömörítés: LZ Javítások: •Előre feltölteni az ablakot Winsize-elonezsize karakterrel és utána adjuk a megfelelő stringeket a bináris fához. De mivel töltsük fel előre? •Lehet kísérletezni az index és a kódhossz bitjeinek növelésével. •„ghost buffer” a szöveg ablak végére, ami az ablak első 17 karakterét tartalmazza (16 a mérete az elorenez ablaknak) így a modulo aritmetika kihagyható, de ezt karban kell tartani. •Blokkolt I/O. •String duplikátumok kezelése: a fába nem tesszük, de az ablakban benne van. 18
Tömörítés: LZ 78 •jelsorozat, szótárat használ, •ez a szótár a tömörítés végéig él (nem kerülnek ki belőle elemek), fokozatosan bővül (tanul) az új jelsorozatokkal, •ha megtelik nem vesz fel újat, •induláshoz a szótárnak 1 eleme van: üres string. A szótár felépítése: jelsorozat, kód. 19
Tömörítés: LZ 78 A tömörítés elve: •adott pozíción vagyunk •megkeressük a szövegben azt a leghosszabb jelsorozatot, ami már benne van a szótárban (kezdetben csak rövidebbek, később hosszabbak), •a tömörített állományba a kódot írjuk ki, •megtalált rész + az őt követő karakter együtt mint egy új jelsorozat bekerül a szótárba új kódértékkel, •a tömörített fájlba kiírja a követő karaktert is, így nem kell a szótárat is hozzáírni, hanem az felépíthető a dekódolás során. 20
Tömörítés: LZ 78 Példa: WAWATOSOWA Tömörítve: 0W | 0A | 1A | 0T | 0O | 0S | 5W | 2- Vissza: WAWATOSOWA 21 kód jelsor"WAWATOSOW kód jelsor"WAWATOSOW
Tömörítés: LZW LZW (1984 Terry Welsh) Az LZ 78 javított változata, induláskor az összes jellel feltöltjük a szótárt. A kódolás menete: •megkeressük a szótárban is meglévő leghosszabb részt és kiírjuk a kódját, •ez a rész is a követő karakter új jelsorozatként kerül a szótárba, •a követő karakteren indulva indulva folytatjuk a vizsgálatot. 22
Tömörítés: LZW WAWATOSO W | A | 256 | T | O | S | O WAWATOSO WWWWWW W | 256 | " WAAWWATTOOSSO 256 WA WWWWW
Tömörítés Forrás: Mark Nelson: The Data Compression Book M&T, ISBN