aula 280

Pensando a solução para o Código de Huffman – Algoritmo de Huffman

Nesta aula nós ainda não iremos escrever nenhuma linha de código para implementar o Algoritmo de Huffman. Isso porque uma etapa extremamente importante e normalmente ignorada por quem está aprendendo a programar é a etapa de “pensar a solução do problema antes de codificar”.

Na aula anterior nós vimos que o Algoritmo de Huffman é composto de várias etapas ou passos lógicos e faz uso de diferentes estruturas de dados. Isso nos trás algumas perguntas:

  • quantas estruturas exatamente precisamos?
  • qual o tipo de cada estrutura (vetor, matriz, lista, fila, pilha, árvore, tabela hash, etc)?
  • qual o tipo de dado de cada uma das estruturas?
  • qual o tamanho destas estruturas?

Pensar nestas questões antes de iniciar a escrita do código pode poupar muito trabalho no futuro. É isso que faremos nesta aula.

Na aula anterior nós aprendemos que o Algoritmo de Huffman é uma sequência de passos lógicos que pode ser sintetizado mais ou menos assim:
– Criar a tabela de frequência
– Gerar uma Lista ou fila ordenada pela frequência
– Criar uma Árvore binária conhecida como árvore de huffman
– Gerar a tabela de códigos também conhecida como dicionário
– Codificar
– Decodificar

Nesta etapa é extremamente importante rabiscar, anotar ideias. Definir uma hierarquia, o que acontece primeiro? O que acontece por último? Divida o problema em partes e tente resolver cada parte por vez. O mais comum nesta etapa para quem está aprendendo é olhar para o problema de forma geral e ficar perdido, sem saber por onde começar.

Na solução que iremos desenvolver nas próximas aulas eu irei dividir o problema de compactar um texto com o algoritmo de huffman em 6 partes:
– Ler o texto a ser comprimido e gerar a tabela de frequência;
– Gerar a lista / fila (como serão os nós desta estrutura?);
– Gerar a árvore de huffman;
– Gerar a tabela de códigos (dicionário);
– Comprimir o texto;
– Descomprimir o texto.

Observe que com esta simples ação de dividir o problema em partes já conseguirmos perceber uma ordem lógica. Por exemplo, é impossível gerar a lista / fila ordenada sem antes ler o texto e gerar a tabela de frequência. Assim como não é possível gerar a tabela de códigos (dicionário) sem antes gerar a árvore de huffman.

Agora, vamos pensar na implementação de cada uma testas partes de forma individual.

1) Como gerar a tabela de frequência?

A tabela de frequência é a tabela com a quantidade de ocorrências de cada caracter no texto a ser comprimido. Como o texto pode ser grande, o ideal é que não seja necessário buscar um determinado caracter nesta tabela, mas que seja possível um acesso direto à posição que contêm a frequência do caracter. Isso é possível fazendo uso dos códigos da tabela ASCII.

Já vimos no início do curso que ao imprimirmos um caracter usando a máscara %d será impresso o código do respectivo caracter na tabela ASCII. Assim como, ao imprimir um número (no intervalo de 0 a 255) com %c, será impresso o respectivo caracter na tabela ASCII representado por aquele número.

A instrução printf(“%d”, ‘a’); irá imprimir o número 97, que é o código ASCII do caracter ‘a’, enquanto a instrução printf(“%c”, 97); irá imprimia ‘a’, que é o caracter na tabela ASCII identificado pelo código 97.

Sabendo disso, podemos criar um vetor de inteiros de tamanho 256 (índices variando de 0 a 255) para ser a tabela de frequência e inicializar esse vetor com zero. Assim, para cada caracter do texto a ser comprimido, conseguimos um acesso direto ao vetor com seu código da tabela ASCII, dispensando a necessidade de busca.

2) Gerar a lista / fila (como serão os nós desta estrutura?)

Nesta etapa precisamos gerar uma lista encadeada ou uma fila de prioridade ordenada. A estrutura em si a ser escolhida não faz muita diferença, mas o que irá formar esta estrutura pode fazer muita diferença e há diversas formas de fazer isso. Eu vou adotar aqui uma representação, você pode escolher uma representação diferente.

Os nós desta estrutura serão unidos de dois em dois para formar a árvore de huffman. Então, o nó desta estrutura deve possuir dois ponteiros para receber os filhos à direita e à esquerda ou, deve possuir em seu interior, um ponteiro para o nó que formará a árvore.

Aqui eu criarei apenas um nó com cinco campos diferentes:
– um campo para um caracter;
– um campo para a frequência do caracter;
– um ponteiro próximo a ser usado na lista / fila
– um ponteiro esquerda a ser usado na árvore;
– um ponteiro direita a ser usado na árvore.

Assim, conseguimos usar o mesmo nó na criação e manipulação das duas estruturas de dados.

possiveis nos
Representação dos possíveis nós que podem ser criados.

3) Gerar a árvore de huffman

Usando a terceira representação para os nós apresentada na figura acima, fica fácil construir a árvore de huffman. Basta fazermos uma repetição para remover os dois nós de menor frequência (no início) e uni-los com um terceiro nó enquanto a quantidade de nós for maior que 1.

Quando esta repetição terminar, nossa lista / fila terá apenas um nó e este nó será a raiz da árvore de huffman, como apresentado na figura abaixo.

árvore de huffman
Representação da Árvore de Huffman após a união de todos os nós.

4) Gerar a tabela de códigos (dicionário)

Ao percorrer a árvore de huffman gerada no passo anterior conseguimos obter o novo conjunto de códigos para cada caracter. Precisamos agora de uma tabela para armazenar esses códigos. Esta tabela será o nosso novo dicionário.

Podemos pensar esta tabela como uma matriz de strings. Como cada linha terá o código de um caracter diferente, sua quantidade de linhas será o tamanho da tabela ASCII, enquanto a quantidade de colunas será a altura da árvore de huffman + 1.

Novamente conseguimos fazer um acesso direto pelo código ASCII sem a necessidade de busca.

dicionario
Representação do dicionário como uma matriz de strings.

5) Comprimir o texto

Nesta etapa já temos o novo dicionário criado no passo anterior. Então estamos prontos para codificar o texto. Para fazer isso basta obter cada caracter do texto, buscar seu código no dicionário e ir concatenando em uma longa string. Ao final do processo, teremos uma longa string de zeros e uns que representa o texto codificado.

1111000101100010101…

Observe que gerar uma string de zeros e uns é uma possibilidade. Outra possibilidade é, para cada caracter codificado, já escrever seu código em um arquivo, assim não é necessário manter em memória uma longa string de zeros e uns.

6) Descomprimir o texto

Para decodificar precisamos da árvore de huffman montada no passo 3. O processo agora é o inverso do passo anterior. Para cada caracter codificado, verificamos se é um 0 ou um 1. Se for um zero, caminhamos para a esquerda na árvore partindo da raiz, se for um 1, caminhamos então para a direita. Quando chegarmos a um nó folha, nó sem filhos, significa que acabamos de decodificar um caracter, imprimirmos o caracter do nó folha na tela (ou concatenamos em uma longa string, ou ainda escrevemos em um arquivo) e voltamos para a raiz da árvore. Este processo será repetido até que todos os zeros e uns tenham sido lidos e decodificados.

Ao final teremos o texto original que foi codificado, sem nenhuma perda.

Nas aulas seguintes iniciaremos o processo de desenvolvimento do Algoritmo de Huffma.

Deixe um comentário

14 − 9 =

Wagner Gaspar

Capixaba de São Gabriel da Palha, Espírito Santo. Bacharel em Ciência da Computação pela Universidade Federal do Amazonas e mestre em informática pela Universidade Federal do Espírito Santo.