Ybadoo - Soluções em Software Livre
Tutoriais
Programação Orientada a Objetos

(Goodrich, 2007) Implemente a compressão e descompressão de documentos baseada na codificação de Huffman, apresentando a modelagem do projeto em Unified Modeling Language (UML) e a sua implementação.

 

Node.java Leaf.java InternalNode.java CodeTree.java CanonicalCode.java FrequencyTable.java BitInputStream.java BitOutputStream.java Encoder.java Decoder.java Huffman.java HuffmanException.java Application.java

Arquivo Node.java

/*************************************************************************
 * Copyright (C) 2009/2021 - Cristiano Lehrer (cristiano@ybadoo.com.br)  *
 *                  Ybadoo - Solucoes em Software Livre (ybadoo.com.br)  *
 *                                                                       *
 * Permission is granted to copy, distribute and/or modify this document *
 * under the terms of the GNU Free Documentation License, Version 1.3 or *
 * any later version published by the  Free Software Foundation; with no *
 * Invariant Sections,  no Front-Cover Texts, and no Back-Cover Texts. A *
 * A copy of the  license is included in  the section entitled "GNU Free *
 * Documentation License".                                               *
 *                                                                       *
 * Ubuntu 16.10 (GNU/Linux 4.8.0-39-generic)                             *
 * OpenJDK Version "1.8.0_121"                                           *
 * OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)               *
 *************************************************************************/

package com.ybadoo.tutoriais.poo.tutorial07.exercicio08;

/**
 * Representação do nó na árvore de Huffman
 */
public interface Node
{

}

Arquivo Leaf.java

/*************************************************************************
 * Copyright (C) 2009/2021 - Cristiano Lehrer (cristiano@ybadoo.com.br)  *
 *                  Ybadoo - Solucoes em Software Livre (ybadoo.com.br)  *
 *                                                                       *
 * Permission is granted to copy, distribute and/or modify this document *
 * under the terms of the GNU Free Documentation License, Version 1.3 or *
 * any later version published by the  Free Software Foundation; with no *
 * Invariant Sections,  no Front-Cover Texts, and no Back-Cover Texts. A *
 * A copy of the  license is included in  the section entitled "GNU Free *
 * Documentation License".                                               *
 *                                                                       *
 * Ubuntu 16.10 (GNU/Linux 4.8.0-39-generic)                             *
 * OpenJDK Version "1.8.0_121"                                           *
 * OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)               *
 *************************************************************************/

package com.ybadoo.tutoriais.poo.tutorial07.exercicio08;

/**
 * Representação da folha na árvore de Huffman
 */
public class Leaf implements Node
{
  /**
   * Símbolo armazenado na folha
   */
  private int symbol;

  /**
   * Inicializar a folha na árvore de Huffman
   *
   * @param symbol símbolo armazenado na folha
   */
  public Leaf(int symbol)
  {
    if (symbol < 0)
    {
      throw new IllegalArgumentException("Symbol value must be non-negative");
    }

    this.symbol = symbol;
  }

  /**
   * Retornar o símbolo armazenado na folha
   *
   * @return símbolo armazenado na folha
   */
  public int getSymbol()
  {
    return symbol;
  }
}

Arquivo InternalNode.java

/*************************************************************************
 * Copyright (C) 2009/2021 - Cristiano Lehrer (cristiano@ybadoo.com.br)  *
 *                  Ybadoo - Solucoes em Software Livre (ybadoo.com.br)  *
 *                                                                       *
 * Permission is granted to copy, distribute and/or modify this document *
 * under the terms of the GNU Free Documentation License, Version 1.3 or *
 * any later version published by the  Free Software Foundation; with no *
 * Invariant Sections,  no Front-Cover Texts, and no Back-Cover Texts. A *
 * A copy of the  license is included in  the section entitled "GNU Free *
 * Documentation License".                                               *
 *                                                                       *
 * Ubuntu 16.10 (GNU/Linux 4.8.0-39-generic)                             *
 * OpenJDK Version "1.8.0_121"                                           *
 * OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)               *
 *************************************************************************/

package com.ybadoo.tutoriais.poo.tutorial07.exercicio08;

/**
 * Representação do nó interno na árvore de Huffman
 */
public class InternalNode implements Node
{
  /**
   *  Nó esquerdo na árvore de Huffman
   */
  private final Node left;

  /**
   * Nó direito na árvore de Huffman
   */
  private final Node right;

  /**
   * Inicializar o nó interno na árvore de Huffman
   *
   * @param left nó esquerdo na árvore de Huffman
   * @param right nó direito na árvore de Huffman
   */
  public InternalNode(Node left, Node right)
  {
    this.left = left;

    this.right = right;
  }

  /**
   * Retornar o nó esquerdo na árvore de Huffman
   *
   * @return nó esquerdo na árvore de Huffman
   */
  public Node getLeft()
  {
    return left;
  }

  /**
   * Retornar o nó direito na árvore de Huffman
   *
   * @return nó direito na árvore de Huffman
   */
  public Node getRight()
  {
    return right;
  }
}

Arquivo CodeTree.java

/*************************************************************************
 * Copyright (C) 2009/2021 - Cristiano Lehrer (cristiano@ybadoo.com.br)  *
 *                  Ybadoo - Solucoes em Software Livre (ybadoo.com.br)  *
 *                                                                       *
 * Permission is granted to copy, distribute and/or modify this document *
 * under the terms of the GNU Free Documentation License, Version 1.3 or *
 * any later version published by the  Free Software Foundation; with no *
 * Invariant Sections,  no Front-Cover Texts, and no Back-Cover Texts. A *
 * A copy of the  license is included in  the section entitled "GNU Free *
 * Documentation License".                                               *
 *                                                                       *
 * Ubuntu 16.10 (GNU/Linux 4.8.0-39-generic)                             *
 * OpenJDK Version "1.8.0_121"                                           *
 * OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)               *
 *************************************************************************/

package com.ybadoo.tutoriais.poo.tutorial07.exercicio08;

import java.util.ArrayList;
import java.util.List;

/**
 * Representação da árvore de Huffman
 */
public class CodeTree
{
  /**
   * Armazena o código para cada símbolo 
   */
  private List<List<Integer>> codes;

  /**
   * Raiz da árvore de Huffman 
   */
  private final InternalNode root;

  /**
   * Inicializar a árvore de Huffman
   *
   * @param root raiz da árvore de Huffman
   * @param symbolLimit quantidade de símbolos
   */
  public CodeTree(InternalNode root, int symbolLimit)
  {
    if (symbolLimit < 2)
    {
      throw new IllegalArgumentException("At least 2 symbols needed");
    }

    this.root = root;

    codes = new ArrayList<>();

    for (int i = 0; i < symbolLimit; i++)
    {
      codes.add(null);
    }

    buildCodeList(root, new ArrayList<Integer>());
  }

  /**
   * Retornar o código de Huffman do símbolo especificado
   *
   * @param symbol símbolo especificado
   * @return código de Huffman do símbolo especificado
   */
  public List<Integer> getCode(int symbol)
  {
    if (symbol < 0)
    {
      throw new IllegalArgumentException("Illegal symbol");
    }

    if (codes.get(symbol) == null)
    {
      throw new IllegalArgumentException("No code for given symbol");
    }

    return codes.get(symbol);
  }

  /**
   * Retornar a raiz da árvore de Huffman
   *
   * @return raiz da árvore de Huffman
   */
  public InternalNode getRoot()
  {
    return root;
  }

  /**
   * Construção recursiva da árvore de Huffman
   *
   * @param node nó atual
   * @param prefix prefixo do código de Huffman
   */
  private void buildCodeList(Node node, List<Integer> prefix)
  {
    if (node instanceof InternalNode)
    {
      InternalNode internalNode = (InternalNode) node;

      prefix.add(0);
      buildCodeList(internalNode.getLeft(), prefix);
      prefix.remove(prefix.size() - 1);

      prefix.add(1);
      buildCodeList(internalNode.getRight(), prefix);
      prefix.remove(prefix.size() - 1);
    }
    else
    {
      Leaf leaf = (Leaf) node;

      if (leaf.getSymbol() >= codes.size())
      {
        throw new IllegalArgumentException("Symbol exceeds symbol limit");
      }

      if (codes.get(leaf.getSymbol()) != null)
      {
        throw new IllegalArgumentException("Symbol has more than one code");
      }

      codes.set(leaf.getSymbol(), new ArrayList<Integer>(prefix));
    }
  }
}

Arquivo CanonicalCode.java

/*************************************************************************
 * Copyright (C) 2009/2021 - Cristiano Lehrer (cristiano@ybadoo.com.br)  *
 *                  Ybadoo - Solucoes em Software Livre (ybadoo.com.br)  *
 *                                                                       *
 * Permission is granted to copy, distribute and/or modify this document *
 * under the terms of the GNU Free Documentation License, Version 1.3 or *
 * any later version published by the  Free Software Foundation; with no *
 * Invariant Sections,  no Front-Cover Texts, and no Back-Cover Texts. A *
 * A copy of the  license is included in  the section entitled "GNU Free *
 * Documentation License".                                               *
 *                                                                       *
 * Ubuntu 16.10 (GNU/Linux 4.8.0-39-generic)                             *
 * OpenJDK Version "1.8.0_121"                                           *
 * OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)               *
 *************************************************************************/

package com.ybadoo.tutoriais.poo.tutorial07.exercicio08;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Classe responsável pela representação canônica do código de Huffman,
 * descrevendo apenas o comprimento do código de cada símbolo
 */
public class CanonicalCode
{
  /**
   * Array com os comprimentos do código de cada símbolo
   */
  private int[] codeLengths;

  /**
   * Inicializar a representação canônica do código de Huffman, a partir da árvore de código especificada
   *
   * @param tree árvore de código a ser analizada
   * @param symbolLimit número máximo de símbolos presentes na árvore de código
   */
  public CanonicalCode(CodeTree tree, int symbolLimit)
  {
    if (symbolLimit < 2)
    {
      throw new IllegalArgumentException("At least 2 symbols needed");
    }

    codeLengths = new int[symbolLimit];

    buildCodeLengths(tree.getRoot(), 0);
  }

  /**
   * Inicializar a representação canônica do código de Huffman, a partir da matriz especificada de comprimentos de código
   *
   * @param codeLengths array of symbol code lengths
   */
  public CanonicalCode(int[] codeLengths)
  {
    validate(codeLengths);

    this.codeLengths = codeLengths.clone();

    Arrays.sort(this.codeLengths);

    int currentLevel = this.codeLengths[this.codeLengths.length - 1];

    int numNodesAtLevel = 0;

    for (int i = (this.codeLengths.length - 1); (i >= 0) && (this.codeLengths[i] > 0); i--)
    {
      int codeLength = this.codeLengths[i];

      while (codeLength < currentLevel)
      {
        if ((numNodesAtLevel % 2) != 0)
        {
          throw new IllegalArgumentException("Under-full Huffman code tree");
        }

        numNodesAtLevel = numNodesAtLevel / 2;

        currentLevel = currentLevel - 1;
      }

      numNodesAtLevel = numNodesAtLevel + 1;
    }

    while (currentLevel > 0)
    {
      if ((numNodesAtLevel % 2) != 0)
      {
        throw new IllegalArgumentException("Under-full Huffman code tree");
      }

      numNodesAtLevel = numNodesAtLevel / 2;

      currentLevel = currentLevel - 1;
    }

    if (numNodesAtLevel < 1)
    {
      throw new IllegalArgumentException("Under-full Huffman code tree");
    }

    if (numNodesAtLevel > 1)
    {
      throw new IllegalArgumentException("Over-full Huffman code tree");
    }

    System.arraycopy(codeLengths, 0, this.codeLengths, 0, codeLengths.length);
  }

  /**
   * Returns the code length of the specified symbol value
   *
   * @param symbol the symbol value to query
   * @return the code length of the symbol, which is non-negative
   */
  public int getCodeLength(int symbol)
  {
    if ((symbol < 0) || (symbol >= codeLengths.length))
    {
      throw new IllegalArgumentException("Symbol out of range");
    }

    return codeLengths[symbol];
  }

  /**
   * Returns the symbol limit for this canonical Huffman code
   *
   * @return the symbol limit, which is at least 2
   */
  public int getSymbolLimit()
  {
    return codeLengths.length;
  }

  /**
   * Returns the canonical code tree for this canonical Huffman code
   *
   * @return the canonical code tree
   */
  public CodeTree toCodeTree()
  {
    List<Node> nodes = new ArrayList<>();

    for (int i = max(codeLengths); i >= 0; i--)
    {
      List<Node> newNodes = new ArrayList<>();

      if (i > 0)
      {
        for (int j = 0; j < codeLengths.length; j++)
        {
          if (codeLengths[j] == i)
          {
            newNodes.add(new Leaf(j));
          }
        }
      }

      for (int j = 0; j < nodes.size(); j += 2)
      {
        newNodes.add(new InternalNode(nodes.get(j), nodes.get(j + 1)));
      }

      nodes = newNodes;
    }

    return new CodeTree((InternalNode) nodes.get(0), codeLengths.length);
  }

  /**
   * Recursive helper method for the above constructor
   *
   * @param node actual node
   * @param depth depth of the tree
   */
  private void buildCodeLengths(Node node, int depth)
  {
    if (node instanceof InternalNode)
    {
      InternalNode internalNode = (InternalNode) node;

      buildCodeLengths(internalNode.getLeft(), depth + 1);

      buildCodeLengths(internalNode.getRight(), depth + 1);
    }
    else
    {
      int symbol = ((Leaf) node).getSymbol();

      if (symbol >= codeLengths.length)
      {
        throw new IllegalArgumentException("Symbol exceeds symbol limit");
      }

      codeLengths[symbol] = depth;
    }
  }

  /**
   * Returns the maximum value in the given array, which must have at least 1 element
   *
   * @param array array
   * @return maximum value in the array
   */
  private int max(int[] array)
  {
    int result = array[0];

    for (int x : array)
    {
      result = Math.max(x, result);
    }

    return result;
  }

  /**
   * Validate the array of symbol code lengths
   *
   * @param codeLengths array of symbol code lengths
   */
  private void validate(int[] codeLengths)
  {
    if (codeLengths.length < 2)
    {
      throw new IllegalArgumentException("At least 2 symbols needed");
    }

    for (int codeLength : codeLengths)
    {
      if (codeLength < 0)
      {
        throw new IllegalArgumentException("Illegal code length");
      }
    }
  }
}

Arquivo FrequencyTable.java

/*************************************************************************
 * Copyright (C) 2009/2021 - Cristiano Lehrer (cristiano@ybadoo.com.br)  *
 *                  Ybadoo - Solucoes em Software Livre (ybadoo.com.br)  *
 *                                                                       *
 * Permission is granted to copy, distribute and/or modify this document *
 * under the terms of the GNU Free Documentation License, Version 1.3 or *
 * any later version published by the  Free Software Foundation; with no *
 * Invariant Sections,  no Front-Cover Texts, and no Back-Cover Texts. A *
 * A copy of the  license is included in  the section entitled "GNU Free *
 * Documentation License".                                               *
 *                                                                       *
 * Ubuntu 16.10 (GNU/Linux 4.8.0-39-generic)                             *
 * OpenJDK Version "1.8.0_121"                                           *
 * OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)               *
 *************************************************************************/

package com.ybadoo.tutoriais.poo.tutorial07.exercicio08;

import java.util.PriorityQueue;
import java.util.Queue;

/**
 * Representação da tabela de frequência dos símbolos
 */
public class FrequencyTable
{
  /**
   * Representação do nó utilizado na construção da tabela de frequência
   */
  private class NodeWithFrequency implements Comparable<NodeWithFrequency>
  {
    /**
     * Valor da frequência
     */
    public final long frequency;

    /**
     * Símbolo
     */
    public final int lowestSymbol;

    /**
     * Nó
     */
    public final Node node;

    /**
     * Inicializar o nó auxiliar
     *
     * @param node nó
     * @param lowestSymbol símbolo
     * @param frequency frequência
     */
    public NodeWithFrequency(Node node, int lowestSymbol, long frequency)
    {
      this.node = node;

      this.lowestSymbol = lowestSymbol;

      this.frequency = frequency;
    }

    /* (non-Javadoc)
     * @see java.lang.Comparable#compareTo(java.lang.Object)
     */
    @Override
    public int compareTo(NodeWithFrequency other)
    {
      if (frequency < other.frequency)
      {
        return -1;
      }

      if (frequency > other.frequency)
      {
        return 1;
      }

      if (lowestSymbol < other.lowestSymbol)
      {
        return -1;
      }

      if (lowestSymbol > other.lowestSymbol)
      {
        return 1;
      }

      return 0;
    }

    /* (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object object)
    {
      if (this == object)
      {
        return true;
      }

      if (object == null)
      {
        return false;
      }

      if (getClass() != object.getClass())
      {
        return false;
      }

      NodeWithFrequency other = (NodeWithFrequency) object;
      
      if (frequency != other.frequency)
      {
        return false;
      }

      if (lowestSymbol != other.lowestSymbol)
      {
        return false;
      }

      if (node == null)
      {
        if (other.node != null)
        {
          return false;
        }
      }
      else if (!node.equals(other.node))
      {
        return false;
      }

      return true;
    }

    /* (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode()
    {
      final int prime = 31;

      int result = 1;

      result = prime * result + (int) (frequency ^ (frequency >>> 32));
      result = prime * result + lowestSymbol;
      result = prime * result + ((node == null) ? 0 : node.hashCode());

      return result;
    }
  }

  /**
   * Frequências
   */
  private int[] frequencies;

  /**
   * Inicializar a tabela de frequência dos símbolos
   *
   * @param frequencies Frequências
   */
  public FrequencyTable(int[] frequencies)
  {
    if (frequencies.length < 2)
    {
      throw new IllegalArgumentException("At least 2 symbols needed");
    }

    this.frequencies = frequencies.clone();

    for (int frequency : this.frequencies)
    {
      if (frequency < 0)
      {
        throw new IllegalArgumentException("Negative frequency");
      }
    }
  }

  /**
   * Retorna a árvore de Huffman ideal para as freqüências de símbolos contidas na tabela
   *
   * @return árvore de Huffman ideal para as freqüências de símbolos contidas na tabela
   */
  public CodeTree buildCodeTree()
  {
    Queue<NodeWithFrequency> pqueue = new PriorityQueue<>();

    for (int i = 0; i < frequencies.length; i++)
    {
      if (frequencies[i] > 0)
      {
        pqueue.add(new NodeWithFrequency(new Leaf(i), i, frequencies[i]));
      }
    }

    for (int i = 0; i < frequencies.length && pqueue.size() < 2; i++)
    {
      if (frequencies[i] == 0)
      {
        pqueue.add(new NodeWithFrequency(new Leaf(i), i, 0));
      }
    }

    while (pqueue.size() > 1)
    {
      NodeWithFrequency x = pqueue.remove();

      NodeWithFrequency y = pqueue.remove();

      pqueue.add(
          new NodeWithFrequency(
              new InternalNode(x.node, y.node), Math.min(x.lowestSymbol, y.lowestSymbol),
              x.frequency + y.frequency));
    }

    return new CodeTree((InternalNode) pqueue.remove().node, frequencies.length);
  }

  /**
   * Retornar a freqüência do símbolo especificado nesta tabela de freqüência
   *
   * @param symbol símbolo especificado
   * @return freqüência do símbolo especificado nesta tabela de freqüência
   */
  public int get(int symbol)
  {
    checkSymbol(symbol);

    return frequencies[symbol];
  }

  /**
   * Retornar o número de símbolos nesta tabela de freqüência
   *
   * @return número de símbolos nesta tabela de freqüência
   */
  public int getSymbolLimit()
  {
    return frequencies.length;
  }

  /**
   * Aumentar a frequência do símbolo especificado nesta tabela de frequências
   *
   * @param symbol símbolo especificado
   */
  public void increment(int symbol)
  {
    checkSymbol(symbol);

    if (frequencies[symbol] == Integer.MAX_VALUE)
    {
      throw new IllegalStateException("Maximum frequency reached");
    }

    frequencies[symbol]++;
  }

  /**
   * Configurar a frequência do símbolo especificado nesta tabela de frequências para o valor especificado
   *
   * @param symbol símbolo especificado
   * @param frequency valor especificado
   */
  public void set(int symbol, int frequency)
  {
    checkSymbol(symbol);

    if (frequency < 0)
    {
      throw new IllegalArgumentException("Negative frequency");
    }

    frequencies[symbol] = frequency;
  }

  /**
   * Verificar se o símbolo especificado está contido no intervalo especificado
   *
   * @param symbol símbolo especificado
   */
  private void checkSymbol(int symbol)
  {
    if ((symbol < 0) || (symbol >= frequencies.length))
    {
      throw new IllegalArgumentException("Symbol out of range");
    }
  }
}

Arquivo BitInputStream.java

/*************************************************************************
 * Copyright (C) 2009/2021 - Cristiano Lehrer (cristiano@ybadoo.com.br)  *
 *                  Ybadoo - Solucoes em Software Livre (ybadoo.com.br)  *
 *                                                                       *
 * Permission is granted to copy, distribute and/or modify this document *
 * under the terms of the GNU Free Documentation License, Version 1.3 or *
 * any later version published by the  Free Software Foundation; with no *
 * Invariant Sections,  no Front-Cover Texts, and no Back-Cover Texts. A *
 * A copy of the  license is included in  the section entitled "GNU Free *
 * Documentation License".                                               *
 *                                                                       *
 * Ubuntu 16.10 (GNU/Linux 4.8.0-39-generic)                             *
 * OpenJDK Version "1.8.0_121"                                           *
 * OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)               *
 *************************************************************************/

package com.ybadoo.tutoriais.poo.tutorial07.exercicio08;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;

/**
 * Classe responsável pela leitura do fluxo de bits de entrada
 */
public class BitInputStream extends InputStream
{
  /**
   * Byte atual, no intervalo de 0x00 a 0xFF ou -1 se o fim do fluxo for atingido
   */
  private int currentByte;

  /**
   * Fluxo de bits de entrada
   */
  private final InputStream inputStream;

  /**
   * Número de bits restantes no byte atual, sempre entre 0 e 7 (inclusive)
   */
  private int numBitsRemaining;

  /**
   * Inicializar a leitura do fluxo de bits de entrada
   *
   * @param inputStream fluxo de bits de entrada
   */
  public BitInputStream(InputStream inputStream)
  {
    this.inputStream = inputStream;
  }

  /* (non-Javadoc)
   * @see java.io.InputStream#read()
   */
  @Override
  public int read() throws IOException
  {
    if (currentByte == -1)
    {
      throw new EOFException();
    }

    if (numBitsRemaining == 0)
    {
      currentByte = inputStream.read();

      if (currentByte == -1)
      {
        throw new EOFException();
      }

      numBitsRemaining = 8;
    }

    numBitsRemaining = numBitsRemaining - 1;

    return (currentByte >>> numBitsRemaining) & 1;
  }

  /* (non-Javadoc)
   * @see java.io.InputStream#close()
   */
  @Override
  public void close() throws IOException
  {
    inputStream.close();

    currentByte = -1;
  }
}

Arquivo BitOutputStream.java

/*************************************************************************
 * Copyright (C) 2009/2021 - Cristiano Lehrer (cristiano@ybadoo.com.br)  *
 *                  Ybadoo - Solucoes em Software Livre (ybadoo.com.br)  *
 *                                                                       *
 * Permission is granted to copy, distribute and/or modify this document *
 * under the terms of the GNU Free Documentation License, Version 1.3 or *
 * any later version published by the  Free Software Foundation; with no *
 * Invariant Sections,  no Front-Cover Texts, and no Back-Cover Texts. A *
 * A copy of the  license is included in  the section entitled "GNU Free *
 * Documentation License".                                               *
 *                                                                       *
 * Ubuntu 16.10 (GNU/Linux 4.8.0-39-generic)                             *
 * OpenJDK Version "1.8.0_121"                                           *
 * OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)               *
 *************************************************************************/

package com.ybadoo.tutoriais.poo.tutorial07.exercicio08;

import java.io.IOException;
import java.io.OutputStream;

/**
 * Classe responsável pela escrita do fluxo de bits de saída
 */
public class BitOutputStream extends OutputStream
{
  /**
   * Byte atual, no intervalo de 0x00 a 0xFF, onde os bits são acumulados
   */
  private int currentByte;

  /**
   * Número de bits acumulados no byte atual, sempre entre 0 e 7 (inclusive)
   */
  private int numBitsFilled;

  /**
   * Fluxo de bits de saída
   */
  private OutputStream outputStream;

  /**
   * Inicializar a escrita do fluxo de bits de saída
   *
   * @param outputStream fluxo de bits de saída
   */
  public BitOutputStream(OutputStream outputStream)
  {
    this.outputStream = outputStream;
  }

  /* (non-Javadoc)
   * @see java.io.OutputStream#write(int)
   */
  @Override
  public void write(int bit) throws IOException
  {
    if ((bit != 0) && (bit != 1))
    {
      throw new IllegalArgumentException("Argument must be 0 or 1");
    }

    currentByte = (currentByte << 1) | bit;

    numBitsFilled = numBitsFilled + 1;

    if (numBitsFilled == 8)
    {
      outputStream.write(currentByte);

      currentByte = 0;

      numBitsFilled = 0;
    }
  }

  /* (non-Javadoc)
   * @see java.io.OutputStream#close()
   */
  @Override
  public void close() throws IOException
  {
    while (numBitsFilled != 0)
    {
      write(0);
    }

    outputStream.close();
  }
}

Arquivo Encoder.java

/*************************************************************************
 * Copyright (C) 2009/2021 - Cristiano Lehrer (cristiano@ybadoo.com.br)  *
 *                  Ybadoo - Solucoes em Software Livre (ybadoo.com.br)  *
 *                                                                       *
 * Permission is granted to copy, distribute and/or modify this document *
 * under the terms of the GNU Free Documentation License, Version 1.3 or *
 * any later version published by the  Free Software Foundation; with no *
 * Invariant Sections,  no Front-Cover Texts, and no Back-Cover Texts. A *
 * A copy of the  license is included in  the section entitled "GNU Free *
 * Documentation License".                                               *
 *                                                                       *
 * Ubuntu 16.10 (GNU/Linux 4.8.0-39-generic)                             *
 * OpenJDK Version "1.8.0_121"                                           *
 * OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)               *
 *************************************************************************/

package com.ybadoo.tutoriais.poo.tutorial07.exercicio08;

import java.io.IOException;
import java.io.OutputStream;

/**
 * Codificação da árvore de Huffman num fluxo de bits
 */
public class Encoder
{
  /**
   * Fluxo de bits de saída
   */
  private OutputStream outputStream;

  /**
   * Árvore de Huffman
   */
  private CodeTree codeTree;

  /**
   * Inicializar o codificador de Huffman
   *
   * @param codeTree árvore de Huffman
   * @param outputStream fluxo de bits de saída
   */
  public Encoder(CodeTree codeTree, OutputStream outputStream)
  {
    this.codeTree = codeTree;

    this.outputStream = outputStream;
  }

  /**
   * Codificar a árvore de Huffman num fluxo de bits
   *
   * @param symbol símbolo a ser codificado
   * @throws IOException problemas na gravação do fluxo de bits
   */
  public void write(int symbol) throws IOException
  {
    for (int code : codeTree.getCode(symbol))
    {
      outputStream.write(code);
    }
  }
}

Arquivo Decoder.java

/*************************************************************************
 * Copyright (C) 2009/2021 - Cristiano Lehrer (cristiano@ybadoo.com.br)  *
 *                  Ybadoo - Solucoes em Software Livre (ybadoo.com.br)  *
 *                                                                       *
 * Permission is granted to copy, distribute and/or modify this document *
 * under the terms of the GNU Free Documentation License, Version 1.3 or *
 * any later version published by the  Free Software Foundation; with no *
 * Invariant Sections,  no Front-Cover Texts, and no Back-Cover Texts. A *
 * A copy of the  license is included in  the section entitled "GNU Free *
 * Documentation License".                                               *
 *                                                                       *
 * Ubuntu 16.10 (GNU/Linux 4.8.0-39-generic)                             *
 * OpenJDK Version "1.8.0_121"                                           *
 * OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)               *
 *************************************************************************/

package com.ybadoo.tutoriais.poo.tutorial07.exercicio08;

import java.io.IOException;
import java.io.InputStream;

/**
 * Decodificação do fluxo de bits numa árvore de Huffman
 */
public class Decoder
{
  /**
   * Fluxo de bits de entrada
   */
  private InputStream inputStream;

  /**
   * Árvore de Huffman
   */
  private CodeTree codeTree;

  /**
   * Inicializar a decodificação do fluxo de bits numa árvore de Huffman
   *
   * @param codeTree árvore de Huffman
   * @param inputStream fluxo de bits de entrada
   */
  public Decoder(CodeTree codeTree, InputStream inputStream)
  {
    this.codeTree = codeTree;

    this.inputStream = inputStream;
  }

  /**
   * Decodificar o fluxo de bits numa árvore de Huffman
   *
   * @return símbolo lido
   * @throws IOException problemas na leitura do fluxo de bits
   */
  public int read() throws IOException
  {
    InternalNode currentNode = codeTree.getRoot();

    while (true)
    {
      int bit = inputStream.read();

      Node nextNode;

      if (bit == 0)
      {
        nextNode = currentNode.getLeft();
      }
      else
      {
        nextNode = currentNode.getRight();
      }

      if (nextNode instanceof InternalNode)
      {
        currentNode = (InternalNode) nextNode;
      }
      else
      {
        return ((Leaf) nextNode).getSymbol();
      }
    }
  }
}

Arquivo Huffman.java

/*************************************************************************
 * Copyright (C) 2009/2021 - Cristiano Lehrer (cristiano@ybadoo.com.br)  *
 *                  Ybadoo - Solucoes em Software Livre (ybadoo.com.br)  *
 *                                                                       *
 * Permission is granted to copy, distribute and/or modify this document *
 * under the terms of the GNU Free Documentation License, Version 1.3 or *
 * any later version published by the  Free Software Foundation; with no *
 * Invariant Sections,  no Front-Cover Texts, and no Back-Cover Texts. A *
 * A copy of the  license is included in  the section entitled "GNU Free *
 * Documentation License".                                               *
 *                                                                       *
 * Ubuntu 16.10 (GNU/Linux 4.8.0-39-generic)                             *
 * OpenJDK Version "1.8.0_121"                                           *
 * OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)               *
 *************************************************************************/

package com.ybadoo.tutoriais.poo.tutorial07.exercicio08;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * Compressão de documentos utilizando a codificação de Huffman
 */
public class Huffman
{
  /**
   * Fluxo de entrada (arquivo de entrada)
   */
  private InputStream inputStream;
  
  /**
   * Fluxo de saída (arquivo de saída)
   */
  private OutputStream outputStream;
  
  /**
   * Construir a tabela de frequências dos caracteres contidos no arquivo de entrada
   *
   * @param file arquivo de entrada
   * @return tabela de frequências dos caracteres contidos no arquivo de entrada
   * @throws IOException problemas na leitura do arquivo de entrada
   */
  private FrequencyTable buildFrequencyTable(File file) throws IOException
  {
    FrequencyTable frequencies = new FrequencyTable(new int[257]);

    InputStream inputStream = new BufferedInputStream(new FileInputStream(file));

    try
    {
      while (true)
      {
        int bit = inputStream.read();

        if (bit == -1)
        {
          break;
        }

        frequencies.increment(bit);
      }
    }
    finally
    {
      inputStream.close();
    }

    return frequencies;
  }
  
  /**
   * Comprimir o arquivo de entrada no arquivo de saída
   *
   * @param inputFile arquivo de entrada
   * @param outputFile arquivo de saída
   * @throws HuffmanException problemas na execução do algoritmo de Huffman
   */
  public void compress(File inputFile, File outputFile) throws HuffmanException
  {
    FrequencyTable frequencyTable;

    try
    {
      frequencyTable = buildFrequencyTable(inputFile);
    }
    catch (IOException exception)
    {
      throw new HuffmanException(exception);
    }

    frequencyTable.increment(256);

    CodeTree code = frequencyTable.buildCodeTree();

    CanonicalCode canonicalCode = new CanonicalCode(code, 257);

    code = canonicalCode.toCodeTree();

    try
    {
      inputStream = new BufferedInputStream(new FileInputStream(inputFile));
    }
    catch (FileNotFoundException exception)
    {
      throw new HuffmanException(exception);
    }

    try
    {
      outputStream = new BitOutputStream(new BufferedOutputStream(new FileOutputStream(outputFile)));
    }
    catch (FileNotFoundException exception)
    {
      throw new HuffmanException(exception);
    }

    try
    {
      writeCodeLengthTable(canonicalCode);

      writeData(code);
    }
    catch (IOException exception)
    {
      throw new HuffmanException(exception);
    }
    finally
    {
      close();
    }
  }
  
  /**
   * Descomprimir o arquivo de entrada no arquivo de saída
   *
   * @param inputFile arquivo de entrada
   * @param outputFile arquivo de saída
   * @throws HuffmanException problemas na execução do algoritmo de Huffman
   */
  public void descompress(File inputFile, File outputFile) throws HuffmanException
  {
    try
    {
      inputStream = new BitInputStream(new BufferedInputStream(new FileInputStream(inputFile)));
    }
    catch (FileNotFoundException exception)
    {
      throw new HuffmanException(exception);
    }

    try
    {
      outputStream = new BufferedOutputStream(new FileOutputStream(outputFile));
    }
    catch (FileNotFoundException exception)
    {
      throw new HuffmanException(exception);
    }

    try
    {
      CanonicalCode canonCode = readCodeLengthTable();

      readData(canonCode.toCodeTree());
    }
    catch (IOException exception)
    {
      throw new HuffmanException(exception);
    }
    finally
    {
      close();
    }
  }
  
  /**
   * Fechar os arquivos de entrada e saída
   * 
   * @throws HuffmanException problemas na execução do algoritmo de Huffman
   */
  private void close() throws HuffmanException
  {
    try
    {
      if(inputStream != null)
      {
        inputStream.close();
      }

      if(outputStream != null)
      {
        outputStream.close();
      }
    }
    catch (IOException exception)
    {
      throw new HuffmanException(exception);
    }
  }
  
  /**
   * Ler a tabela com os códigos de Huffman
   *
   * @return tabela com os códigos de Huffman
   * @throws IOException problemas com a leitura dos códigos de Huffman
   */
  private CanonicalCode readCodeLengthTable() throws IOException
  {
    int[] codeLengths = new int[257];

    for (int i = 0; i < codeLengths.length; i++)
    {
      int val = 0;

      for (int j = 0; j < 8; j++)
      {
        val = (val << 1) | inputStream.read();
      }

      codeLengths[i] = val;
    }

    return new CanonicalCode(codeLengths);
  }
  
  /**
   * Ler os dados compactados
   *
   * @param codeTree árvore de Huffman
   * @throws IOException problemas com a leitura dos dados compactados
   */
  private void readData(CodeTree codeTree) throws IOException
  {
    Decoder decoder = new Decoder(codeTree, inputStream);

    while (true)
    {
      int symbol = decoder.read();

      if (symbol == 256)
      {
        break;
      }

      outputStream.write(symbol);
    }
  }
  
  /**
   * Escrever a tabela de compressão utilizada no arquivo de entrada
   *
   * @param canonicalCode códigos de Huffman
   * @throws IOException problemas na escrita da tabela
   */
  private void writeCodeLengthTable(CanonicalCode canonicalCode) throws IOException
  {
    for (int i = 0; i < canonicalCode.getSymbolLimit(); i++)
    {
      int codeLength = canonicalCode.getCodeLength(i);

      if (codeLength >= 256)
      {
        throw new RuntimeException("The code for a symbol is too long");
      }

      for (int j = 7; j >= 0; j--)
      {
        outputStream.write((codeLength >>> j) & 1);
      }
    }
  }
  
  /**
   * Escrever os dados
   *
   * @param codeTree árvore de Huffman
   * @throws IOException problemas na escrita dos dados
   */
  private void writeData(CodeTree codeTree) throws IOException
  {
    Encoder encoder = new Encoder(codeTree, outputStream);

    while (true)
    {
      int bit = inputStream.read();

      if (bit == -1)
      {
        break;
      }

      encoder.write(bit);
    }

    encoder.write(256);
  }
}

Arquivo HuffmanException.java

/*************************************************************************
 * Copyright (C) 2009/2021 - Cristiano Lehrer (cristiano@ybadoo.com.br)  *
 *                  Ybadoo - Solucoes em Software Livre (ybadoo.com.br)  *
 *                                                                       *
 * Permission is granted to copy, distribute and/or modify this document *
 * under the terms of the GNU Free Documentation License, Version 1.3 or *
 * any later version published by the  Free Software Foundation; with no *
 * Invariant Sections,  no Front-Cover Texts, and no Back-Cover Texts. A *
 * A copy of the  license is included in  the section entitled "GNU Free *
 * Documentation License".                                               *
 *                                                                       *
 * Ubuntu 16.10 (GNU/Linux 4.8.0-39-generic)                             *
 * OpenJDK Version "1.8.0_121"                                           *
 * OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)               *
 *************************************************************************/

package com.ybadoo.tutoriais.poo.tutorial07.exercicio08;

public class HuffmanException extends Exception
{
  /**
   * Identificador de serialização da classe
   */
  private static final long serialVersionUID = 1L;
  
  /**
   * Constructs a new exception with null as it's detail message
   */
  public HuffmanException()
  {
    super();
  }
  
  /**
   * Constructs a new exception with the specified detail message
   *
   * @param message the detail message
   */
  public HuffmanException(String message)
  {
    super(message);
  }
  
  /**
   * Constructs a new exception with the specified cause
   *
   * @param cause the cause
   */
  public HuffmanException(Throwable cause)
  {
    super(cause);
  }
  
  /**
   * Constructs a new exception with the specified detail message and cause
   *
   * @param message the detail message
   * @param cause the cause
   */
  public HuffmanException(String message, Throwable cause)
  {
    super(message, cause);
  }
}

Arquivo Application.java

/*************************************************************************
 * Copyright (C) 2009/2021 - Cristiano Lehrer (cristiano@ybadoo.com.br)  *
 *                  Ybadoo - Solucoes em Software Livre (ybadoo.com.br)  *
 *                                                                       *
 * Permission is granted to copy, distribute and/or modify this document *
 * under the terms of the GNU Free Documentation License, Version 1.3 or *
 * any later version published by the  Free Software Foundation; with no *
 * Invariant Sections,  no Front-Cover Texts, and no Back-Cover Texts. A *
 * A copy of the  license is included in  the section entitled "GNU Free *
 * Documentation License".                                               *
 *                                                                       *
 * Ubuntu 16.10 (GNU/Linux 4.8.0-39-generic)                             *
 * OpenJDK Version "1.8.0_121"                                           *
 * OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)               *
 *************************************************************************/

package com.ybadoo.tutoriais.poo.tutorial07.exercicio08;

import java.io.File;

/**
 * Classe responsável pela execução da compressão de Huffman
 */
public class Application
{
  /**
   * Método principal da linguagem de programação Java
   *
   * @param args argumentos da linha de comando
   */
  public static void main(String[] args)
  {
    if (args.length == 3)
    {
      if ("-x".equals(args[0]))
      {
        try
        {
          new Huffman().compress(new File(args[1]), new File(args[2]));
        }
        catch (HuffmanException exception)
        {
          exception.printStackTrace();
        }
        finally
        {
          System.exit(0);
        }
      }

      if ("-c".equals(args[0]))
      {
        try
        {
          new Huffman().descompress(new File(args[1]), new File(args[2]));
        }
        catch (HuffmanException exception)
        {
          exception.printStackTrace();
        }
        finally
        {
          System.exit(0);
        }
      }  
    }

    System.err.println("Usage: java Application -[x|c] inputFile outputFile");

    System.exit(1);
  }
}

Goodrich, Michael. (2007). Estrutura de dados e algoritmos em Java. 4ª edição. Porto Alegre: Bookman. 600 páginas.

Nayuki. (2016). Reference Huffman coding. Disponível em https://www.nayuki.io/page/reference-huffman-coding.