Upload
rafaelvitalino
View
218
Download
23
Embed Size (px)
DESCRIPTION
Compactação de dados.
Citation preview
UNIVERSIDADE FEDERAL DE GOIÁS ESCOLA DE ENGENHARIA ELÉTRICA E DE COMPUTAÇÃO
PROGRAMA PARA COMPACTAÇÃO DE DADOS UTILIZANDO CÓDIGO DE
HUFFMAN
Diego Vidier Oda Arruda Rodrigo Silva Goes
Orientador: Prof. Dr. Getúlio Antero de Deus Júnior
Goiânia 2003
DIEGO VIDIER ODA ARRUDA
RODRIGO SILVA GOES
2
PROGRAMA PARA COMPACTAÇÃO DE DADOS UTILIZANDO CÓDIGO DE
HUFFMAN
Dissertação apresentada ao Curso de Engenharia de Computação da Escola de Engenharia Elétrica e de Computação da Universidade Federal de Goiás, como parte para aprovação na disciplina de Estágio e Projeto Final. Área de Concentração: Compactação de Dados. Orientador: Prof. Dr. Getúlio Antero de Deus Júnior.
Goiânia 2003
3
DIEGO VIDIER ODA ARRUDA RODRIGO SILVA GOES
PROGRAMA PARA COMPACTAÇÃO DE
DADOS UTILIZANDO CÓDIGO DE HUFFMAN
Projeto Final defendido e aprovado em oito de janeiro de dois mil e quatro, pela Banca Examinadora constituída pelos professores:
______________________________________ Prof. Dr. Getúlio Antero de Deus Júnior
Presidente da Banca
______________________________________ Prof. Dr. Reinaldo Gonçalves Nogueira
1º membro da Banca
______________________________________ Prof. Msc. Carlos Galvão Pinheiro Junior
2º membro da Banca
5
AGRADECIMENTOS
À Universidade Federal de Goiás, pelo aprimoramento
proporcionado nas lides da Engenharia de Computação.
Aos coordenadores, professores e funcionários da Escola de
Engenharia Elétrica e de Computação, pelo ambiente de estudos e meios
colocados à disposição.
Aos companheiros de turma, pela camaradagem e amizade.
Em especial ao nosso professor e orientador Getúlio Antero de Deus
Júnior pela dedicação, incentivo e compreensão.
À nossa família que anonimamente vem ao longo dos anos sendo o
suporte sem o qual este não seria possível.
Ao nosso Grandioso Pai Celestial.
6
SUMÁRIO
Pág.
RESUMO........................................................................ 07
ABSTRACT.................................................................... 08
INTRODUÇÃO.............................................................. 01
CAPÍTULO 1 ................................................................. 02
PREMILINARES TEÓRICAS..................................... 02 1.1 Histórico........................................................................... 02 1.2 Código de Huffman.......................................................... 07 1.3 Estrutura de Dados........................................................... 17 1.4 Compactação ................................................................... 21 1.5 Técnicas de Compactação............................................... 24
1.6 Compactação Estatística ................................................ 34 1.7 Compactação Fixa versus Compressão Auto-adaptável. 35 1.8 O código de Lempel-Ziv ................................................ 36 1.9 Codificação Lempel-Ziv ................................................ 41
CAPÍTULO 2 ............................................................... 45
O PROBLEMA.............................................................. 45
2.1 Introdução........................................................................ 45 2.2 Proposta para implementação do compactador................ 48 2.3 Desempenho do compactador implementado.................. 53 2.4 Testes realizados.............................................................. 53 2.5 Aplicações....................................................................... 55 CONCLUSÃO............................................................... 57 REFERÊNCIAS BIBLIOGRÁFICAS........................ 58
7
RESUMO
É impossível pensarmos nos dias de hoje num mundo computacional sem pensarmos nas formas de armazenamento de dados. Isto implica articularmos todos os aspectos do armazenamento. Desde como gravaremos fisicamente, como transmitiremos estes dados de formas mais eficientes até em formas de compactação destes dados.
Neste trabalho, ressaltamos o aspecto particular da compactação de dados, que busca mecanismos para reduzir o tamanho dos dados utilizando alguma lógica matemática adequada ao tipo de aplicação considerada, de tal forma que possamos na outra ponta obtermos os dados originais utilizando a lógica inversa.
Neste sentido, tratamos especificamente da compactação de arquivos textos, dando uma noção geral sobre compactadores e sua história, sobre de que se trata o Código de Huffman e finalmente permeando nossa experiência na implementação desse código e nossas conclusões empíricas, comparando inclusive com outros códigos eficientes como Lempel-Ziv, expressos através de índices de compactação.
8
ABSTRACT It is impossible think nowadays in a computational world without
think in the ways of data storing. It means to explore every aspect of storing. Since how phisically we record it, how eficientily transmit these data until ways of data compression.
In this work, we hilight especificaly this aspect, the data compression, wich looks for mechanisms to reduce these data length using some mathematical logic that fits to the type of considered aplication, in a way that we are capable on the other side get the original datas by taking an inverse logic.
On this way, we treat especificaly of text file compression, giving a general explanation about compactors and its history, as what Huffman Code talk about and finaly exchaging our implementation experience and our empirical conclusions, comparing e with another eficient codes like Lempel-Ziv.
9
INTRODUÇÃO
A compactação consiste basicamente numa forma de
armazenamento reduzido de um conjunto de dados. Primeiramente,
abordaremos a evolução da comunicação de dados tratando dos códigos que
foram desenvolvidos para que esta comunicação fosse possível. Permearemos
desde códigos primitivos como, Código Morse e Baudot, até os códigos
modernos.
Faremos um estudo detalhado sobre Código de Huffman, que é o
tema central de nosso trabalho, e suas aplicações confrontando-o com outros
códigos existentes como o de Lempel-Ziv.
Aprofundamos também nosso estudo sobre Estruturas de Dados,
tratando especificamente sobre árvores binárias, estrutura imprescindível na
construção do Código de Huffman.
Em seguida, apresentamos alguns conceitos relativos a compressão
e compactação de dados. Separando conceitualmente a definição destes
termos, inclusive com o esboço de algumas técnicas de compressão.
No capítulo, apresentamos a proposta de implementação do
algoritmo de Huffman, para demonstrarmos que ele é um algoritmo ótimo
para caracteres. Consideramos vários aspectos do compactador e viabilizamos
uma solução para a compactação de arquivos tipo texto. Com a utilização de
arquivos-entradas de prova levantamos e respondemos algumas perguntas
realizados. Além disso algumas considerações a respeito do desempenho e
discussão sobre índices de compactação foi realizado.
10
CAPÍTULO 1
PREMILINARES TEÓRICAS
1.1 - Histórico
A comunicação de dados constitui o processo de comunicação de
informações em estado binário entre dois ou mais pontos. Às vezes, a
comunicação de dados é chamada de comunicação de informática, porque a
maioria das informações trocadas hoje em dia é transferida entre dois ou mais
computadores ou entre computadores e terminais, impressoras ou outros
dispositivos periféricos. Os dados podem ser elementares como os símbolos
binários 1 e 0, ou complexos como os caracteres representados pelas teclas
em um teclado de máquina de escrever. Em todo caso, os caracteres ou
símbolos representam informações [1].
O campo de comunicação de dados representa uma das tecnologias
de mais rápida evolução. Utilizando semicondutores especializados
desenvolvidos para realizar o processamento de sinais e a compactação, estão
sento desenvolvidos vários aplicativos de multimídia que orientam a
comunicação de dados em direção ao transporte de informações, dados e
vídeo.
É importante entendermos a comunicação de dados por causa de sua
significação no mundo de hoje. A comunicação de dados é usada comumente
em negócios e está sendo cada vez mais empregada também em ambientes
domésticos. Quer seja a transmissão de informações de um computador
central para máquina de um caixa eletrônico, a seleção de um programa de
TV a cabo no sistema "pay-per-view" ou o download de um videogame desde
um BBS para um computador pessoal, a comunicação de dados está se
11
transformando em uma parte integrante de nossas atividades diárias. Na
realidade, muitos leitores dificilmente conseguirão passar um dia interiro sem
se beneficiarem de alguma atividade executada ou aperfeiçoada pelo uso da
comunicação de dados. Quanto mais moderna uma sociedade, mais ela
depende da comunicação de dados [1].
A comunicação de dados moderna envolve o uso de aparelhos
elétricos ou eletrônicos para transmissão de informações sob a forma de
símbolos e caracteres entre dois pontos. Pelo fato da eletricidade, as ondas de
rádio e as ondas luminosas serem todas formas de energia eletromagnética,
poderíamos afirmar ainda sem grande exagero que as primeiras formas de
comunicação, como os sinais de fumaça das fogueiras dos índios americanos
ou a reflexão da luz solar em espelho também eram formas do mesmo tipo de
comunicação de dados. Para levar essa idéia mais adiante, imagine as nuvens
de fumaça como símbolos discretos, exatamente como os símbolos usados
nos atuais sistemas de comunicação.
A descoberta e a utilização da eletricidade introduziram muitas
possibilidades novas para os códigos de comunicação. Uma das primeiras
propostas, apresentada em uma revista escocesa em 1753, era simples mas
teve implicações profundas para o hardware. Essa idéia era a de estender 26
fios paralelos de uma cidade à outra, um fio para cada letra do alfabeto. Um
inventor suíço construiu um sistema protótipo baseado nesse princípio de 26
fios, mas a tecnologia de fabricação de fios nessa época eliminou a
possibilidade de uso sério dessa idéia. A complexidade de estender 26 fios
para denotar as 26 letras do alfabeto indicava a necessidade de uma técnica
mais eficiente [1].
Em 1833, Carl Friedrick Gauss usou um código baseado em uma
matriz 5 por 5 de 25 letras (as letra I e J eram combinadas), para enviar
mensagens pela deflexão de uma agulha para a direita ou esquerda de uma a
12
cinco vezes. O primeiro conjunto de deflexões indicava a linha e o segundo
indicava a coluna. Essa técnica pode ser considerada equivalente a especificar
uma coluna e uma linha para denotar uma letra, permitindo o uso de uma
única linha em lugar de 26 linhas.
Um dos desenvolvimentos mais significativos em comunicação de
dados aconteceu no século XIX, quando o americano Samuel F. B. Morse
inventou o telégrafo elétrico. Embora outros inventores tenham trabalhado na
idéia de usar eletricidade para se comunicarem, a invenção de Morse eram
sem dúvida a mais importante, porque ele juntou a mente humana (a
inteligência) com o equipamento de comunicação, com a decodificação
baseado na capacidade auditiva da pessoa que recebia a mensagem, e também
de seu conhecimento do código desenvolvido por Morse [1].
Quando a tecla do telégrafo na estação A era pressionada, a corrente
elétrica fluía pelo sistema e o induzido na estação B era atraído para a bobina,
emitindo um sinal sonoro à medida que atingia o ponto de parada. Quando a
tecla era liberada na estação A, ela abria o circuito elétrico e o induzido do
receptor na estação B era forçado para sua posição aberta por uma mola,
atingindo o outro ponto de parada e emitindo um ruído um pouco diferente.
Assim o receptor do telégrafo emitia dois ruídos distintos. Se o
tempo entre cliques sucessivos do receptor era curto, ele representava um
ponto; se era mais longo, representava um traço. O operador de transmissão
convertia os caracteres das palavras de uma mensagem a ser enviada em uma
série de pontos e traços. O operador receptor interpretava esses pontos e
traços como caracteres; assim, as informações eram transmitidas do ponto A
para o ponto B.
Quando Morse desenvolveu seu código, diz a lenda que ele
examinou a quantidade de tipos nas caixas que uma tipografia usava para
armazenar caracteres ingleses e dígitos. Morse atribuiu códigos curtos a
13
caracteres e dígitos que ocorriam com menor freqüência. Isso explica por que
foi atribuído um ponto à letra E, o caráter usado mais freqüentemente no
idioma inglês, e por que foi atribuído um traço à letra T, o segundo caráter de
uso mais freqüente.
Morse desenvolveu o telégrafo inicialmente em 1832, mas foi
somente bem mas tarde que ele demonstrou seu uso com sucesso. A
demonstração mais conhecida ocorreu em 1844, quando Morse transmitiu por
fios de Washington para Baltimore a mensagem: "Deus seja louvado!".
Tendo em vista que a entrega de correspondência pelo Pony Express
era o meio de comunicação típico antes do telégrafo, esse método muito mais
veloz logo se tornou um sucesso. O equipamento era simples e rudimentar: a
tecla e o receptor acústico continham cada um apenas uma peça móvel. A
força e a fraqueza do sistema (e sua única complexidade real) eram a mente
humana - os operadores de transmissão e recepção. Na época da guerra civil
americana, uma linha de telégrafo se espalhou pelo continente, cruzando as
pradarias e os desertos para conectar a Califórnia ao restante dos Estados
Unidos.
Foi a partir dessa inovação tecnológica histórica, ou seja, a criação
do telégrafo, que a Companhia Telegráfica da União Oeste (do inglês:
Western Union Telegraph Company) tornou-se conhecida. Na época em que o
telefone foi inventado, cerda de 20 anos depois, a indústria do telégrafo era
grande, com muitas empresas fornecendo serviços para quase todas as cidade
e vilas nos Estados Unidos. Em 1866, o telégrafo conectava as nações do
mundo, com a instalação do cabo de transatlântico entre os Estados Unidos e
a França.
A importância do telégrafo de Morse não é apenas histórica. Boa
parte da terminologia que se desenvolveu em torno do sistema Morse ainda é
usada atualmente. Por exemplo, considere os termos "marca" e "espaço". Se
14
um dispositivo fosse organizado de modo que o papel se movimentasse
continuamente sob uma caneta presa ao induzido do receptor telegráfico, seria
feita uma marca no papel quando o induzido fosse atraído para a bobina.
Poderíamos fazer referência ao estado de fluxo de corrente na linha,
como estado de marcação, e ao estado sem fluxo de corrente na linha como
estado de espaço. Note que isso resulta em um dispositivo de dois estados,
que pode ser considerado o precursor do sistema binário usado para transferir
informações e para controlar a operação dos computadores. Os padrões
mundiais para comunicação da dados de hoje ainda utilizam os termos marca
e espaço, com a condição de fluxo decorrente no canal de transmissão sendo
chamada condição de marcação.
O mais importante da terminologia oriunda do princípio de
operação do telégrafo foi o sistema de comunicação de dois estados. O fio
telegráfico (canal) entre os operadores encontra-se em um destes dois estados:
ou a corrente está fluindo ou não. Essa estrutura simples tem sido repetida
continuamente no desenvolvimento de sistemas de comunicação de dados.
Um sistema de comunicação de dois estados é o mais simples, o
mais fácil de construir e o mais confiável. Os dois estados podem ser ligado e
desligado (como no telégrafo), mais e menos (com o fluxo de corrente em
sentidos opostos), claro e escuro ( como ao acender e apagar uma lanterna
para transmitir código), 1 e 0 (o conceito usado nos computadores) ou alguma
outra estrutura com apenas dois valores possíveis. Um sistema de dois estados
ou de dois valores é chamado de "sistema binário".
15
1.2 - Código de Huffman
1.2.1 - Códigos
Os números 0 e 1 são símbolos do sistema binário. Um dígito
binário é comunente chamado um bit. As mudanças de linha individuais em
dados digitais (como marca e espaço) são chamadas bits, e cada bit recebe a
atribuição do valor 0 ou 1 [1].
O sistema binário emprega a notação posicional da mesma maneira
que o conhecido sistema decimal, exceto pelo fato de cada posição ter
somente dois valores possíveis, em vez de 10.
Podemos definir potências de dois no sistema posicional binário, no
qual o peso relativo de cada posição é duas vezes o peso da posição
imediatamente à direita.
Os computadores são mais eficientes se seus caminhos e
registradores internos tiverem o comprimento igual a alguma potência de dois
(2 bits, 4 bits, 8 bits, 16 bits e assim por diante). Um agrupamento comumente
usado é o de 8 bits. Um grupo de 8 bits é chamado um byte ou um octeto.
Durante o período inicial da informática, dos anos 50 aos anos 70,
foram usados agrupamentos diferentes de bits para formar um caractere de
computador. A arquitetura interna dos computadores desenvolvidos durante
esse período utilizava entre cinco e doze bits para definir caracteres únicos ou
bytes de computador. A falta de padrões sobre o número de bits que
representava um byte resultou em diversos padrões sobre o número de bits
que representações usarem o termo "octeto" em sua literatura para fazer
referência a um agrupamento de oito bits. Embora quase todos os
computadores modernos sejam projetados para grupar oito bits em um byte, a
16
maioria das organizações que criam padrões continua a usar o termo para
fazer referência a um grupo de oito bits.
Uma característica comum dos sistemas de comunicação é o uso de
um dispositivo inteligente para converter um caractere ou símbolo em sua
forma codificada e vice-versa. No sistema Morse, os "dispositivos"
inteligentes eram os operadores de telégrafo que convertiam os caracteres em
pontos e traços.
Operadores de telégrafo qualificados sempre estavam em falta, e o
trabalho era difícil e exaustivo. Por essa razão, tornou-se necessário criar um
meio elétrico ou mecânico de codificar os caracteres. Entretanto, era
essencialmente impossível automatizar as operações de transmissão e
recepção, em virtude da duração variável dos pontos e traços no código Morse
e do fato de que os códigos correspondentes aos caracteres eram formados por
diferentes quantidades de pontos e traços. Assim, surgiu a necessidade de um
código que tivesse o mesmo número de elementos de sinalização de igual
duração para cada caractere.
Códigos são interpretações padrões (combinadas com antecedência)
entre elementos de sinalização e caracteres. Os códigos usados em sistemas de
comunicação de dados já estão definidos, e o conjunto de códigos é
incorporado ao equipamento. Provavelmente, o único momento em que o
usuário poderia ter necessidade de lidar com códigos seria quando estivesse
criando a interface entre duas máquinas (como computadores e impressoras)
de fabricantes diferentes.
Caracteres são letras, numerais, os espaços, os sinais de pontuação e
outros sinais e símbolos em um teclado. Os sistemas de comunicação usam
caracteres de controle que não são impressos, mas esses caracteres também
devem ser codificados. Alguns desses caracteres de controle (por exemplo, os
caracteres de retorno de carro ou tabulação) também estão presentes no
17
teclado, embora muitos não estejam. No caso de muitos caracteres de controle
não localizados diretamente em um teclado, o usuário pode digitar seus
códigos no teclado pressionando certos pares de teclas. Por exemplo, a ação
de pressionar a tecla de controle (Ctrl) e a tecla G ao mesmo tempo (Ctrl + G)
resulta no código de tecla não-imprimível BEL, que pode ser usado dentro de
um programa para transmitir um alarme auditivo à pessoa que opera um
terminal de computador ou um computador pessoal.
Um elemento de sinalização é algo enviado através de um canal de
transmissão usado para representar um caractere. Assim os pontos e traços (ou
marcas e espaços) do código Morse são elementos de sinalização.
As definições de caracteres e elementos de sinalização ilustram o
motivo pelo qual as máquinas e as pessoas precisam de modos diferentes para
representar informações. As pessoas reconhecem com rapidez e
confiabilidade os caracteres impressos por suas formas distintas, mas é difícil
e dispendioso uma máquina fazer o mesmo. Por outro lado, as máquinas
podem manipular com facilidade longas seqüências de elementos de
sinalização de dois estados, como marcas e espaços ou uns e zeros, mas é
difícil uma pessoa conseguir o mesmo com alguma precisão.
1.2.2 - Código de Baudot
O código de Morse era inadequado para codificação e decodificação
por máquina, devido aos problemas causados pelos comprimentos variáveis
dos códigos correspondentes aos caracteres. No início do século XX, quando
se desenvolveu o interesse na substituição de operadores de telégrafo
humanos por máquinas, já existiam vários códigos adequados. O mais
proeminente desses códigos havia sido inventado na década de 1870 por um
18
francês chamado Emile Baudot. Tendo em vista que o código de Baudot,
usava o mesmo número de elementos de sinalização (marcas e espaços) para
representar cada caractere, ele era mais apropriado para codificação e
decodificação por máquina [1].
Infelizmente, o número de elementos de sinalização se limitava a
cinco, em conseqüência de problemas de sincronização dos dispositivos
eletromecânicos. O código de cinco bits só podia gerar 32 combinações
possíveis. Esse código não era suficiente para representar os 26 caracteres do
alfabeto, os 10 dígitos decimais, os sinais de pontuação e o caractere de
espaço. Para superar essa limitação, Baudot usou dois caracteres de controle
de deslocamento - o deslocamento de letras (LTRS) e o deslocamento de
figuras (FIGS) - para permitir ao conjunto de códigos representar todos os
caracteres que pareciam necessários na ocasião. Os códigos de deslocamento
não representam caracteres imprimíveis; em vez disso, os códigos selecionam
um entre dois conjuntos de caracteres, cada composto por 26 a 28 caracteres.
Embora a invenção de Baudot não tenha revolucionado
imediatamente a telegrafia (devido à dificuldade que os operadores humanos
tinham para enviar códigos de igual comprimento), ela forneceu a base para o
desenvolvimento posterior do aparelho de telex.
1.2.3 - Códigos Modernos
O código de Baudot e suas variações foram a espinha dorsal da
comunicação por quase meio século, mas claramente deixavam muito a
desejar. O pessoal da indústria jornalística considerou um problema a falta de
diferenciação entre letras maiúsculas e minúsculas. Os editores de jornais
19
inventaram um código de seis níveis para designar a diferença entre letras
maiúsculas e minúsculas. Esse era apenas um exemplo da necessidade geral.
A comunicação moderna exigia um código que pudesse representar
todos os caracteres imprimíveis e ainda deixar espaço para operações de
verificação de erros e formatação. Considerando-se as operações de
formatação, um código deve admitir os caracteres de avanço de linha e
retorno de carro, bem como o avanço de formulário e a tabulação horizontal e
vertical, porque o uso desses caracteres permite que as operações de terminal
sejam conduzidas à medida que ocorrem. O código tinha de tornar possível a
decodificação sem depender da recepção correta de transmissões anteriores, e
também tinha de permitir a decodificação por máquina. Talvez o mais
importante de tudo, o novo código precisava ser expansível [1].
Durante a década de 1960, foram desenvolvidos vários códigos de
transmissão de dados. A maioria deles caiu no esquecimento, restando três
códigos predominantes:
- O CCITT International Alphabet N 2 é um código isolado de
cinco bits que ainda é usado para transmissão de telex.
- O EBCDIC (do inglês: Extended Binary-Coded-Decimal
Interchange Code), desenvolvido pela IBM, é usado principalmente
para comunicação síncrona em sistemas conectados a computadores
de grande porte (mainframes).
- O código ASCII (do inlgês: American Standard Code for
information Interchange) foi definido pelo ANSI (do inlgês :
American National Standards Institute) nos Estados Unidos e pela
ISO (do inglês: Internacional Organization form Standardization)
em todo o mundo.
20
Quando surge uma necessidade clara de padronização, os padrões
passam a existir de duas maneiras. Em um modo um único fabricante (em
especial um dominante) pode definir um padrão para seus próprios produtos, e
o restante da indústria pode segui-lo. Foi isso o que a IBM fez. Ela criou o
código EBCDIC de oito bits que permitiu a representação de 256 caracteres.
O mundo provavelmente seria melhor se o EBCDIC tivesse se tornado o
padrão, porque ele incluía caracteres exclusivos suficientes para permitir
praticamente qualquer representação. Porém, só a IBM e as empresas que
montam equipamentos compatíveis com os da IBM adotaram o EBCDIC.
O código ASCII de sete bits, formalmente conhecido como ANSI
Standard X3.4-1077, pode representar 128 caracteres, mas nem todos eles
correspondem a símbolos impressos. Estão incluídos no conjunto de
caracteres todas as letras do alfabeto inglês (maiúsculas e minúsculas), os
numerais de 0 a 9, sinais de pontuação e muitos símbolos. Esse conjunto de
códigos padrão é usado em praticamente todos os computadores pequenos e
seus periféricos, como também em computadores de grande porte na maior
parte do mundo.
O código Morse tinha um número variável de elementos (pontos e
traços) para cada caractere e era bastante restrito - apenas letras, numerais e
alguns sinais de pontuação. O código de cinco bits tinha um número constante
de elementos e alguns outros caracteres especiais, mas ainda não conseguia
distinguir entre letras maiúsculas e minúsculas.
O ASCII não apenas permite o uso de letras maiúsculas e
minúsculas, mas também tem uma grande regularidade que a princípio pode
não ser aparente. Por exemplo, para converter qualquer caractere alfabético
maiúsculo (de A até Z) em minúsculo, é necessário apenas trocar o bit 6 de
zero para um. Outra característica é a de que os bits de 4 até 7 dos caracteres
numéricos (0, 1, 2, 3, 4, 5, 6, 7, 8 e 9) são os valores BCD (do inglês: Binary-
21
Coded-Decimal) dos caracteres. Outra vantagem do padrão ASCII é que
podem ser representados 128 caracteres diferentes pelos sete bits usados no
código (em vez dos 32 caracteres que podem ser representados no código de
cinco bits).
Uma variação do código ASCII, comumente chamado "ASCII
estendido", obteve ampla aceitação com a introdução do computador pessoal
pela IBM em 1981. O IBM PC e os computadores compatíveis reconhecem
um código de oito bits, com as primeiras sete posições admitindo o padrão
ANSI. A posição de bit adicional estende o conjunto ASCII do computador,
permitindo a representação de 128 caracteres adicionais. Essa extensão torna
possível o uso de códigos estendidos pelos desenvolvedores de software para
funções como marcações de fim de parágrafo e indicadores de impressão em
negrito em um sistema de processamento de textos.
1.2.4 - Código de Huffman
Supõe-se que um texto seja constituído de um conjunto de símbolos
(ou caracteres) S = {s1,...,sn), n > 1. É conhecida a freqüência fi vezes ao
longo do texto, 1 <= i <= n. Deseja-se atribuir um código a cada símbolo, de
modo a compactar o texto todo. A restrição que se coloca é que nenhum
código seja prefixo de algum outro. Ou seja, os códigos procurados são
aqueles dados por uma árvore binária de prefixo. Para cada nó interno v, a
aresta que conduz ao filho esquerdo de v é rotulada com zero, enquanto o
rótulo daquela que conduz ao direito é igual a um. Cada símbolo si está
associado a uma folha da árvore. Os códigos dos símbolos são seqüências
binárias. O código de si está associado a uma folha da árvore. Os códigos dos
símbolos são seqüências binárias. O código de si é igual à seqüência dos
rótulos das arestas, do caminho desde a raiz da árvore até a folha
22
correspondente a si. A figura 1 ilustra um exemplo de códigos de prefixo. O
código do símbolo s4 é 011, pois a seqüência dos rótulos das arestas, desde a
raiz até s4, é 011 [3].
Figura 1 – Códigos de prefixo
Uma vantagem da utilização de códigos de prefixo é a facilidade
existente para executar as tarefas de codificação e decodificação. Por
exemplo, suponha os códigos dados pela árvore da figura 1. Suponha o
seguinte texto:
s4 s3 s3 s1 s3 s1 s4 s5 s1 s3 s3 s3 s3 s3 s2 s3 s5 s2 s2 s2 s4
Utilizando-se a tabela da figura 1(a), obtém-se a seguinte
codificação:
01101010101000101000111000101010101010101010101000101101000100
0100011
23
Para decodificar o texto, basta percorrê-lo da esquerda para a
direita, ao mesmo tempo em que a árvore é percorrida, repetidamente, da raiz
para as folhas. Nesse percurso, toma-se o caminho para a esquerda ou direita,
na árvore, de acordo com o dígito correspondente encontrado no texto, 0 ou 1,
respectivamente. Toda vez que uma folha é atingida, um símbolo
decodificado foi encontrado. Nesse caso, retoma-se o processo reiniciando-se
o percurso na árvore a partir da raiz, para decodificar o próximo símbolo.
No exemplo, o texto codificado por 0. Significa que se deve tomar o
caminho da esquerda da raiz. O dígito seguinte é 1, o que conduz para a
direita desse último nó. Como o terceiro dígito é 1, a folha correspondente ao
símbolo s4 é alcançada. O texto decodificado inicia-se, pois, por s4. Retoma-
se então a raiz da árvore. O próximo dígito na codificação é 0. O caminho a
seguir é o do filho esquerdo da raiz. Em seguida, 1, após 0 e após 1. Com isso,
atinge-se a folha correspondente a s3. Assim sendo, detectou-se que o
símbolo seguinte a s4 no texto é s3. A nova subseqüência 0101 conduziria,
novamente, ao símbolo s3. O texto decodificado inicia-se, então, por s4, s3,
s3. E assim por diante, até esgotar a seqüência binária.
É imediato verificar que, uma vez conhecida a árvore de prefixo
correspondente, o processo de codificação pode ser realizado em um número
de passos linear ao tamanho da seqüência binária codificada.
Uma questão central é o comprimento do texto codificado, isto é, o
número de dígitos binários da codificação. O comprimento da seqüência
binária produzida por uma árvore binária de prefixo T é denominado custo de
T e denotado por c(T). No exemplo anterior, o custo da árvore da figura 1(b) é
igual a 69, pois a sequência binária obtida possui 69 dígitos. O objetivo
consiste, então, em minimizar c(T), ou seja, construir T de modo que c(T) seja
mínimo. Uma árvore de prefixo que satisfaça esta condição é denominada
24
mínima ou árvore de Huffman. Codificando-se o texto do exemplo anterior
com árvore da figura 2(b), obtém-se a seqüência:
110001010101110100101000001110100111111111110
que possui 45 dígitos. Logo, o custo da árvore da figura 2(b) é igual a 45.
Uma melhoria considerável em relação à arvore da figura 1(b).
Figura 2 – Árvore de Huffman.
Seja T uma árvore de prefixo correspondente a um dado texto, onde
cada símbolo si ocorre fi vezes. Seja li o comprimento do código binário do
símbolo si. É imediato verificar que cada símbolo si contribui com fi.li
unidades no custo c(T) = Somatório fi.li, para 1 < n < i . Este valor
corresponde ao conhecido comprimento de caminho externo ponderado de T.
Se todas as freqüências são idênticas, então T é uma árvore binária completa e
c(T) é o comprimento de caminho externo de T.
25
1.3 – Estrutura de Dados
O algoritmo para construir a árvore de Huffman para um dado
conjunto de n > 1 símbolos si de freqüências fi, 1 <= i <= n. O processo
utiliza a técnica conhecida como algoritmo guloso. Este consiste na
construção da árvore mínima de forma iterativa. A árvore é construída das
folhas para a raiz, ou seja, os códigos são determinados de trás para a frente.
De um modo geral, o processo corresponde a obter subcódigos para
subconjuntos de símbolos. Cada um desses subcódigos corresponde a uma
subárvore. O passo geral iterativo produz a fusão de duas dessas subárvores,
em uma única. O processo se encerra quando o número de subárvores se
reduz a um [3].
Seja T' uma subárvore binária de prefixo. Isto é, as folhas de T' são
símbolos selecionados dentre {s1, ..., sn}. Defini-se f(T'), a freqüência de T',
como sendo a soma das freqüências dos símbolos si presentes nas folhas de
T'. Por exemplo, os números às freqüências das subárvores de raiz nos
respectivos nós.
O processo de construção da árvore de Huffman é simples. Para
iniciar, definem-se n subárvores, cada qual consistindo em um único nó
contendo o símbolo si, 1 <= i <= n. A freqüência de cada uma dessas n
subárvores é, pois, igual à freqüência do símbolo a ela correspondente. O
passo geral, iterativamente, seleciona as duas subárvoes T'e T'', tais que f(T') e
f(T'') sejam as duas menores freqüências. Essas duas subárvores de custo
mínimo são fundidas em uma única árvore T, de acordo com a operação #.
Tal operação consiste em criar um novo nó cujos filhos esquerdo e direito são
as raízes de T' e T'', respectivamente, conforme a figura 3. É indiferente a
escolha dentre T'e T'' para subárvore esquerda ou direita desse nó. Observe
que a freqüência f(T'# T'') da nova subárvore T'# T'' é igual a f(T') = F(T''). O
26
algoritmo termina quando restar apenas uma única subárvore, ou seja, em n-1
execuções da operação #. A figura 4 ilustra a aplicação do algoritmo à
construção da árvore ótima para o conjunto de símbolos e freqüências da
figura 2(a). A árvore obtida pelo algoritmo é exatamente aquela ilustrada na
figura 2(b).
Figura 3 – A Operação #.
Para implementar o processo de construção da árvore de Huffman,
observe que a cada iteração é necessário determinar as duas subárvores T'e T''
da coleção de menor freqüência. As subárvores T'e T'' são então eliminadas e
substituídas pela subárvore T' # T''. Deve-se utilizar, portanto, uma estrutura
de dados que suporte as operações básicas de minimização, inclusão e
exclusão. Por exemplo, uma lista de prioridades.
#
27
S1 S2 S3 S4 S5
5 S2 S4
S5 S1
5
S5 S1
7
S4 S2
12
5
S5 S1
7
S4 S2
S3
S3
12
5
S5 S1
7
S4 S2
21
S3
Figura 4 – Aplicação do algoritmo de Huffman.
O processo de construção da árvore de Huffman é dado pelo
algoritmo 1. As freqüências f1, ..., fn, n > 1 são consideradas conhecidas,
devendo-se construir previamente uma lista de prioridades com elas. Cada
elemento desta lista corresponde a uma árvore T' composta de um único nó,
com freqüência fi, 1 <= i <= n. Os procedimentos utilizados para manipular a
lista de prioridades está invertida. Então, o procedimento mínimo retira a
menor prioridade e rearruma a lista. O procedimetno inserir (T, f, F), que
28
inclui o elemento T, de prioridade f, na lista F, é igual ao algoritmo abaixo.
Ao final, a árvore de Huffman é a correspondente àquela que restou na lista
de prioridades F.
Algoritmo 1 - Construção da árvore de Huffman.
Para i := 1, n-1 faça
Mínimo(T', f, F); mínimo(T'', f, F)
T := T' # T''
F(T) := f(T') = f(T'')
Inserir (T, f, F)
É imediato determinar a complexidade do algoritmo. Cada operação
de minimização ou inclusão na lista de prioridade pode ser efetuada em O(log
n) passos. A operação # requer apenas um número constante de passos. Há
um total de n-1 interações. Logo, a complexidade é O(n log n).
Para verificar que o algoritmo encontra a árvore de custo mínimo,
utiliza-se o seguinte lema: sejam si símbolos com freqüências fi, 1 <= i <= n,
n > 1, tais que f1 e f2 são as duas menores frequências. Então existe uma
árvore de Huffman para esses símbolos, em que os nós correspondentes a s1 e
s2 são irmãos localizados no último nível da árvore [3].
O teorema seguinte comprova a correção do algoritmo: seja T a
árvore construída pelo algoritmo de Huffman para as freqüências f1,..., fn, n>
1. Então, T é mínima.
29
1.4 – Compactação
1.4.1 - Compactação
Suponha que se deseje armazenar um arquivo de grande porte em
algum tipo de memória, primária ou secundária. Para melhor utilizar os
recursos disponíveis, deseja-se também minimizar, de alguma forma e na
medida do possível, o espaço de memória utilizado. Uma forma de tentar
resolver esse problema consiste em codificar o conteúdo do arquivo de
maneira apropriada.
Se o arquivo codificado for menor do que o original, pode-se
armazenar a versão codificada em vez do arquivo propriamente dito. Isto
representaria um ganho de memória. Naturalmente, uma tabela de códigos
seria também armazenada, para permitir a decodificação do arquivo. Essa
tabela seria utilizada pelos algoritmos de codificação e decodificação, os
quais cumpririam a tarefa de realizar tais operações de forma automática.
O problema descrito é conhecido como compactação de dados. A
sua importância foi muito importante nos primeiros anos de computação.
Naquela época, a questão de economia de memória era crítica, devido ao seu
elevado custo. Com o passar do tempo, a memória foi se tornando cada vez
mais abundante, como relativo decréscimo de custo. Entretanto, alguns
programas de aplicação atualmente utilizados requerem muita memória. Entre
esses, por exemplo, encontra-se programas que oferecem recursos visuais aos
seus usuários.
O problema de economia de memória ainda permanece na
atualidade. O advento e larga utilização de redes de computadores para a
importância da compactação de dados. Além da economia de memória,
procura-se também diminuir o custo da transmissão dos arquivos na rede:
uma maior compactação dos dados de um arquivo corresponderia a um
30
número menor de dados a transmitir, o que implicaria um custo de
transmissão menor. A hipótese concreta, nesse caso, é que o custo de
transmissão é proporcional à quantidade de dados a transmitir.
Ao contrário do que possa parecer, compactação não é somente
reduzir o tamanho de um arquivo: há várias outras aplicações que utilizam a
compactação de dados.
A redução do espaço físico é mais comumente utilizada em bancos
de dados que, incorporando a compressão no projeto de seus registros,
permite um significativo ganho em termos de ocupação em disco e velocidade
de acesso.
Como foi mencionado anteriormente, a utilização mais conhecida
da compactação de dados é a redução do espaço ocupado por arquivos. De
fato, esta é aplicação mais comum e mais difundida comercialmente. Afinal,
dado um dispositivo restrito de armazenamento que é um disquete, e um
grande depositório de arquivos que é um disco rígido, faz-se evidente a
necessidade de muitas vezes realizar-se a redução do tamanho de arquivos
para transportá-los ou simplesmente armazená-los.
A compactação também é utilizada para agilizar a transmissão de
dados basicamente de duas formas: alterando a taxa de transmissão e
permitindo o aumento no número de terminais de uma rede.
Se possuímos um modem que opera a 9600 bps (bits por segundo),
é possível que ele transmita como se estivesse a 14400 bps? Sim! Basta que
ele permita a compressão de dados transmitidos, ampliando sua capacidade de
transferência de informação. Na verdade, o modem continuará transmitindo a
uma taxa de transmissão de 9600 bps, mas a taxa de transferência de
informação estará ampliada para 14400 bps. A compressão de dados permite,
portanto, o aumento na velocidade de transmissão de dados.
31
Outra vantagem da compactação em comunicação, derivada do
aumento de velocidade, é a possibilidade de expansão de uma rede de
computadores. Como uma ampliação do número de terminais de uma rede
baixa o desempenho, dado o aumento do tráfego de dados, torna-se necessária
uma transmissão mais rápida de dados. Há, portanto, duas alternativas: trocar
os modem, utilizando modelos mais velozes, ou incorporar um chip com
algoritmo de compressão aos modens existentes. Como a primeira alternativa
é mais cara, a segunda permite que se alcance o mesmo desempenho com
muito menor custo. A compactação de dados, neste caso, permite a ampliação
de uma rede de computadores de uma forma alternativa mais barata.
1.4.3 - Tipos de Compactação
A compactação lógica refere-se ao projeto de representação
otimizada de dados. Um exemplo clássico é o projeto de um banco de dados
utilizando seqüências de bits para a representação de campos de dados. No
lugar de seqüências de caracteres ou inteiros, utiliza-se bits, reduzindo
significativamente o espaço de utilização do banco de dados.
Este tipo de compactação é possível de ser efetivada em campos
projetados para representar dados constantes, como datas, códigos e quaisquer
outros campos formados por números. A característica lógica da compactação
encontra-se no fato dos dados já serem comprimidos no momento do
armazenamento, não ocorrendo sua transformação de dados estendidos para
comprimidos.
A compactação física é aquela realizada sobre dados existentes, a
partir dos quais é verificada a repetição de caracteres para efetivar a redução
32
do número de elementos de dados. Existem dois tipos de técnicas para
sinalizar a ocorrência de caracteres repetidos:
- Um deles indica o caracter (ou conjunto de caracteres) repetido
através da substituição por um caracter especial;
- Outras técnicas indicam a freqüência de repetição de caracteres e
representam isto através de seqüências de bits.
1.5 – Técnicas de compactação
1.5.1 - Seleção de Caracteres Indicadores
Antes da discussão das técnicas propriamente ditas, é necessário o
esclarecimento de como são codificados os caracteres especiais a serem
substituídos pelos caracteres repetidos neste tipo de compressão física. Há
basicamente 3 formas de representação denominadas run-length , run-length
estendido e inserção e deleção.
1.5.1.1 - Codificação run-length
Quando temos um arquivo onde ocorre uma repetição contínua de
determinado caracter, por exemplo AAAAAAAAAAAA, é possível sua
representação através da codificação run-length , da seguinte forma:
Ce 12 A
onde A é o caracter repetido 12 vezes, o que é indicado pelo caracter especial
Ce. O caracter especial é um daqueles caracteres não-imprimíveis que
conhecemos no código ASCII, por exemplo.
33
Cabe salientar que esta codificação ocupa somente 3 bytes. Ora,
como representar um inteiro como um byte? Deve-se ter a consciência que em
um byte pode ser armazenado um valor de 0 a 255, ou seja, pode-se indicar,
como apenas um byte até 256 ocorrências de caracter. Caso ocorra mais do
que isso, deve utilizar outra representação mais eficiente.
Como representaremos um número de repetições maior que 256?
Utilizando o run-length estendido. A diferença deste para o run-length
simples é que há caracteres delimitadores no início e no fim da seqüência de
codificação:
SO R A 980 SI
onde SO (do inglês: shift out) é um caracter especial indicador de início de
uma seqüência de caracteres definida pelo usuário até que SI (do inglês: shift
in) seja encontrado. Esta é uma propriedade de códigos de caracteres, como o
ASCII e EBCDIC. O caracter R indica a compressão run-length, onde o
caracter A é indicado como repetido 980 vezes. Como o valor 980 ultrapassa
o limite de 256 de um byte, torna-se necessária a utilização de mais um byte
para a representação do valor.
1.5.1.2 - Codificação por inserção e deleção
O que fazer quando não for possível a colocação de caracteres
especiais? Há determinados casos que os caracteres especiais utilizados
conflitam com outros aplicativos, de transmissão de dados, por exemplo.
Desta forma, utiliza-se um caracter convencional como indicador de
compactação.
34
Como na língua portuguesa utiliza-se muito pouco as letras K, W,
Y, pode-se, por exemplo, indicar a compressão de um arquivo de texto na
forma:
K 12 A.
1.5.2 - Exemplos de Técnicas de Compressão Orientada a Caracter
As técnicas de compressão orientadas a caracter não são as mais
eficientes ou as mais sofisticadas. Pelo contrário, em geral elas são utilizadas
num primeiro nível de compressão multinível, onde os demais níveis podem
ser técnicas estatísticas de compressão.
Aqui serão analisados 8 técnicas de compressão orientada a
caracter, de modo a dar uma noção das possíveis aplicações deste tipo de
compressão.
1.5.2.1 - Supressão de Caracteres Nulos
Nesta técnica, temos como objetivo comprimir apenas os caracteres
nulos ou brancos. Em geral, são comprimidos caracteres correspondentes ao
espaço em branco.
Para a compactação, utiliza-se a seguinte seleção de caracteres
indicadores: Ce N , onde Ce é um caracter especial e N é o número (em
binário) de caracteres brancos repetidos seqüencialmente.
Então, por exemplo, uma seqüência do tipo:
...vazio. Exatamente o que...
Pode ser comprimida como sendo:
...vazio.Ce8Exatamente o que...
35
o que demonstra uma redução de 6 bytes no trecho apresentado.
Outras formas de indicação de compressão podem ser utilizadas,
com visto na seção anterior. Como trata-se de uma compressão de brancos,
não há necessidade de explicação do caracter comprimido.
1.5.2.2 - Mapeamento de Bit
Quando é sabida a existência de múltiplas ocorrências não
consecutivas de determinado caracter no arquivo a compactar, utiliza-se a
compressão por mapeamento de bit. Esta técnica utiliza-se de um byte no
arquivo comprimido para indicar, através dos bits, a ocorrência do caractere
repetido.
Desta forma, caso desejarmos comprimir todos os caracteres "a" de
um texto, devemos indicá-lo no mapa de bits. Cada mapa descreve 8 bytes,
um por bit do arquivo manipulado. Portanto, para letra encontrada a cada
trecho de 8 bytes, será assinalada no mapa de bits. Por exemplo:
... abacate ...
será descrito como:
... Mbbcte...
onde Mb é o mapa de bits correspondente à compressão do caracter "a" . Este
mapa é composto, para o exemplo, dos bits:
0 1 0 1 0 1 1 1
onde o primeiro zero indica a presença de um caracter "a" na primeira
posição, valor um em seguida indica um caracter diferente, e assim por diante,
até completar o mapa. Convenciona-se, portanto, que o bit 0 indica a presença
do caracter a compactar e o bit 1 a sua ausência.
36
1.5.2.3 - Comprimento de Fileira
Esta técnica aplica a indicação semelhante à run-length de
compressão. O formato permite a determinação do caracter repetido:
Ce C N,
onde Ce é o caracter especial, C é o caracter repetido e N é o número (binário)
de repetições. Como podemos perceber, esta técnica também permite apenas a
compressão de um caracter por vez.
1.5.2.4 - Compactação de Meio Byte
Este tipo de compactação é utilizado quando encontramos uma
seqüência de bits em comum nos caracteres de determinado arquivo. Um tipo
de repetição de bits de grande ocorrência é a que acontece em caracteres
numéricos. Por exemplo, o conjunto binário da tabela ASCII para os
caracteres numéricos é dado pela tabela 1.
Tabela 1 – Conjunto binário da tabela ASCII.
Decimal Binário 0 0011 0000 1 0011 0001 2 0011 0010 3 0011 0011 4 0011 0100 5 0011 0101 6 0011 0110 7 0011 0111 8 0011 1000 9 0011 1001
37
Se isolarmos os primeiros 4 bits de cada byte de caracter numérico,
veremos que há uma constância de valores. Estes valores constantes são um
exemplo de objetos de compactação de meio byte.
A compactação de meio byte propriamente dita consiste na seguinte
seqüência de caracteres indicadores:
Ce N C1 C2 C3 C4 C5...
onde Ce é o caracter especial, N é o número (binário) de caracteres
comprimidos e Cn é a metade do caracter comprimido. Na seqüência de
caracteres indicadores apresentada são apresentados 5 caracteres porque este é
o mínimo para que a compressão seja válida, uma vez que até 4 caracteres ela
necessita um número de byte igual ou maior. Observe também que o número
de repetições está ocupando apenas meio byte. Isso significa que ele pode
indicar apenas até 16 caracteres, ou seja, este formato permite a compactação
de uma seqüência de, no máximo, 16 caracteres.
Mas então surge a pergunta: não há como estender este formato para
permitir a compactação de uma seqüência maior de caracteres? A resposta é
sim, existe a compactação de byte inteiro, onde o número N ocupa 1 byte, ao
invés de meio. Neste formato estendido, o restante dos caracteres indicadores
permanecem os mesmos, sendo alterado apenas a representação do número de
caracteres. Com a representação de byte inteiro, a capacidade de compressão
aumenta para 256 caracteres, o que permite um ganho expressivo em se
tratando de longas seqüências de caracteres.
1.5.2.5 - Codificação Diatômica
Esta técnica de compactação permite a representação de um par de
caracteres em apenas um caracter especial. Normalmente utilizam-se tabelas
38
com pares de caracteres e sua freqüência de ocorrência em determinado tipo
de arquivo. Obviamente procura-se substituir os caracteres de maior
freqüência, associando a cada dupla um caracter especial.
Em texto da língua portuguesa, por exemplo, duplas de ocorrência
freqüente são a letra "a" acentuada com til seguido da letra o "ao" e a letra "e"
com acento agudo seguido de um espaço em branco "é". A cada uma dessas
seqüência deve-se atribuir um caracter especial para nos permitir a
compactação através da codificação diatômica. Obviamente, estes são apenas
dois exemplos de duplas de caracteres, numa tabela normal para compactação
utilizam-se mais de 20 duplas para que seja obtida uma compressão razoável.
1.5.2.6 - Substituição de Padrões
A substituição de padrões é semelhante à codificação diatômica,
pois também ocorre a substituição de um conjunto de caracteres por um
caracter especial.
PALAVRA => Ce
O processamento desta técnica é semelhante ao da substituição de
padrões, com a diferença de que são avaliados um número maior de
caracteres. Desta forma, são estabelecidas tabelas de palavras de maior
freqüência de ocorrência para substituição com o caracter especial.
A utilização mais comum para este tipo de compactação é a de
arquivos de programas de linguagens de programação. Uma vez que as
linguagens contém diversas palavras que se repetem freqüentemente em
programas, utiliza-se esta característica para a sua compactação.
39
Uma variante da substituição de padrões para permitir a codificação
de um maior número de palavras é a utilização de dois caracteres para
indicação da ocorrência de determinada palavra:
C N,
onde C é um caracter escolhido para indicar a compactação e N é o número
(binário) da palavra a substituir. Isso permite a codificação de até 256
palavras reservadas, o que anteriormente era limitado ao número de caracteres
especiais que poderíamos utilizar.
Por exemplo, as palavras reservadas begin e end da linguagem
Pascal poderiam ser, por exemplo, substituídas pelos códigos $1 e $2. As
demais palavras reservadas da linguagem também poderiam ser codificadas
desta maneira, permitindo uma compactação considerável de um arquivo de
programa.
Como esta técnica assemelha-se muito à da codificação diatômica,
deve tomar como base para a programação o algoritmo e o fluxograma
apresentados para aquela técnica. Desta forma não serão apresentados formas
distintas de programação.
1.5.2.7 - Codificação Relativa
Esta técnica de compactação é realizável quando existe a
transmissão de seqüências de caracteres numéricos. Estas seqüências podem
ser valores dentro de intervalos definidos ou valores binários.
A transmissão de valores intervalares pode ser exemplificada por
transmissões de dados provenientes de sinais de sensores. Um exemplo de
transmissão binária é o fax. Nestes dois casos, suas transmissões podem ser
comprimidas através da codificação relativa. Para cada um dos casos existe
40
um processamento adequado aos tipos de valores envolvidos, por isso,
veremos como é o processamento para a codificação de valores intervalares e
para valores binários.
1.5.2.8 - Valores Intervalares
Neste primeiro tipo, a compactação somente é válida se existe um
intervalo definido de valores, caso contrário não ocorre uma compressão
satisfatória. Isso porque este tipo de compressão baseia-se na colocação da
variação numérica de um valor a outro, desta forma:
Val Var1 Var2...
onde Val é o primeiro valor da seqüência, VarN é a variação do valor anterior
para o atual. Exemplificando de forma numérica, seja a seguinte seqüência de
caracteres numéricos:
10.3 10.1 10.8 10.4 10.4 10.9 10.2,
pode ser compactada através da codificação relativa, ficando os caracteres na
forma:
10.3 -.2 .7 -.4 0 .5 -.7,
ou seja, numa seqüência dentro de um intervalo definido entre 10.0 e 10.9, foi
possível a compressão de um conjunto de 34 para 24 caracteres.
A implementação desta técnica resume-se a apenas no
processamento da gravação direta da diferença entre os dois valores anteriores
no campo atual a comprimir. Não há nenhum outro esquema a seguir para
descrever o processo.
1.5.2.9 - Valores Binários
41
Como afirmado anteriormente, o fax utiliza-se desta forma de
compactação, portanto a comparação existente não será mais entre um valor e
outro, mas entre uma linha e a seguinte. Cada linha é um vetor de valores
binários, que representam o mapa de bits do desenho da página. O que ser
quer é transmitir apenas a variação de uma linha para outra, evitando-se a
transmissão de toda a linha.
Então, um trecho de linha binária poderia ser:
... 01001011100010101...
e a sua seguinte:
... 01001011111100001...,
a diferença seria em 5 bits:
... _________MMMM_M__... .
A representação da transmissão dessa diferença entre linhas pode
ser feita de duas formas: indicando a posição da mudança a partir do início ou
a posição a partir da mudança anterior. Além da posição da mudança,
normalmente indica-se a quantidade de mudanças de ocorreram.
Para o trecho de linha apresentado anteriormente, pode-se
representar como ponto fixo da mudança seguido da quantidade de bits
alterados:
529 4 534 1,
caso se fosse esta a única alteração na linha, que normalmente possui 1728
bits, seria uma compactação expressiva.
Compactando-se a partir do cálculo do deslocamento, o número de
bits a transmitir se reduz ainda mais:
529 4 5 1,
supondo que não há nenhuma alteração anterior a que está apresentada, a
posição inicial é fixa e as demais relativas umas às outras, contabilizando-se
apenas o numero de bits de distância de uma a outra.
42
1.6 - Compactação Estatística
A idéia da compressão estatística é realizar uma representação
otimizada de caracteres ou grupos de caracteres. Caracteres de maior
freqüência de utilização são representados por códigos binários pequenos, e
os de menor freqüência são representados por códigos proporcionalmente
maiores. Neste tipo de compactação portanto, não necessitamos saber qual
caracter vai ser compactando, mas é necessário, porém, ter o conhecimento da
probabilidade de ocorrência de todos os caracteres sujeitos à compressão.
Caso não seja possível a tabulação de todos os caracteres sujeitos à
compressão, utiliza-se uma técnica adequada para levantamento estatístico
dos dados a compactar, formando tabelas de probabilidades.
Esta Teoria baseia-se no princípio físico da Entropia. A Entropia é a
propriedade de distribuição de energia entre os átomos, tendendo ao
equilíbrio. Sempre que um sistema físico possui mais ou menos quantidade de
energia que outro sistema físico em contato direto, há troca de energia entre
ambos até que atinjam a entropia, ou seja, o equilíbrio da quantidade de
energia existente nos sistemas.
Ao atingir o estado de equilíbrio, sabe-se que estes sistemas estão
utilizando o mínimo de energia possível para sua manutenção, e assim se
manterão até que outro sistema interaja sobre eles.
Aplicada à informação, a Teoria da Entropia permite a concepção
de uma teoria da Harmonia, ou seja, um ponto de equilíbrio onde a
informação pode ser representada por uma quantidade mínima de símbolos.
Para chegarmos a esta representação ideal, basta que tenhamos a quantidade
de símbolos utilizada e a probabilidade de ocorrência deles. Com base nisso, é
possível calcular a quantidade média de bits por intervalo de símbolo.
43
1.7 - Compactação Fixa versus Compressão Auto-adaptável
Caso as probabilidades nas quais foram concebidos os códigos da
tabela forem distintos para determinado arquivo a compactar, este será
prejudicado na compactação. Para resolver este caso utiliza-se uma tabela
adaptável.
A tabela adaptável consiste na mesma tabela que conhecemos,
contendo o caracter e seu código, acrescida de mais uma coluna contendo a
contagem de ocorrência do caracter no arquivo a compactar. Efetua-se,
portanto, uma contagem de quantos caracteres ocorrem no arquivo, e ordena-
se a coluna dos caracteres em ordem crescente a partir desta coluna da
contagem, mantendo a coluna de código inalterada. Como resultado, obtém-se
uma tabela com os caracteres de ocorrência mais freqüente sendo codificados
com menos bits.
Tabela 2 – Caracter, código e a contagem.
Caracter Código Contagem
C1 0 0 C2 10 0 C3 110 0 C4 1110 0 C5 1111 0
Considere a tabela 2, contendo o caracter, o código e a contagem.
Caso o caracter C3 ocorra 21 vezes, o C2 18 vezes, C1 10 vezes, o C5 3 vezes
e o C4 2 vezes, a tabela 3 mostra a nova configuração.
44
Tabela 3 – Nova configuração.
Caracter Código Contagem C3 0 21 C2 10 18 C1 110 10 C5 1110 3 C4 1111 2
Comparando-se a tabela 2 e 3, a codificação continuou a mesma,
mas a correspondência foi alterada para a situação específica do arquivo
comprimido em questão. Observamos, portanto, a necessidade de realizarmos
um processamento de leitura e cálculo anterior à compressão propriamente
dita, o que implica em maior tempo gasto do que com a utilização da tabela
fixa.
Com a compactação auto-adaptável ganha-se, obviamente, uma
compactação mais eficiente. Por outro lado, há um problema pela própria
variabilidade da tabela: para a descompactação, há a necessidade de se anexar
a tabela ao arquivo compactado, para que os códigos correspondentes possam
ser devidamente decodificados.
1.8 - O código de Lempel-Ziv
Um código alternativo é o código de Lempel-Ziv. Por volta de1977,
Abraham Lempel e Jacob Ziv desenvolveram uma classe de compactação de
dados com dicionário adaptativo chamado de Lempel-Ziv.
Posteriormente, Terry Welch, melhorou o código de Lempel-Ziv,
criando o hoje chamado de código LZW que constrói um dicionário dos
grupos de caracteres mais freqüentemente usados. É importante ressaltar que
antes que o arquivo seja descompactado é necessário que se envie o dicionário
de compactação. Este método é melhor para compactação de arquivos textos
45
cujo conteúdo é de caracteres ASCII, mas não tão bom para arquivos de
imagens, cujo conteúdo possui amostras repetitivas de dígitos binários que
podem não ser múltiplos de 8 bits [3].
O algoritmo LZW é organizado por uma tabela de tradução,
chamada de tabela de seqüência de caracteres, que mapeia esta seqüência de
dados de entrada em códigos de tamanhos fixos. O uso de códigos de 12 bits é
comum. A tabela de seqüência de caracteres LZW tem uma propriedade pré-
estabelecida que para toda seqüência de caracteres já tabulada sua seqüência
de caractres anterior já esteja na tabela. Isto é, se uma seqüência de caractres
ωK, composta de uma string ω e algum único caracter K, estiver na tabela,
então ω está na tabela. K é chamado de caracter extendido da seqüência de
caractres ω . A tabela de seqüência de caracteres é inicializada para conter
todo o conjunto de caracteres únicos [3].
Abaixo o algoritmo LZW de compactação proposto por Terry
Welch:
Inicializar tabela que conterá caracteres
Ler a primeira entrada de caracteres – prefixo ω
Passo: Ler próxima entrada de caracter K
Se K não for suficiente (entrada esgotada): código(ω) -> saída; SAI
Se ωK existir na tabela de strings: ωK – ω; repetir Passo.
Senão ωK não está na tabela: código(ω) -> saída;
ωK-> tabela de strings;
K-> repetir Passo.
Abaixo o algoritmo LZW de compactação proposto por Terry
Welch:
Descompactação:
Leia código primeira entrada -> CÓDIGO ->VELHOcódigo;
46
Com CÓDIGO = codigo(K); K->saída;
Próximo Código:
Leia próxima entrada de código -> CÓDIGO ->INcódigo;
Se não há código: Saia.. senão
Próximo Símbolo:
Se CÓDIGO = código(ωK); K->saída;
código(ω) -> CÓDIGO;
Repetir próximo símbolo
Senão se CÓDIGO = código(K): K-> saída;
VELHOcódigo, K-> tabela de string
INcódigo->VELHOcódigo;
Repetir próximo código.
Colocamos na figura 5 abaixo uma tabela que será utilizada no
exemplo da figura 6.
TABELA DESTRINGS
TABELA ALTERNATIVA
abc
abc
abc
abc
abbaabccbbabbabaaaaaaaaa
456789101112
1b2a4c3b5b8a1a10a11a
456789101112
Figura 5 – Tabela de Strings
Esta tabela é inicializada com três valores de código para três
47
caracteres. Utilizamos esta tabela de strings para codificarmos os símbolos de
entrada mostrados na figura 6 abaixo.
a b a b c b a b a b a a a a a a a
4
1 2 4 3 5 8 1 10 11 1
5 7 9 11
6 8 1210
Símbolos de entradaCódigos de saídaNovas stringsadicionadasà tabela
Figura 6 – Um exemplo de compactaçaõ
Nesta figura, os dados de entrada estão sendo lidos da esquerda para
direita, começando pelo caracter “a”. À medida que vão sendo encontradas
seqüências de caracteres diferentes, um novo código é atribuído à seqüência e
adicionado à tabela.
Na figura 7 temos um exemplo de descompactação utilizando LZW.
Códigos de entrada
Dados desaída
Strings adicionadosà tabela
1 2 4 3 5 8 1 10 11 v v v v v v v v v a b 1b c 2a 5b a 1a 10a v v v v v a b 2a a 1a v v b aa b ab c ab bab a aa aaa
4 6 8 10
5 7 9 11
Figura 7 – Exemplo de descompactação.
Nesta figura temos que o código é traduzido por realocação
recursiva do código com um código de prefixo e seu caracter extendido
proveniente da tabela de strings, figura 5. Por exemplo, o código 5 é
48
substituído pelo código 2 e “a”, e então o código 2 é substituído por “b”.
Existe também o VLC (do inglês: Variable-Length-Code) LZW que
usa uma variação do algoritmo LZW onde códigos de tamanho variado são
usados para sobrescrever amostras detectadas dos dados originais. Ele usa um
dicionário construído a partir de trechos encontrados nos dados originais.
Cada novo trecho semelhante é inicializado no dicionário e um endereço
indexado é usado para substituir a faixa compactada. O trasmissor e o
receptor mantém o mesmo dicionário.
A parte VLC do algoritmo é baseada num tamanho de código inicial
(o código inicial de LZW), que especifica o número inicial de bits usados para
compactação. Quando o número de amostras detectadas pelo compactador no
stream de entrada exceder o número de amostras codificáveis então o número
de bits do código LZW é acrescido de um. O tamanho do código é
inicialmente transmitido para que o receptor saiba o tamanho do dicionário e
o tamanho das palavras-código.
Em 1985 , o algoritmo LZW foi patenteado pela Sperry Corp. Ele é
usado no formato de arquivo GIF e é similar a técnica usada para compactar
dados no V.42bis modems.
O código de Lempel-Ziv, assim como o código de Huffman, possui
algumas desvantagens. A compressão LZ substitui as amostras repetitivas
detectadas através de referências ao dicionário. Infelizmente quanto maior o
dicionário, maior o número de bits necessários para a referência. O valor
ótimo do dicionário também varia quanto aos diferentes tipos de dados.
quanto mais variável os dados, menor será o valor ótimo para o dicionário [3].
1.9 - Codificação Lempel-Ziv
A compactação Lempel-Ziv é um tipo de compactação de cadeia
que forma strings de caracteres a partir de sua ocorrência, criando tabelas de
49
strings conforme vão acontecendo. Dada a interessante abordagem utilizada
nesse tipo de compactação, melhor do que as anteriormente estudadas, a
codificação Lempel-Ziv foi aprimorada e tornada padrão para a compressão
de dados transmitidos via modem. Esta técnica padrão baseada na Lempel-Ziv
é a recomendação V. 42 bis do CCITT (do inglês: Consultative Committee for
International Telephone and Telegraph).
A codificação Lempel-Ziv utiliza probabilidades de cadeias de
caracteres ao invés de apenas um caracter. Qual a vantagem disso?
Em primeiro lugar, vamos analisar qual é a influência da
probabilidade sobre o número teórico de bits para representação de
determinado conjunto de caracteres. Para tanto, vamos pegar os caracteres C1
e C2 para verificar a relação entre probabilidade e número teórico de bits.
Para sabermos este número teórico de bits precisamos calcular a harmonia,
que nos dá o número de bits por intervalo de símbolo. Assim, se C1 tiver
probabilidade 0,8 e C2 tiver 0,2, sua entropia será 0,72, e assim
sucessivamente, conforme mostra a tabela 4.
Tabela 4 - Entropia.
C1 C2 Entropia 0,8 0,2 0,72 0,7 0,3 0,88 0,6 0,4 0,97 0,5 0,5 1
Comparando os de C1 e C2 na tabela 4, quanto mais distinta for a
pordentagem, maior é o aproveitamento de bits. Para aproveitarmos isso como
forma de compactação, basta criarmos conjuntos de caracteres de forma a
acumular porcentagens.
50
Assim, podemos acumular os caracteres em cadeias do tipo C1C1,
C1C2, etc. Para verificarmos se há ganho no comprimento médio de bits,
vamos iniciar analisando as probabilidades de cadeias de 2 caracteres na
tabela 5 , onde foi estabelecido C1 com probabilidade 0,7 e C2 com 0,3.
Tabela 5 – Probabilidade de ocorrência.
C1 C2 0,49 C1 C2 0,21 C2 C1 0,21 C2 C2 0,09
A probabilidade de ocorrência da cadeia C1C1 é de 0,7*0,7 = 0,49,
ou 49%. O mesmo raciocínio aplica-se às demais combinações. Utilizando a
codificação Huffman, podemos estabelecer o tamanho médio ocupado pelo
grupo de cadeias da tabela anterior.
Tabela 6 – Tamanho médio de bits
C1 C1 0 C1 C2 10 C2 C1 110 C2 C2 111
Obtendo-se os códigos binários, que podem ser observados na
tabela 6, pode-se calcular o tamanho médio de bits ocupado, dado por:
T = 0,49*1 + 0,21*2 + 0,21*3 + 0,09*3 = 1,81 bits
O valor de 1,81 bits para representação da cadeia pode parecer
grande, mas se dividirmos por 2, teremos 0.905 bit por caracter, menor que 1
bit por caracter no caso de codificarmos o bit 0 para C1 e bit 1 para C2.
51
Seguindo este raciocínio, podemos ampliar a idéia para um número maior de
caracteres e chegaremos à conclusão de que, quanto maior a cadeia, mais
caracteres serão passíveis de compactação.
Desta forma, a Lempel-Ziv utiliza um conjunto de regras para
analisar as cadeias de um conjunto de caracteres pré-definido. As cadeias são
montadas e codificadas à medida em que são lidos os caracteres a codificar. A
forma de composição destas cadeias está descrita na técnica V. 42 bis descrita
a seguir.
A Lempel-Ziv foi melhorada e é utilizada como uma técnica padrão
para transmissão através de modens. Esta técnica possui um tamanho máximo
de cadeia que pode variar de 6 a 250 caracteres para formação de cadeia.
Existe um dicionário com 512 cadeias básicas codificadas, mas este não é
restrito a elas.
O dicionário de cadeias é dinâmico e pode ter cadeias acrescentadas
e retiradas em tempo de compactação. A formação de novas cadeias é baseada
na comparação das cadeias lidas com as existentes no dicionário. Se a cadeia
lida não existe, ela é acrescentada e recebe um código que a representa como
cadeia compactada.
Como o modem transmissor é o que faz a compactação e o
dicionário é dinâmico, como o modem receptor irá decifrar os códigos para
descompactá-los? Simples, a cada nova cadeia formada no transmissor são
enviados os caracteres ou a primeira subcadeia que a compõe e o código
correspondente à nova cadeia, para que exista uma réplica do dicionário no
receptor.
É utilizada uma estrutura de dados em árvore para implementação
do dicionário de cadeias. A partir de uma cadeia pré-definida é formada uma
árvore para processamento das cadeias. Seja a cadeia pré-definida DA , a
52
partir da qual derivam-se as cadeias DATA e DADO. Para esta seqüência, a
estrutura será dada pela figura 5.
Figura 8 – Árvore para processamento das cadeias
Os valores ao lado de cada caracter é o código correspondente à
cadeia composta até aquele nó da árvore, ou seja, a cadeia DA possui código
300, a cadeia DAT possui código 342, e assim por diante. Desta forma,
amplia-se o dicionário sem prejudicar as codificações existentes e permite, ao
mesmo tempo, a verificação de variações deste códigos.
Existe ainda, a possibilidade de substituição de códigos pouco
utilizados. Uma vez definida um número N de códigos possíveis, ao atingir
este número, procura-se um código que corresponda a um nó folha que não
esteja sendo utilizado na compactação em curso. Caso exista, a nova cadeia
utiliza o código da cadeia antiga não utilizada. Ocorre, portanto, eliminação
de códigos do dicionário. Esta é a técnica padrão V. 42 bis utilizada para
compressão na transmissão de dados via modem. Ela baseia-se na codificação
em cadeia de Lempel-Ziv, utilizando-se da multiplicação das probabilidades
de ocorrência de caracteres, para, em cima das estatísticas de cadeias, permitir
a compressão de bits utilizados na transmissão de dados.
342 T
343 A
DA 300
D 378
O 379
53
CAPÍTULO 2
O PROBLEMA
2.1 - Introdução
Basicamente compactar dados significa reduzir o seu tamanho
original de tal maneira que possamos reconstituí-lo originalmente sem perda
de informação. Ela pode ser aplicada em arquivos de dados, documentos,
imagens, etc. A figura 6 mostra o processo de compactação.
Figura 9 - Ilustrando compactação de Dados
COMPACTAÇÃO
DESCOMPACTAÇÃO
MEIO
SÍMBOLOS
DE ENTRADA
SEQUÊNCIA
DE
CARACTERES
SAÍDA DE
SÍMBOLOS
DADOS
DADOS
COMPACTADOS CÓDIGO
DADOS ARMAZENADOS/ LINHA DE COMUNICAÇÃO
54
As duas técnicas mais utilizadas é a de código estatístico e a de
supressão de seqüências repetitivas. Vamos fazer uma digressão sobre estas
duas técnicas.
O mais popular método de compactação que utiliza código
estatístico é o Código de Huffman que consiste em traduzir pedaços de
tamanho fixo em símbolos de tamanho variável. Ele procura uma maneira de
gerar códigos para os símbolos de entrada cujo tamanho destes código em bits
é aproximadamente log2(probabilidade do símbolo), onde a probabilidade do
símbolo é a freqüência relativa de ocorrência de um símbolo dado expresso
em probabilidade. Por exemplo, se o símbolo “Y” é encontrado 0,1 por cento
do tempo, então ele será representado por 10 bits. Na implementação de nosso compactador utilizando Código de
Huffman, tomamos um arquivo .txt que desejamos compactar e geramos um
arquivo .huf cujo conteúdo é inteligível apenas quando aplicamos o processo
inverso o que chamamos de descompactação.
Como propósito de pesquisa, optamos por fazer uma filtragem do
arquivo texto inicial, que consiste basicamente em considerar somente
caracteres maiúsculos; ou seja, qualquer texto que utilize nosso programa só
poderá ser processado, para nível de pesquisa, se todos seus caracteres forem
maiúsculos. Entretanto, o compactador de Huffman implementado funciona
tanto para caracteres maiúsculos como para caracteres minúsculos.
Um formulário no programa implementado a partir da leitura de um
texto retorna uma estatística de freqüência de caracteres.
Para obter uma estatística fixa, fizemos uma compilação de cem
arquivos textos diferentes das mais diversas fontes, em língua portuguesa,
aplicando o programa implementado, cujos resultados foram tabulados e cuja
tabulação será utilizada para se estimar a média em percentagem de caracteres
de um texto qualquer em língua portuguesa.
55
Tabela 7 - Estatística Fixa.
Caracter Cadeia de bits Huffman Percentagem
espaço 000 16,90%E 101 11,40%A 111 8,53%O 0011 7,88%S 0111 6,77%R 1001 5,36%U 1101 4,10%M 00101 4,00%N 01001 3,90%I 01000 3,80%D 01011 3,47%P 01101 2,99%T 10001 2,87%C 10000 2,69%Q 001001 2,14%L 010101 1,66%. 110011 1,13%Ã 110010 1,00%, 0010001 0,90%| 0101001 0,90%| 0101000 0,90%
G 0110011 0,75%Ê 0110010 0,70%F 1100001 0,63%H 00100001 0,48%B 01100001 0,48%Á 01100011 0,40%Ç 01100010 0,35%É 11000001 0,30%Z 001000001 0,25%I 011000001 0,23%X 110000001 0,15%- 110000000 0,12%Ó 0010000001 0,12%Í 0110000001 0,10%Â 0110000001 0,0005%W 01100000000 0,00025% K 001000000001 0,00025% “ 0010000000000 0,00025% ! 001000000011 0 00025%
56
A estatística fixa obtida para a língua portuguesa poderá ser
utilizada para que possamos montar a árvore de Huffman, utilizando sempre
as freqüências padrões, o que pode gerar uma compactação não ótima.
Evitamos armazenar uma tabela de correspondência de caracteres com sua
seqüência de bits montada dinamicamente pela árvore de Huffman gerada ao
se compactar o arquivo. A tabela 7 terá que ser de alguma forma armazenada,
ou como cabeçalho do arquivo compactado, ou como algum arquivo anexo.
2.2 - Proposta para implementação do compactador de Huffman
Como proposta para implementação do compactador de dados
dividimos o programa em dois formulários principais. O primeiro formulário
é mostrado na figura abaixo.
Figura 10 – Formulário Huffman
57
Ele mostra dinamicamente o processo de compactação e
descompactação, desde a filtragem dos dados, montagem da estatística
ordenada por código ASCII, montagem da estatística ordenada por freqüência
de incidência, montagem da tabela de Huffman, montagem da cadeia de bits,
até a descompactação a partir da leitura da árvore de Huffman montada
dinamicamente e da leitura seqüencial da cadeia de bits.
O outro formulário, mostrado na figura abaixo, apresenta o processo
de compactação nos retornando os índices de compactação, o tempo levado
para montar a árvore de Huffman, o tempo levado para montar a cadeia de
bits, o arquivo .huf gerado e o tempo de descompactação.
Figura 11 – Formulário de Compactação
Como vimos, o código de Huffman consiste basicamente em
ordenar por taxa de incidência os caracteres de um arquivo texto, montar a
árvore binária utilizando o algoritmo de Huffman e posteriormente, em fase
de compactação, percorrer a árvore montando seqüencialmente nossa cadeia
58
de bits. Esta cadeia de bits será gravada como um arquivo .huf, em binário,
para que possamos conseguir fisicamente um arquivo final reduzido.
A linguagem utilizada para implementação do compactador
implementado foi a Linguagem C++, utilizando o Builder como compilador.
Esta linguagem é poderosíssima no que tange a recursos oferecidos de
orientação a objeto, permitindo-nos utilizar uma filosofia que está
modificando os paradigmas da programação, o conceito de "reutilizar,
reutilizar, reutilizar".
Nesse sentido, utilizamos uma ferramenta de muito impacto em
C++ para implementação inicial de nosso código de Huffman: as STL's (do
inglês: Standart Template Library). A STL é uma biblioteca padrão de
gabaritos e foi desenvolvida por Alexander Stepanov e Meng Lee na Hewlett-
Packard. Baseada na pesquisa de campo da programação genérica e teve
também contribuições significativas de David Musser.
Além de se utilizar de gabaritos, uma STL gera códigos que o
programador não necessariamente precisa digitar, combinando a
generalização de gabaritos com a disponibilidade de bibliotecas padrões. As
STL's possuem três componentes-chaves, contêineres, iteradores e algoritmos.
Em nosso projeto, utilizamos um conteiner chamado de multimap
que possui as seguintes características: mapeamento um para muitos,
duplicatas não-permitidas e pesquisa rápida usando chaves. Está na classe de
contêineres associativos. Contêineres associativos da STL destinam-se a
fornecer acesso direto para armazenar e recuperar elementos via chaves. Ao
todo existem quatro contêineres associativos: multiset, set, multimap e map.
Em cada contêiner, as chaves são mantidas em ordem classificada.
Iterar através de um contêiner associativo percorre o mesmo em uma ordem
classificada para aquele contêiner. As classes multiset e set fornecem
operações para manipular conjuntos de valores nos quais os próprios valores
59
são as chaves, isto é, não há um valor separado associado a cada chave. A
principal diferença entre um multiset e um set é que um multiset permite
chaves duplicadas e um set não permite. As classes multimap e map fornecem
operações para manipular valores associados com chaves (estes valores às
vezes são referidos como valores mapeados). A principal diferença entre um
multimap e um map é que um multimap permite chaves duplicadas com
valores associados.
Em nosso projeto utilizamos o multimap, para armazenagem e
recuperação rápidas de chaves e valores associados (pares chave/valor).
Quando se insere em um multimap, um objeto pair, que contém a chave e o
valor, é usado. A ordenação das chaves é determinada por um objeto função
comparadora. Por exemplo, em um multimap que usa inteiros como tipo de
chave, as chaves podem ser classificadas em ordem ascendente ordenando as
chaves com o objeto função comparadora less<int>. Chaves duplicadas são
permitidas em um multimap, de modo que valores múltiplos podem ser
associados com uma chave única. Isto é chamado freqüentemente de um
relacionamento um para muitos. Um multimap suporta iteradores
bidirecionais mas não iteradores de acesso aleatório.
Depois dessa breve incursão sobre STL's, voltemos ao algoritmo de
Huffman. Nosso primeiro passo, é contar quantos caracteres de cada símbolo
aparecem no texto e ordená-los por critério de aparição. Conseguimos isso
processando a cadeia de strings (texto inicial), caracter por caracter na
implementação do compactador, linha por linha e a medida que processamos
este texto alimentamos o multimap, que com o objeto função comparadora
adequado nos retornará a lista completamente ordenada.
A segunda fase do Código de Huffman consiste em montar a árvore
de Huffman da estatística concluída. É imprescindível que se utilize uma
árvore binária. Somente com esta estrutura de dados é que iremos conseguir
60
um código unicamente decodificável.
Uma árvore binária é uma N-ary tree cujo N seja igual a dois. Há
algumas características interessantes que provem da limitação de N = 2. Por
exemplo, há um interessante relacionamento entre árvores binárias e o sistema
de números binários. Árvores binárias são úteis para representação de
expressões matemáticas envolvendo operações matemáticas, tais como adição
e multiplicação.
Uma árvore binária T é um conjunto finito de nós os quais possuem
as seguintes propriedades. O conjunto pode estar vazio;
Ou o conjunto consiste em uma raiz, r, e exatamente duas árvores
binárias TL e TR distintas, T = {r, TL, TR}.
A árvore TL é chamada subárvore de T, e a árvore TR é chamada
subárvore de T.
Árvores binárias são quase sempre consideradas árvores ordenadas.
Então, subárvores TL e TR são chamadas subárvores da esquerda e da direita,
respectivamente.
Para implementação de árvores binárias temos uma classe em C++
cuja hierarquia é mostrada na figura 9. São inclusas General Trees, N-ary
trees e Binary Trees. Os vários tipos de árvores são vistas como classes de
contêineres.
Figura 12 – Estrutura hierárquica de classes da árvore binária
GeneralTree
NaryTree
BinaryTree
Ownershipe
Object
Container Tree
61
2.3 - Desempenho do compactador de Huffman implementado
Para arquivos textos maiores que 40 KB, o compactador de arquivos
implementado, apresentou uma ligeira demora na compactação, e na
descompactação que tende a crescer exponencialmente com o aumento linear
do arquivo. Isso porque ao compactarmos, temos que fazer sucessivas leituras
na árvore de Huffman para montarmos a cadeia de caracteres. Ao
descompactarmos, temos que fazer o processo inverso com a sobrecarga das
comparações com o cabeçalho.
Constatamos um problema inerente ao Código de Huffman: a
complexidade de descompactação. O método básico para interpretar cada
código é ler cada bit da seqüência. Operacionalmente, uma decisão lógica é
feita para cada bit. Em geral, descompactação com símbolos de tamanho
variável provê um custo/desempenho em decadência à medida que o volume
de dados aumente.
2.4 - Testes realizados
Realizamos alguns teste s comparativos utilizando três compactadores
comerciais e o nosso programa que utiliza o Código de Huffman, afim de que
possamos comparar os índices de compactação.
Os compactadores utilizados foram: Gzip [7], Winrar [8], Winzip [9] e
Huffman. Utilizamos dois arquivos para teste: Manual de Sobrevivência.txt - um
arquivo texto cujo tamanho original é de 3.972 bytes - e Programa.cpp que é
arquivo código-fonte do compactador de Huffman implementado. Os resultado
obtidos estão na tabela 8.
62
Tabela 8 - Comparação índices de compactação médios.
Nº Arquivo Tamanho
(bytes)
Gzip
(bytes)
WinRar
(bytes)
Winzip
(bytes)
Huffman
(bytes)
1. .txt 3.972 1.721 1.827 1.855 3.715
2. .cpp 4.105 1.469 1.529 1.575 2.557
3. .log 16.497 1.541 1.578 1.664 9.842
4. .htm 5.419 1.809 1.879 1.909 1.075
5. .obj 6.642 2.103 2.205 2.256 2.450
Para o primeiro arquivo (Manual de Sobrevivência.txt) obtivemos o
melhor índice de compactação para o programa Gzip [7], exatamente 43,33 %.
Em seguida, vem o Winrar [8] com 45,50%, depois o Winzip [9] com 48,92% e
o Huffman com 93,53%.
Para o segundo arquivo (Programa.cpp) tivemos um índice de
compactação para o código de Huffman de 62,28%. Em seguida, vem o Gzip [7]
com índice de 35,78%, depois o Winrar [8] com 37,25% e o Winzip [9] com
38,37%.
Percebemos que, no primeiro caso, o Código de Huffman teve um
índice de compactação em termos absoluto e relativo pior que no segundo caso.
Isto se deve a dois motivos: primeiramente quanto menor a quantidade de
símbolos diferentes entre si, melhor será o índice de compactação para o
Compactador de Huffman. Não é o que acontece no primeiro arquivo, cujo texto
está repleto de símbolos diferentes num universo total de caracteres
relativamente pequeno, algo como 3.972 caracteres. Em segundo lugar, o código
de Huffman, estabelece a menor cadeia de bits para o símbolo que apareceu a
maior quantidade de vezes. Obtemos um arquivo com índice de compactação
menor, quanto maior for a quantidade de vezes que esse símbolo apareça. Isto é
63
o que ocorre num arquivo.cpp, pois neste arquivo, temos uma espantosa soma de
espaços que o compactador de Huffman reduziu extraordinariamente bem.
2.5 - Aplicações
Muitos programas de compactação atualmente utilizam programas
que usam Lempel-Ziv ou Huffman, ou ainda as duas técnicas. As duas
técnicas são sem perda de dados. Em geral, Huffman é o mais eficiente,
porém, requer dois passos sobre os dados, enquanto Lempel-Ziv usa somente
um passo. Este mecanismo de um único passo é obviamente importante no
momento em que gravamos num disco rígido ou quando a decodificação ou
decodificação dos dados ocorre em comunicações em tempo real. Uma das
variantes mais utilizadas é o LZS, pertencente a Stac Eletronics (que foi a
primeira companhia a produzir um driver compactado, chamado de Stacker).
A Microsoft incluiu uma variação deste programa, chamada de DoubleSpace
no DOS versão 6 e Drive-Space no Windows 95.
A técnica LZS é tipicamente usada em dispositivos de recuperação
de dados de massa, tais como dispositivos de fitas, onde a compactação pode
tanto ser implementada em hardware como em software, o que permite que o
dispositivo de fita armazene pelo menos duas vezes sua capacidade física.
O volume de compactação depende do tipo de arquivo que está
sendo compactado. Arquivos aleatórios, tais como programas executáveis ou
arquivos de código objeto, tipicamente possuem baixa compactação,
resultando num arquivo que está entre 50 a 95% do arquivo original. Ainda,
imagens e arquivos de animação tendem a tem uma alta compactação e
normalmente resulta num arquivo que está entre 2 a 20% do arquivo original.
Devemos ressaltar que um arquivo já compactado não há virtualmente ganho
algum em compactá-lo novamente, a menos que uma nova técnica seja usada.
64
Tipicamente arquivos produzidos pelo método de compactação
LZ77/LZ78 são ZIP, ARJ, LZH, Z, entre outros. Huffman é usado em
utilitários ARC e PKARC, e em comandos UNIX.
DriveSpace e DoubleSpace são programas utilizados em sistemas
PC para compactação de arquivos em drives de disco rígido. Eles se utilizam
de uma mistura de códigos Huffman e Lempel-Ziv, onde o Código de
Huffman é utilizado para diferenciação entre dados (valores literais) e o LZ é
usado para referências.
O Gráfico de Interfaces Gráficas (GIF) utiliza um algoritmo de
compactação baseado no Lempel-Ziv-Welsh (LZW). Ao compactar uma
imagem o programa de compactação mantém uma lista de parcelas de cadeias
de caracteres foram achadas previamente. Quando uma amostra repetida é
encontrada, o item referido é substituído por um ponteiro ao original. Desde
que imagens tendam a conter muitos valores repetidos, o formato GIF é uma
boa técnica de compactação.
Os programas UNIX compactam e descompactam utilizando o
código adaptativo de Lempel-Ziv. Esta é, em geral, uma opção melhor do que
os programas baseados no Código de Huffman. Quando possível, o programa
de compactação adiciona um .z no arquivo compactado. Este arquivos podem
ser originariamente recuperados usando o descompactador ou programas zcat.
O utilitário freeware zôo baseado em UNIX aplica o algoritmo de
Lempel-Ziv. Ele pode armazenar e extrair seletivamente gerações múltiplas
do mesmo arquivo.
65
CONCLUSÃO
Como conclusão de nosso trabalho ressaltamos a importância dos
compactadores. Através destes poderosos instrumentos que diminui o tempo
de transmissão de dados e diminui o espaço ocupado por arquivos. Este
trabalho enfocou duas vertentes de compactadores que são utilizados
atualmente: compactadores estatísticos e compactadores com dicionários, o
primeiro, é representado pelo Código de Huffman e o segundo, pelo LZW.
Quanto ao nosso compactador Huffman implementado, vimos que
existem alguns problemas que são inerentes ao próprio Código de Huffman:
tais como a expressiva queda na performance do compactador para arquivos
grandes, e a ineficiência do código ao trabalhar com árvores binárias com
leituras recursivas para formação do Código de Huffman e montagem da
cadeia de bits e sua leitura acompanhado pela árvore de Huffman para
executar o processo de descompactação. Aliás, este é um dos motivos pelo
qual o Código LZW é mais usado.
Em nosso compactador implementado, por exemplo, temos que para
arquivos maiores que 15KB, temos uma demora de até cinco minutos para
executar o processo de compactação e descompactação.
Para trabalhos futuros, podemos propor a junção do Código LZW e
o Código de Huffman numa estrutura híbrida que será aplicada para cada tipo
de arquivo em particular, aplicando um ou outro algoritmo ou os dois
simultaneamente em maior ou menor proporção.
66
REFERÊNCIAS BIBLIOGRÁFICAS
[1] HELD, Gilbert, Comunicação de Dados. Rio de Janeiro: Editora Campus,
1999.
[2] SZWARCFITER, J. L. & MARKENZON, L. Estrutura de dados e seus algoritmos. Rio de Janeiro: Editora LTC, 1994.
[3] TERRY A. Sperry Research Center. A Technique for High-Performance
Data Compression. IEEE, 1984.
[4] DEITEL, H. M. e DEITEL, P. J. C++: como programar. Porto Alegre: Editora Bookman, 2001.
[5] PREISS, B. R. Data Structures and Algorithms with object – oriented desing patterns in C++. New York: Editora John Wiley & Sons, Inc, 1999.
[6] ZIV, J. e LEMPEL A., “Compression of individual sequences via variable-rate coding”. IEEE Transactions on Information Theory, Vol. it-24, n 5. september 1978, 530 a 536.
[7] www.gzip.org
[8] www.rarlab.com
[9] www.winzip.com