Algorytmy

Kodowanie Huffmana - omówienie i implementacje

fractal-1938690_960_720

W informatyce każdą informację możemy przedstawić na wiele sposobów. Przykładem może być prosta konwersja z typu całkowitego int na typ tekstowy string. Zabieg taki jest czasem wymagany, aby mieć łatwiejszą kontrolę nad programem lub, po prostu, aby wyświetlić użytkownikowi dane w odpowiedniej formie. Zamiana sposobu zapisu informacji jest również powszechnie wykorzystywana w procesie kompresji danych. Dzisiaj omówimy więc wspólnie jeden z ważniejszych algorytmów kompresji bezstratnej, jakim jest kodowanie Hoffmana.

Czym jest kompresja danych?

Krótko mówiąc kompresja danych ma na celu zminimalizowanie redundancji i tym samym zmniejszenie rozmiaru pliku.

kodowanie Huffmana - zbiór przed kompresją

Za przykład weźmy 12-elementowy zbiór. Łatwo zauważyć, że składa się on z 3 rodzajów figur geometrycznych. Z pozoru może wydawać się mało rozbudowany, jednak gdyby go powtórzyć 1000-krotnie mielibyśmy już 12000 elementów. Taką ilość elementów ciężej już sobie wyobrazić. Co można zrobić, by uprościć przedstawienie zbioru? Zamiast wypisywać tą samą figurę wielokrotnie pod rząd, wystarczy zapisać ją raz i dopisać ilość jej wystąpień. Powyższy zbiór możemy więc uprościć do:

kodowanie Huffmana - zbiór po kompresji

W ten sposób zredukowaliśmy ilość elementów do sześciu i mimo, że nadal trudno by było sobie wyobrazić sześciotysięczny ciąg elementów, to jednak byłoby ich dwukrotnie mniej, niż przed "kompresją". Podsumowując kompresją możemy nazwać proces, którego celem jest zredukowanie wielkości zbioru danych, poprzez zastępowanie powtarzających się ciągów prostszymi symbolami.

Algorytmy kompresji danych możemy podzielić na stratne i bezstratne. Kompresja bezstratna oznacza, że po dekompresji (procesie odwrotnym do kompresji) stan pliku będzie identyczny, jak przed kompresją. Stosowana jest np. w formatach GIF, PNG, czy wszędzie tam, gdzie ważne jest, aby każdy szczegół pliku był zachowany. Kodowanie Hoffmana, o którym będziemy dzisiaj mówić, jest przykładem kompresji bezstratnej.

Kompresja stratna oznacza, że po kompresji pewne szczegóły pliku są nieodwracalnie tracone i po dekompresji zostaną one pominięte. Kompresję stratną spotyka się np. w plikach JPEG, MP3, czy MPEG.

Kodowanie Huffmana - zasada działania

Kodowanie Huffmana polega na zastępowaniu złożonych partii danych pojedynczymi, prostymi kodami (ciągami bitów). W efekcie dane wyjściowe są znacznie krótsze od wejściowych.

By zrozumieć istotę kodowania Huffmana, przejdźmy do przykładu. Weźmy ciąg znaków "programowanie". Jak pewnie dobrze wiecie, w klasycznym kodowaniu ASCII każdy znak reprezentowany jest przez jeden bajt danych, czyli 8 bitów. Za pomocą ośmiu bitów można zapisać 256 znaków. W powyższym słowie zapisanym w formacie ASCII nie wykorzystaliśmy jednak ich wszystkich, a mimo to zajęliśmy łącznie 13 bajtów. Nie potrzebujemy więc aż 256 znaków. Nasz ciąg składa się z liter z podanego zbioru : {p, r, o, g, a, m, w, n, i, e}. Wykorzystujemy więc jedynie 10 znaków z 256. Oznacza to, że na pojedynczy znak wystarczą nam 4 bity (24 = 16 > 10), zamiast ośmiu. Zauważ jednak, że im więcej liter wykorzystamy w tekście, tym mniej efektywny będzie ten algorytm. Gdyby wykorzystać w pliku wszystkie 256 znaków, to jego działanie nie miałoby żadnego efektu.

Kodowanie Huffmana - algorytm

Jak działa kodowanie Huffmana? W przypadku zbiorów znaków w pierwszej kolejności sprawdzana jest ilość wystąpień każdej litery ze zbioru, a następnie przypisywane jest im odpowiednia wartość. Znaki występujące najczęściej dostają najkrótsze kody, zaś te występujące rzadziej - dłuższe. W tym celu wykorzystuje się drzewo binarne, w które wpisywane są znaki i ich częstotliwości wystąpień. Jeżeli nie wiesz czym jest drzewo binarne, to odsyłam Cię do tego wpisu.

Działanie algorytmu możemy rozpisać następująco:

  1. Pobierz tekst od użytkownika;
  2. Zlicz ilość wystąpień dla każdego znaku;
  3. Dla każdego znaku utwórz liście drzewa binarnego; wartością liścia jest ilość wystąpień dla danego znaku
  4. Wybierz dwa liście o najmniejszych wartościach
  5. Utwórz nowy liść, który będzie kontenerem dla wybranych liści; jego wartość jest sumą ich wartości; mniejszy element będzie dzieckiem po lewo, większy dzieckiem po prawo
  6. Powtarzaj kroki 4-5, aż nie pozostanie jeden element - korzeń drzewa.

Na poniższej animacji prześledzisz działanie kodowania Huffmana, dla słowa "banany":

Kodowanie Huffmana - implementacja C++ Java Python

Mając schemat działania pozostało nam już tylko wcielić go w życie :) Zanim przejdziemy do faktycznego kodowania, musimy najpierw przygotować sobie odpowiednie obiekty i funkcje.

Na początek utwórzmy obiekt liścia - będzie on budulcem naszego drzewa. Musimy w nim zawrzeć jego wartość, wskaźniki na potomne liście oraz znak jaki reprezentuje. Dobrze jest również stworzyć przyjmujący wszystkie te wartości jako argumenty. Dzięki temu zaoszczędzimy sobie nieco wysiłku. Proponuję również dodać metodę typu logicznego, która zwróci, czy dany liść nie jest kontenerem dla innych liści.

C++

//Tworzymy strukturę liścia, który będzie podstawowym budulcem drzewa kodowania.
//Definiujemy w niej wskaźniki na lewe dziecko i prawe dziecko,
//jej wartość(ilość wystąpień/ich sumę), znak do kodowania, a także zakodowany znak.
struct Node{
 Node *left;
 Node *right;
 char character;
 int value;
 Node(char c, int v, Node* l=nullptr, Node* r=nullptr){ //tworzymy konstruktor struktury
 	left = l;
 	right = r;
 	character = c;
 	value = v;
 }
 ~Node(){ //pamiętajmy, że zawsze powinno się wyczyścić wszelkie zmienne
 	delete right; //dlatego zwalniamy pamięc zajętą przez dwójkę dzieci
 	delete left; //z racji, iż jest to ten sam destruktor, funkcja ta wywoła się również u usuniętych dzieci
 }
 bool isLeaf(){
 	return character != '\0'; //tworzymy funkcję pomocniczą, która wskażę, czy jest to liść zawierający jakiś znak
 }
};

Java

class Node { //tworzymy pomocniczą klasę, która przyda nam się przy tworzeniu drzewa binarnego
    private int value; //przechowywana wartość
    private Character character = null; //przechowywany znak
    public Node left = null; //lewe dziecko danego liścia
    public Node right = null; //prawe dziecko danego liścia
    public Node(char letter, int val) { //tworzymy konstruktor liścia dla znaków
        value = val;
        character = letter;
    }
    public Node(int val) { //tworzymy konstruktor dla liści będących sumą dzieci
        value = val;
    }
    public int getValue() { //funkcja zwracająca wartość liścia
        return value;
    }
    public Character getCharacter() {//funkcja zwracająca znak liścia
        return character;
    }
    public boolean isLeaf() { //funkcja zwracająca, czy liść nie jest kontenerem 
 return character != null;
    }
}

Python

class Node:
    value = 0
    right = None
    left = None
    character = ""

    def isLeaf(self): #pomocnicza metoda, która pozwoli określić, czy liść przechowuje znak
        return self.character != ""

    def __init__(self, val, ch): #definiujemy konstruktor
        self.value = val
        self.character = ch

Gdy mamy już obiekt-liść,  to musimy stworzyć kolejny obiekt, który będzie efektywnie te liście porównywać. Obiekt ten będzie miał znaczący wpływ na strukturę drzewa:

C++

//Tworzymy strukturę, która pomoże nam przy tworzeniu drzewa kodowania.
//Będzie ona porównywać dwa liście z drzewa/
struct comparator{
 bool operator() (Node *a, Node *b){
  if(a->value != b->value) //jeżeli liście są różnej wartości
  return a->value > b->value; //wykonaj zwykłe porównanie
 if(!a->isLeaf() && b->isLeaf()) //jeżeli są równe i drugi jest kontenerem, to zawsze będzie on uznawany jako większy
  return false;
 if(!b->isLeaf() && a->isLeaf()) //jeżeli są równe i drugi jest kontenerem, to zawsze będzie on uznawany jako większy
  return true;
 if(a->isLeaf() && b->isLeaf()) //jeżeli oba są liściami
  return a->character > b->character; //to decyduje kolejność alfabetyczna
        return true; // jeżeli żaden z powyższych warunków nie zostanie spełniony, zwracamy true
 }
};

Java

class NodeComparator implements Comparator < Node > { //tworzymy pomocniczą klasę, która przyda się przy porównywaniu liści drzewa; korzystamy z interfejsu Comparator

    @Override
    public int compare(Node n1, Node n2) {
 if(n1.getValue() != n2.getValue()) //jeżeli liście są różnej wartości
  return n1.getValue() - n2.getValue(); //wykonaj zwykłe porównanie
 if(n1.isLeaf() && !n2.isLeaf()) //jeżeli są równe i drugi jest kontenerem, to zawsze będzie on uznawany jako większy
  return 1;
 if(n2.isLeaf() && !n1.isLeaf()) //jeżeli są równe i drugi jest kontenerem, to zawsze będzie on uznawany jako większy
  return -1;
 if(n1.isLeaf() && n2.isLeaf()) //jeżeli oba są liściami
  return n1.getCharacter() - n2.getCharacter(); //to decyduje kolejność alfabetyczna
        return -1; // jeżeli żaden z powyższych warunków nie zostanie spełniony, zwracamy -1 - mniejszość
    }
}

Python

W Pythonie poniższą funkcję należy dodać do ciała klasy Node:

#funkcja __lt__ pomoże nam przy tworzeniu priority_queue
    def __lt__(self, other):
        if self.value != other.value: #wykonujemy normalne porównanie, jeżeli liście są różne;
            return self.value < other.value
        if not self.isLeaf() and other.isLeaf(): # w przeciwnym wypadku ważniejsze są liście mające znak
            return True
        if self.isLeaf() and not other.isLeaf(): # w przeciwnym wypadku ważniejsze są liście mające znak
            return False
        if self.isLeaf() and other.isLeaf(): #jeżeli jednak oba maja znak, to decyduje kolejność alfabetyczna
            return ord(self.character[0]) < ord(other.character[0])
        return True

Mając już potrzebne obiekty możemy przystąpić do tworzenia drzewa. Stworzymy więc funkcję o argumencie tekstowym, która zwróci obiekt Node będący korzeniem drzewa.  Zanim jednak w ogóle zaczniemy je budować, musimy najpierw zliczyć znaki i stworzyć na ich podstawie liście drzewa. Utworzone liście dodamy do obiektu typu Priority Queue, który będzie porównywać je przy pomocy przygotowanej wyżej funkcji. Priority Queue jest obiektem o podobnej zasadzie działania, co stos, z tą róznicą, że dane nie zawsze są wrzucane na "wierzch". Zamiast tego przy dodawaniu dokonuje się porównania i na podstawie jego wyniku element jest umieszczany na odpowiedniej pozycji.

C++

//Definiujemy metodę, która stworzy drzewo kodowania Huffmana, a następnie zwróci jego korzeń
//Argumentem jest tekst, który chcemy zakodować
Node * createHuffmanTree(string line){
 map<char, int> counter; //korzystamy z obiektu map, aby zliczyć wystąpienia dla każdego znaku
 for(char c : line){
  if(counter.find(c) == counter.end()){ //jeżeli znaku nie ma w mapie
   counter[c] = 1; //dodajemy go
  }
  else{
   counter[c]++; //jeżeli jest, zwiększamy jego ilość wystąpień
  }
 }
 
 //Tworzymy obiekt priority_queue, który będzie przechowywać nieprzypisane elementy drzewa.
 //Obiekt priority_queue pozwoli nam stworzyć uporządkowaną listę liści drzewa - najmniejsze będą najwyżej.
 //Do porównywania elementów wykorzystujemy wcześniej zdefiniowaną strukturę comparator.
 priority_queue<Node*, vector<Node*>, comparator> nodes; 

 for(auto entry : counter){
  nodes.push(new Node(entry.first, entry.second)); //tworzymy liście drzewa, bazując na znakach i ich ilości wystąpień
 }
}

Java

private static Node createHuffmanTree(String line) {
  HashMap < Character, Integer > occurences = new HashMap < Character, Integer > (); //tworzymy mapę, która przechowa ilość wystąpień dla każdego znaku
         for (char c: line.toCharArray()) { //następnie zliczamy je w pętli
             if (occurences.containsKey(c)) {
                 occurences.put(c, occurences.get(c) + 1);
             } else {
                 occurences.put(c, 1);
             }
         }
         NodeComparator nc = new NodeComparator(); //tworzymy instancję pomocniczej klasy
         PriorityQueue < Node > nodes = new PriorityQueue < Node > (occurences.size(), nc); //korzystamy z PriorityQueue - w uproszczeniu jest to lista, która zadba, aby nasze elementy były zawsze posortowane; korzysta ona z naszego Comparatora
         for (Entry < Character, Integer > entry: occurences.entrySet()) { //zamieniamy znaki i ich ilość wystąpień na liście, a następnie dodajemy je do utworzonej listy
             Node n = new Node(entry.getKey(), entry.getValue());
             nodes.add(n);
         }
}

Python

#Definiujemy metodę, która stworzy drzewo kodowania Huffmana, a następnie zwróci jego korzeń
#Argumentem jest tekst, który chcemy zakodować
def createTree(text):
    occurences = {}
    for c in text: #zliczamy wystąpienia każdego znaku w tekście
        if occurences.__contains__(c):
            occurences[c] += 1
        else:
            occurences[c] = 1
    # Tworzymy obiekt PriorityQueue(), który będzie przechowywać nieprzypisane elementy drzewa.
    # Obiekt priority_queue pozwoli nam stworzyć uporządkowaną listę liści drzewa - najmniejsze będą najwyżej.
    nodes = PriorityQueue()
    for c in occurences.keys(): #tworzymy liście drzewa, bazując na znakach i ich ilości wystąpień, a następnei dodajmy do listy
        node = Node(occurences[c], c)
        nodes.put(node)

Następnie łączymy dwa liście o najmniejszej wartości, tworząc tym samym liść-kontener, którego wartością będzie suma wartości tych dwóch liści. Następnie dodajemy go do listy pozostałych liści. Kroki te powtarzamy, aż na liście nie zostanie jeden element - korzeń naszego drzewa. Wtedy to możemy zwrócić go jako wynik naszej funkcji - drzewo jest już wtedy zbudowane.

C++

//Definiujemy metodę, która stworzy drzewo kodowania Huffmana, a następnie zwróci jego korzeń
//Argumentem jest tekst, który chcemy zakodować
Node * createHuffmanTree(string line){
 map<char, int> counter; //korzystamy z obiektu map, aby zliczyć wystąpienia dla każdego znaku
 for(char c : line){
  if(counter.find(c) == counter.end()){ //jeżeli znaku nie ma w mapie
   counter[c] = 1; //dodajemy go
  }
  else{
   counter[c]++; //jeżeli jest, zwiększamy jego ilość wystąpień
  }
 }
 
 //Tworzymy obiekt priority_queue, który będzie przechowywać nieprzypisane elementy drzewa.
 //Obiekt priority_queue pozwoli nam stworzyć uporządkowaną listę liści drzewa - najmniejsze będą najwyżej.
 //Do porównywania elementów wykorzystujemy wcześniej zdefiniowaną strukturę comparator.
 priority_queue<Node*, vector<Node*>, comparator> nodes; 

 for(auto entry : counter){
  nodes.push(new Node(entry.first, entry.second)); //tworzymy liście drzewa, bazując na znakach i ich ilości wystąpień
 }
 Node *root; //definiujemy zmienną, która docelowo będzie korzeniem naszego drzewa
 while(nodes.size() > 1){ //następnie iterujemy, dopóki w nodes nie zostanie ostatni element - korzeń drzewa
  Node *n1 = nodes.top(); //pobieramy pierwszy, najmniejszy element z priority queue
  nodes.pop(); //a następnie go usuwamy
  Node *n2 = nodes.top(); //ponawiamy powyższe kroki, aby uzyskać drugi, najmniejszy element
  nodes.pop();
  if(n1->value == n2->value && !n1->isLeaf()){ //jeżeli oba liście mają tą samą wartość, a jeden z nich jest kontenerem, to powinien on być traktowany jako większy element
   Node *pom = n1; //dlatego w takiej sytuacji podmieniamy wskaźniki
   n1 = n2;
   n2 = pom;
  }
  root = new Node('\0', n1->value + n2->value, n1, n2); //tworzymy liść-kontener, który będzie przechowywać dwa powyższe elementy i sumę ich wartości
  nodes.push(root); //a następnie dodajemy go do priority queue
 }
 return root; //gdy w priority queue zostanie tylko jeden wyraz, to oznacza, że drzewo zostało zbudowane, a wyraz ten jest jego korzeniem; zwracamy go
}

Java

private static Node createHuffmanTree(String line) {
  HashMap < Character, Integer > occurences = new HashMap < Character, Integer > (); //tworzymy mapę, która przechowa ilość wystąpień dla każdego znaku
         for (char c: line.toCharArray()) { //następnie zliczamy je w pętli
             if (occurences.containsKey(c)) {
                 occurences.put(c, occurences.get(c) + 1);
             } else {
                 occurences.put(c, 1);
             }
         }
         NodeComparator nc = new NodeComparator(); //tworzymy instancję pomocniczej klasy
         PriorityQueue < Node > nodes = new PriorityQueue < Node > (occurences.size(), nc); //korzystamy z PriorityQueue - w uproszczeniu jest to lista, która zadba, aby nasze elementy były zawsze posortowane; korzysta ona z naszego Comparatora
         for (Entry < Character, Integer > entry: occurences.entrySet()) { //zamieniamy znaki i ich ilość wystąpień na liście, a następnie dodajemy je do utworzonej listy
             Node n = new Node(entry.getKey(), entry.getValue());
             nodes.add(n);
         }
         Node rootNode = null; //zmienna pomocnicza przechowująca korzeń drzewa - finalnie ma być on liściem przechowującym wartość równą długości wpisanego wyrazu
         while (nodes.size() > 1) { //dopóki na liście nie pozostał jeden element(korzeń)

             Node n1 = nodes.peek(); //pobieramy najmniejszy element z listy
             nodes.poll(); //a następnie go usuwamy
             Node n2 = nodes.peek(); //ponownie pobieramy najmniejszy element z listy
             nodes.poll(); //i ponownie go usuwamy; w ten sposób pobraliśmy dwa najmniejsze elementy z listy
             Node parent = new Node(n1.getValue() + n2.getValue()); //tworzymy liść, który będzie przechowywał powyższe elementy
             if(n1.getValue() == n2.getValue() && !n1.isLeaf()) { /jeżeli oba liście mają tą samą wartość, a jeden z nich jest kontenerem, to powinien on być traktowany jako większy element
 Node pom = n1; //dlatego w takiej sytuacji podmieniamy wartości
 n1 = n2;
 n2 = pom;
             }
             rootNode = parent; // ustawiamy go tymczasowo jako korzeń
             parent.left = n1; // mniejszy element ustawamy jako jego lewe dziecko
             parent.right = n2;// większy jako jego prawe dziecko
             nodes.add(parent); //następnie dodajemy rodzica jako samodzielny element do listy
         }
         return rootNode;
 }

Python

#Definiujemy metodę, która stworzy drzewo kodowania Huffmana, a następnie zwróci jego korzeń
#Argumentem jest tekst, który chcemy zakodować
def createTree(text):
    occurences = {}
    for c in text: #zliczamy wystąpienia każdego znaku w tekście
        if occurences.__contains__(c):
            occurences[c] += 1
        else:
            occurences[c] = 1
    # Tworzymy obiekt PriorityQueue(), który będzie przechowywać nieprzypisane elementy drzewa.
    # Obiekt priority_queue pozwoli nam stworzyć uporządkowaną listę liści drzewa - najmniejsze będą najwyżej.
    nodes = PriorityQueue()
    for c in occurences.keys(): #tworzymy liście drzewa, bazując na znakach i ich ilości wystąpień, a następnei dodajmy do listy
        node = Node(occurences[c], c)
        nodes.put(node)
    rootNode = None #tworzymy zmienna przechowujaca docelowo korzen drzewa
    while nodes.qsize() > 1: #następnie iterujemy, dopóki w nodes nie zostanie ostatni element - korzeń drzewa
        n1 = nodes.get() #pobieramy pierwszy, najmniejszy element z PriorityQueue
        n2 = nodes.get() #pobieramy kolejny, najmniejszy element z PriorityQueue
        #jeżeli oba liście mają tą samą wartość, a jeden z nich jest kontenerem, to powinien on być traktowany jako większy element
        if n1.value == n2.value and not n1.isLeaf():
            pom = n1 #dlatego w takiej sytuacji podmieniamy wskaźniki
            n1 = n2
            n2 = pom
        parent = Node(n1.value + n2.value, "") #tworzymy liść-kontener, który będzie przechowywać dwa powyższe elementy i sumę ich wartości
        rootNode = parent #ustawiamy go na aktualny korzen
        parent.left = n1 #i dodajemy mu dzieci
        parent.right = n2
        nodes.put(parent) #a następnie dodajemy go do PriorityQueue
    return rootNode #nasze drzewo jest gotowe - zwracamy korzeń

Mamy już utworzone nasze drzewo. Możemy teraz zakodować jego wartości i stworzyć tablicę kodowania. W tym celu stworzymy funkcję rekurencyjną, która wykona się dla wszystkich elementów po lewo i po prawo dla każdego elementu. Jeżeli natrafimy na koniec drzewa, przerywamy rekurencję. Jeżeli temat rekurencji nie jest Ci do końca znany, rzuć okiem na wpis, w którym go wytłumaczyliśmy.  

C++

//Tworzymy rekurencyjną funkcję, która zakoduje elementy zależnie od ich pozycji w drzewie.
//Jako argumenty podajemy korzeń drzewa, tekst pomocniczy przy rekurencji, a także mapę kodowania
void encodeNodes(Node *node, string val, map<char, string> *map) {
 if(node == nullptr){ //warunek STOP funkcji rekurencyjnej:
  return; //jeżeli trafimy na koniec drzewa, przerywamy działanie
 }
 if(node->isLeaf()){ //jeżeli jest to liść przechowujący znak
  cout << node->character << " : " + val << endl; //to drukujemy go i jego zakodowaną wartość
  map->insert({node->character, val}); //a następnie dodajemy obie wartości do mapy
 }
 encodeNodes(node->left, val + '0', map); //wywołujemy jeszcze raz funkcję dla lewej strony drzewa - rekurencja
 encodeNodes(node->right, val + '1', map); //wywołujemy jeszcze raz funkcję dla prawej strony drzewa - rekurencja
}

Java

private static void encodeValues(Node node, String txt, HashMap < Character, String > encodedValues) { //tworzymy rekurencyjną funkcję, która nada liścią odpowiednie wartości
        if (node == null) {
            return;
        }
        if (node.getCharacter() != null) { //jeżeli liść posiada swój znak
            System.out.println(node.getCharacter() + ":" + txt); 
            encodedValues.put(node.getCharacter(), txt); //dodajemy jego zakodowaną wartość do tablicy
        }
        encodeValues(node.left, txt + "0", encodedValues); //wywołujemy funkcję rekurencyjnie
        encodeValues(node.right,txt + "1", encodedValues); //dla obu dzieci; w ten sposób całemu drzewu zostanie przypisana wartość
    }

Python

#tworzymy funkcję, która zakoduje drzewo
#jednocześnie zakoduje ona podany przez użytkownika tekst
def encodeValues(n, str, txt):
    if n is None: #jeżeli trafimy na koniec drzewa
        return txt #przerywamy rekurencję
    if n.isLeaf(): #jeżeli przechowuje on znak
        print(n.character + " : " + str) #to drukujemy go wraz z zakodowaną wartością
        txt = txt.replace(n.character, str) #a następnie podmieniamy znak w tekście z zakodowaną wartościa
    txt = encodeValues(n.left, str + "0", txt) #wykonujemy te same działania dla lewej części drzewa - rekurencja
    txt = encodeValues(n.right, str + "1", txt) #wykonujemy te same działania dla prawej części drzewa - rekurencja
    return txt #na koniec zwracamy zakodowany tekst

Na sam koniec pozostało nam już tylko zakodowanie tekstu, przy użyciu utworzonej tablicy kodowania:

C++

int main(){
 //pobieramy najpierw tekst od użytkownika
 cout << "Kodowanie Huffmana. Podaj tekst, który chcesz zakodować:" << endl;
 string line;
 getline(cin, line);
 
 //tworzymy drzewo bazując na tekście
 Node *root = createHuffmanTree(line);
 
 //kodujemy wartości w drzewie, drukujemy je i dodajemy do nowej mapy, która powiąże znaki z ich zakodowanymi wartościami
 cout<< "Oto tablica kodowania Huffmana:" << endl;
 map<char, string> encodedValues;
 encodeNodes(root, "", &encodedValues);
 
 //kodujemy tekst, bazując na utworzonej mapie kodowania
 string encodedLine = "";
 for(char c : line){
  encodedLine  += encodedValues[c];
 }
 cout << "Oto twoj tekst po zakodowaniu: " << encodedLine << endl;
}

 Java

public static void main(String[] args) {
        System.out.println("Kodowanie Huffmana. Podaj tekst, który chcesz zakodować:");
        Scanner sc = new Scanner(System.in);
        String line = sc.nextLine(); //pobieramy wiersz od użytkownika
        sc.close(); //pamiętając o zwalnianiu zasobów
        Node rootNode = createHuffmanTree(line);
        HashMap < Character, String > encodedValues = new HashMap < Character, String > (); //inicjalizujemy mapę, która przechowa tablicę kodowania
        System.out.println("Oto tablica kodowania:");
        encodeValues(rootNode, "", encodedValues); //przypisujemy odpowiednie wartości gałęziom i liściom
        String output = "";
        for (char c: line.toCharArray()) {
            output += encodedValues.get(c); //następnie kodujemy wyraz, korzystając z utworzonej tablicy kodowania
        }
        System.out.println();
        System.out.println("Oto twój tekst zakodowany metodą Huffmana: " + output);
    }

Python

word = input("Kodowanie Huffmana. Podaj tekst, który chcesz zakodować:").rstrip()
rootNode = createTree(word)
print("Oto tablica kodowania:")
word = encodeValues(rootNode, "", word)
print ("Oto tekst po zakodowaniu: " + word)

Kodowanie Huffmana - odkodowanie

Powyższa procedura kodowania jest całkowicie odwracalna, o ile nadal mamy dostęp do drzewa lub tablicy kodowania. W pierwszym przypadku procedura jest prosta. Iterujemy poprzez każdy bit w kodzie. Jeżeli natrafimy na zero, idziemy w lewą stronę drzewa, jeżeli natomiast trafimy na jeden - wybieramy prawy liść. Gdy posiada on swój znak, to wstawiamy go w zmienną pomocniczą i wracamy na początek drzewa. Jeżeli jednak jest to kontener, to idziemy dalej, dopóki nie trafimy na liść zawierający znak. Czynności te powtarzamy do samego końca zakodowanego tekstu. W ich wyniku w zmiennej pomocniczej będzie znajdować się odkodowany tekst.

C++

//Tworzymy funkcję, która odkoduje zakodowany tekst bazując na zbudowanym drzewie.
string decode(Node *root, string encoded){
 string decoded = ""; //zmienna pomocnicza
 Node *currentNode = root; //wskaźnik na aktualnie sprawdzany liść
 for(char c : encoded){ //iterujemy poprzez zakodowany tekst
  if(c == '0'){ //jeżeli iterowany znak jest zerem, to oznacza, że musimy iść w lewo
   if(currentNode->left->isLeaf()){ //jeżeli dziecko po lewo jest liściem przechowującym znak
    decoded += currentNode->left->character; //dodajemy ten znak do zmiennej pomocniczej
    currentNode = root; //a następnie wracamy na początek drzewa
   }
   else{
    currentNode = currentNode->left; //jeżeli trafiliśmy na kontener, ustawiamy na niego wskaźnik
   }
  
  }
  else{ //jeżeli jest to inny znak (1), to przechodzimy na prawą stronę
   if(currentNode->right->isLeaf()){ //jeżeli dziecko po prawo jest liściem przechowującym znak
    decoded += currentNode->right->character;//dodajemy ten znak do zmiennej pomocniczej
    currentNode = root;//a następnie wracamy na początek drzewa
   }
   else{
    currentNode = currentNode->right;//jeżeli trafiliśmy na kontener, ustawiamy na niego wskaźnik
   }
  
  }
  
 }
 return decoded; //na koniec zwracamy odkodowany tekst
 
}

Java

private static String decode(Node root, String encoded){
  String decoded = ""; //zmienna pomocnicza
  Node currentNode = root; //aktualnie sprawdzany liść
  for(char c : encoded.toCharArray()){ //iterujemy poprzez zakodowany tekst
   if(c == '0'){ //jeżeli iterowany znak jest zerem, to oznacza, że musimy iść w lewo
    if(currentNode.left.isLeaf()){ //jeżeli dziecko po lewo jest liściem przechowującym znak
     decoded += currentNode.left.getCharacter(); //dodajemy ten znak do zmiennej pomocniczej
     currentNode = root; //a następnie wracamy na początek drzewa
    }
    else{
     currentNode = currentNode.left; //jeżeli trafiliśmy na kontener, ustawiamy go jako aktualny
    }
   
   }
   else{ //jeżeli jest to inny znak (1), to przechodzimy na prawą stronę
    if(currentNode.right.isLeaf()){ //jeżeli dziecko po prawo jest liściem przechowującym znak
     decoded += currentNode.right.getCharacter();//dodajemy ten znak do zmiennej pomocniczej
     currentNode = root;//a następnie wracamy na początek drzewa
    }
    else{
     currentNode = currentNode.right;//jeżeli trafiliśmy na kontener, ustawiamy go jako aktualny
    }
   
   }
   
  }
  return decoded; //na koniec zwracamy odkodowany tekst
  
 }

Python

#definiujemy funkcję, która odkoduje tekst na bazie utworzonego drzewa
def decode(root, text):
    decoded = "" #zmienna pomocnicza
    currNode = root #aktualnie rozważany element drzewa
    for char in text: #iterujemy poprzez znaki zakodowanego tekstu
        if char == '0': #jeżeli jest to 0, to oznacza, że musimy iśc na lewą stronę
            if currNode.left.isLeaf(): #jeżeli element po lewo jest liściem
                decoded += currNode.left.character #to dodajemy jego znak do zmiennej pomocniczej
                currNode = root #a następnie wracamy na początek drzewa
            else:
                currNode = currNode.left #jeżeli trafiliśmy na kontener, to od niego zaczniemy nast. iterację
        else: #jeżeli jest to 1, to oznacza, że musimy iśc na prawą stronę
            if currNode.right.isLeaf(): #jeżeli element po prawo jest liściem
                decoded += currNode.right.character #to dodajemy jego znak do zmiennej pomocniczej
                currNode = root #a następnie wracamy na początek drzewa
            else:
                currNode = currNode.right #jeżeli trafiliśmy na kontener, to od niego zaczniemy nast. iterację
    return decoded #zwracamy odkodowana wartosc
grupa wsparcia matura z informatyki

Podsumowanie

W ten oto sposób udało nam się wspólnie zaimplementować kodowanie Huffmana. Z racji na swoją niską wydajność, praktycznie wcale nie korzysta się z niego samodzielnie. Wykorzystywany jest on jednak jako ostatni etap kompresji, najczęściej w połączeniu z kilkoma innymi metodami. Niemniej jednak jest to algorytm warty poznania i zrozumienia. Kto wie, może spotkacie się z nim na maturze? Poniżej znajdziecie pełną implementację kodowania Huffmana:

C++

#include <iostream>
#include <queue>
#include <string>
#include <map>
#include <vector>
#include <algorithm>

using namespace std;

//Tworzymy strukturę liścia, który będzie podstawowym budulcem drzewa kodowania.
//Definiujemy w niej wskaźniki na lewe dziecko i prawe dziecko,
//jej wartość(ilość wystąpień/ich sumę), znak do kodowania, a także zakodowany znak.
struct Node{
 Node *left;
 Node *right;
 char character;
 int value;
 Node(char c, int v, Node* l=nullptr, Node* r=nullptr){ //tworzymy konstruktor struktury
  left = l;
  right = r;
  character = c;
  value = v;
 }
 ~Node(){ //pamiętajmy, że zawsze powinno się wyczyścić wszelkie zmienne
  delete right; //dlatego zwalniamy pamięc zajętą przez dwójkę dzieci
  delete left; //z racji, iż jest to destruktor, funkcja ta wywoła się również u usuniętych dzieci
 }
 bool isLeaf(){
  return character != '\0'; //tworzymy funkcję pomocniczą, która wskażę, czy jest to liść zawierający jakiś znak
 }
};

//Tworzymy strukturę, która pomoże nam przy tworzeniu drzewa kodowania.
//Będzie ona porównywać dwa liście z drzewa/
struct comparator{
 bool operator() (Node *a, Node *b){
  if(a->value != b->value) //jeżeli liście są różnej wartości
  return a->value > b->value; //wykonaj zwykłe porównanie
 if(!a->isLeaf() && b->isLeaf()) //jeżeli są równe i drugi jest kontenerem, to zawsze będzie on uznawany jako większy
  return false;
 if(!b->isLeaf() && a->isLeaf()) //jeżeli są równe i drugi jest kontenerem, to zawsze będzie on uznawany jako większy
  return true;
 if(a->isLeaf() && b->isLeaf()) //jeżeli oba są liściami
  return a->character > b->character; //to decyduje kolejność alfabetyczna
        return true; // jeżeli żaden z powyższych warunków nie zostanie spełniony, zwracamy true
 }
};

//Definiujemy metodę, która stworzy drzewo kodowania Huffmana, a następnie zwróci jego korzeń
//Argumentem jest tekst, który chcemy zakodować
Node * createHuffmanTree(string line){
 map<char, int> counter; //korzystamy z obiektu map, aby zliczyć wystąpienia dla każdego znaku
 for(char c : line){
  if(counter.find(c) == counter.end()){ //jeżeli znaku nie ma w mapie
   counter[c] = 1; //dodajemy go
  }
  else{
   counter[c]++; //jeżeli jest, zwiększamy jego ilość wystąpień
  }
 }
 
 //Tworzymy obiekt priority_queue, który będzie przechowywać nieprzypisane elementy drzewa.
 //Obiekt priority_queue pozwoli nam stworzyć uporządkowaną listę liści drzewa - najmniejsze będą najwyżej.
 //Do porównywania elementów wykorzystujemy wcześniej zdefiniowaną strukturę comparator.
 priority_queue<Node*, vector<Node*>, comparator> nodes; 

 for(auto entry : counter){
  nodes.push(new Node(entry.first, entry.second)); //tworzymy liście drzewa, bazując na znakach i ich ilości wystąpień
 }
 Node *root; //definiujemy zmienną, która docelowo będzie korzeniem naszego drzewa
 while(nodes.size() > 1){ //następnie iterujemy, dopóki w nodes nie zostanie ostatni element - korzeń drzewa
  Node *n1 = nodes.top(); //pobieramy pierwszy, najmniejszy element z priority queue
  nodes.pop(); //a następnie go usuwamy
  Node *n2 = nodes.top(); //ponawiamy powyższe kroki, aby uzyskać drugi, najmniejszy element
  nodes.pop();
  if(n1->value == n2->value && !n1->isLeaf()){ //jeżeli oba liście mają tą samą wartość, a jeden z nich jest kontenerem, to powinien on być traktowany jako większy element
   Node *pom = n1; //dlatego w takiej sytuacji podmieniamy wskaźniki
   n1 = n2;
   n2 = pom;
  }
  root = new Node('\0', n1->value + n2->value, n1, n2); //tworzymy liść-kontener, który będzie przechowywać dwa powyższe elementy i sumę ich wartości
  nodes.push(root); //a następnie dodajemy go do priority queue
 }
 return root; //gdy w priority queue zostanie tylko jeden wyraz, to oznacza, że drzewo zostało zbudowane, a wyraz ten jest jego korzeniem; zwracamy go
}

//Tworzymy rekurencyjną funkcję, która zakoduje elementy zależnie od ich pozycji w drzewie.
//Jako argumenty podajemy korzeń drzewa, tekst pomocniczy przy rekurencji, a także mapę kodowania
void encodeNodes(Node *node, string val, map<char, string> *map) {
 if(node == nullptr){ //warunek STOP funkcji rekurencyjnej:
  return; //jeżeli trafimy na koniec drzewa, przerywamy działanie
 }
 if(node->isLeaf()){ //jeżeli jest to liść przechowujący znak
  cout << node->character << " : " + val << endl; //to drukujemy go i jego zakodowaną wartość
  map->insert({node->character, val}); //a następnie dodajemy obie wartości do mapy
 }
 encodeNodes(node->left, val + '0', map); //wywołujemy jeszcze raz funkcję dla lewej strony drzewa - rekurencja
 encodeNodes(node->right, val + '1', map); //wywołujemy jeszcze raz funkcję dla prawej strony drzewa - rekurencja
}

//Tworzymy funkcję, która odkoduje zakodowany tekst bazując na zbudowanym drzewie.
string decode(Node *root, string encoded){
 string decoded = ""; //zmienna pomocnicza
 Node *currentNode = root; //wskaźnik na aktualnie sprawdzany liść
 for(char c : encoded){ //iterujemy poprzez zakodowany tekst
  if(c == '0'){ //jeżeli iterowany znak jest zerem, to oznacza, że musimy iść w lewo
   if(currentNode->left->isLeaf()){ //jeżeli dziecko po lewo jest liściem przechowującym znak
    decoded += currentNode->left->character; //dodajemy ten znak do zmiennej pomocniczej
    currentNode = root; //a następnie wracamy na początek drzewa
   }
   else{
    currentNode = currentNode->left; //jeżeli trafiliśmy na kontener, ustawiamy na niego wskaźnik
   }
  
  }
  else{ //jeżeli jest to inny znak (1), to przechodzimy na prawą stronę
   if(currentNode->right->isLeaf()){ //jeżeli dziecko po prawo jest liściem przechowującym znak
    decoded += currentNode->right->character;//dodajemy ten znak do zmiennej pomocniczej
    currentNode = root;//a następnie wracamy na początek drzewa
   }
   else{
    currentNode = currentNode->right;//jeżeli trafiliśmy na kontener, ustawiamy na niego wskaźnik
   }
  
  }
  
 }
 return decoded; //na koniec zwracamy odkodowany tekst
 
}

int main(){
 //pobieramy najpierw tekst od użytkownika
 cout << "Kodowanie Huffmana. Podaj tekst, który chcesz zakodować:" << endl;
 string line;
 getline(cin, line);
 
 //tworzymy drzewo bazując na tekście
 Node *root = createHuffmanTree(line);
 
 //kodujemy wartości w drzewie, drukujemy je i dodajemy do nowej mapy, która powiąże znaki z ich zakodowanymi wartościami
 cout<< "Oto tablica kodowania Huffmana:" << endl;
 map<char, string> encodedValues;
 encodeNodes(root, "", &encodedValues);
 
 //kodujemy tekst, bazując na utworzonej mapie kodowania
 string encodedLine = "";
 for(char c : line){
  encodedLine  += encodedValues[c];
 }
 cout << "Oto twoj tekst po zakodowaniu: " << encodedLine << endl;
 
 
 //odkodowujemy tekst, aby sprawdzić działanie
 cout << "Oto tekst po odkodowaniu: " << decode(root, encodedLine) << endl;
 
 //sprzątamy po sobie :)
 delete root;
 return 0;
}

Java

package matura;

import java.util.Comparator;
import java.util.HashMap;
import java.util.Map.Entry;

import java.util.PriorityQueue;
import java.util.Scanner;

class Node { //tworzymy kolejną, pomocniczą klasę, która przyda nam się przy tworzeniu drzewa binarnego
    private int value; //przechowywana wartość
    private Character character = null; //przechowywany znak
    public Node left = null; //lewe dziecko danego liścia
    public Node right = null; //prawe dziecko danego liścia
    public Node(char letter, int val) { //tworzymy konstruktor liścia dla znaków
        value = val;
        character = letter;
    }
    public Node(int val) { //tworzymy konstruktor liścia dla liści będących sumą dzieci
        value = val;
    }
    public int getValue() { //funkcja zwracająca wartość liścia
        return value;
    }
    public Character getCharacter() {//funkcja zwracająca znak liścia
        return character;
    }
    public boolean isLeaf() {
 return character != null;
    }
}

class NodeComparator implements Comparator < Node > { //tworzymy pomocniczą klasę, która przyda się przy porównywaniu liści drzewa; korzystamy z interfejsu Comparator

    @Override
    public int compare(Node n1, Node n2) {
 if(n1.getValue() != n2.getValue()) //jeżeli liście są różnej wartości
  return n1.getValue() - n2.getValue(); //wykonaj zwykłe porównanie
 if(n1.isLeaf() && !n2.isLeaf()) //jeżeli są równe i drugi jest kontenerem, to zawsze będzie on uznawany jako większy
  return 1;
 if(n2.isLeaf() && !n1.isLeaf()) //jeżeli są równe i drugi jest kontenerem, to zawsze będzie on uznawany jako większy
  return -1;
 if(n1.isLeaf() && n2.isLeaf()) //jeżeli oba są liściami
  return n1.getCharacter() - n2.getCharacter(); //to decyduje kolejność alfabetyczna
        return -1; // jeżeli żaden z powyższych warunków nie zostanie spełniony, zwracamy -1 - mniejszość
    }
}

public class Kodowanie_Huffman {

 private static Node createHuffmanTree(String line) {
  HashMap < Character, Integer > occurences = new HashMap < Character, Integer > (); //tworzymy mapę, która przechowa ilość wystąpień dla każdego znaku
         for (char c: line.toCharArray()) { //następnie zliczamy je w pętli
             if (occurences.containsKey(c)) {
                 occurences.put(c, occurences.get(c) + 1);
             } else {
                 occurences.put(c, 1);
             }
         }
         NodeComparator nc = new NodeComparator(); //tworzymy instancję pomocniczej klasy
         PriorityQueue < Node > nodes = new PriorityQueue < Node > (occurences.size(), nc); //korzystamy z PriorityQueue - w uproszczeniu jest to lista, która zadba, aby nasze elementy były zawsze posortowane; korzysta ona z naszego Comparatora
         for (Entry < Character, Integer > entry: occurences.entrySet()) { //zamieniamy znaki i ich ilość wystąpień na liście, a następnie dodajemy je do utworzonej listy
             Node n = new Node(entry.getKey(), entry.getValue());
             nodes.add(n);
         }
         Node rootNode = null; //zmienna pomocnicza przechowująca korzeń drzewa - finalnie ma być on liściem przechowującym wartość równą długości wpisanego wyrazu
         while (nodes.size() > 1) { //dopóki na liście nie pozostał jeden element(korzeń)

             Node n1 = nodes.peek(); //pobieramy najmniejszy element z listy
             nodes.poll(); //a następnie go usuwamy
             Node n2 = nodes.peek(); //ponownie pobieramy najmniejszy element z listy
             nodes.poll(); //i ponownie go usuwamy; w ten sposób pobraliśmy dwa najmniejsze elementy z listy
             Node parent = new Node(n1.getValue() + n2.getValue()); //tworzymy liść, który będzie przechowywał powyższe elementy
             if(n1.getValue() == n2.getValue() && !n1.isLeaf()) {
 Node pom = n1;
 n1 = n2;
 n2 = pom;
             }
             rootNode = parent; // ustawiamy go tymczasowo jako korzeń
             parent.left = n1; // mniejszy element ustawamy jako jego lewe dziecko
             parent.right = n2;// większy jako jego prawe dziecko
             nodes.add(parent); //następnie dodajemy rodzica jako samodzielny element do listy
         }
         return rootNode;
 }
 
 
 
 
 private static void encodeValues(Node node, String txt, HashMap < Character, String > encodedValues) { //tworzymy rekurencyjną funkcję, która nada liścią odpowiednie wartości
        if (node == null) {
            return;
        }
        if (node.getCharacter() != null) { //jeżeli liść posiada swój znak
            System.out.println(node.getCharacter() + ":" + txt); 
            encodedValues.put(node.getCharacter(), txt); //dodajemy jego zakodowaną wartość do tablicy
        }
        encodeValues(node.left, txt + "0", encodedValues); //wywołujemy funkcję rekurencyjnie
        encodeValues(node.right,txt + "1", encodedValues); //dla obu dzieci; w ten sposób całemu drzewu zostanie przypisana wartość
    }
 
 
 private static String decode(Node root, String encoded){
  String decoded = ""; //zmienna pomocnicza
  Node currentNode = root; //aktualnie sprawdzany liść
  for(char c : encoded.toCharArray()){ //iterujemy poprzez zakodowany tekst
   if(c == '0'){ //jeżeli iterowany znak jest zerem, to oznacza, że musimy iść w lewo
    if(currentNode.left.isLeaf()){ //jeżeli dziecko po lewo jest liściem przechowującym znak
     decoded += currentNode.left.getCharacter(); //dodajemy ten znak do zmiennej pomocniczej
     currentNode = root; //a następnie wracamy na początek drzewa
    }
    else{
     currentNode = currentNode.left; //jeżeli trafiliśmy na kontener, ustawiamy go jako aktualny
    }
   
   }
   else{ //jeżeli jest to inny znak (1), to przechodzimy na prawą stronę
    if(currentNode.right.isLeaf()){ //jeżeli dziecko po prawo jest liściem przechowującym znak
     decoded += currentNode.right.getCharacter();//dodajemy ten znak do zmiennej pomocniczej
     currentNode = root;//a następnie wracamy na początek drzewa
    }
    else{
     currentNode = currentNode.right;//jeżeli trafiliśmy na kontener, ustawiamy go jako aktualny
    }
   
   }
   
  }
  return decoded; //na koniec zwracamy odkodowany tekst
  
 }
 
    public static void main(String[] args) {
        System.out.println("Kodowanie Huffmana. Podaj tekst, który chcesz zakodować:");
        Scanner sc = new Scanner(System.in);
        String line = sc.nextLine(); //pobieramy wiersz od użytkownika
        sc.close(); //pamiętając o zwalnianiu zasobów
        Node rootNode = createHuffmanTree(line);
        HashMap < Character, String > encodedValues = new HashMap < Character, String > (); //inicjalizujemy mapę, która przechowa tablicę kodowania
        System.out.println("Oto tablica kodowania:");
        encodeValues(rootNode, "", encodedValues); //przypisujemy odpowiednie wartości gałęziom i liściom
        String output = "";
        for (char c: line.toCharArray()) {
            output += encodedValues.get(c); //następnie kodujemy wyraz, korzystając z utworzonej tablicy kodowania
        }
        System.out.println();
        System.out.println("Oto twój tekst zakodowany metodą Huffmana: " + output);
        System.out.println();
        System.out.println("Oto odkodowany tekst: " + decode(rootNode, output));

    }
}

Python

from queue import PriorityQueue #importujemy PriorityQueue

#tworzymy klasę Node, która będzie podstaowym budulcem drzewa kodowania
#będzie ona przechowywać wartość, lewe i prawe dziecko, zakodowaną wartość, a także znak
class Node:
    value = 0
    right = None
    left = None
    character = ""

    def isLeaf(self): #pomocnicza metoda, która pozwoli określić, czy liść przechowuje znak
        return self.character != ""

    def __init__(self, val, ch): #definiujemy konstruktor
        self.value = val
        self.character = ch

    #funkcja __lt__ pomoże nam przy tworzeniu priority_queue
    def __lt__(self, other):
        if self.value != other.value: #wykonujemy normalne porównanie, jeżeli liście są różne;
            return self.value < other.value
        if not self.isLeaf() and other.isLeaf(): # w przeciwnym wypadku ważniejsze są liście mające znak
            return True
        if self.isLeaf() and not other.isLeaf(): # w przeciwnym wypadku ważniejsze są liście mające znak
            return False
        if self.isLeaf() and other.isLeaf(): #jeżeli jednak oba maja znak, to decyduje kolejność alfabetyczna
            return ord(self.character[0]) < ord(other.character[0])
        return True

#Definiujemy metodę, która stworzy drzewo kodowania Huffmana, a następnie zwróci jego korzeń
#Argumentem jest tekst, który chcemy zakodować
def createTree(text):
    occurences = {}
    for c in text: #zliczamy wystąpienia każdego znaku w tekście
        if occurences.__contains__(c):
            occurences[c] += 1
        else:
            occurences[c] = 1
    # Tworzymy obiekt PriorityQueue(), który będzie przechowywać nieprzypisane elementy drzewa.
    # Obiekt priority_queue pozwoli nam stworzyć uporządkowaną listę liści drzewa - najmniejsze będą najwyżej.
    nodes = PriorityQueue()
    for c in occurences.keys(): #tworzymy liście drzewa, bazując na znakach i ich ilości wystąpień, a następnei dodajmy do listy
        node = Node(occurences[c], c)
        nodes.put(node)
    rootNode = None #tworzymy zmienna przechowujaca docelowo korzen drzewa
    while nodes.qsize() > 1: #następnie iterujemy, dopóki w nodes nie zostanie ostatni element - korzeń drzewa
        n1 = nodes.get() #pobieramy pierwszy, najmniejszy element z PriorityQueue
        n2 = nodes.get() #pobieramy kolejny, najmniejszy element z PriorityQueue
        #jeżeli oba liście mają tą samą wartość, a jeden z nich jest kontenerem, to powinien on być traktowany jako większy element
        if n1.value == n2.value and not n1.isLeaf():
            pom = n1 #dlatego w takiej sytuacji podmieniamy wskaźniki
            n1 = n2
            n2 = pom
        parent = Node(n1.value + n2.value, "") #tworzymy liść-kontener, który będzie przechowywać dwa powyższe elementy i sumę ich wartości
        rootNode = parent #ustawiamy go na aktualny korzen
        parent.left = n1 #i dodajemy mu dzieci
        parent.right = n2
        nodes.put(parent) #a następnie dodajemy go do PriorityQueue
    return rootNode #nasze drzewo jest gotowe - zwracamy korzeń

#tworzymy funkcję, która zakoduje drzewo
#jednocześnie zakoduje ona podany przez użytkownika tekst
def encodeValues(n, str, txt):
    if n is None: #jeżeli trafimy na koniec drzewa
        return txt #przerywamy rekurencję
    if n.isLeaf(): #jeżeli przechowuje on znak
        print(n.character + " : " + str) #to drukujemy go wraz z zakodowaną wartością
        txt = txt.replace(n.character, str) #a następnie podmieniamy znak w tekście z zakodowaną wartościa
    txt = encodeValues(n.left, str + "0", txt) #wykonujemy te same działania dla lewej części drzewa - rekurencja
    txt = encodeValues(n.right, str + "1", txt) #wykonujemy te same działania dla prawej części drzewa - rekurencja
    return txt #na koniec zwracamy zakodowany tekst

#definiujemy funkcję, która odkoduje tekst na bazie utworzonego drzewa
def decode(root, text):
    decoded = "" #zmienna pomocnicza
    currNode = root #aktualnie rozważany element drzewa
    for char in text: #iterujemy poprzez znaki zakodowanego tekstu
        if char == '0': #jeżeli jest to 0, to oznacza, że musimy iśc na lewą stronę
            if currNode.left.isLeaf(): #jeżeli element po lewo jest liściem
                decoded += currNode.left.character #to dodajemy jego znak do zmiennej pomocniczej
                currNode = root #a następnie wracamy na początek drzewa
            else:
                currNode = currNode.left #jeżeli trafiliśmy na kontener, to od niego zaczniemy nast. iterację
        else: #jeżeli jest to 1, to oznacza, że musimy iśc na prawą stronę
            if currNode.right.isLeaf(): #jeżeli element po prawo jest liściem
                decoded += currNode.right.character #to dodajemy jego znak do zmiennej pomocniczej
                currNode = root #a następnie wracamy na początek drzewa
            else:
                currNode = currNode.right #jeżeli trafiliśmy na kontener, to od niego zaczniemy nast. iterację
    return decoded #zwracamy odkodowana wartosc

word = input("Kodowanie Huffmana. Podaj tekst, który chcesz zakodować:").rstrip()
rootNode = createTree(word)
print("Oto tablica kodowania:")
word = encodeValues(rootNode, "", word)
print ("Oto tekst po zakodowaniu: " + word)
word = decode(rootNode, word)
print("Oto odkodowany tekst: " + word)
Dyskusja archiwalna

Komentarze (1)

Komentarze pochodzą z poprzedniej wersji bloga i pozostają tylko do odczytu.

  1. Khasreto

    Super artykuł ;) Fajnie ze masz czas robić coś takiego.