semmixkolor i percepcja

Kompresja LZW

Kompresor LZW trzyma jeden bieżący ciąg w i przedłuża go o kolejne symbole, dopóki taki ciąg jest w słowniku. Gdy już nie jest, wysyła kod w, dopisuje nowy ciąg do słownika i zaczyna od nowa od ostatniego symbolu. Ogólny opis algorytmu jest na stronie algorytm LZW.

Algorytm

słownik = wszystkie pojedyncze symbole, ponumerowane od 0 w = "" (pusty ciąg) dla każdego symbolu c z danych: jeśli w + c jest w słowniku: w = w + c w przeciwnym razie: wyślij kod(w) dopisz w + c do słownika pod kolejnym wolnym numerem w = c na końcu: wyślij kod(w)

Przebieg dla przykładu

Alfabet A = 0, B = 1. Dane: A B A B A B A.

Krokwcw + c w słowniku?Wysłany kodNowy wpis
1""Atak (A)
2ABnie0 (A)AB = 2
3BAnie1 (B)BA = 3
4ABtak (AB)
5ABAnie2 (AB)ABA = 4
6ABtak (AB)
7ABAtak (ABA)
koniecABA4 (ABA)
wynik: 0 1 2 4

Zwróć uwagę na krok 7: wpis ABA = 4 powstał w kroku 5 i już w kroku 7 został użyty. Kompresor wysyła więc kod 4 tuż po tym, jak go utworzył. To właśnie ten przypadek wymaga specjalnej obsługi przy dekompresji.

Ile to oszczędza

Przy krótkich danych niewiele: 7 symboli to 4 kody, ale kody muszą mieć więcej bitów niż symbole, bo słownik rośnie. Zysk pojawia się przy długich, powtarzalnych danych. Dla klasycznego przykładu TOBEORNOTTOBEORTOBEORNOT (24 znaki, alfabet 6 liter) LZW daje 16 kodów. Dla obrazu z dużymi plamami jednego koloru jeden kod obejmuje często dziesiątki pikseli.

Słownik w praktyce

Szukanie "czy w + c jest w słowniku" musi być szybkie. Zwykle nie przechowuje się całych ciągów, tylko pary (kod prefiksu, symbol) w tablicy z haszowaniem: ciąg w + c to po prostu (kod(w), c). Dzięki temu każdy krok to jedno wyszukiwanie w tablicy, niezależnie od długości ciągu.

Kod

// dane: tablica symboli 0..alphabetSize-1, wynik: tablica kodów
function lzwCompress(data, alphabetSize) {
  const dict = new Map();                 // klucz "prefiks,symbol" → kod
  let next = alphabetSize;
  let w = -1;                             // kod bieżącego ciągu, -1 = pusty
  const out = [];
  for (const c of data) {
    if (w === -1) { w = c; continue; }
    const key = w + ',' + c;
    if (dict.has(key)) { w = dict.get(key); continue; }
    out.push(w);
    dict.set(key, next++);
    w = c;
  }
  if (w !== -1) out.push(w);
  return out;
}

lzwCompress([0, 1, 0, 1, 0, 1, 0], 2);   // [0, 1, 2, 4]

W GIF dochodzą do tego kody o zmiennej długości, kody "wyczyść" i "koniec" oraz pakowanie bitów do bajtów. Opisuje to strona kompresja LZW w GIF.