KODOWANIE

Transkrypt

KODOWANIE
KODOWANIE
Jednym z problemów, z którymi spotykamy się w informatyce, jest problem właściwego
wykorzystania pamięci. Konstruując algorytm staramy się zwykle nie tylko o
zminimalizowanie kosztów czasowych algorytmu, ale także o dobrą złożoność pamięciową
algorytmu. Ale co zrobić, jeśli to dane są bardzo duże? Jak je przechowywać w pamięci
komputera? I tu przychodzą nam z pomocą techniki kompresji danych, tzn. metody
kodowania danych w takiej postaci, która pozwala zapisać ten sam zbiór informacji
wykorzystując dużo mniej miejsca. Oczywiście, zależy nam tylko na takich metodach, które
pozwalają szybko zakodować dane, oraz szybko i jednoznacznie odkodować zakodowaną
informację.
Przykład 1
Przypuśćmy, że pewien zbiór danych zawiera 106 znaków. Jeśli każdy z tych znaków jest
reprezentowany przez liczbę z przedziału 0-255, to na zapisanie jednego znaku musimy
zużyć 8 bitów (256 = 28). Wynika stąd, że na zapisanie całego pliku zużyjemy 8 × 106 bitów.
Gdybyśmy jednak mieli dodatkową informację, np. że w danych występują jedynie cyfry od 0
do 9, to moglibyśmy na znak przeznaczyć tylko 4 bity co pozwoliłoby zakodować cały zbiór
na 4 × 106 bitach. Przyporządkowanie znakom kodu mogłoby wyglądać np. tak:
0- 0000, 1- 0001, 2- 0010, 3- 0011 itd... 9-1001.
Dekodowanie zakodowanego pliku jest, przy takim kodzie, banalnie proste: odczytujemy
kolejne cztery bity (słowo kodowe) i w słowniczku sprawdzamy jaki to znak. Na przykład,
ciąg bitów 0010001100100011 jest kodem liczby 2323. Ponieważ słownik składa się tylko z
dziesięciu elementów, więc w najgorszym razie odszukanie jednego znaku wymagać będzie
porównania z dziesięcioma słowami kodowymi. Nie musimy jednak przeszukiwać słownika
sekwencyjnie. Przedstawmy zbiór słów kodowych w postaci drzewa binarnego (drzewa
kodowego), w którego liściach przechowywane są kodowane znaki, a każde przejście w lewo
odpowiada bitowi 0, a przejście w prawo- bitowi 1. W ten sposób każda ścieżka od korzenia
do liścia odpowiada słowu kodującemu znak zapamiętany w liściu, por. rysunek 11.2.☺
1
Definicja 1 Sposób kodowania, w którym na każdy znak przeznaczamy taką samą liczbę
bitów nazywamy kodem stałej długości.
Pytanie: Ile bitów trzeba przeznaczyć na zakodowanie tekstu złożonego z 107 znaków, jeśli
użyto kodu o stałej długości, a tekst składa się tylko z liczb naturalnych oddzielonych
przecinkami lub spacjami, oraz z liter x, y, z?
(odp: 4 bity – razem 4*10^7)
Zauważmy, że w przykładzie 2.1 niektóre liście drzewa kodowego nigdy nie będą potrzebne,
bo ciąg bitów prowadzący do nich, nigdy nie wystąpi w zakodowanym tekście. Na przykład
ciąg 1111 nie jest kodem żadnego ze znaków tego tekstu, więc nie wystąpi w zakodowanym
tekście. Co więcej, gdyby cyfry 9 i 8 występowały w tekście tylko niewielką liczbę razy, to i
tak do ich zakodowania użyjemy aż 4 bitów. Czy to nie jest marnotrawstwo? A może
zrezygnować ze stałej długości kodu i długość kodu uzależnić od częstości występowania
tego znaku w kodowanym tekście. Zasada jest prosta: znakom, które występują często
przypisujemy krótkie kody. Wynika stąd, że kody będą miał różne długości.
Definicja 2.2 Sposób kodowania, w którym znakom przypisujemy różne długości nazywamy
kodem zmiennej długości.
Przykład 2
Niech w danym pliku, cyfry 0 i 1 występują 3 ×106 razy, a pozostałe cyfry jedynie po 5×105
razy. Wtedy plik zawierający 107 znaków można zakodować używając tylko 8×106 bitów,
stosując kod 00 dla cyfry 0, 01 dla cyfry 1 oraz czterobitowe kody dla cyfr 2, 3, ..., 8, 9.
Liczba bitów potrzebna do zakodowania tekstu, dla którego znamy częstości występowania
znaków , wynosi
Σa∈Α f(a) × dT(a),
gdzie f(a) jest częstością występowania znaku a, a dT(a) jest długością kodu dla znaku a.
Problem, który się teraz pojawia, to jak zbudować kod uzależniający długość kodu od
częstości występowania znaków, w taki sposób, by można go było jednoznacznie odczytać
(dekodować). Warunek ten spełniają kody prefiksowe.
Definicja 2.3 Kod posiadający własność, że żadne słowo kodowe nie jest prefiksem żadnego
innego słowa kodowego, nazywa się kodem prefiksowym, dokładniej nie istnieją dwa słowa
kodowe, że a=(a1,...,an ) i b= (b1,..., bm) takie że n<m oraz a1=b1, a2=b2,..., an=bn.
Kody prefiksowe są bardzo wygodne, gdyż dekodowanie jest niezwykle proste: odczytujemy
pierwsze słowo kodowe znajdujące się na początku zakodowanego tekstu i usuwamy go.
Ponieważ żadne słowo kodowe nie jest prefiksem innego słowa, więc to pierwsze słowo jest
jednoznacznie wyznaczone. Po jego usunięciu postępowanie możemy powtórzyć.
Identyfikację słowa znakomicie upraszcza reprezentacja kodu w postaci drzewa binarnego
(drzewa kodowego), por. przykład 2.1.
2
Twierdzenie 1.1 Jeśli istnieje optymalne kodowanie, to zawsze można znaleźć kod
prefiksowy, który go realizuje.
Uwaga Optymalny kod jest zawsze reprezentowany przez lokalnie pełne drzewo binarne, por.
wykład V, definicja 1.1. Zatem, jeśli dany jest alfabet A, to drzewo optymalnego kodu ma |A|
liści oraz dokładnie |A|-1 wierzchołków wewnętrznych.
Pytanie: Ile bitów wymaga kod stałej długości, a ile kod zmiennej długości, dla zakodowania
tekstu złożonego z 1000 znaków, w którym występuje 8 różnych znaków, a każdy z taką samą
częstością?
(odp: 3000 bitów w obu przypadkach)
Konstrukcja drzewa kodowego Huffmana
Konstrukcję optymalnego kodu prefiksowego zaproponował David Huffman. W tym punkcie
wykładu przedstawimy metodę budowy drzewa kodowego dla optymalnego kodu
prefiksowego Huffmana.
Drzewo kodowe Huffmana jest drzewem binarnym, lokalnie pełnym. W każdym wierzchołku
wewnętrznym tego drzewa pamiętamy sumę częstości znaków występujących w poddrzewach
tego wierzchołka. Każdy wierzchołek drzewa będzie więc miał, oprócz referencji do lewego i
prawego poddrzewa, atrybut f, oznaczający częstość występowania znaku lub grupy znaków
odpowiadających drzewu o korzeniu w tym węźle. W liściach będą pamiętane dodatkowo
znaki kodowanego alfabetu. Do zapamiętania alfabetu oraz wierzchołków tworzonego
drzewa, algorytm Huffmana używa kolejki priorytetowej.
Budowanie algorytmu
1. Utwórz kolejkę priorytetową pq zawierającą węzły tworzonego drzewa. Początkowo
elementami kolejki są liście drzewa. Porządek elementów w kolejce priorytetowej zależy od
częstości przypisanej znakom.
2. Drzewo kodowe buduje się od dołu, od liści, które są traktowane jako drzewa z jednym
tylko węzłem. W każdym kroku algorytmu, zamiast kolejnych dwóch wierzchołków, których
częstości są najmniejsze, wstawiamy do kolejki priorytetowej pq nowy węzeł, którego
etykietą jest suma częstości przypisanych usuniętym węzłom. Punkt 2 powtarzamy, tak
długo, aż w kolejce priorytetowej pozostanie tylko jeden element. Będzie to korzeń drzewa
kodowego.
Algorytm Huffmana można zaimplementować na wiele sposobów, które zależą od konkretnej
implementacji kolejki priorytetowej. Jedną z możliwości, jest użycie kopca
zaimplementowanego w tablicy. Opracowanie szczegółów tego algorytmu pozostawiamy
Czytelnikowi jako ćwiczenie. Zwróćmy uwagę, że algorytm Huffmana w pewnych
przypadkach może działać niejednoznacznie, w tym sensie, że jeśli częstości występowania
dwóch grup znaków są takie same, to wybór kolejności odpowiadających im węzłów można
ustalić dowolnie, gdyż nie ma on wpływu na długość otrzymanego kodu, ale drzewa kodowe
mogą się różnić.
3
Koszt algorytmu
Algorytm rozpoczynamy od utworzenia kolejki priorytetowej, której elementami są liście
tworzonego drzewa kodowego. Koszt utworzenia tej kolejki, o ile zastosujemy algorytm
konstrukcji kopca w tablicy, wynosi O(n), gdzie n jest liczbą znaków kodowanego alfabetu.
W drugiej części algorytmu konstruujemy drzewo. Zauważmy, że po n-1 krokach, gdzie n jest
liczbą znaków kodowanego alfabetu, kolejka zawiera tylko jeden węzeł. Rzeczywiście, w
każdej iteracji, z kolejki są wyjmowane dwa elementy i wstawiany jeden nowy węzeł.
Oznacza to, że po każdej iteracji liczba elementów zmniejsza się o jeden. Jeśli kolejka
priorytetowa została zaimplementowana jako kopiec, to każda z wykonywanych w pętli
operacji kosztuje O(lg n) porównań. Wynika stąd, że koszt wykonania pętli "for" możemy
oszacować z góry przez O(n × lg n). Ostatecznie, koszt całego algorytmu możemy oszacować
przez O(n × lg n).
Uwaga Algorytm Huffmana jest algorytmem zachłannym w tym sensie, że w każdym kroku
wybiera węzły o najmniejszej częstości.
Twierdzenie 4.1 Algorytm HuffmanKod buduje drzewo optymalnego kodu prefiksowego.
Przykład
Przyjmijmy, że w pewnym tekście występują tylko litery A, F, H, M, N, U, a ich częstości
występowania w tysiącach wynoszą odpowiednio:40,8,9,11,7,25. Kolejne fazy działania
algorytmu Huffmana przedstawiono na rysunku 11.3.
4
Z otrzymanego drzewa kodowego łatwo odczytujemy kody liter A - 0, F - 1101, H - 1110, M
- 1111, N - 1100, U - 10 . Ciąg 11101011011101111101100 jest kodem słowa HUFFMAN.
Pytani: Czy koszt algorytmu Huffmana zmieni się, jeśli zamiast struktury kopca, użyjemy
tablicy uporządkowanej za pomocą optymalnego algorytmu sortowania, a wstawianie nowego
węzła do tablicy zrealizujemy tak jak w algorytmie InsertionSort?
(odp. Tak)
Zadania:
• Napisz algorytm, który mając dane drzewo kodowe, odczytuje zakodowany tekst.
• Dla danego tekstu, wypisz ciąg znaków alfabetu, z którego jest on zbudowany, wraz z
częstością występowania wszystkich znaków.
• (a) Na podstawie danego drzewa kodowego zbuduj "słowniczek" kodów w postaci tablicy.
(b) Na podstawie danej tablicy kodów (słowniczka), zbuduj drzewo kodowe.
• Zaproponuj rekurencyjną procedurę budowy drzewa kodowego Huffmana.
• Napisz implementację algorytmu konstrukcji drzewa Huffmana używając kopca, jako
implementacji kolejki priorytetowej.
• Znajdź optymalny kod Huffmana dla znaków a1,... an, jeśli ich częstości są określone
rekurencyjnie
a1= a2=1, ai+1 = ai + ai-1.
5