Wymagane jest zapoznanie się z następującymi podrozdziałami:
OL013 - Lista.
OL014 - Algorytm wyszukiwania dwóch najmniejszych elementów na liście.
OL015 - Drzewo binarne, algorytm inorder.
Kompresja danych polega na takim kodowaniu informacji, aby w wyniku otrzymać jak najkrótszy ciąg bitów. Algorytm Huffmana realizuje zadanie kompresji tworząc bezprzystankowe kody binarne, gdzie symbole występujące najczęściej w kodowanej informacji otrzymują kody najkrótsze, a symbole występujące rzadziej otrzymują kody dłuższe.
Dla zobrazowania zasady działania algorytmu Huffmana załóżmy, iż mamy skompresować tekst zawierający 8 symboli A, B, C, D, E, F, G i H:
EAEACDGDGEGGFGDFGHGAGGBEEEDGHH
Jeśli zastosujemy normalne kodowanie binarne, to każdemu z 8 symboli musimy przypisać kod 3 bitowy, np:
A - 000, B - 001, C - 010, D - 011, E - 100, F - 101, G - 110, H - 111
Wtedy powyższy napis można przedstawić w postaci strumienia bitów:
100000100000010011110011110100110110101110011101110111110000110110001100100100011110111111
Jak łatwo policzyć, strumień ten zawiera 30 znaków x 3 bity na znak = 90 bitów.
Policzmy teraz ilość wystąpień każdego znaku w tekście. Otrzymamy następujące wartości:
Znak A B C D E F G H Częstość 3 1 1 4 6 2 10 3 Ze znaku i jego częstości tworzymy węzeł-liść drzewa binarnego. Węzły nie są jeszcze połączone w drzewo. Umieszczamy je na liście dwukierunkowej:
Z listy wybieramy dwa węzły o najmniejszych częstościach występowania:
Wyjęte z listy węzły łączymy w drzewo binarne. Korzeń staje się nowym elementem, którego częstość jest równa sumie częstości obu liści. Korzeń wstawiamy z powrotem na listę:
Ostatnie operacje (wybranie z listy dwóch węzłów o najmniejszej częstości, utworzenie z nich drzewa i wstawienie korzenia na listę) powtarzamy dotąd, aż lista będzie zawierała tylko jeden element - binarne drzewo kodu Huffmana:
Zgodnie z drzewem wynikowym otrzymaliśmy następujące kody dla poszczególnych znaków:
A - 100, B - 00000, C - 00001, D - 001, E - 11, F - 0001, G - 01, H - 101
Tekst zakodowany za pomocą tych kodów ma postać:
11100111000000100101001011101010001010010001011010110001010000011111100101101101
a oryginalny był następujący:
100000100000010011110011110100110110101110011101110111110000110110001100100100011110111111
Nawet wzrokowo widzimy, iż kod Huffmana jest krótszy. Dokonaliśmy zatem kompresji danych. Kod Huffmana daje efektywną kompresję tylko wtedy, gdy częstości występowania symboli różnią się od siebie. W przeciwnym wypadku powstaje kod o stałej liczbie bitów na symbol. W rzeczywistych systemach kompresji danych musi być jeszcze przekazana informacja o strukturze drzewa kodowego Huffmana, które pozwala odczytać skompresowany strumień danych.
Na wejście programu podajemy wiersz znaków. Na wyjściu dla każdego znaku otrzymujemy informację o jego liczbie wystąpień oraz odpowiadający mu kod Huffmana. Informacja ta może posłużyć do zakodowania tekstu.
Program wykorzystuje klasę listy dwukierunkowej - TdoubleList oraz funkcję rekurencyjnego przechodzenia przez drzewo binarne - inorder.
// I Liceum Ogólnokształcące // im. K. Brodzińskiego // w Tarnowie //-------------------------- // Koło informatyczne 2006/7 //-------------------------- // P016 #include <iostream> #include <string> using namespace std; // deklaracja węzła drzewa binarnego //---------------------------------- struct TBTnode { TBTnode * parent, * left, * right; char c; }; // deklaracja typu elementu listy //------------------------------- struct TlistElement { TlistElement * next, * prev; int key; TBTnode * node; }; // deklaracja typu klasy listy dwukierunkowej //------------------------------------------- class TdoubleList { private: TlistElement * front, * back; unsigned cnt; public: // konstruktor //------------ TdoubleList() { front = back = NULL; cnt = 0; } // Zwraca długość listy //--------------------- unsigned size() { return cnt; } // Dodaje element na początku listy i zwraca jego adres //----------------------------------------------------- TlistElement * push_front(TlistElement * p) { p->next = front; p->prev = NULL; if(front) front->prev = p; front = p; if(!back) back = front; cnt++; return front; } // Dodaje element na końcu listy i zwraca jego adres //-------------------------------------------------- TlistElement * push_back(TlistElement * p) { if(back) back->next = p; p->next = NULL; p->prev = back; back = p; if(!front) front = back; cnt++; return back; } // Dodaje element p1 za elementem p2 i zwraca adres p1 //---------------------------------------------------- TlistElement * insert(TlistElement * p1, TlistElement * p2) { p1->next = p2->next; p1->prev = p2; p2->next = p1; if(p1->next) p1->next->prev = p1; else back = p1; cnt++; return p1; } // Usuwa element z początku listy i zwraca adres usuniętego elementu //------------------------------------------------------------------ TlistElement * pop_front() { TlistElement * p; if(front) { p = front; front = front->next; if(!front) back = NULL; else front->prev = NULL; cnt--; return p; } else return NULL; } // Usuwa element z końca listy i zwraca adres usuniętego elementu //--------------------------------------------------------------- TlistElement * pop_back() { TlistElement * p; if(back) { p = back; if(p == front) front = back = NULL; else { back = back->prev; back->next = NULL; }; cnt--; return p; } else return NULL; } // usuwa z listy element p i zwraca adres usuniętego elementu TlistElement * erase(TlistElement * p) { TlistElement * p1; if(p->prev) p->prev->next = p->next; else front = p->next; if(p->next) p->next->prev = p->prev; else back = p->prev; cnt--; return p; } // zwraca n-ty element listy. Jeśli lista posiada mniej niż n elementów, // zwraca NULL. Elementy numerujemy od 1. //---------------------------------------------------------------------- TlistElement * index(unsigned n) { TlistElement * p; if((!n) || (n > cnt)) return NULL; else if(n == cnt) return back; else if(n < cnt / 2) { p = front; while(--n) p = p->next; return p; } else { p = back; while(cnt > n++) p = p->prev; return p; } } // wyświetla kolejno klucze wszystkich elementów listy //---------------------------------------------------- void showKeys() { TlistElement * p; if(!front) cout << "Lista jest pusta." << endl; else { p = front; while(p) { if(p->node) cout << p->node->c << " : " << p->key << endl; p = p->next; } } } // Wyszukuje na liście dwa najmniejsze elementy //--------------------------------------------- void find2min(TlistElement * &p1, TlistElement * &p2) { TlistElement * p; p1 = front; p2 = front->next; if(p1->key > p2->key) { TlistElement * x; x = p1; p1 = p2; p2 = x; } p = front->next->next; while(p) { if(p->key < p2->key) { p2 = p; if(p1->key > p2->key) { TlistElement * x; x = p1; p1 = p2; p2 = x; } } p = p->next; } } }; // Funkcja rekurencyjne przechodzenia drzewa binarnego //---------------------------------------------------- void inorder(TBTnode * n, char c[], int lenc) { if(!(n->left)) { cout << n->c << " : "; for(int i = 0; i < lenc; i++) cout << c[i]; cout << endl; } else { c[lenc] = '0'; inorder(n->left,c,lenc + 1); c[lenc] = '1'; inorder(n->right,c,lenc + 1); } } //****************************************************** //** Przykładowa implementacja algorytmu Huffmana ** //****************************************************** main() { int i; // Ze standardowego wejścia odczytujemy wiersz znaków string line; // przechowuje odczytany tekst getline(cin,line); // Zliczamy liczbę wystąpień każdego znaku int ccount[256]; // liczniki znaków for(i = 0; i < 256; i++) ccount[i] = 0; for(i = line.size() - 1; i >= 0; i--) ccount[line[i]]++; // tworzymy listę pojedynczych węzłów drzewa binarnego, // na której umieszczamy tylko znaki występujące w // odczytanym wierszu - pomijamy znaki nieobecne. TdoubleList dl; TlistElement * p1, * p2, * p; TBTnode * n; for(i = 255; i >= 0; i--) if(ccount[i]) { n = new TBTnode; n->parent = n->left = n->right = NULL; n->c = i; p = new TlistElement; p->node = n; p->key = ccount[i]; dl.push_front(p); } // wyświetlamy znaki wraz z ich częstościami w wierszu cout << endl; dl.showKeys(); cout << endl; // tworzymy drzewo binarne Huffmana while(dl.size() > 1) { // na liście wyszukujemy dwa najmniejsze elementy dl.find2min(p1,p2); // z węzłów zawartych w p1 i p2 tworzymy nowy węzeł n = new TBTnode; n->parent = NULL; n->left = p1->node; n->right = p2->node; n->left->parent = n->right->parent = n; // nowy węzeł drzewka umieszczamy w p1 p1->node = n; // obliczamy wartość klucza p1->key += p2->key; // p2 usuwamy z listy i z pamięci - nie jest już potrzebne delete dl.erase(p2); } // wywołujemy rekurencyjną funkcję inorder, która // przejdzie przez całe drzewko wypisując kody Huffmana // poszczególnych liści - znaków char hcodes[256]; // przechowuje kody Huffmana odczytane z drzewka inorder(dl.index(1)->node,hcodes,0); // gotowe - kończymy program cout << endl << endl; system("PAUSE"); } DAAFDDGFDSESESFSFDDFADFDAFDAFDDFSFDSFDSFDSDXFDDFSFDFFDAFDFDXFDFDGFDGFDSDAAAASAAD SASASASASASASASAAAAAAAAASASSAWASAAAAAASASASDADDSADSADSSAASAAAADSADSSDAAAAADSASAD SAAAAAAAASDADSADSADSASASASSSAAAAASSADSASADSAAAAFDFESDSADSADDSADADSFFFDSDSDAAAAAA A : 92 D : 53 E : 3 F : 28 G : 3 S : 58 W : 1 X : 2 A : 0 S : 10 W : 110000 X : 110001 E : 110010 G : 110011 F : 1101 D : 111
Powyższy program jest tylko częścią systemu kompresji Huffmana - określa on kody dla poszczególnych znaków zawartych w kompresowanym tekście. Zastanów się nad rozbudową programu, która obejmie również fazę kodowania znaków tekstu wg otrzymanych kodów. A może zaprojektujesz kompletną aplikację kompresji dowolnych plików binarnych oraz ich dekompresji? W takim przypadku zastanów się, jak przekazać w skompresowanym pliku informację o zastosowanym przy kompresji drzewie Huffmana. Informacja ta jest niezbędna do wydobycia znaków ze skompresowanego pliku.
I Liceum Ogólnokształcące |
Pytania proszę przesyłać na adres email: i-lo@eduinf.waw.pl
W artykułach serwisu są używane cookies. Jeśli nie chcesz ich otrzymywać,
zablokuj je w swojej przeglądarce.
Informacje dodatkowe