Algoritmo de Compresión Huffman

En previsión del comienzo del curso, "Algorithms for Developers" ha preparado para usted una traducción de otro material útil.




La codificación de Huffman es un algoritmo de compresión de datos que formula la idea básica de la compresión de archivos. En este artículo, hablaremos sobre la codificación de longitud fija y variable, códigos decodificados de forma única, reglas de prefijos y la construcción de un árbol Huffman.

Sabemos que cada carácter se almacena como una secuencia de 0 y 1 y toma 8 bits. Esto se denomina codificación de longitud fija porque cada carácter usa el mismo número fijo de bits para almacenar.

Digamos que el texto está dado. ¿Cómo podemos reducir la cantidad de espacio requerido para almacenar un personaje?

La idea básica es la codificación de longitud variable. Podemos utilizar el hecho de que algunos caracteres en el texto son más comunes que otros ( ver aquí ) para desarrollar un algoritmo que represente la misma secuencia de caracteres con menos bits. Al codificar una longitud variable, asignamos un número variable de bits a los caracteres dependiendo de la frecuencia de su aparición en este texto. En última instancia, algunos caracteres pueden ocupar solo 1 bit y otros 2 bits, 3 o más. El problema con la codificación de longitud variable es solo la posterior decodificación de la secuencia.

¿Cómo, conociendo la secuencia de bits, decodificarlo de manera única?

Considere la cadena "aabacdab" . Tiene 8 caracteres, y cuando se codifica una longitud fija, se necesitarán 64 bits para almacenarlo. Tenga en cuenta que la frecuencia de los caracteres "a", "b", "c" y "d" es 4, 2, 1, 1, respectivamente. Tratemos de imaginar "aabacdab" con menos bits, utilizando el hecho de que "a" es más común que "b" y "b" es más común que "c" y "d" . Para empezar, codificamos "a" usando un bit igual a 0, "b" asignamos un código de dos bits 11, y usando tres bits 100 y 011 codificamos "c" y"D" .

Como resultado, tendremos éxito:

una0 0
sionce
C100
re011


Por lo tanto, codificamos la cadena "aabacdab" como 00110100011011 (0 | 0 | 11 | 0 | 100 | 011 | 0 | 11) utilizando los códigos presentados anteriormente. Sin embargo, el principal problema será la decodificación. Cuando intentamos decodificar la línea 00110100011011 , obtenemos un resultado ambiguo, ya que puede representarse como:

0|011|0|100|011|0|11    adacdab
0|0|11|0|100|0|11|011   aabacabd
0|011|0|100|0|11|0|11   adacabab 

...
etc.

Para evitar esta ambigüedad, debemos asegurarnos de que nuestra codificación satisfaga un concepto como una regla de prefijo , lo que a su vez implica que los códigos se pueden decodificar de una sola manera. Una regla de prefijo asegura que ningún código sea el prefijo de otro. Por código, nos referimos a los bits utilizados para representar un personaje en particular. En el ejemplo anterior, 0 es el prefijo 011 , que viola la regla del prefijo. Entonces, si nuestros códigos satisfacen la regla del prefijo, entonces podemos decodificar de forma exclusiva (y viceversa).

Repasemos el ejemplo anterior. Esta vez asignaremos los caracteres "a", "b", "c" y "d" Códigos que satisfacen la regla del prefijo.

una0 0
si10
C110
re111


Con esta codificación, la cadena "aabacdab" se codificará como 00100100011010 (0 | 0 | 10 | 0 | 100 | 011 | 0 | 10) . Y aquí 00100100011010 podemos decodificar de forma única y volver a nuestra línea original "aabacdab" .

Codificación Huffman


Ahora que hemos descubierto la codificación de longitud variable y una regla de prefijo, hablemos de la codificación de Huffman.

El método se basa en la creación de árboles binarios. En él, un nodo puede ser finito o interno. Inicialmente, todos los nodos se consideran hojas (hojas), que representan el símbolo en sí y su peso (es decir, la frecuencia de ocurrencia). Los nodos internos contienen el peso del personaje y se refieren a dos nodos descendientes. Por acuerdo general, el bit "0" representa un seguimiento en la rama izquierda, y "1" representa a la derecha. En un árbol completo hay N hojas y N-1 nodos internos. Se recomienda que al construir un árbol Huffman, los caracteres no utilizados se descarten para obtener códigos de longitud óptima.

Utilizaremos la cola de prioridad para construir el árbol Huffman, donde el nodo con la frecuencia más baja tendrá la máxima prioridad. Los pasos de construcción se describen a continuación:

  1. Cree un nodo hoja para cada personaje y agréguelos a la cola de prioridad.
  2. Mientras está en línea para más de una hoja, haga lo siguiente:
    • Elimine los dos nodos con la prioridad más alta (con la frecuencia más baja) de la cola;
    • Cree un nuevo nodo interno donde estos dos nodos sean herederos, y la frecuencia de ocurrencia será igual a la suma de las frecuencias de estos dos nodos.
    • Agregue un nuevo nodo a la cola de prioridad.
  3. El único nodo restante será la raíz, esto finalizará la construcción del árbol.

Imagine que tenemos un texto que consta solo de los caracteres "a", "b", "c", "d" y "e", y las frecuencias de su aparición son 15, 7, 6, 6 y 5, respectivamente. A continuación se muestran ilustraciones que reflejan los pasos del algoritmo.











La ruta desde la raíz a cualquier nodo final almacenará el código de prefijo óptimo (también conocido como el código Huffman) correspondiente al carácter asociado con ese nodo final.


Árbol de Huffman

A continuación encontrará la implementación del algoritmo de compresión de Huffman en C ++ y Java:

#include <iostream>
#include <string>
#include <queue>
#include <unordered_map>
using namespace std;

// A Tree node
struct Node
{
	char ch;
	int freq;
	Node *left, *right;
};

// Function to allocate a new tree node
Node* getNode(char ch, int freq, Node* left, Node* right)
{
	Node* node = new Node();

	node->ch = ch;
	node->freq = freq;
	node->left = left;
	node->right = right;

	return node;
}

// Comparison object to be used to order the heap
struct comp
{
	bool operator()(Node* l, Node* r)
	{
		// highest priority item has lowest frequency
		return l->freq > r->freq;
	}
};

// traverse the Huffman Tree and store Huffman Codes
// in a map.
void encode(Node* root, string str,
			unordered_map<char, string> &huffmanCode)
{
	if (root == nullptr)
		return;

	// found a leaf node
	if (!root->left && !root->right) {
		huffmanCode[root->ch] = str;
	}

	encode(root->left, str + "0", huffmanCode);
	encode(root->right, str + "1", huffmanCode);
}

// traverse the Huffman Tree and decode the encoded string
void decode(Node* root, int &index, string str)
{
	if (root == nullptr) {
		return;
	}

	// found a leaf node
	if (!root->left && !root->right)
	{
		cout << root->ch;
		return;
	}

	index++;

	if (str[index] =='0')
		decode(root->left, index, str);
	else
		decode(root->right, index, str);
}

// Builds Huffman Tree and decode given input text
void buildHuffmanTree(string text)
{
	// count frequency of appearance of each character
	// and store it in a map
	unordered_map<char, int> freq;
	for (char ch: text) {
		freq[ch]++;
	}

	// Create a priority queue to store live nodes of
	// Huffman tree;
	priority_queue<Node*, vector<Node*>, comp> pq;

	// Create a leaf node for each character and add it
	// to the priority queue.
	for (auto pair: freq) {
		pq.push(getNode(pair.first, pair.second, nullptr, nullptr));
	}

	// do till there is more than one node in the queue
	while (pq.size() != 1)
	{
		// Remove the two nodes of highest priority
		// (lowest frequency) from the queue
		Node *left = pq.top(); pq.pop();
		Node *right = pq.top();	pq.pop();

		// Create a new internal node with these two nodes
		// as children and with frequency equal to the sum
		// of the two nodes' frequencies. Add the new node
		// to the priority queue.
		int sum = left->freq + right->freq;
		pq.push(getNode('\0', sum, left, right));
	}

	// root stores pointer to root of Huffman Tree
	Node* root = pq.top();

	// traverse the Huffman Tree and store Huffman Codes
	// in a map. Also prints them
	unordered_map<char, string> huffmanCode;
	encode(root, "", huffmanCode);

	cout << "Huffman Codes are :\n" << '\n';
	for (auto pair: huffmanCode) {
		cout << pair.first << " " << pair.second << '\n';
	}

	cout << "\nOriginal string was :\n" << text << '\n';

	// print encoded string
	string str = "";
	for (char ch: text) {
		str += huffmanCode[ch];
	}

	cout << "\nEncoded string is :\n" << str << '\n';

	// traverse the Huffman Tree again and this time
	// decode the encoded string
	int index = -1;
	cout << "\nDecoded string is: \n";
	while (index < (int)str.size() - 2) {
		decode(root, index, str);
	}
}

// Huffman coding algorithm
int main()
{
	string text = "Huffman coding is a data compression algorithm.";

	buildHuffmanTree(text);

	return 0;
}

import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;

// A Tree node
class Node
{
	char ch;
	int freq;
	Node left = null, right = null;

	Node(char ch, int freq)
	{
		this.ch = ch;
		this.freq = freq;
	}

	public Node(char ch, int freq, Node left, Node right) {
		this.ch = ch;
		this.freq = freq;
		this.left = left;
		this.right = right;
	}
};

class Huffman
{
	// traverse the Huffman Tree and store Huffman Codes
	// in a map.
	public static void encode(Node root, String str,
							  Map<Character, String> huffmanCode)
	{
		if (root == null)
			return;

		// found a leaf node
		if (root.left == null && root.right == null) {
			huffmanCode.put(root.ch, str);
		}


		encode(root.left, str + "0", huffmanCode);
		encode(root.right, str + "1", huffmanCode);
	}

	// traverse the Huffman Tree and decode the encoded string
	public static int decode(Node root, int index, StringBuilder sb)
	{
		if (root == null)
			return index;

		// found a leaf node
		if (root.left == null && root.right == null)
		{
			System.out.print(root.ch);
			return index;
		}

		index++;

		if (sb.charAt(index) == '0')
			index = decode(root.left, index, sb);
		else
			index = decode(root.right, index, sb);

		return index;
	}

	// Builds Huffman Tree and huffmanCode and decode given input text
	public static void buildHuffmanTree(String text)
	{
		// count frequency of appearance of each character
		// and store it in a map
		Map<Character, Integer> freq = new HashMap<>();
		for (int i = 0 ; i < text.length(); i++) {
			if (!freq.containsKey(text.charAt(i))) {
				freq.put(text.charAt(i), 0);
			}
			freq.put(text.charAt(i), freq.get(text.charAt(i)) + 1);
		}

		// Create a priority queue to store live nodes of Huffman tree
		// Notice that highest priority item has lowest frequency
		PriorityQueue<Node> pq = new PriorityQueue<>(
										(l, r) -> l.freq - r.freq);

		// Create a leaf node for each character and add it
		// to the priority queue.
		for (Map.Entry<Character, Integer> entry : freq.entrySet()) {
			pq.add(new Node(entry.getKey(), entry.getValue()));
		}

		// do till there is more than one node in the queue
		while (pq.size() != 1)
		{
			// Remove the two nodes of highest priority
			// (lowest frequency) from the queue
			Node left = pq.poll();
			Node right = pq.poll();

			// Create a new internal node with these two nodes as children 
			// and with frequency equal to the sum of the two nodes
			// frequencies. Add the new node to the priority queue.
			int sum = left.freq + right.freq;
			pq.add(new Node('\0', sum, left, right));
		}

		// root stores pointer to root of Huffman Tree
		Node root = pq.peek();

		// traverse the Huffman tree and store the Huffman codes in a map
		Map<Character, String> huffmanCode = new HashMap<>();
		encode(root, "", huffmanCode);

		// print the Huffman codes
		System.out.println("Huffman Codes are :\n");
		for (Map.Entry<Character, String> entry : huffmanCode.entrySet()) {
			System.out.println(entry.getKey() + " " + entry.getValue());
		}

		System.out.println("\nOriginal string was :\n" + text);

		// print encoded string
		StringBuilder sb = new StringBuilder();
		for (int i = 0 ; i < text.length(); i++) {
			sb.append(huffmanCode.get(text.charAt(i)));
		}

		System.out.println("\nEncoded string is :\n" + sb);

		// traverse the Huffman Tree again and this time
		// decode the encoded string
		int index = -1;
		System.out.println("\nDecoded string is: \n");
		while (index < sb.length() - 2) {
			index = decode(root, index, sb);
		}
	}

	public static void main(String[] args)
	{
		String text = "Huffman coding is a data compression algorithm.";

		buildHuffmanTree(text);
	}
}

Nota: la memoria utilizada por la cadena de entrada es 47 * 8 = 376 bits, y la cadena codificada solo toma 194 bits, es decir los datos se comprimen en aproximadamente un 48%. En el programa C ++ anterior, usamos la clase de cadena para almacenar la cadena codificada para que el programa sea legible.

Dado que las estructuras de datos efectivas de la cola de prioridad requieren tiempo O (log (N)) para insertarse, y en un árbol binario completo con N hojas hay 2N-1 nodos, y el árbol Huffman es un árbol binario completo, el algoritmo funciona para O (Nlog (N )) tiempo, donde N es el número de caracteres.

Fuentes:


en.wikipedia.org/wiki/Huffman_coding
en.wikipedia.org/wiki/Variable-length_code
www.youtube.com/watch?v=5wRPin4oxCo



.



All Articles