Huffman Compressing Algorithm

Este documento explica el algoritmo de compresión de Huffman, sus conceptos básicos y los procesos involucrados tanto en la compresión como en la descompresión de archivos.

Campobit: Operaciones de Bajo Nivel y Aritmética de Bits

En muchos algoritmos de compresión (como el de Huffman) se requiere manipular datos a nivel de bits para optimizar el espacio. Un campobit es una estructura que nos permite almacenar una secuencia de bits en una variable numérica y realizar operaciones sobre ellos.

La operación básica que veremos es bits_agregar, que añade un bit (0 o 1) al final de nuestro campo de bits. Para entenderla, pensemos en el concepto de desplazamiento de bits: mover un bit a la izquierda o a la derecha. En este caso, usamos el desplazamiento para posicionar el nuevo bit en el lugar correcto.

Por ejemplo, supongamos que tenemos una estructura campobits con:


bits->bits  = 0001 1000 1000   (en binario)
bits->tamano = 12
      

Y queremos agregar un bit 1:

  1. Incremento del tamaño: Se incrementa bits->tamano de 12 a 13, ya que se añade un bit más.
  2. Cálculo de la máscara: Se calcula 0x1 << (bits->tamano - 1), lo que equivale a 0x1 << 12. Esto genera una máscara en binario:
    0001 0000 0000 0000
  3. Actualización del campo de bits: Se utiliza la operación OR (|) para combinar el valor actual de bits->bits con la máscara, de forma que se agrega el bit 1 en la posición correcta sin alterar los otros bits:
    bits->bits = 0000 0001 1000 1000 OR = 0001 0000 0000 0000 ------------------------------ Resultado = 0001 0001 1000 1000

Si en lugar de un 1 se quisiera agregar un 0, no es necesario modificar bits->bits (ya que se asume que sus posiciones están en 0) y solo se incrementa bits->tamano.


/**
 * Función: bits_agregar
 *
 * Descripción:
 *   Esta función añade un bit (0 o 1) al final del campo de bits de la estructura 'campobits'.
 *   Se utiliza aritmética de bits para posicionar el nuevo bit en la ubicación correcta. El campo
 *   'tamano' de la estructura indica cuántos bits se han almacenado hasta el momento y se usa para
 *   calcular la posición en la que se debe agregar el nuevo bit.
 *
 * Parámetros:
 *   - campobits* bits: Puntero a la estructura que contiene:
 *         • bits->bits: El valor numérico que almacena los bits. Se asume que está inicializado a 0.
 *         • bits->tamano: La cantidad actual de bits que se han agregado.
 *   - int bit: Valor del bit a agregar (debe ser 0 o 1).
 *
 * Funcionamiento:
 *   1. **Verificación de seguridad:** Se confirma que el puntero 'bits' no sea nulo y que
 *      aún exista espacio para agregar otro bit, comparando 'bits->tamano' con el máximo número
 *      de bits que puede almacenar 'bits->bits'.
 *
 *   2. **Incremento del tamaño:** Se incrementa 'bits->tamano' en 1, ya que se está añadiendo un nuevo bit.
 *
 *   3. **Cálculo de la máscara y actualización:**
 *      - Si el bit a agregar es 1, se crea una máscara para posicionarlo correctamente. La máscara se
 *        calcula como:
 *
 *            0x1 << (bits->tamano - 1)
 *
 *        Esto desplaza el número 1 a la izquierda tantas posiciones como el número de bits ya agregados,
 *        ubicando el nuevo 1 en la posición adecuada.
 *
 *      - Se actualiza 'bits->bits' aplicando la operación OR (|) entre el valor actual y la máscara.
 *        Esto coloca el bit 1 en su posición sin alterar los otros bits.
 *
 *      - Si el bit es 0, no es necesario modificar 'bits->bits' ya que se asume que todas sus posiciones
 *        están inicialmente a 0; simplemente se incrementa 'bits->tamano'.
 *
 * Ejemplo detallado:
 *   Supongamos que tenemos una variable de tipo 'campobits' inicializada de la siguiente forma:
 *
 *       bits->bits  = 0001 1000 1000   (en binario)
 *       bits->tamano = 12
 *
 *   Y queremos agregar un bit 1:
 *
 *   a) **Incremento del tamaño:**  
 *      - Nuevo valor: bits->tamano pasa de 12 a 13.
 *
 *   b) **Cálculo de la máscara:**  
 *      - Se calcula: 0x1 << (13 - 1) = 0x1 << 12  
 *      - Esto coloca un 1 en la posición 12 (contando desde 0), lo que equivale a la representación:
 *
 *            0001 0000 0000 0000  (en binario)
 *
 *   c) **Actualización del campo de bits:**  
 *      - Se realiza la operación OR:
 *
 *            bits->bits  =  0000 0001 1000 1000
 *          | máscara     =  0001 0000 0000 0000
 *          -------------------------------------
 *            Resultado   =  0001 0001 1000 1000
 *
 * Nota:
 *   Se asume que 'bits->bits' está inicialmente en 0, por lo que agregar un bit 0 no requiere modificar
 *   su valor, solo se incrementa 'bits->tamano'.
 */
      

¿Qué es el algoritmo de Huffman?

El algoritmo de Huffman es un método de compresión sin pérdida que asigna códigos de longitud variable a los caracteres, de tal forma que los caracteres más frecuentes tienen códigos más cortos. Esto permite reducir el tamaño total del archivo comprimido.

Procesos en la compresión

La compresión con Huffman se realiza a través de varios pasos fundamentales:

1. Calcular Frecuencias

Se analiza el archivo de entrada para determinar la frecuencia de cada carácter. El proceso consiste en:


// Ejemplo de función en C para calcular frecuencias:
static int calcular_frecuencias(int* frecuencias, char* entrada) {
  // Inicializa el arreglo de frecuencias en 0.
  for (int i = 0; i < 256; i++) {
    frecuencias[i] = 0;
  }
  // Lee el archivo y cuenta ocurrencias...
}
      

2. Construir el Árbol de Huffman

Utilizando las frecuencias obtenidas:


// Ejemplo de uso del heap para construir el árbol:
static Arbol crear_huffman(int* frecuencias) {
  PQ pq = pq_create();
  // Insertar cada carácter con su frecuencia en el heap...
  // Combinar nodos hasta obtener el árbol completo.
}
      

3. Codificar y Escribir el Árbol

Una vez construido el árbol, el siguiente paso es:


// Ejemplo de función para codificar:
static int codificar(Arbol T, char* entrada, char* salida) {
  // Escribir el árbol de Huffman en el archivo...
  // Recorrer el archivo y escribir el código correspondiente a cada carácter.
}
      

4. Crear Tabla de Códigos y Gestión del Campo de Bits

Para generar la tabla de códigos se recorre el árbol de Huffman. En cada llamada recursiva se agrega un bit (0 para el subárbol izquierdo y 1 para el derecho) al campobits acumulado. Se pueden utilizar dos enfoques:

¿Qué hace bits_remove_last?

Esta función elimina el último bit del campobits decrementando el contador de tamano y limpiando el bit correspondiente mediante una máscara. Esto es especialmente útil en recorridos recursivos donde se necesita retroceder el estado del código.


/**
 * Función: bits_remove_last
 *
 * Descripción:
 *   Elimina el último bit añadido al campo de bits, reduciendo 'tamano' en uno
 *   y limpiando esa posición en 'bits->bits' mediante una máscara.
 *
 * Funcionamiento:
 *   1. Se decrementa 'bits->tamano' en 1.
 *   2. Se crea una máscara: ~(0x1 << bits->tamano) para poner a 0 el bit eliminado.
 *   3. Se actualiza 'bits->bits' aplicando la operación AND con la máscara.
 */
static int bits_remove_last(campobits* bits) {
    if (!bits || bits->tamano <= 0){
        return -1; // Error: No bits to remove
    }
    int last_bit = (bits->bits >> (bits->tamano - 1)) & 0x1;
    bits->tamano--;
    bits->bits &= ~(0x1 << bits->tamano);
    return last_bit;
}
      

Comparación:

Resumen: Usar copias locales resulta en un código más seguro y fácil de mantener, minimizando errores en entornos recursivos. Aunque esto implica un mayor uso de memoria, el impacto es limitado: en el peor de los casos, el uso máximo será el tamaño del campobit multiplicado por la profundidad del árbol. Incluso con n nodos, esta profundidad rara vez supera los 100 niveles debido a la distribución balanceada del árbol de Huffman. Por otro lado, modificar in situ con bits_remove_last puede ahorrar memoria pero incrementa la complejidad y el riesgo de errores. La elección depende del balance entre legibilidad, seguridad y eficiencia, siendo el enfoque con copias una opción más robusta y mantenible en la práctica.

Procesos en la descompresión

La descompresión utiliza el archivo comprimido y el árbol previamente almacenado para restaurar el contenido original. El proceso involucra:

1. Lectura y Reconstrucción del Árbol

Se lee el árbol que fue escrito en el archivo comprimido:


// Ejemplo de función para leer el árbol:
static Arbol leer_arbol(BitStream bs) {
  // Reconstruir el árbol a partir de los bits leídos.
}
      

2. Decodificar el Archivo

Con el árbol reconstruido, se procede a decodificar la secuencia de bits:


// Ejemplo de función para decodificar:
static void decodificar(BitStream in, BitStream out, Arbol arbol) {
  // Procesar el stream de bits y escribir el contenido decodificado.
}
      

Resumen

El algoritmo de Huffman, implementado en C, se fundamenta en:

Estos pasos aseguran que se logre una compresión sin pérdida, aprovechando la frecuencia de aparición de cada carácter para reducir el espacio necesario para almacenarlos.