Kodowanie Huffmana

by Jerry Sky



Przykład kompresji MP3

Schemat kompresji MP3 w sporym uproszczeniu składa się z trzech kroków:

  1. Dźwięk jest próbkowany z zadaną częstotliwością (np. 44100 próbek na sekundę) i każda próbka jest opisana przez liczbę rzeczywistą. Otrzymujemy w tens sposób ciąg liczb rzeczywistych: s1,,sTs_1,\dots,s_T. Dla przykładu 50 minut muzyki będzie opisane przy pomocy T=506044100=132300000T = 50 \cdot 60 \cdot 44100 = 132300000 liczb rzeczywistych (dla uproszczenia zakładamy jeden kanał – dźwięk mono)
  2. Każda liczba rzeczywista sts_t jest przybliżana przez najbliższą liczbę z ustalonego zbioru Γ\Gamma. Zbiór ten jest dobrany w taki sposób, aby jak najlepiej opowiadać ograniczeniom ludzkiego słuchu, a ciąg liczb wykorzystujący wartości ze zbioru Γ\Gamma powinien być nierozróżnialny dla człowieka.
  3. Otrzymany ciąg liczb ze zbioru Γ\Gamma o długości TT jest zapisywany jako ciągi bitowe.

Przykładowe kodowanie MP3

Przyjrzyjmy się temu ostatniemu krokowi. Powiedzmy, że Γ={A,B,C,D}\Gamma = \{A,B,C,D\} jest czteroelementowym zbiorem. Najprostszy sposób opisania ciągami bitowymi wartości ze zbioru Γ\Gamma będzie nadanie kolejnych dwu-bitowych kodów np. w następujący sposób: AA będzie miało kod 0000, BB będzie miało kod 0101, CC będzie miało kod 1010, a DD będzie miało kod 1111. Zatem zapisanie przykładowych 50 minut muzyki będzie wymagało 264.6264.6 megabitów. Zauważmy, że odczytanie ciągu bitowego odpowiadającego ciągowi wartości ze zbioru Γ\Gamma jest łatwe: czytamy ciąg bitowy od początku do końca biorąc po 2 bity i konwertując je na wartości ze zbioru Γ\Gamma.

Niepotrzebne bity

Zastanówmy się, czy da się zapisać interesujący nas ciąg w bardziej efektywny sposób (przy użyciu mniejszej ilości bitów)? Możemy to uczynić posiadając dodatkową informację, np. w postaci częstotliwości występowania w interesującym nas ciągu wartości ze zbioru Γ\Gamma. Powiedzmy, że wartość AA występuje 30000003000000 razy, BB występuje 7220000072200000 razy, CC występuje 3700000037000000 razy, a DD występuje 2010000020100000 razy.

Idea pozwalająca na lepszą kompresję ciągu polega na tym, że częściej występującej wartości chcemy nadać kod bitowy o mniejszej liczbie bitów, czyli będziemy stosowali kodowanie o zmiennej liczbie bitów.

Zauważmy, że to wiąże się z możliwymi problemami z konwersją ciągu bitowego z powrotem na wartości ze zbioru Γ\Gamma. Dla przykładu, jeśli wartości BB nadamy kod 00 wartości CC kod 11, a wartości AA kod 0101 i otrzymamy ciąg 001001 to nie jesteśmy w stanie stwierdzić, czy to jest zapis ciągu BBCBBC, czy może BABA?
Rozwiązaniem tego problemu są kody prefiksowe, czyli takie, że żaden kod nie jest prefiksem innego kodu.

Zastosowanie kodu Huffmana

Teraz możemy już zdefiniować problem, który znany jest jako kodowanie Huffmana:

Input: tablica częstotliwości f[1,,n]f[1,\dots,n] wartości ze zbioru Γ\Gamma, gdzie Γ=n|\Gamma| = n.

Output: prefiksowe kody binarne przypisane do wartości ze zbioru Γ\Gamma minimalizujące długość ciągu bitowego odpowiadającemu ciągowi o wejściowych częstotliwościach.

Zauważmy, że ukorzenione drzewo binarne (czyli takie, którego węzły mają dwóch potomków lub są liśćmi) może reprezentować kodowanie prefiksowe w następujący sposób:

Drzewo kodowe dla wcześniej zaprezentowanego przykładu:

example

Dla przykładowego drzewa kodowego długość wyjściowego kodu będzie wynosić 30000003+722000001+370000002+201000003=215.53000000 \cdot 3 + 72200000 \cdot 1 + 37000000 \cdot 2 + 20100000 \cdot 3 = 215.5 megabitów, czyli ponad 18%18\% lepiej niż poprzednio.

Zauważmy, że aby wykonać konwersję kodu binarnego na wartości ze zbioru Γ\Gamma wystarczy przejść wielokrotnie drzewo od korzenia do liścia, tak że jeśli trafimy na 00 to przechodzimy do lewego potomka, jeśli trafimy na 11 to przechodzimy do prawego potomka, a jeśli dotrzemy do liścia to zapisujemy odpowiadającą mu wartość.

Formalnie optymalne kodowanie jest takie, które będzie minimalizować długość ciągu binarnego odpowiadającemu ciągowi wartości ze zbioru Γ\Gamma, czyli: długosˊcˊ kodu całego ciągu==i=1nfidługosˊcˊ kodu i-tej wartosˊci ze zbioru Γ \text{długość kodu całego ciągu} = \\ =\sum_{i=1}^{n} f_i \cdot \text{długość kodu } i\text{-tej wartości ze zbioru } \Gamma

Możemy zauważyć, że dla każdej wartości αΓ\alpha \in \Gamma długość kodu bitowego odpowiadającego α\alpha jest równa głębokości liścia odpowiadającego wartości α\alpha w drzewie kodowym. Zatem możemy zapisać długość kodu całego ciągu jako: długosˊcˊ kodu całego ciągu==i=1nfigłębokosˊcˊ lisˊcia odp. i-tej wartosˊci \text{długość kodu całego ciągu} = \\ = \sum_{i=1}^{n} f_i \cdot \text{głębokość liścia odp. } i\text{-tej wartości}

Powyższy wzór mówi nam, że dwa symbole z najmniejszą częstotliwością muszą występować w drzewie kodowym, bo w przeciwnym przypadku możemy otrzymać tylko większą długość kodu całego ciągu. Obserwacja ta może być podstawą do pierwszego zachłannego kroku budowy drzewa kodowego.

Zauważmy również, że możemy zdefiniować częstotliwość węzłów wewnętrznych drzewa, która będzie równa sumie częstotliwości potomków danego węzła. Częstotliwość ta odpowiada dokładnie ile razy dany wierzchołek będzie odwiedzony podczas przechodzenia drzewa przy konwersji kodów. Zatem możemy jeszcze inaczej zapisać długość ciągu binarnego na wyjściu: długosˊcˊ kodu całego ciągu=vT, vrootfv \text{długość kodu całego ciągu} = \sum_{v \in T,~ v \neq \mathrm{root}} f_v gdzie TT jest drzewem kodowym, fvf_v jest częstotliwością węzła vv należącego do drzewa TT. Do długości kodu nie dodajemy częstotliwości korzenia drzewa, ponieważ (jak wcześniej zostało to zdefiniowane) przejście krawędzi odpowiada bitowi 00 lub 11, więc będąc w korzeniu nie dokładamy bitów do ciągu wyjściowego.

Wykorzystując powyższe obserwacje możemy skonstruować algorytm zachłanny budujący optymalne drzewo kodowe, a co za tym idzie kody binarne przypisane do wartości ze zbioru Γ\Gamma w następujący sposób:

Pseudokod algorytmu

Huffman(f[1,,n])(f[1,\dots,n]):

  1. \triangleright niech QQ będzie kolejką priorytetową gdzie priorytetami są częstotliwości; im mniejsza częstotliwość tym większy priorytet
  2. for i1i \gets 1 to nn:
    1. insert(Q,(i,f[i]))(Q, (i, f[i]))
  3. for kn+1k \gets n+1 to 2n12n-1:
    1. (i,f[i])(i, f[i]) \gets ExtractMin(Q)(Q)
    2. (j,f[j])(j, f[j]) \gets ExtractMin(Q)(Q)
    3. \triangleright stwórz węzeł o indeksie kk z potomkami ii oraz jj
    4. f[k]=f[i]+f[j]f[k] = f[i] + f[j]
    5. insert(Q,(k,f[k]))(Q, (k, f[k]))

Złożoność obliczeniowa: Używając np. kopca binarnego w celu implementacji kolejki priorytetowej otrzymujemy algorytm o złożoności O(nlogn)O(n\log n), bo w pętlach długości nn wykonujemy stałą liczbę operacji o złożoności O(logn)O(\log n).

More