Algorytmy Kompresji Bezstratnej
Metody zachowujące wszystkie oryginalne dane: kodowanie Huffmana, metody słownikowe (LZW), RLE.
Filozofia Kompresji Bezstratnej
Podstawową obietnicą jest idealna wierność. Kiedy kompresujesz plik, a następnie go dekompresujesz, wynik jest bit po bicie, matematycznie identycznym klonem oryginału. Żadne dane nie są tracone, zmieniane ani nawet nieznacznie przearanżowane. Ta gwarancja jest nienegocjowalna dla szerokiej gamy typów danych, w tym dokumentów tekstowych, programów komputerowych, zapisów finansowych i danych naukowych, gdzie nawet jeden błędny bit mógłby mieć katastrofalne konsekwencje.
Ale skoro żadna informacja nie jest odrzucana, to jak możliwa jest kompresja? Odpowiedź leży w koncepcji nadmiarowości (redundancji). Rzeczywiste dane rzadko są losowe; są pełne wzorców, powtórzeń i przewidywalnych struktur. Algorytmy bezstratne to w istocie zaawansowane narzędzia do identyfikowania tej nadmiarowości i tworzenia nowej, bardziej wydajnej reprezentacji danych, która eliminuje to marnotrawstwo. Działają one nie przez usuwanie informacji, ale przez jej sprytniejsze ponowne zakodowanie. W tej sekcji zagłębimy się w wewnętrzne działanie najważniejszych i fundamentalnych algorytmów bezstratnych, które stanowią trzon nowoczesnego przechowywania i przesyłania danych. Zbadamy główne rodziny tych technik: metody statystyczne, takie jak kodowanie Huffmana, metody słownikowe, jak LZW, oraz proste schematy oparte na powtórzeniach, jak kodowanie długości serii.
Kompresja Statystyczna: Kodowanie w Oparciu o Prawdopodobieństwo
Jedną z najpotężniejszych form redundancji jest redundancja statystyczna. W każdej wystarczająco dużej próbce danych pewne symbole lub wartości pojawiają się znacznie częściej niż inne. Na przykład w języku polskim samogłoski 'a' i 'i' oraz spółgłoska 'z' są niezwykle powszechne, podczas gdy 'ź' czy 'ń' są rzadkie. Standardowy schemat kodowania, taki jak ASCII, używa stałej liczby bitów (zazwyczaj 8) dla każdego znaku, niezależnie od tego, jak często jest on używany. Jest to nieefektywne. Metody kompresji statystycznej wykorzystują te różnice w częstotliwościach do tworzenia bardziej zwartych kodów.
Naczelna zasada jest prosta: przypisz bardzo krótkie kody binarne do najczęstszych symboli, a progressively dłuższe kody binarne do najrzadszych symboli. Daje to w efekcie wiadomość o krótszej średniej liczbie bitów na symbol, co prowadzi do kompresji.
Kodowanie Huffmana: Klasyczna Implementacja
Opracowane przez Davida A. Huffmana w 1952 roku, jest archetypicznym algorytmem kompresji statystycznej. Zapewnia on optymalną metodę przypisywania tych kodów o zmiennej długości do zbioru symboli, biorąc pod uwagę ich częstotliwości. Proces można podzielić na kilka kluczowych kroków.
Krok 1: Analiza CzęstotliwościPierwszym krokiem jest odczytanie danych źródłowych i zliczenie liczby wystąpień każdego unikalnego symbolu. Pozwala to określić prawdopodobieństwo lub częstotliwość każdego symbolu. Użyjmy prostego przykładowego ciągu: "ala ma kota".
Analizujemy ciąg i zliczamy znaki (włączając spację):
| Znak | Częstotliwość |
|---|---|
| 'a' | 4 |
| ' ' (spacja) | 2 |
| 'l' | 1 |
| 'm' | 1 |
| 'k' | 1 |
| 'o' | 1 |
| 't' | 1 |
Następnie, na podstawie tych częstotliwości, konstruujemy specjalne drzewo binarne. Proces jest iteracyjny:
- Utwórz węzeł liściasty dla każdego unikalnego symbolu, oznaczając go symbolem i jego częstotliwością.
- Wielokrotnie znajdź dwa węzły w kolekcji o najniższych częstotliwościach.
- Połącz te dwa węzły w nowy wewnętrzny węzeł nadrzędny. Częstotliwość tego nowego węzła nadrzędnego jest sumą częstotliwości jego dwóch potomków.
- Kontynuuj ten proces, aż wszystkie węzły zostaną połączone w jeden korzeń.
Ta struktura drzewa w naturalny sposób umieszcza najrzadsze symbole głębiej w drzewie, a najczęstsze bliżej korzenia.
Krok 3: Przypisywanie Kodów BinarnychGdy drzewo jest już zbudowane, przypisujemy kody binarne, przechodząc przez nie. Powszechną konwencją jest oznaczanie każdej lewej gałęzi jako '0', a każdej prawej jako '1'. Kod binarny dla dowolnego symbolu to sekwencja zer i jedynek napotkana na ścieżce od korzenia drzewa do liścia tego symbolu.
| Znak | Częstotliwość | Kod Huffmana |
|---|---|---|
| 'a' | 4 | 0 |
| ' ' | 2 | 10 |
| 'l' | 1 | 110 |
| 'm' | 1 | 1110 |
| 'k' | 1 | 11110 |
| 'o' | 1 | 111110 |
| 't' | 1 | 111111 |
Oryginalny ciąg "ala ma kota" jest teraz kodowany poprzez zastąpienie każdego znaku jego nowym kodem binarnym. Najważniejszą cechą kodów Huffmana jest właściwość prefiksowa. Oznacza to, że żaden kod nie jest początkiem (prefiksem) żadnego innego kodu. Na przykład '0' jest kodem dla 'a', i żaden inny kod nie zaczyna się od '0'. Ta właściwość jest kluczowa, ponieważ pozwala dekoderowi na jednoznaczne parsowanie skompresowanego strumienia bitów bez potrzeby stosowania specjalnych separatorów między kodami.
Dekoder po prostu czyta strumień bitów bit po bicie, przechodząc przez drzewo Huffmana (które musi być wysłane wraz z danymi lub być predefiniowane), aż dotrze do węzła liściastego. Gdy liść zostanie osiągnięty, symbol jest wypisywany, a proces powtarza się od korzenia drzewa.
Kompresja Słownikowa: Znajdowanie Powtarzających Się Fraz
Choć metody statystyczne są potężne, skupiają się na częstotliwości pojedynczych symboli. A co z redundancją na większą skalę, jak całe słowa czy frazy, które się powtarzają? Tutaj z pomocą przychodzą metody kompresji słownikowej. Zamiast liczyć symbole, znajdują one powtarzające się sekwencje i zastępują je krótkimi odwołaniami.
Algorytm LZW (Lempel-Ziv-Welch)
Algorytm LZW jest klasyczną i szeroko stosowaną techniką słownikową, znaną z wykorzystania w formacie obrazów GIF. Działa on, budując słownik ciągów znaków "w locie" podczas kompresji.
Przykład LZW Krok po KrokuSkompresujmy ciąg "TATATAKNIETAK".
- Inicjalizacja: Słownik zaczyna od wszystkich pojedynczych znaków. Powiedzmy: T=1, A=2, K=3, N=4, I=5, E=6.
- Krok 1:
- Kompresor czyta 'T'. Jest w słowniku.
- Czyta następny znak 'A'. Ciąg "TA" NIE jest w słowniku.
- Wypisuje kod dla 'T' (czyli 1).
- Dodaje "TA" do słownika z nowym kodem (np. 7).
- Bieżącym ciągiem staje się 'A'.
- Krok 2:
- Bieżący ciąg to 'A'. Następny znak to 'T'. "AT" NIE jest w słowniku.
- Wypisuje kod dla 'A' (czyli 2).
- Dodaje "AT" do słownika (kod 8).
- Bieżącym ciągiem staje się 'T'.
- Krok 3:
- Bieżący ciąg to 'T'. Następny to 'A'. "TA" JEST w słowniku (właśnie go dodaliśmy, kod 7).
- Kontynuuje. Bieżący ciąg to "TA". Następny znak to 'T'. "TAT" NIE jest w słowniku.
- Wypisuje kod dla "TA" (czyli 7).
- Dodaje "TAT" do słownika (kod 9).
- Bieżącym ciągiem staje się 'T'.
- Kontynuacja...Proces jest kontynuowany aż do końca ciągu. Algorytm znajdzie również powtarzające się sekwencje "AK" i "TAK".
- Wynik: Oryginalny, dłuższy ciąg znaków jest kodowany jako sekwencja krótszych kodów numerycznych. Ta sekwencja liczb może być zapisana przy użyciu mniejszej liczby bitów niż oryginalne znaki. Dekompresor jest w stanie odtworzyć dokładnie ten sam słownik w miarę odczytywania kodów, dzięki czemu proces jest idealnie odwracalny bez potrzeby przesyłania samego słownika.
Ta technika jest bardzo skuteczna dla danych z wieloma powtarzającymi się sekwencjami, takich jak tekst, kod komputerowy i pewne typy obrazów, jak widać w formacie GIF, gdzie skutecznie kompresuje poziome wzory.
Kodowanie Długości Serii (RLE): Kompresja Prostoty
Kodowanie Długości Serii (Run-Length Encoding) jest prawdopodobnie najbardziej intuicyjnym ze wszystkich algorytmów kompresji. Jest przeznaczone do jednego specyficznego typu redundancji: długich, ciągłych serii tej samej wartości danych.
Zasada RLE
Zamiast przechowywać każdy symbol w powtarzalnej sekwencji, RLE przechowuje tylko dwie informacje: sam symbol i liczbę jego powtórzeń.
Eksplorator RLE
Zmieniaj przykłady, aby zobaczyć jak długie serie symboli zamieniają się w pary licznik/wartość.
Świetne dla sylwetek, kodów kreskowych i ikon z dużymi płaskimi obszarami.
| # | licznik | wartość |
|---|---|---|
| 1 | 8 | W |
| 2 | 4 | B |
| 3 | 1 | W |
| 4 | 7 | B |
Każdy wiersz przechowuje długość serii oraz symbol.
Interaktywny schemat pokazuje, jak RLE zamienia powtarzane piksele lub znaki w pary licznik/wartosc i ile bajtow mozna w ten sposob zaoszczedzic.
Przykład: Prosty Obraz BinarnyWyobraź sobie pojedynczą linię czarno-białego obrazu reprezentowaną przez tę sekwencję (B=biały, C=czarny):
Surowa reprezentacja wymagałaby 22 jednostek danych. Używając RLE, możemy to zakodować jako:
Ta reprezentacja, składająca się tylko z 8 jednostek danych (4 liczniki i 4 wartości), jest znacznie krótsza.
Kiedy Stosować RLE
RLE jest niezwykle skuteczne dla danych charakteryzujących się dużymi, jednolitymi obszarami. To czyni je doskonałym wyborem dla:
- Grafik komputerowych, takich jak ikony, logotypy i wykresy.
- Prostych animacji.
- Wyników niektórych procesów analizy obrazu, gdzie regiony są segmentowane według koloru.
Jest ono jednak notorycznie nieskuteczne, a nawet może przynieść efekt przeciwny do zamierzonego (zwiększając rozmiar pliku), na danych z bardzo małą liczbą serii, takich jak fotografie czy dane zaszumione. RLE jest używane jako część starszych formatów, takich jak PCX, BMP i TIFF.
Podejścia Hybrydowe: To co Najlepsze z Obu Światów
Najpotężniejsze systemy kompresji bezstratnej używane obecnie często nie opierają się na jednym algorytmie, ale są sprytnymi hybrydami, które łączą siłę różnych technik. Doskonałym przykładem jest algorytm Deflate, który jest silnikiem wszechobecnych formatów archiwów ZIP i GZIP, a także formatu obrazów PNG. Deflate łączy algorytm słownikowy LZ77 z kodowaniem Huffmana. Najpierw używa LZ77 do znajdowania i zastępowania powtarzających się ciągów wskaźnikami. Wynikiem tego etapu jest mieszanka literałów (znaków, które nie były częścią powtarzającej się sekwencji) i wskaźników. Ten mieszany strumień jest następnie dalej kompresowany za pomocą kodowania Huffmana, które przypisuje krótkie kody do najczęstszych literałów i wskaźników. Ten dwuetapowy proces pozwala Deflate na wydajne przechwytywanie zarówno powtórzeń na dużą skalę, jak i wzorców statystycznych na małą skalę, co skutkuje doskonałą, uniwersalną wydajnością kompresji.