A Lempel-Ziv-Welch algoritmus

A Lempel-Ziv-Welch (LZW) egy általános veszteségmentes tömörítési eljárás, amelyet viszonylag könnyű implementálni és ezért meglehetősen elterjedt. A feladatban ennek részeit kell elkészítenünk.

Az alapszótár elkészítése

A tömörítés alapja egy szótár, amelybe folyamatosan felvesszük a tömörítendő szövegben megjelenő különböző mintákat. Mivel először még nem ismerünk ilyen mintákat, készítünk egy általános, egykarakteres mintákat tartalmazó alapszótárat.

Feltételezzük, hogy egyszerű, 7 bites ASCII kódolású karakterláncokat akarunk tömöríteni, ezért egy 128 elemű alapszótárat hozunk létre. Ez tehát tartalmazza 0-tól 127-ig String típusú értékként az ASCII-táblázat karaktereit.

dictionary :: [String]

Test>
["A", "B", "C", "D"] :: [String]
Test>
128 :: Int

Segítség: Használjuk a Data.Char.chr függvényt!

Az illeszkedő kezdőszeletek megkeresése

A tömörítés során először a szótárból kell megkeresnünk a tömörítendő szöveg elejére illeszkedő leghosszabb szót. Ezt most két lépésben tesszük meg.

Elsőként szükségünk lesz arra, hogy egy adott szöveg esetén meg tudjuk mondani, hogy a szótárból mely elemek illeszkednek. Mivel később be akarjuk ezeket azonosítani (az illeszkedő részlet a szótárban hol helyezkedik el), ezért emellett meg kell adnunk azok sorszámát is a szótáron belül.

prefixes :: String -> [String] -> [(Int,String)]

Test>
[(0, "al"), (1, "alma")] :: [(Int, String)]
Test>
[(97, "a")] :: [(Int, String)]

Segítség: Használjuk a Data.List.isPrefixOf függvényt!

A leghosszabb illeszkedő kezdőszelet kiválasztása

Másodsorban meg kell tudnunk ezek közül határozni a leghosszabbat.

Ekkor a listából a pár második elemének hossza szerint kell kiválasztani azt, amelyik a legnagyobb. Üres listákra a függvény nem értelmezett.

longest :: [(Int,String)] -> (Int,String)

Test>
(5, "aaaaa") :: (Int, String)
Test>
(1, "dab") :: (Int, String)

A tömörítendő szöveg inkrementális feldolgozása

Ezek után már meg tudjuk írni azt a függvényt, amellyel a feldolgozandó szövegből tudjuk ,,kiharapni’’ azt a részét (az elejéről), amely a leghosszabban illeszkedett rá a szótárból.

Az eredmény egy rendezett hármas, amely a következő részekből kell álljon:

munch :: [String] -> String -> (Int,String,String)

Test>
(97, "a", "lmafa") :: (Int, String, String)
Test>
(129, "alm", "afa") :: (Int, String, String)

A szótár bővítése

Továbbá még szükségünk lesz arra, hogy a szótárt a későbbiekben bővíteni tudjuk. Ekkor a szótárt ábrázoló listához hozzáfűzünk egy újabb elemet úgy, hogy mellé vesszük még a feldolgozandó szövegben rákövetkező karaktert.

Azonban az új elemet csak akkor vesszük fel a szótárba, ha van a fennmaradó szövegben rákövetkező karakter. Amikor nincs, akkor nem vesszük fel, hiszen akkor ugyanazt a szót tennénk vissza a szótárba, amelyet korábban kivettük! Ugyanígy nem vesszük fel, ha az adott szó szerepel már a szótárunkban.

append :: [String] -> String -> String -> [String]

Test>
True :: Bool
Test>
True :: Bool

Bekódolás

A kódolást fix, 8 bites hosszúságú kódszavakkal akarjuk megvalósítani, ezért a függvénnyel kapcsolatban még azt is kikötjük, hogy a szótár 256 bejegyzésnél többen ne tartalmazhasson. Ha tehát a mérete e felé nőne, akkor egyszerűen eldobjuk a beszúrandó elemet, függetlenül attól, hogy egyébként be tudtunk volna szúrni.

Ezekből már meg tudjuk adni a betömörítést végző függvényt.

Itt a munch felhasználásával addig veszünk le (inkrementálisan) szeleteket a szövegből, amíg az el nem fogy. Közben pedig a szótár tartalmát folyamatosan frissítjük az append alkalmazásával.

encode :: [String] -> String -> [Int]

Test>
[97, 128] :: [Int]
Test>
[108, 97, 128, 130, 97] :: [Int]
Test>
[97, 128, 98, 130, 99, 132] :: [Int]

Betömörítés

A felhasználó számára készítünk még egy olyan függvényt, amellyel az eredményt egy karakterlánc formájában adjuk meg.

Ekkor tulajdonképpen ki kell egészíteni az encode függvény működését azzal, hogy átalakítjuk a kódokat karakterekké.

compress :: String -> String

Test>
"la\128\130a" :: String
Test>
"a\128b\130c\132" :: String

Segítség: Használjuk a Data.Char.chr függvényt!

Kikódolás

Az utolsó rész a kitömörítést végző függvény elkészítése.

Ekkor megkapjuk az alapszótárat, a tömörített szöveget ábrázoló kódsorozatot, majd ebből állítjuk vissza az eredeti változatát. Ennek a működése a következő. Olvassuk a tömörített szöveget ábrázoló kódsorozatot, és:

Észrevehetjük tehát, hogy tulajdonképpen minden lépésben egyetlen kódot fejtünk csak vissza. Amikor viszont van lehetőségünk, akkor a rákövetkező kód segítségével generáljuk a visszafejtéshez használt szótárt.

Egy részletes példán keresztül:

Test>
"lalalala" :: String
Test>
"lalalala" :: [Char]
Test>
"lalalala" :: [Char]
Test>
"lalalala" :: [Char]
Test>
"lalalala" :: [Char]
Test>
"lalalala" :: [Char]
decode :: [String] -> [Int] -> String

Test>
"aaa" :: String
Test>
"lalalala" :: String
Test>
"aaabbbccc" :: String

Kitömörítés

Ebből is elő tudunk állítani egy barátságosabb felhasználói függvényt, amellyel ismét csak karakterláncok között képzünk le (a tömörítendő szövegből a tömörítettbe).

Hasonlóan a compress függvényhez, itt tulajdonképpen a decode függvényt hívjuk meg, csak előtte még a bemenet karaktereit átalakítjuk kódot ábrázoló egész számokká.

Értelemszerűen, ha betömörítünk, majd kitömörítünk egy szöveget, akkor az eredetit kell visszakapnunk, tehát a decompress a compress inverz művelete lesz.

decompress :: String -> String

Test>
"lalalala" :: String
Test>
"aaabbbccc" :: String

Segítség: Használjuk a Data.Char.ord függvényt!