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