Você está na página 1de 75

UNIVERSIDADE COMUNITÁRIA DA REGIÃO DE

CHAPECÓ
ACEA
CURSO DE CIÊNCIA DA COMPUTAÇÃO

Estrutura de Arquivos

José Carlos Toniazzo

Chapecó, 20010/1
Índice

Índice....................................................................................................................................................2
1.Mídias de armazenamento secundário ..............................................................................................3
1.1. Introdução.................................................................................................................................3
1.2. Fitas Magnéticas .......................................................................................................................4
1.3. Discos Magnéticos....................................................................................................................8
1.4. Jornada de um Byte ................................................................................................................16
2. Organização de Arquivos ...............................................................................................................19
2.1. Introdução...............................................................................................................................19
2.2. Terminologia ..........................................................................................................................19
2.3. Introdução aos Arquivos Seqüenciais.....................................................................................20
2.4. Introdução aos Arquivos Seqüenciais Indexados ...................................................................21
2.5. Introdução aos Arquivos Indexados .......................................................................................23
2.6. Introdução aos Arquivos Diretos............................................................................................24
2.7. Introdução aos Arquivos Invertidos .......................................................................................25
2.8. Quadro Comparativo entre as Organização de Arquivos .......................................................26
2.9. Exercícios ...............................................................................................................................26
3. Arquivos em C ...............................................................................................................................27
3.1. Abrindo e fechando um arquivo .............................................................................................27
3.2. Lendo e escrevendo caracteres em arquivos...........................................................................29
3.3. Outros comandos de acesso a arquivos ..................................................................................31
3.4. Fluxos padrão .........................................................................................................................36
4. Compressão de Dados...............................................................................................................37
4.1. Introdução...............................................................................................................................37
4.2. Tipos de Compressão .............................................................................................................38
4.3. Compressão Orientada a Caracter ..........................................................................................38
4.4. Compressão Estatística ...........................................................................................................44
5. Algoritmos para classificação externa ......................................................................................50
5.1. Método da seleção ..................................................................................................................50
5.2. Ler o arquivo e colocar na memória a tabela de chaves.........................................................52
5.3. Mergesort de arquivos ............................................................................................................53
5.4. Arquivos dinâmicos................................................................................................................55
6. Métodos de Pesquisa.................................................................................................................55
6.1. Pesquisa Sequencial................................................................................................................55
6.2. Pesquisa Binária .....................................................................................................................56
6.3. Hashing...................................................................................................................................56
7. Algoritmos para indexação .......................................................................................................65
7.1. Índice Invertido.......................................................................................................................65
7.2. Arquivos de Assinatura ..........................................................................................................67
7.3. Algoritmos de Ranking...........................................................................................................69
Referências .........................................................................................................................................75
1.Mídias de armazenamento secundário

1.1. Introdução

Nos últimos 20 anos, a capacidade de armazenamento on-line aumentou em cerca de três vezes
em relação da potência em CPU. Contudo, a memória representa talvez a área de maior limitação no
desenvolvimento de sistemas de computação mais avançados.
A memória interna fornece os requisitos de armazenamento da(s) unidade(s) central (ais) de
processamento para a execução de programas, incluindo programas de aplicação e programas de
sistemas como montadores, carregadores, editores de ligação, compiladores, rotinas utilitárias e rotinas
supervisoras do sistema operacional. A capacidade de armazenamento da memória interna é limitada
pelo custo das unidades da memória interna. Os requisitos de armazenamento para os programas e
dados sobre os quais eles operam geralmente excedem a capacidade da memória interna da maioria dos
sistemas de computação. Assim, há a necessidade de se ter outros recursos de armazenamento para
armazenar programas e dados.
Qualquer pessoa que crie programas para armazenamento e/ou dados deve ser capaz de
selecionar (se o programador tiver oportunidade de selecionar) os meios adequados nos quais
armazenar os arquivos e deve também estar atento para a adequação dos mecanismos de
armazenamento às organizações de arquivo utilizadas. Por exemplo, as fitas magnéticas permitem
apenas um acesso seqüencial aos dados, enquanto que discos, tambores e memórias magnéticas são
dispositivos de armazenamento que permitem o acesso aleatório. Com meios de acesso seqüencial,
somente o próximo registro ou registro anterior pode ser localizado facilmente; esse tipo de meio é
recomendado quando ler e gravar o próximo registro é o tipo mais comum de operação. Com os meios
de acesso aleatório, um comando de hardware é usado para posicionar o mecanismo de acesso a cada
parte do meio de armazenamento que pode ser endereçado. Mecanismos de acesso aleatório são
excelentes para a localização de um registro de dado que tenha endereço conhecido.
Quando os programadores de sistemas selecionam mecanismos externos de armazenamento
para armazenar arquivos de dados e arquivos de programas, há vários itens que devem ser levados em
consideração:

1. Capacidade. O total de dados que pode ser armazenado no meio de armazenamento.


2. Portabilidade. Fitas magnéticas e alguns discos são removíveis, permitindo o
armazenamento off-line de arquivos.
3. Custo relativo. O custo de um mecanismo aumenta proporcionalmente com a velocidade e
conveniência de acesso aos registros.
4. Tamanho do registro. O tamanho de um conjunto de dados contínuos que podem ser
endereçados num mecanismo.
5. Método de acesso. O acesso direto (aleatório) a qualquer registro num mecanismo pode ser
possível, ou então o equipamento pode restringir somente ao acesso seqüencial.
6. Velocidade de transferência de dados. A velocidade, geralmente em bits, ou caracteres por
segundo, na qual os dados podem ser transferidos entre a memória interna e o mecanismo.
7. Tempo de posicionamento. Os mecanismos de discos e de tambor com cabeças de
leitura/gravação móveis geralmente devem preceder cada operação de ler/gravar com uma
procura que fisicamente consiste em mover a cabeça de leitura/gravação sobre a trilha que
contém o registro desejado.
8. Latência. Depois de um comando de ler/gravar ser aceito pelo mecanismo, normalmente
este leva algum tempo adicional, o tempo de espera (ou tempo de retardo rotacional), antes
que o inicio do registro a ser acessado esteja sob a cabeça de leitura/gravação e que a
transferência de dados possa começar; é o tempo de partida para a fita magnética alcançar a
sua velocidade de operação a partir da posição “parada” ou o tempo de retardo rotacional
dos discos e tambores rotativos.
Outra propriedade dos mecanismos externos de armazenamento que pode ser importante para
algumas aplicações é a capacidade do equipamento poder ser compartilhado simultaneamente por dois
ou mais usuários. Um arquivo que possa – ou deva – ser acessado por diversos usuários
simultaneamente deve ser armazenado num equipamento que permita o compartilhamento, tal como o
disco magnético. Os mecanismos de armazenamento com acesso seqüencial não são apropriados para
esse tipo de compartilhamento.
Algumas das propriedades mencionadas acima não são características de todos os mecanismos de
armazenamento discutidos neste capítulo. Por exemplo, não há tempo de procura para os discos e
tambores de cabeças fixas e grandes memórias magnéticas.
Capacidade, tempo de acesso (quanto tempo leva para se ler os dados num dispositivo),
velocidade de transferência de dados e custo por bit são as características básicas de desempenho dos
mecanismos de armazenamento. Quando uma tecnologia de armazenamento está sendo avaliada, a
densidade da área (em bits por polegada quadrada) é o número chave a ser considerado. Quanto maior a
densidade da área, menor o custo por bit e - da mesma forma – menor o tempo médio de acesso para
qualquer capacidade dada.
Os meios e equipamentos de armazenamentos discutidos neste capítulo são fitas magnéticas,
discos e tambores fixos e removíveis. A capacidade de armazenamento e o tempo de acesso de cada
tecnologia de armazenamento são apresentadas. Além disso, são dadas equações para cálculo do espaço
de armazenamento ocupado por um arquivo e o total de tempo necessário para se acessar um bloco de
registros lógicos. Os cálculos de espaço são importantes por duas razões:
1. O programador pode ter que especificar explicitamente o total de espaço de armazenamento
requerido por um arquivo quando ele for criado.
2. O programador pode desejar saber se o arquivo caberá ou não num único volume.

Em algumas situações não é desejável expandir o arquivo em mais de um volume, a menos que
todos os volumes estejam disponíveis on-line; do contrário, os volumes podem ter que ser montados e
desmontados diversas vezes durante o processamento do arquivo. Considerações de tempo para
dispositivos de armazenamento externo são normalmente mais importantes para um programador do
que considerações de espaço. O tempo é importante porque um programador pode ter limitações de
tempo de acesso para a recuperação de dados que devem ser considerados ao escolher um
armazenamento externo no qual armazenar um arquivo. Outra razão importante para apresentar e
estudar cálculos de tempo e espaço é que isso fortalece a compreensão da operação dos equipamentos.

1.2. Fitas Magnéticas

A fita magnética foi o primeiro meio externo de armazenamento a ser largamente usado e
continua a ser um dos meios mais comuns de armazenamento para dados que podem ser processados de
forma seqüencial. As fitas magnéticas são importantes meios externos de armazenamento pelas razões
seguintes:
1. São baratas se comparadas a outros meios de armazenamento.
2. São compactas e portáteis, o que facilita o armazenamento off-line.
3. Devido ao baixo custo, são ideais para uso em armazenamento de reserva off-line e para
arquivamento de arquivos.

A aparência física da fita magnética é similar à das fitas usadas em gravadores. A fita é feita de
material plástico coberto com uma substância magnetizável. Os dados são codificados na fita, caractere
por caractere. Um certo número de pistas corre ao longo da fita, com uma pista para cada posição de bit
na representação codificada de um caractere.
1.2.1. Paridade

Além das pistas que registram os dados, há uma pista adicional que registra o bit de paridade
usado para o processamento de erro. As fitas podem ter de 7 ou 9 pistas. A fita 9 pistas é mais usada do
que a de 7. A Figura 1 ilustra uma seção de fita de 9 pistas. Os dados são gravados na fita de 9 pistas
tanto na forma binária como a codificação EBCDIC. E cada caractere é representado na fita pela
seqüência de conjunto de bits de 0 ou 1, da pista 2 à 9. Os dados podem ser gravados com paridade par
ou ímpar. O bit da pista 1 é o bit da paridade; é gravado na pista 1 pela unidade de fita á medida que os
dados são gravados na fita. Quando a fita é gravado na paridade ímpar, o bit de paridade é gravado
igual a 1 quando o número de bits 1, nas pistas 2 a 9, for ímpar. A fita da Figura 1 é escrita na paridade
par; o número de bits 1 da pista 2 à pista 9 é par; por isso o bit 1 é gravado na pista 1. A paridade ímpar
é similar, exceto com o bit da pista 1 é gravado “1” de forma a indicar que o número de bits 1, da pista
1 à 9, é ímpar.

Figura 1- Porção da fita de 9 pistas

O bit de paridade serve como controle de exatidão dos dados gravados quando eles forem lidos
da fita. Quando uma fita é lida, a unidade de fita controla o número de bits 1 nas pistas de 1 a 9 para
certificar-se de que este número está em conformidade com a paridade da fita. Se não estiver ocorre a
interrupção de erro da paridade. A rotina de interrupção faz então com que o registro em que ocorreu o
erro de paridade seja relido diversas vezes pela unidade de fita até que o registro seja lido sem erro de
paridade ou que as tentativas de leitura tenham alcançado um limite determinado pelo sistema.
O motivo pelo qual o registro é lido diversas vezes é que as fitas são vulneráveis à poeira e
outras substâncias estranhas a ela e por isso pode acontecer que a unidade de fita não funcione
adequadamente. Assim, em alguns casos um erro de paridade pode ocorrer mesmo que o registro esteja
gravado corretamente, o que será verificado por uma nova leitura.
A paridade acima descrita é às vezes chamada apenas de paridade simples ou paridade vertical.
Algumas fitas têm um caractere de paridade longitudinal gerado quando os dados são gravados na fita.
O caractere da paridade longitudinal, chamado de caractere de verificação por redundância, é gerado da
seguinte maneira:
1. Cada pista é examinada longitudinalmente e o número de bits 1 da pista é contado.
2. Se o número de bits “1” da pista for ímpar, então “1” é gravado no caractere de verificação por
redundância na posição correspondente ao número da pista.
3. Se o número de bits “1” da pista for par, então “0” é gravado no caractere de verificação por
redundância.
4. O caractere de verificação por redundância é gravado no final da fita.

Quando tanto a paridade vertical quanto a longitudinal são usadas, é menos provável que um
erro escape da detecção.
Os dados são gravados na fita de 7 pistas na forma binária ou na codificação BCD. A fita de 7
pistas é similar à de 9, exceto que os caracteres são gravados verticalmente da pista 2 a 7. O bit de
paridade é gravado também na pista 1.
1.2.2. Separação entre registros

Os dados são escritos (lidos) na (da) fita por uma unidade de fita à medida que a fita passa pelo
dispositivo da cabeça de leitura/gravação. Uma unidade de fita é projetada para ler ou gravar apenas
quando a fita magnética está se movendo a uma determinada velocidade. Os dados gravados nas fitas
magnéticas o são no formato ilustrado na Figura 2.

Figura 2- Formato da fita magnética

Registros físicos consecutivos são separados por um separados de registros (IRG), que permite
uma fita iniciar e parar entre comando individuais de ler ou gravar. Nenhum dado é registrado no
separador de registros.
Ao término de um comando de ler ou gravar, parte do separador de registros passa pela cabeça
de leitura/gravação. Se a operação seguinte na mesma fita for iniciada durante este tempo, o movimento
da fita prossegue sem interrupção. Contudo, se a operação seguinte não ocorrer logo, a fita pára. E será
preciso algum tempo para que a fita alcance a velocidade ideal no próximo comando. A principal
função de um separador de registros é criar espaço suficiente que permita à fita parar após o comando
Entrada/Saída, e permita que ela chegue à velocidade requerida para ler ou gravar sem distorção. O
separador de registros tem normalmente 0,75 polegadas de comprimento nas fitas de 7 pistas e 0,6 nas
fitas de 9 pistas. O efeito negativo do separador de registro é diminuir o volume de dados que podem
ser armazenados numa fita.

1.2.3. Rebobinamento da fita

Uma fita normalmente tem um ponto de carga indicando o ponto de partida da fita a partir do
qual os dados podem ser gravados. Quando a fita é colocada na unidade, é posicionada de forma a que
o ponto de carga esteja pronto para um comando de Entrada/Saída. As fitas magnéticas podem ser
rebobinadas ao ponto de carga por um comando de rebobinar. Algumas unidades de fita possuem uma
alta velocidade para rebobinamento, que se aplica apenas quando o número de caracteres a ser
rebobinado é maior que determinado valor, digamos um ou dois milhões; para números menores de
caracteres, o rebobinamento é feito na velocidade normal de leitura/gravação.

1.2.4. Cálculo de espaço e Tempo para Fita Magnética

A quantidade de dados que podem ser armazenados numa fita magnética depende do tamanho
dos blocos e da densidade em que eles podem ser registrados na fita. Grandes blocos, resultando num
alto fator de bloco, fazem com que a fita seja mais usada para armazenar dados e menos utilizadas pelo
separador de registros. Além disso, o tempo médio para ler/gravar um registro físico é inversamente
proporcional ao fator de bloco já que menos separadores de registros devem ser utilizados. Para utilizar
de forma eficiente o armazenamento em fita e para minimizar o tempo de leitura/gravação, o fator de
bloco deve ser relativamente grande; contudo, uma ou mais memórias intermediárias, suficientemente
grandes para receber um bloco, devem existir na memória interna. Por outro lado, já que a memória
interna é geralmente algo de difícil obtenção, as memórias intermediárias não podem ser
arbitrariamente grandes. Daí o tamanho do blocos ser determinado, em certa medida, pela quantidade
da memória interna disponível para a memória intermediária. Existe um ponto de equilíbrio entre a
capacidade de armazenamento da fita e o tempo de leitura/gravação, de um lado, e a quantidade de
memória interna disponível para as memórias intermediárias.
A densidade da fita é expressa em bits por polegada (bpi), bytes por polegada ( bpi) ou em
caracteres por polegada (cpi). As densidade mais comuns de gravação de fitas magnéticas são 556 cpi,
800 cpi, 1600 cpi e 6250 cpi. A Tabela 1 dá as características, inclusive a densidade, de algumas
unidades de fita disponíveis no mercado.

Fabricante CDC Univac CDC IBM Telex


Modelo 626 Uniservo 12 679-7 3420-4 6420-66
Velocidade 75 pol/seg 42,7 pol/seg 200 pol/seg 75 pol/seg 125 pol/seg
Densidade 800 carac/pol 1600 carac/pol 6250 bytes/pol 6250 6250 bytes/pol
bytes/pol
Velocidade de 120K carac/seg 68K carac/seg 1,25M bytes/seg 470K 780K bytes/seg
Transferência bytes/seg

Cálculos de espaço

O espaço ocupado por um bloco depende do:


1. Comprimento do registro lógico.
2. Fator de bloco.
3. Densidade da fita.

A Tabela 2 contém uma lista de símbolos usados nas equações apresentadas abaixo, tanto para espaço
quanto para tempo.

Símbolo Descrição
NL Número de registros lógicos de um arquivo
BF Fator de bloco
IRG Separador de registros (em polegadas)
DEN Densidade (cpi ou bpi)
SPD Velocidade (polegadas por segundo)
LRL Comprimento do registro lógico (caracteres)
ST Espaço total (polegadas)
TT Tempo total (segundos)
TA Tempo de início/parada ou tempo para passar sobre o IR
(milissegundos)

O espaço total (ST) necessário para um arquivo de registros lógicos (NL) é dado pela equação

ST = NL/BF x (IRG + (BF x LRL)/DEN )

O valor entre parênteses em é o espaço ocupado por um único bloco. O espaço total necessário para um
arquivo seqüencial com NL = 10.000, BF= 3, LRL=160 caracteres, DEN=800 cpi e IRG=0,75
polegadas é

ST = 10000/3 x (0,75 pol + (3 x 160 caracteres) / 800 cpi )


ST = 4500,9 polegadas = 375 pés = 114,3 metros
Cálculos de Tempo

O tempo para se ler um bloco consiste em:


1. Um tempo para ultrapassar um separador de registro.
2. Um tempo de ler o bloco em si.

O tempo total para leitura de um arquivo de registro NL, se partirmos do princípio de que não há
rebobinamento, é dado por

TT = (NL/BF) x [ TA + {(BF x LRL) / (SPD x DEN)} ]

A quantidade entre parênteses é o total de tempo necessário para se ultrapassar um separador de


registro mais o tempo para a leitura do bloco. O produto de SPD x DEN é a velocidade de
transferência de dados. A velocidade de transferência de dados das unidades de fita mais comuns varia
de cerca de 30.000 a aproximadamente 7500.000 caracteres por segundo. Tendo-se SPD=75
polegadas/segundo, TA=12,6 mseg (o valor de TA é o tempo de partida/parada e não o tempo de
mobilidade), BF=3, LRL=160 caracteres, DEN= 800 cpi e IRG= 0,75 polegadas, o tempo de leitura de
um arquivo de 10.000 registros lógicos com uma parada total após cada leitura é

TT = 10000/3 x (0,0126seg + (3 x 160 caracteres) / (75 pol/seg x 800cpi) )


TT = 68,68 seg

Os requisitos de espaço e acesso para um arquivo em fita magnética (e para a maioria dos demais
meios de armazenamento) diminuem à medida que aumenta o fator de bloco.

1.2.5. Vantagens e Desvantagens de Fita Magnética

Uma limitação da fita magnética é que os registros não podem ser acessados aleatoriamente; ou
seja, os registros devem ser processados na ordem em que estão gravados na fita. Acessar um registro
seqüencialmente significa ler todos os outros registros que o antecedem. As fitas magnéticas têm a
vantagem de ser compactas, portáteis e adequadas para o armazenamento off-line. Além disso, são um
meio relativamente barato.

1.3. Discos Magnéticos

Os discos magnéticos são dispositivos de armazenamento de acesso direto tanto para


armazenamento de dados on-line como off-line. Em comparação com a fita magnética, oferece tempo
de acesso relativamente menor e alta velocidade para transferência de dados. Os fabricantes de
mecanismos de discos conseguiram aumentar a densidade de área dos meios de gravação em muitas
vezes ao longo dos anos; assim, continuam sendo adequados para o armazenamento de grandes
arquivos.
O espetacular sucesso do disco magnético advém de certas características inerentes desta
tecnologia:
1. Meios de armazenamento de baixo custo em superfícies magnéticas homogêneas.
2. Acesso que permite que dezenas de milhões de bits compartilhem uma cabeça de leitura/gravação.
3. Os limites da tecnologia estavam muito acima da demanda, permitindo assim muito espaço para
crescimento e expansão da capacidade e desempenho dos mecanismos.
Há dois tipos de discos em uso: os discos fixos (também chamados de discos de cabeça fixa) e
os discos removíveis (também chamados de discos de cabeça móvel). Os discos em mecanismos de
disco fixo não são removidos da unidade de disco (exceto para manutenção), enquanto que os discos
removíveis podem ser facilmente retirados de uma unidade de disco e substituído, armazenado ou
transportado. Os discos removíveis são os mais comuns, dentre os dois tipos, porque facilitam o
armazenamento de arquivos off-line e o custo/bit é menor.

1.3.1 Discos Fixos

O disco fixo da Figura 3 consiste em seis pratos com material ferromagnético em sua superfície
proporcionando o meio de armazenamento.

Figura 3- Vista lateral de um disco com cabeça fixa com 10 superfícies de


leitura/gravação por superfície

As superfícies mais externas dos pratos de cima e de baixo às vezes não são usadas para
armazenamento de dados, já que podem ser facilmente danificadas. Cada superfície é dividida em anéis
concêntricos. (ver Figura 4), chamados de trilhas. Cada trilha é dividida freqüentemente em setores
que é a menor porção endereçável de um disco. As trilhas que estão diretamente em cima e abaixo uma
da outra formam um cilindro. Quando um comando READ( ) é formado para um byte em particular
num arquivo em disco, o SO do computador localiza a superfície, a trilha e o setor e lê o setor inteiro
para uma área especial na RAM chamada buffer, então procura o byte solicitado do buffer.
O disco da Figura 3 tem dez trilhas por superfície. Os dados são registrados nas trilhas pelas
cabeças de leitura/gravação, uma para cada pista, reunidas num conjunto de cabeças de leitura/gravação
que é fixo. Cada cabeça flutua acima ou abaixo da superfície (cerca de 0,001 polegadas da superfície),
enquanto o disco está rodando a uma velocidade alta e constante. Embora as trilhas variem de
comprimento, todas elas – num determinado disco - são capazes de armazenar a mesma quantidade de
dados; a razão para isso é que a densidade de gravação das trilhas mais internas é maior do que a
densidade de gravação(bits por polegada) das mais externas.
Figura 4- Vista de cima da superfície de um disco com 200 trilhas principais e três
trilhas alternativas (trilhas 200,201,202)

Já que um disco fixo tem uma cabeça de leitura/gravação para cada pista, não há necessidade de
tempo de posicionamento de colocar a cabeça de leitura/gravação na pista adequada da superfície;
assim sendo, o tempo de acesso para um disco de cabeça fixa é baixo, porque o tempo de
posicionamento é zero. O maior componente do tempo de acesso para um disco de cabeça fixa é o
tempo de latência.
Os discos de cabeça fixa são normalmente usados em sistemas dedicados a uma ou a poucas
aplicações e quando os arquivos devem ficar on-line como necessidade de um pequeno tempo de
acesso.

1.3.2. Discos Removíveis

Como mencionado acima, o disco de cabeça móvel é o mais comum, dentre os dois tipos de
mecanismos usados, porque os discos são removíveis e o custo/bit do armazenamento é menor. Há
apenas uma cabeça de leitura/gravação por superfície do disco; o conjunto de cabeças de
leitura/gravação deve ser movido para dentro e para fora a fim de permitir o acesso a todas as trilhas de
cada superfície. As cabeças de leitura/gravação geralmente movem-se em conjunto e apenas uma
cabeça pode transferir dados de cada vez. A Figura 5 mostra um corte lateral do disco de cabeça móvel,
e a Figura 4 mostra uma vista de cima de um disco de cabeça móvel, com 203 pistas1*. Em alguns
sistema de discos de cabeça móvel, as cabeças de leitura/gravação não são rigidamente fixadas no
conjunto de montagem de cabeças (como estão na Figura 5) e podem ser movidas para dentro e para
fora da superfície independente uma da outra. Se o disco da Figura 5 tiver 200 pistas principais por
superfície, então a unidade do conjunto de cabeças podem mover-se horizontalmente para 200 posições
diferentes. Cada uma dessas posições identifica um cilindro; um cilindro de dados é o total de dados
acessível com um posicionamento da unidade de cabeças de leitura/gravação. O tempo necessário para
mover as cabeças de leitura/gravação para o seguinte chama-se tempo de posicionamento. Este
movimento é usualmente a parte mais lenta no processo de leitura em um disco.
Já que o mecanismo do conjunto de cabeças se move, uma grande superfície de gravação pode
ser coberta com apenas algumas cabeças de leitura/gravação. Um disco de cabeça fixa com a mesma
área de gravação de um disco de cabeça móvel tem um custo/bit maior devido ao custo das muitas
cabeças de leitura/gravação necessárias.

1
* As trilha de 0 a 199 são as principais. As de 200 a 2002 são trilhas alternativas usadas para substituir
trilhas danificadas e algumas das principais que estejam com defeito.
Figura 5- Vista lateral de um disco de cabeça móvel com 10 superfícies

Já que a unidade do conjunto de cabeças num disco de cabeça móvel move-se, ela pode ser retirada da
superfície dos discos, permitindo, assim, que os discos sejam facilmente removidos das unidades de
disco e substituído por outro disco.

1.3.3. Estimação da Capacidade de um Disco

O diâmetro de um disco varia de 3 à 14 polegadas e sua capacidade de menos que 100.000 bytes
e trilhões de bytes. Em HD´s típicos, os pratos superior e o inferior possuem apenas uma superfície
utilizada para leitura e gravação e os demais (internos) utilizam ambas superfícies. Sendo assim, o
número de trilhas por cilindro depende do números de pratos.
A quantidade de dados que pode ser mantidos numa trilha depende da densidade (bits/polegada)
da trilha.
Já que um cilindro consiste de um grupo de trilhas, uma trilha consiste de um grupo de setores e
um setor consiste de um grupo de bytes, é fácil calcular a capacidade de uma trilha, de um cilindro e do
disco:

Capacidade de trilha(CT) = Número de setores por trilha (NST) X Bytes por setor (BS)
Capacidade de cilindro(CC) = Números de trilha por cilindro (NTC) X Capacidade de trilha(CT)
Capacidade do disco (CD) = Número de cilindros (NC) X Capacidade do cilindro (CC)

Se sabemos o número de bytes num arquivo, podemos usar estes relacionamentos para
computar a quantidade de espaços em disco que um arquivo provavelmente utilizará.

Exemplo: Suponde que podemos armazenar um arquivo com 20.000 registros de tamnho fixo num
disco com as seguintes características:

• Número de bytes por setor = 512


• Número de setores por trilha = 40
• Número de trilhas por cilindro = 11
Quantos cilindros são necessários pra armazenar o arquivo, sendo que cada registro necessita de
256 bytes? Já que cada setor mantém 2 registros, o arquivo necessita:

_ 20.000 = 10.000 setores


2
Um cilindro pode conter: 40 x 11 = 440 setores

Assim, o número de cilindros necessários é aproximadamente:

10.000 = 22,7 cilindros


440

Ou, usando-se as fórmulas:

TamArq
NumCilindros=
CC
TamArq= NumReg x TamReg= 20.000 x 256= 5120000
CC= NTC x NST x BS = 11 x 40 x 512 = 225280
5120000
NumCilindros= = 22,7
225280

1.3.4. Organização de setores

Existem duas maneiras básicas de organizar os dados num disco: por setores e por blocos
definidos pelo usuário.
Organização física dos setores. Há diversas visões que se pode ter da organização de setores
numa trilha. A mais simples mostra os setores adjacentes (veja Figura 6 a). esta é uma maneira
perfeitamente adequada de ver um arquivo logicamente, mas não é uma maneira muito boa de
armazenar os setores fisicamente. Não se pode, geralmente, ler uma série de setores que estão todos na
mesma trilha, um após o outro. Isto é porque, após a leitura dos dados, o computador gasta uma certa
quantidade de tempo para processa-los antes de aceitar mais dados. Assim, se os setores logicamente
adjacentes foram colocados fisicamente adjacentes no disco, após a leitura de um setor perder-se-ia o
inicio do setor seguinte enquanto o computador estava processando a informação já lida.
Conseqüentemente, só se consegue ler um setor em cada rotação do disco.
Uma abordagem para se resolver este problema é intercalado os setores, isto é, colocar diversos
setores físicos entre setores logicamente adjacentes. Supondo que nosso disco tenha um fator de
intercalação igual a 5. A figura 6 b mostra o arranjo físico dos setores em uma trilha com a capacidade
para 32 setores. Se observarmos esta figura podemos ver que para ler os 32 setores o disco fará apenas
5 rotações, ao passo que na organização anterior (Figura 6 a) seria necessário 32 rotações.
Figura 6

- Duas divisões da organização de 32 setores numa trilha. (a) Visão


mais simples da organização de setores nas trilhas. (b) Setores
intercalados com um fator de intercalação: 5. Setores logicamente
adjacentes ocorrem em intervalos de 5 setores físicos.

Clusters: Uma terceira visão de organização de setores, também projetada para melhorar a
performance, é uma visão mantida pelo gerenciador de arquivos do sistema operacional. Quando um
programa acessa um arquivo, é tarefa do gerenciador mapear as partes lógicas do arquivo para suas
correspondentes localizações físicas. Para fazer isto, ele enxerga o arquivo como uma série de cluster
(agrupamento) de setores. Um cluster é um número fixo de setores contíguos2, assim todos os cluster
num disco são do mesmo tamanho, e uma vez que um cluster foi encontrado no disco, todos os setores
neste cluster podem ser acessados sem a necessidade de um posicionamento adicional.
Para ver um arquivo como uma série de clusters e ainda manter a visão de setores, o gerenciador
liga os setores lógicos ao cluster físico ao qual eles pertencem usando a tabela de alocação de arquivos
(FAT- file allocation table). A FAT contém uma lista ligada de todos os cluster num arquivo ordenado
de acordo com a ordem lógica dos setores que eles contêm.
Em muitos sistemas, os administrador do sistema decide quantos setores existirá em um cluster.
Por exemplo, na estrutura física de discos usado pelo sistema VAX, o administrador estabelece o
tamanho do cluster a ser usado num disco quando é inicializado. O valor default é três setores de 512
bytes por cluster, mas o tamanho do cluster pode ser fixado para qualquer valor entre 1 e 65535 setores.
Já que clusters representam grupos de setores fisicamente contíguos, clusters maiores garante a leitura
de mais setores sem fazer posicionamentos.
Extensão3: A visão final de organização de setores representa mais uma tentativa de enfatizar a
contigüidade física de setores num arquivo, conseqüentemente o posicionamento ainda mais. Se existe
muito espaço livre em disco, pode ser possível fazer um arquivo consistir de clusters inteiramente
contíguos. Quando acontece isto dizemos que o arquivo consiste de uma extensão: todos os setores,
trilhas e cilindros (se é grande o suficiente) formam um conjunto contíguo. Isso é uma boa situação,
especificamente porque significa que o arquivo como todo pode ser acessado com o mínimo número
de posicionamento.
Se não há espaço contíguo o suficiente para conter um arquivo inteiro, o arquivo é dividido em
duas ou mais parte não contíguas. Cada parte é uma extensão. Quando novos clusters são adicionados à
um arquivo, o gerenciador de arquivo tenta coloca-los fisicamente contíguos a partir do fim do arquivo,

2
Não é bem fisicamente contíguo; o grau de contigüidade física é determinado pelo fator de intercalação.
3
Palavra mais próxima para a tradução da palavra inglesa extent.
mas se não há espaço para fazer isso, então deve-se adicionar um ou mais extensões.

Fragmentação: Geralmente, todos os setores num determinado disco deve conter o mesmo
número de bytes. Se, por exemplo, o tamanho de um setor é 512 bytes e o tamanho de todos os
registros num arquivo é 300 bytes, não haverá uma ocupação conveniente entre registros e setores. Há
duas maneiras de lidar com esta situação: armazenar um registro por setor, ou permitir que um registro
ultrapasse um setor, de modo que o início de um registro poderá ser encontrado num setor e o seu final
em outro.
A primeira opção tem a vantagem que qualquer registro pode ser recuperado em apenas um setor,
mas tem a desvantagem de deixar uma enorme quantidade de espaço sem uso dentro de cada setor. Esta
perda de espaço dentro de um setor é chamada de fragmentação interna. A segunda opção tem a
vantagem de não perder espaço com a fragmentação interna, mas tem a desvantagem que os registros
são recuperados acessando dois setores.
Outra fonte potencial de fragmentação interna resulta do uso de clusters. Lembre-se que o
cluster é a menor unidade de espaço que pode ser alocada para um arquivo. Quando o número de bytes
num arquivo não é o múltiplo exato do tamanho do cluster, há fragmentação interna na última extensão
do arquivo. Por exemplo, se cluster consiste de três setores de 512 bytes, um arquivo contendo um
byte usaria 1536 no disco, 1535 bytes estaria perdido devido a fragmentação interna.

1.3.5. Organização de Blocos

Às vezes trilhas de discos não são divididas em setores,mas em números inteiros de blocos
definidos pelo usuário cujo tamanho pode variar. Os blocos podem ser de tamanho fixo ou variável,
dependendo das exigências do projetista do arquivo. Assim como setores, blocos são freqüentemente
referidos como registro físico. A Figura 7 ilustra a diferença entre uma visão dos dados numa trilha
dividida em setores e outra em blocos.

Figura 7- Organização em setores x Organização em blocos. (a)organização em setores: sem


considerar o tamanho de um registro ou outro agrupamento de dados, os dados estão agrupados
fisicamente em setores. Todo acesso envolve transmissão de um número integral de setores. (b)
organização em bloco: a quantidade de dados transmitidos num acesso dependem do tamanho do
bloco.

Uma organização em blocos não apresenta o problema de fragmentação de setores porque os


blocos podem variar em tamanho para preencher a organização lógica dos dados. Um bloco é
geralmente organizado para manter um número integral de registros lógicos. O termo fator de bloco é
usado para indicar o número de registros que são armazenados em cada bloco num arquivo. Assim, se
possuíssemos um arquivo com registros de 300 bytes, um esquema de endereçamento de blocos seria
algum múltiplo de 300 bytes dependendo da necessidade do programa. Assim, não seria perdido espaço
devido a fragmentação interna e não existiria a necessidade de carregar dois blocos para recuperar um
registro.
Os blocos são superiores aos setores quando a alocação física de espaço para registros
corresponde a sua organização lógica. Há discos que permitem o endereçamento por setores e por
blocos. No endereçamento por blocos, cada bloco de dados é geralmente acompanhado por um ou mais
subblocos extras, contendo informações extras sobre o bloco de dados, por exemplo o tamanho, em
bytes, dos dados contidos nos blocos.

1.3.6. Marcadores Interblocos

Blocos e setores exigem que uma certa quantidade de espaço seja alocada no disco na forma de
marcadores. Alguns dos marcadores consistem de informação que são armazenadas no disco durante a
preformatação, que é feita antes que o disco possa ser usado.
Em discos endereçáveis por setores, a preformatação envolve armazenar, no inicio de cada setor,
informações como: endereço do setor, endereço da trilha e condições (se o setor é útil ou defeituoso). A
preformatação também coloca gaps (espaços) e marcas de sincronização para ajudar o mecanismo de
leitura/escrita distinguir os setores.
Em discos endereçáveis por blocos, alguns dos marcadores são invisíveis ao programador, mas
alguns deles podem ser encontrados pelo programador. Já que subblocos e interblocos (marcadores)
devem ser fornecidos com todo bloco, existem mais informações que não seja dados propriamente dito,
em relação aos blocos do que em relação aos setores. Desde que o número e tamanho dos blocos
podem variar de uma aplicação para outra, a quantidade relativa de espaço ocupado pelos marcadores
podem variar quando o endereçamento por blocos é usada. Vejamos exemplo a seguir.
Suponha que temos um disco endereçável por bloco com 20.000 bytes por trilha, e a quantidade
de espaço usado por subblocos e interblocos é equivalente a 300 bytes por blocos. Queremos armazenar
um arquivo contendo registros de 100 bytes no disco. Quantos registros podem ser armazenado por
trilha se o fator de bloco é 10, ou se é 60?
Se há registros de 100 por bloco, cada bloco contém 1000 bytes de dados e usa 300 + 1000 ou
1300 bytes de espaço em uma trilha, quando o marcador é levado em conta. O número de blocos pode
ser colocado numa trilha de 20.000 bytes pela expressão:

20.000
= 15,38
1.300

Portanto, 15 blocos ou 150 registros podem ser armazenados por trilha. (observe que o resultado
foi truncado porque um bloco não pode ser colocado em mais de uma trila).
Se há registros de 100 bytes por bloco, cada bloco contém 6.000 bytes de dados e usa 6.300
bytes por trilha. O número de blocos por trilha pode ser expresso como:

20.000
=3
6.300

Apenas 3 blocos ou 180 registros pode ser armazenados por trilha.


Como podemos ver, um maior fator de bloco pode levar a um mais eficiente uso de
armazenamento. Quando os blocos são grandes, menos blocos são necessários para conter um arquivo,
assim há menos espaço consumido pelos 300 bytes do marcador que acompanha cada bloco.
Pode-se concluir deste exemplo que fatores de bloco maiores sempre leva a uma utilização mais
eficiente do disco? Não necessariamente. Já que podemos colocar somente um número inteiro de bloco
numa trilha e as trilhas são de tamanho fixo, quase sempre perde-se espaço no fim da trilha. Logo, tem-
se o problema de fragmentação interna novamente, mas desta vez ela ocorre dentro de uma trilha.
Quanto maior for o tamanho do bloco maior será o potencial de fragmentação interna numa trilha. O
que aconteceria, no exemplo anterior, se fosse considerado o fator de bloco igual a 98? Ou 97?
A flexibilidade introduzida pelo uso de blocos, ao invés de setores, pode resultar num ganho em
tempo e eficiência, pois deixa o programador (ganha mais tarefa também) determinar como os dados
podem ser organizados fisicamente em disco.
1.3.7. O custo de Acesso a disco

Um acesso a disco pode ser dividido em três operações físicas distintas, cada uma com o seu
próprio custo: tempo de posicionamento, atraso rotacional, tempo de transferência.
Tempo de posicionamento: o tempo de posicionamento é o tempo necessário para mover o
cabeçote de I/O ou cilindro correto. O tempo de posicionamento gasto durante um acesso ao disco
depende, é claro, da distância que o braço tem que se mover. Se é necessário acessar um arquivo
seqüencialmente e o arquivo está distribuído em vários arquivos consecutivos, o posicionamento é feito
somente após todas as trilhas de um cilindro forem processadas, também o cabeçote de leitura e escrita
necessita se movimentar apenas a distância de uma trilha. Por outro lado, se é necessário acessar
setores de dois arquivos que estão nos extremos opostos de um disco (um no cilindro mais interno e o
outro no mais externo) o posicionamento se torna mais lento.
O tempo de posicionamento é maior no processo R/W em ambientes multiusuários, onde vários
processo estão concorrendo para o uso do disco, do que em ambiente monousuário, onde o disco é
dedicado a apenas um processo.
O posicionamento envolve diversas operações que gastam tempo, as mais importantes são:

1. Tempo de partida (s)


2. Tempo de travessia ( n)

Estes dois valores podem ser aproximados para uma função linear de forma:

F(n)= m x n + s

Onde m é uma constante que depende de cada drive.

Por exemplo o tempo de posicionamento em um disco de 20 megabytes usados em PC’s pode


ser aproximado por:

f (n)= 0,3 x n + 20 mseg

Atraso rotacional: Atraso rotacional refere-se ao tempo que o disco leva para rodar, de modo
que o setor que se quer ler esteja sob o cabeçote de R/W. Os discos geralmente tem uma rotação de
3600 rpm que é uma revolução por 16,7 mseg. Na média o atraso rotacional é a metade de uma
revolução ou 8,3 mseg.

Tempo de transferência: Uma vez que o setor a ser lido está sob o cabeçote de R/W, os dados
gravados no setor podem ser transferidos. O tempo de transferência é dado pela fórmula:

Tempo de transf = Número de bytes transferidos x tempo de rotação


Número de bytes numa trilha

Se o disco é dividido em setores, o tempo de transferência de um setor depende do número de


setores por trilha. Por exemplo, se há 32 setores/trilha, o tempo necessário para transferir um setor é
1/32 de uma rotação ou 0,5 mseg.

1.4. Jornada de um Byte

O que acontece quando um programa envia um byte para um arquivo num disco? Sabemos como
o programa faz (WRITE(...)) e como os bytes são armazenados num disco, mas não sabemos o que
acontece entre o programa e o disco.
Suponha que será gravado um byte representado pela letra “P” guardado na variável c, do tipo
caractere, para um arquivo chamado TEXT armazenado em algum lugar no disco. Do ponto de vista do
programa, a jornada que o byte pode ser representada pelo comando:

WRITE (TEXT,C,1)

Mas a jornada é muito maior do que sugere este simples comando. É uma jornada marcada por
obstruções, atrasos e, possivelmente, até acidentes. O byte que usar vários meios de transporte
diferentes; alguns lentos outros rápidos.

O byte começa sua jornada em algum lugar na memória principal, como conteúdo da variável c.
O resultado do comando WRITE ( ) é chamada ao SO que possui a tarefa de completar e “fiscalizar” o
resto da jornada deste byte (Figura 8).

1.4.1. O Gerenciador de Arquivo

O sistema Operacional (SO) não é um único programa, mas uma coleção de programas, cada
um projetado para gerenciar uma parte diferente dos recursos do computador. Entre estes programas
estão aqueles que trabalham com tarefas relacionada a arquivos e dispositivos de I/O. Este subconjunto
de subprogramas é chamado de gerenciador de arquivo.
O gerenciador de arquivo verifica as características lógicas do arquivo: se arquivo foi aberto,
para que tipo de arquivo o byte está sendo enviado (arquivo binário, ou texto,etc..), se é permitido
gravar no arquivo etc...
Uma vez identificado o arquivo desejado e verificado se o acesso é legal, o gerenciador de
arquivo deve saber onde no arquivo TEXT depositar o caracter “P”. Como “P” será colocado no fim do
arquivo, o gerenciador de arquivo precisa saber onde se encontra o fim do arquivo – a localização física
do último setor do arquivo. Esta informação é obtida na FAT.

Figura 8- O comando WRITE ( ) no programa diz ao SO enviar um


caracter para o disco e dá ao SO a localização do caractere. Depois o
controle é retomado pelo programa.
1.4.2. O Buffer de I/O

Depois, o gerenciador de arquivo determina se o setor que conterá “P” já está na RAM ou se
precisa ser carregado. O gerenciador de arquivo deve encontrar espaço no buffer de I/O para o byte,
então fazer a leitura deste no disco. Uma vez que o setor já está no buffer na RAM, o gerenciador de
arquivo pode depositar “P” na sua posição apropriada, veja Figura 9.
O sistema de buffer de I/O permite ao gerenciador de arquivo ler e escrever dados em unidades
de tamanho do bloco ou de setor. Em outras palavras, o buffer habilita o gerenciador de arquivo
assegurar que a organização dos dados RAM esteja em conformidade com a organização dos mesmos
no disco.
Na verdade, ao invés de enviar o setor imediatamente para o disco, o gerenciador de arquivo
espera para saber se ele pode acumular mais bytes que vai para o mesmo setor antes de realmente
transmiti-lo ao disco.

Figura 9- O gerenciador de arquivo move “P” da área de dados do


programa para o buffer.

1.4.3. O processador de I/O

Até agora, todas as atividades do nosso byte ocorreram na memória principal (RAM) e
provavelmente foram executadas pela CPU. Agora o byte “viaja” em direção do disco onde o caminho
deve ser mais lento e mais limitado que na RAM.
Devido ao gargalo criado por essas diferenças de velocidade e capacidade do caminho, o byte
pode ter que esperar que um caminho externo esteja disponível.
O processo de montagem e desmontagem de grupos de byte para a transmissão de/para
dispositivos externos são tão especializados que não é adequado deixar a CPU gastar seu valioso tempo
fazendo I/O quando um dispositivo mais simples pode realizar este trabalho, livrando a CPU de realizar
tal tarefa. Tal dispositivo de propósito especial é chamado de processador de entrada/saída (I/O).
Um processador de I/O pode ser um simples chip capaz de pegar um byte e passá-lo à frente, ou
um pequeno computador capaz de executar programas sofisticados e comunicar com muitas
dispositivos independentemente.
Na maioria dos computadores o gerenciador de arquivos diz ao processador de I/O que há
dados no buffer para serem transmitidos ao disco, quantos dados existem e onde colocá-los no disco.
Esta informação poderá vir em forma de um pequeno programa que o sistema operacional constrói e o
processador de I/O executa.
1.4.4. Controlador de Disco

A tarefa de controlar as operações do disco é feita por outro dispositivo, o controlador de


disco. O drive é instruído pelo controlador a mover o cabeçote de R/W para a trilha e o setor do disco
onde o byte será armazenado. O cabeçote de R/W deve posicionar-se na trilha e esperar até que o disco
rotacione de modo que o setor desejado esteja sob o cabeçote. Uma vez que a trilha e o setor foram
localizados, o processador de I/O (ou controlador) pode enviar os bytes, um de cada vez, ao disco.

2. Organização de Arquivos

2.1. Introdução

O armazenamento de pequenos volumes de dados, via de regra, não encerra grandes problemas
no que diz respeito à distribuição dos registros dentro de um arquivo, desde que a freqüência de acessos
aleatórios a registros não seja muito elevada.
A medida que cresce o volume de dados e/ou a freqüência e a complexidade dos acessos,
crescem também os problemas de eficiência do armazenamento dos arquivos e do acesso a seus
registros, sendo a sofisticação das técnicas de armazenamento e recuperação de dados uma
conseqüência da necessidade de acesso rápido a registros pertencentes a grandes arquivos ou,
simplesmente, arquivos muito solicitados.
A maneira intuitiva de armazenar um arquivo consiste na distribuição dos seus registros em
uma ordem arbitrária, um após o outro, dentro da área destinada a contê-lo. Esta ordem pode ser, por
exemplo, aquela na qual os registros são gerados. Isto causa uma dificuldade na localização dos
registros e uma perda de eficiência, porém esta técnica intuitiva é bastante usada, principalmente
durante as fases preliminares da geração de um arquivos.
A seguir, após a apresentação da terminologia usada neste capítulo, são apresentadas
introduções sobre quatro estratégias de organização de arquivos voltadas para o acesso por meio de
chaves primárias, que são Arquivo Seqüencial, Arquivo Seqüencial Indexado, Arquivo Indexado,
Arquivo Direto, e uma, Arquivo Invertido, voltada para acesso por meio de chaves secundárias.

2.2. Terminologia

Um arquivo é formado por uma coleção de registros lógicos, cada um deles representando um
objeto ou entidade.
Um registro lógico, ou simplesmente registro, é formado por uma seqüência de itens, sendo
cada item chamado campo ou atributo. Cada item corresponde a uma característica ou propriedade do
objeto representado.
Cada campo possui um nome, um tipo e um comprimento. O comprimento dos valores de um
atributo pode ser constante para todos os registros do arquivo, ou variável.
O armazenamento de um arquivo é feito, via de regra, por blocos de registros lógicos (um bloco
é chamado registro físico), sendo, em cada leitura ou gravação, lido ou gravado todo um bloco e não
apenas um registro lógico.
Uma chave é uma seqüência de um ou mais campos em um arquivo.
Uma chave primária é uma chave que apresenta um valor diferente para cada registro do
arquivo, de tal forma que, dado um valor da chave primária, é identificado um único registro do
arquivo. Usualmente a chave primária é formada por um único campo.
Uma chave secundária difere de uma primária pela possibilidade de não possuir um valor
diferente para cada registro. Assim, um valor de uma chave secundária identifica um conjunto de
registros.
Chave de acesso é a chave utilizada para identificar o(s) registro(s) desejado(s) em uma
operação de acesso a um arquivo.
Argumento de pesquisa é o valor da chave de acesso em uma operação.
Chave de um registro é o valor de uma chave primária em um particular registro do arquivo
Chave de ordenação é a chave primária usada para estabelecer a seqüência na qual devem ser
dispostos (física ou logicamente) os registros de um arquivo.

2.3. Introdução aos Arquivos Seqüenciais

A organização de arquivos sequenciais é a mais conhecida e mais freqüentemente usada. A


ordem lógica e física dos registros armazenados em um arquivo seqüencial é a mesma . Já que os
registros em arquivos seqüenciais são armazenados em sucessão contínua , acessar o registro N do
arquivo(começando no início do arquivo) requer que os registros N-1 também sejam lidos.
Os Arquivos Seqüenciais são associados, historicamente, a fitas magnéticas, devido a natureza
seqüencial do meio de gravação. Mas os arquivos seqüenciais são também armazenados em
dispositivos de acesso aleatório, quando o acesso a sucessivos registros em alta velocidade é um
requisito do processamento .
O principal uso dos arquivos sequencias é o processamento em série ou sequencial de registros.
Se um mecanismo de leitura/gravação é posicionado para recuperar um registro em particular, então ele
pode acessar rapidamente o registro seguinte do arquivo. A vantagem de poder acessar rapidamente o
registro seguinte torna-se uma desvantagem quando o arquivo é usado para acessar um outro registro
diferente do registro "seguinte". Em média, metade de um arquivo sequencial tem que ser lido para
recuperar um registro.
Os arquivos sequenciais podem ter chave ou não. Cada registro lógico do arquivo com chave
tem um item de dado chamado chave que pode ser usado para ordenar os registros, essa chave é então
chamada chave de ordenação. Os registros em arquivos sequenciais sem chave estão ordenados em
série, sendo que geralmente cada novo registro é colocado no final do arquivo.
Na tabela abaixo, é apresentado um exemplo de arquivo seqüencial, no qual é usado como
chave de ordenação o atributo NÚMERO.

NÚMERO NOME IDADE SALÁRIO


100 Pedro 23 1000
150 Leandro 20 500
200 Rodrigo 19 270
250 Maria 30 5000
300 Celso 27 2500
350 Ana 42 9000
400 João 22 2100
450 Gisele 23 1300
500 Jack 21 800
550 Sandra 24 2400

Esta organização, que representa um aperfeiçoamento em relação àquela na qual os registros são
dispostos aleatoriamente, representa, também, uma perda de flexibilidade por não acomodar com
simplicidade as operações de modificação do arquivo.
O acesso a uma registro, dado um argumento de pesquisa, é facilitado se a chave de acesso
coincide com a chave de ordenação (ou com sua parte inicial), pois, nos demais casos, não há vantagem
na seqüencialidade do arquivo.

2.3.3. Inserção de um Registro

A maneira mais comum de se processar inserções de um registro em um arquivo sequencial S


consiste em montar um arquivo T de transações que contém os registros a serem inseridos ordenados
pela mesma chave de ordenação de S.O arquivo T pode ser usado como uma extensão de S , até
assumir um tamanho que justifique a efetivação da operação de intercalação quando os arquivos S e T
são intercalados, gerando o arquivo A que é uma versão atualizada de S.
A técnica anteriormente descrita é utilizada pois a inserção de um registro isolado tem um
custo proibitivo, pois implicaria no deslocamento de todos os registros com chaves superiores ao que
foi inserido.
Outra maneira de se proceder a inserção de um registro é seguindo este procedimento:

1- Posicionar-se no inicio do arquivo.


2- Identificar posição de inserção.
3- Copiar todos os registros,até o local de inserção , para o arquivo auxiliar.
4- Adicionar registro no arquivo auxiliar.
5- Copiar registros restantes para o arquivo auxiliar.
6- Substituir arquivo antigo pelo arquivo auxiliar

2.4. Introdução aos Arquivos Seqüenciais Indexados

Quando em um arquivo seqüencial o volume de acessos aleatórios torna-se muito grande,


configura-se a necessidade de utilização de uma estrutura de acesso, associada ao arquivo, que ofereça
maior eficiência na localização de um registro identificado por um argumento de pesquisa do que os
métodos vistos para arquivos seqüenciais.
Um arquivo seqüencial, acrescido em um índice (estrutura de acesso) constitui um arquivo
seqüencial indexado.
Um índice é formado por uma coleção de pares, cada um deles associando um valor da chave de
acesso a um endereço no arquivo. Assim, um índice é sempre específico para uma chave de acesso.
Além do arquivo seqüencial e do índice, um arquivo seqüencial indexado possui áreas de
extensão que são utilizadas para a implementação da operação de inserção de registros.
- Índices
A finalidade de um índice é permitir rápida determinação do endereço de um registro do
arquivo, dado um argumento de pesquisa. O endereço identifica a posição onde está armazenado o
registro, na memória secundária.
Usualmente, cada entrada do índice, formada por um par (chave do registro, endereço do
registro), ocupa um espaço bem menor do que o registro de dados correspondente, o que faz com que a
área ocupada pelo índice seja menor do que aquela ocupada pelos dados. Com isto a pesquisa sobre o
índice pode ser feito com maior rapidez do que se fosse feita diretamente sobre o arquivo de dados
correspondente. Este fato constitui a justificativa maior para a utilização dos índices.

Veja a tabela abaixo, que apresenta o arquivo seqüencial indexado:


NÚMERO ENDEREÇO NÚMERO NOME SALÁRIO
100 1 1 100 PEDRO 3000
150 2 2 150 JOÃO 1500
200 3 3 200 MARIA 2500
250 4 4 250 CARLA 3000
300 5 5 300 MAX 2000
|---------ÍNDICE---------| |-----------ÁREA DE DADOS NO DISCO------------|

- Áreas de Extensão
A área de extensão (também chamada área de overflow) destina-se a conter os registros
inseridos, em um arquivo seqüencial indexado, após a criação do arquivo. Ela constitui uma extensão
da área principal de dados do arquivo.
Áreas de extensão são necessárias em arquivos seqüenciais indexados, porque nesses não é
viável a implementação da operação de inserção de registros do mesmo que nos arquivos seqüenciais.
Naquele processo, a maioria dos registros muda de endereço, o que obrigaria uma completa alteração
nas entradas do índice, a cada atualização do arquivo.
Uma possível implementação de áreas de extensão em um arquivo seqüencial indexado consiste
em destinar um em cada registro da área principal um campo de elo para conter o endereço da lista
encadeada de seus sucessores (ou antecessores) alocados na área de extensão, conforme a tabela:

NÚMERO NOME ELO


NÚMERO ENDEREÇO
1 100 PEDRO -
100 1 2 150 JOÃO 10
150 2 3 200 MARIA -
175 2 4 250 CARLA 20
200 3 5 300 MAX -
250 4 |----------ÁREA DE DADOS NO DISCO-----------|
275 4
300 5
|---------ÍNDICE---------|

NÚMERO NOME ELO

10 175 BILL -
20 275 NARA -
30 -
40 -
50 -
|----------------ÁREA DE EXTENSÃO----------------|
2.5. Introdução aos Arquivos Indexados

Nos arquivos seqüenciais indexados, o compromisso de manter os registros fisicamente


ordenados pelo valor da chave de ordenação, com o objetivo de prover um acesso serial eficiente,
acarreta uma série de problemas, principalmente no que diz respeito à operação de inserção de um
registro, conduzindo à necessidade de utilização de áreas de extensão e efetivação de reorganizações
periódicas.
À medida que decresce a freqüência de acessos seriais, relativamente à freqüência de acessos
aleatórios, a manutenção da seqüencialidade física do arquivo encontra uma compensação cada vez
menor em termos de eficiência de acesso, até tornar-se antieconômica.
A partir deste ponto, torna-se mais conveniente o uso de um arquivo indexado, no qual os
registros são acessados sempre através de um ou mais índices, não havendo qualquer compromisso
com a ordem física de instalação dos registros.
A liberdade na escolha do endereço no qual um registro é armazenado representa um ganho de
flexibilidade que permite maior eficiência, principalmente na operação de inserção de um registro,
conduzindo, também, a uma simplificação da estrutura geral do arquivo, sendo dispensados os
mecanismos complexos de administração de áreas de extensão.
Veja a tabela abaixo, que apresenta o indexado:

NÚMERO ENDEREÇO NÚMERO NOME SALÁRIO


100 4 1 200 PAULO 3100
150 3 2 300 JOSÉ 4500
200 1 3 150 MARIA 2500
250 5 4 100 MARISA 5000
300 2 5 250 FABIO 2500
|---------ÍNDICE---------| |-----------ÁREA DE DADOS NO DISCO------------|

- Índices
Em um arquivo indexado, podem existir tantos índices quantas forem as chaves de acesso aos
registros. Um índice consiste de uma entrada para cada registro considerado relevante com relação à
chave de acesso associada ao índice. As entradas do índice são ordenadas pelo valor da chave de
acesso, sendo cada uma delas constituída por um par (chave do registro, endereço do registro). A
seqüencialidade física das entradas no índice visa a tornar mais eficiente o processo de busca e permitir
o acesso serial ao arquivo.
Um índice é dito exaustivo quando possui uma entrada para cada registro do arquivo e seletivo
quando possui entradas apenas para um subconjunto de registros. O subconjunto é definido por uma
condição relativa à chave de acesso e/ou a outros atributos do arquivo.Um exemplo de índice seletivo
seria o índice dos funcionários estáveis (há mais de 10 anos na empresa) sobre o cadastro geral de
funcionários de uma empresa.
O maior problema relacionado com a utilização de arquivos indexados diz respeito à
necessidade de atualização de todos os índices, quando um registro é inserido no arquivo. Atualizações
nos índices também são necessárias quando a alteração de um registro envolve atributos associados a
índices. Nos arquivos seqüenciais indexados, a necessidade de alteração dos índices é eliminada pelo
uso de áreas de extensão e encadeamento na implementação de inserções; no entanto, esta estratégia
não é condizente com a idéia de arquivos indexados, nos quais a manutenção constante dos índices é
necessária.
2.6. Introdução aos Arquivos Diretos

A idéia básica de um arquivo direto consiste na instalação dos registros em endereços


determinados com base no valor de uma chave primária, de modo que se tenha acesso rápido aos
registros especificados por argumentos de pesquisa, sem que haja necessidade de percorrer uma
estrutura auxiliar (índice).
Um arquivo direto é semelhante a um arquivo indexado, no sentido de que, nos dois casos, o
objetivo principal é a obtenção de acesso aleatório eficiente. Em um arquivo direto, aos invés do índice
é usada uma função que calcula o endereço do registro a partir do argumento de pesquisa.
As duas organizações possuem diferenças importantes, além do modo pelo qual é feito o acesso.
Uma delas é o fato de que nos arquivos indexados, ao contrário dos diretos, o endereço onde um
registro é armazenado independe do valor de sua chave, e uma outra, muito importante, diz respeito a
acessos seriais, que nos arquivos indexados são providos por meio de índices e nos arquivos diretos não
são previstos, de acordo com a idéia básica.
Veja a tabela abaixo, que apresenta o arquivo direto:

E=F(chave) NÚMERO NOME SALÁRIO


chave: 150---> ---> E = 3
1 200 PAULO 3100
|-------->
2
3 150 MARIA 2500
4
5 250 FABIO 2500
|-----------ÁREA DE DADOS NO DISCO------------|

- Cálculo de Endereços
O primeiro problema com os arquivos diretos é o da determinação de uma função F, que
transforme o valor da chave C de um registro no endereço E que lhe corresponde no arquivo.
Podemos considerar dois tipos de funções, sendo o primeiro constituído pelas funções
determinísticas, as quais associam um único valor da chave de acesso a cada endereço. Este tipo de
função apresenta vantagens evidentes; no entanto, é impossível, em termos práticos, encontrar uma
função determinística simples para um grande número de registros. Aquelas que poderiam ser usadas
seriam tão complexas que eliminariam as vantagens do acesso direto, além de necessitarem adaptações
a cada inserção sofrida pelo arquivo. Não têm, portanto, maior interesse prático.
O segundo tipo é formado pelas funções probabilísticas, as quais geram para cada valor da
chave um endereço "tão único quanto possível", podendo gerar, para valores distintos de chave, o
mesmo endereço, fato este que é denominado colisão.
- Tratamento das Colisões
Um dos aspectos mais importantes na organização de arquivos diretos diz respeito ao problema
das colisões, que é uma conseqüência do uso de funções não determinísticas para a transformação dos
valores da chave de acesso em endereços do arquivo.
Para se tratar as colisões, as soluções mais freqüentes usadas são Endereçamento Aberto com
Pesquisa Seqüencial e Encadeamento. A primeira consiste em fazer uma busca sobre o arquivo para
localização de um endereço livre, sendo nele armazenado o registro. A pesquisa do endereço livre é de
forma seqüencial, ou seja, se o endereço E gerado pela chave estiver ocupado, o próximo a ser
consultado será o endereço E + 1, E + 2,...,,...,E - 1 até se encontrar um lugar vago para armazenar o
registro.
Na segunda solução, todos ou parte dos registros que colidem em um mesmo endereço são
juntados em uma lista encadeada, à qual se tem acesso por meio do endereço gerado pela função de
aleatorização. As duas estratégias mais usadas são a utilização de áreas de extensão e encadeamento
puro.

2.7. Introdução aos Arquivos Invertidos

Esta organização é baseada em uma mudança nos papeis de registro e atributos, de tal forma
que, em vez de serem coletados os valores dos atributos para cada registro, são identificados os
registros que possuem cada um dos particulares valores da chave de acesso considerada. A cada um dos
valores da chave de acesso, presentes no arquivo, é associada uma lista de identificações de registros,
chamada lista invertidas.
As técnicas usuais na organização de índices são válidas também para este caso, devendo ser
tomado o devido cuidado com o fato de que, em um arquivo invertido, a cada valor da chave de acesso
está associado não apenas um endereço do registro, mas sim um conjunto de endereços dos registros
que possuem aquele valor da chave.
O conjunto de listas invertidas associado a uma chave de acesso é chamado inversão, sendo que
um arquivo invertido pode assumir uma ou mais inversões. Na tabela abaixo, é representado um
arquivo invertido com duas inversões associadas à chave secundária IDADE, uma contendo os
ENDEREÇOS e outra NÚMEROS.

IDADE ENDEREÇOS
20 2 8 9 NÚMERO NOME IDADE

22 1 5 1 350 PEDRO 22

23 4 2 200 GISA 20

25 6 10 3 150 MAX 27

27 3 7 4 250 SANDRA 23
5 400 PAULO 22
IDADE NÚMEROS 6 600 CARLA 25
20 200 300 100 7 450 ROBSON 27
22 350 400 8 300 CELSO 20
23 250 9 100 RENATA 20
25 600 550 10 550 LEANDRO 25
27 150 450

Na primeira inversão, os registros são identificados por seus endereços físicos. Esta modalidade
apresenta a vantagem de permitir o acesso direto ao registro, mas acarreta o problema de que as listas
são válidas apenas para aquela disposição física dos registros, sendo que, caso o arquivo venha a sofrer
uma reorganização que envolva mudança nos endereços dos registros, todas as inversões deverão ser
novamente geradas.
Uma alternativa para este problema consiste na identificação dos registros por meio de uma de
suas chaves primárias, como na segunda inversão. Com isto as listas invertidas passam a ser
independentes da localização física dos registros, havendo, no entanto, perda de eficiência no acesso,
em virtude da necessidade de determinar o endereço do registro uma vez obtida a sua chave primária na
lista.
2.8. Quadro Comparativo entre as Organização de Arquivos

Eis um quadro comparativo, que lista as vantagens e desvantagens das várias organizações de arquivos.

Arquivo Vantagens Desvantagens


Seqüencial - Acessos seqüenciais mais eficientes. - Operações de modificações não são simples.
-Utilizam índices, que agilizam a consulta por estarem na - Necessidades de áreas de extensão, que precisam ser
Seqüencial Indexado
RAM. reorganizadas.
-Não existem áreas de
- Atualização do índice quando da inserção de
Indexado extensão
um registro.
- Registros sem compromisso com armazenamento físico.
- Determinar funções que gerem menor número
Direto -Acesso direto, sem necessidade do índice.
de colisões
- Acesso direto ao registro após localização da lista - As listas invertidas valem apenas para aquela
Invertido
invertida. disposição física do arquivo.

2.9. Exercícios

1. O que é registro?
a) É uma seqüência de itens, chamados de campos ou atributos.
b) É uma seqüência de um ou mais campos em um arquivo.
c) É uma entidade de um banco de dados relacional orientado a objetos.

2. O que é chave primária?


a) É uma seqüência de uma ou mais campos em um arquivo.
b) É uma chave que apresenta uma valor diferente para cada registro.
c) É a chave que abria a sala da porta da sua sala na 1ª série.
d) Uma estrutura de dados usada para identificar um arquivo seqüencial.

3. Marque uma desvantagem do arquivo indexado em relação ao arquivo direto.


a) Nenhuma, ambas são a mesma coisa.
b) Funções compostas exaustivas
c) O uso de índices
d) Chaves de registro

4. Qual é a vantagem do arquivo seqüencial indexado sobre o arq. seqüencial?


a) O uso de índices.
b) O uso de uma função probabilística.
c) O uso de derivadas direcionais.
d) O uso de ponteiros.

5. O que é inversão num arquivo invertido?


a) Consiste na inversão da ordem dos caracteres de um arquivo.
b) É um conjunto de listas invertidas associado a uma chave de acesso.
c) Colisão de endereços.
d) Uma lista de identificações associada a cada chave de acesso.

6. Como são determinados o endereço de um registro num arquivo direto?


a) Através de uma lista invertida.
b) Através de um índice.
c) Através de uma função.
d) Através de chaves secundárias associadas a cada um dos campos.
3. Arquivos em C

3.1. Abrindo e fechando um arquivo

O sistema de entrada e saída do ANSI C é composto por uma série de funções, cujos protótipos
estão reunidos em stdio.h. Todas estas funções trabalham com o conceito de "ponteiro de arquivo".
Este não é um tipo propriamente dito, mas uma definição usando o comando typedef. Esta definição
também está no arquivo stdio.h. Podemos declarar um ponteiro de arquivo da seguinte maneira:

FILE *p;

p será então um ponteiro para um arquivo. É usando este tipo de ponteiro que vamos poder
manipular arquivos no C.

fopen

Esta é a função de abertura de arquivos. Seu protótipo é:

FILE *fopen (char *nome_do_arquivo,char *modo);

O nome_do_arquivo determina qual arquivo deverá ser aberto. Este nome deve ser válido no
sistema operacional que estiver sendo utilizado. O modo de abertura diz à função fopen() que tipo de
uso você vai fazer do arquivo. A tabela abaixo mostra os valores de modo válidos:

Modo Significado
"r" Abre um arquivo texto para leitura. O arquivo deve existir antes de ser aberto.
Abrir um arquivo texto para gravação. Se o arquivo não existir, ele será criado. Se já existir, o conteúdo
"w"
anterior será destruído.
Abrir um arquivo texto para gravação. Os dados serão adicionados no fim do arquivo ("append"), se ele
"a"
já existir, ou um novo arquivo será criado, no caso de arquivo não existente anteriormente.
"rb" Abre um arquivo binário para leitura. Igual ao modo "r" anterior, só que o arquivo é binário.
"wb" Cria um arquivo binário para escrita, como no modo "w" anterior, só que o arquivo é binário.
"ab" Acrescenta dados binários no fim do arquivo, como no modo "a" anterior, só que o arquivo é binário.
"r+" Abre um arquivo texto para leitura e gravação. O arquivo deve existir e pode ser modificado.
Cria um arquivo texto para leitura e gravação. Se o arquivo existir, o conteúdo anterior será destruído.
"w+"
Se não existir, será criado.
Abre um arquivo texto para gravação e leitura. Os dados serão adicionados no fim do arquivo se ele já
"a+"
existir, ou um novo arquivo será criado, no caso de arquivo não existente anteriormente.
"r+b" Abre um arquivo binário para leitura e escrita. O mesmo que "r+" acima, só que o arquivo é binário.
"w+b" Cria um arquivo binário para leitura e escrita. O mesmo que "w+" acima, só que o arquivo é binário.
Acrescenta dados ou cria uma arquivo binário para leitura e escrita. O mesmo que "a+" acima, só que o
"a+b"
arquivo é binário

Poderíamos então, para abrir um arquivo binário para escrita, escrever:


FILE *fp; /* Declaração da estrutura
fp=fopen ("exemplo.bin","wb"); /* o arquivo se chama exemplo.bin e está localizado no diretório
corrente */
if (!fp)
printf ("Erro na abertura do arquivo.");

A condição !fp testa se o arquivo foi aberto com sucesso porque no caso de um erro a função
fopen() retorna um ponteiro nullo (NULL).
Uma vez aberto um arquivo, vamos poder ler ou escrever nele utilizando as funções que serão
apresentadas a seguir.
Toda vez que estamos trabalhando com arquivos, há uma espécie de posição atual no arquivo.
Esta é a posição de onde será lido ou escrito o próximo caractere. Normalmente, num acesso sequencial
a um arquivo, não temos que mexer nesta posição pois quando lemos um caractere a posição no
arquivo é automaticamente atualizada. Num acesso randômico teremos que mexer nesta posição (ver
fseek()).

exit

Aqui abrimos um parênteses para explicar a função exit() cujo protótipo é:

void exit (int codigo_de_retorno);

Para utilizá-la deve-se colocar um include para o arquivo de cabeçalho stdlib.h. Esta função
aborta a execução do programa. Pode ser chamada de qualquer ponto no programa e faz com que o
programa termine e retorne, para o sistema operacional, o código_de_retorno. A convenção mais usada
é que um programa retorne zero no caso de um término normal e retorne um número não nulo no caso
de ter ocorrido um problema. A função exit() se torna importante em casos como alocação dinâmica e
abertura de arquivos pois nestes casos, se o programa não conseguir a memória necessária ou abrir o
arquivo, a melhor saída pode ser terminar a execução do programa. Poderíamos reescrever o exemplo
da seção anterior usando agora o exit() para garantir que o programa não deixará de abrir o arquivo:

#include <stdio.h>
#include <stdlib.h> /* Para a função exit() */
main (void)
{
FILE *fp;
...
fp=fopen ("exemplo.bin","wb");
if (!fp){
printf ("Erro na abertura do arquivo. Fim de programa.");
exit (1);
}
...
return 0;
}

fclose

Quando acabamos de usar um arquivo que abrimos, devemos fechá-lo. Para tanto usa-se a
função fclose():

int fclose (FILE *fp);

O ponteiro fp passado à função fclose() determina o arquivo a ser fechado. A função retorna
zero no caso de sucesso.
Fechar um arquivo faz com que qualquer caracter que tenha permanecido no "buffer" associado
ao fluxo de saída seja gravado. Mas, o que é este "buffer"? Quando você envia caracteres para serem
gravados em um arquivo, estes caracteres são armazenados temporariamente em uma área de memória
(o "buffer") em vez de serem escritos em disco imediatamente. Quando o "buffer" estiver cheio, seu
conteúdo é escrito no disco de uma vez. A razão para se fazer isto tem a ver com a eficiência nas
leituras e gravações de arquivos. Se, para cada caracter que fossemos gravar, tivéssemos que posicionar
a cabeça de gravação em um ponto específico do disco, apenas para gravar aquele caracter, as
gravações seriam muito lentas. Assim estas gravações só serão efetuadas quando houver um volume
razoável de informações a serem gravadas ou quando o arquivo for fechado.
A função exit() fecha todos os arquivos que um programa tiver aberto.

3.2. Lendo e escrevendo caracteres em arquivos


putc

A função putc é a primeira função de escrita de arquivo que veremos. Seu protótipo é:
int putc (int ch,FILE *fp);

Escreve um caractere no arquivo.


O programa a seguir lê uma string do teclado e escreve-a, caractere por caractere em um arquivo
em disco (o arquivo arquivo.txt, que será aberto no diretório corrente).

#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp;
char string[100];
int i;
fp = fopen("arquivo.txt","w"); /* Arquivo ASCII, para escrita */
if(!fp)
{
printf( "Erro na abertura do arquivo");
exit(1);
}
printf("Entre com a string a ser gravada no arquivo:");
gets(string);
for(i=0; string[i]; i++)
putc(string[i], fp); /* Grava a string, caractere a caractere */
fclose(fp);
return 0;
}

Depois de executar este programa, verifique o conteúdo do arquivo arquivo.txt (você pode usar
qualquer editor de textos). Você verá que a string que você digitou está armazenada nele.
getc

Retorna um caractere lido do arquivo. Protótipo:


int getc (FILE *fp);

feof

EOF ("End of file") indica o fim de um arquivo. Às vezes, é necessário verificar se um arquivo
chegou ao fim. Para isto podemos usar a função feof(). Ela retorna não-zero se o arquivo chegou ao
EOF, caso contrário retorna zero. Seu protótipo é:

int feof (FILE *fp);

Outra forma de se verificar se o final do arquivo foi atingido é comparar o caractere lido por
getc com EOF. O programa a seguir abre um arquivo já existente e o lê, caracter por caracter, até que o
final do arquivo seja atingido. Os caracteres lidos são apresentados na tela:

#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *fp;
char c;
fp = fopen("arquivo.txt","r"); /* Arquivo ASCII, para leitura */
if(!fp)
{
printf( "Erro na abertura do arquivo");
exit(0);
}
while((c = getc(fp) ) != EOF) /* Enquanto não chegar ao final do arquivo */
printf("%c", c); /* imprime o caracter lido */
fclose(fp);
return 0;
}

A seguir é apresentado um programa onde várias operações com arquivos são realizadas,
usando as funções vistas nesta página. Primeiro o arquivo é aberto para a escrita, e imprime-se algo
nele. Em seguida, o arquivo é fechado e novamente aberto para a leitura. Verifique o exemplo.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void main()
{
FILE *p;
char c, str[30], frase[80] = "Este e um arquivo chamado: ";
int i;
/* Le um nome para o arquivo a ser aberto: */
printf("\n\n Entre com um nome para o arquivo:\n");
gets(str);
if (!(p = fopen(str,"w"))) /* Caso ocorra algum erro na abertura do arquivo..*/
{ /* o programa aborta automaticamente */
printf("Erro! Impossivel abrir o arquivo!\n");
exit(1);
}
/* Se nao houve erro, imprime no arquivo e o fecha ...*/
strcat(frase, str);
for (i=0; frase[i]; i++)
putc(frase[i],p);
fclose(p);

/* Abre novamente para leitura */


p = fopen(str,"r");
c = getc(p); /* Le o primeiro caracter */
while (!feof(p)) /* Enquanto não se chegar no final do arquivo */
{
printf("%c",c); /* Imprime o caracter na tela */
c = getc(p); /* Le um novo caracter no arquivo */
}
fclose(p); /* Fecha o arquivo */

3.3. Outros comandos de acesso a arquivos

Arquivos pré-definidos

Quando se começa a execução de um programa, o sistema automaticamente abre alguns


arquivos pré-definidos:
 stdin: dispositivo de entrada padrão (geralmente o teclado)
 stdout: dispositivo de saída padrão (geralmente o vídeo)
 stderr: dispositivo de saída de erro padrão (geralmente o vídeo)
 stdaux: dispositivo de saída auxiliar (em muitos sistemas, associado à porta serial)
 stdprn : dispositivo de impressão padrão (em muitos sistemas, associado à porta paralela)
Cada uma destas constantes pode ser utilizada como um ponteiro para FILE, para acessar os
periféricos associados a eles. Desta maneira, pode-se, por exemplo, usar:
ch =getc(stdin);

para efetuar a leitura de um caracter a partir do teclado, ou :

putc(ch, stdout);

para imprimí-lo na tela.

fgets

Para se ler uma string num arquivo podemos usar fgets() cujo protótipo é:

char *fgets (char *str, int tamanho,FILE *fp);


A função recebe 3 argumentos: a string a ser lida, o limite máximo de caracteres a serem lidos e
o ponteiro para FILE, que está associado ao arquivo de onde a string será lida. A função lê a string até
que um caracter de nova linha seja lido ou tamanho-1 caracteres tenham sido lidos. Se o caracter de
nova linha ('\n') for lido, ele fará parte da string, o que não acontecia com gets. A string resultante
sempre terminará com '\0' (por isto somente tamanho-1 caracteres, no máximo, serão lidos).
A função fgets é semelhante à função gets(), porém, além dela poder fazer a leitura a partir de
um arquivo de dados e incluir o caracter de nova linha na string, ela ainda especifica o tamanho
máximo da string de entrada. Como vimos, a função gets não tinha este controle, o que poderia
acarretar erros de "estouro de buffer". Portanto, levando em conta que o ponteiro fp pode ser
substituído por stdin, como vimos acima, uma alternativa ao uso de gets é usar a seguinte construção:
fgets (str, tamanho, stdin);

onde str e' a string que se está lendo e tamanho deve ser igual ao tamanho alocado para a string
subtraído de 1, por causa do '\0'.

fputs

Protótipo:

char *fputs (char *str,FILE *fp);

Escreve uma string num arquivo.

ferror e perror

Protótipo de ferror:

int ferror (FILE *fp);

A função retorna zero, se nenhum erro ocorreu e um número diferente de zero se algum erro
ocorreu durante o acesso ao arquivo.
ferror() se torna muito útil quando queremos verificar se cada acesso a um arquivo teve
sucesso, de modo que consigamos garantir a integridade dos nossos dados. Na maioria dos casos, se um
arquivo pode ser aberto, ele pode ser lido ou gravado. Porém, existem situações em que isto não ocorre.
Por exemplo, pode acabar o espaço em disco enquanto gravamos, ou o disco pode estar com problemas
e não conseguimos ler, etc.
Uma função que pode ser usada em conjunto com ferror() é a função perror() (print error),
cujo argumento é uma string que normalmente indica em que parte do programa o problema ocorreu.
No exemplo a seguir, fazemos uso de ferror, perror e fputs

#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *pf;
char string[100];
if((pf = fopen("arquivo.txt","w")) == NULL)
{
printf("\nNao consigo abrir o arquivo ! ");
exit(1);
}
do
{
printf("\nDigite uma nova string. Para terminar, digite <enter>: ");
gets(string);
fputs(string, pf);
putc('\n', pf);
if(ferror(pf))
{
perror("Erro na gravacao");
fclose(pf);
exit(1);
}
} while (strlen(string) > 0);
fclose(pf);
}

fread

Podemos escrever e ler blocos de dados. Para tanto, temos as funções fread() e fwrite(). O
protótipo de fread() é:

unsigned fread (void *buffer, int numero_de_bytes, int count, FILE *fp);

O buffer é a região de memória na qual serão armazenados os dados lidos. O número de bytes é
o tamanho da unidade a ser lida. count indica quantas unidades devem ser lidas. Isto significa que o
número total de bytes lidos é:

numero_de_bytes*count

A função retorna o número de unidades efetivamente lidas. Este número pode ser menor que
count quando o fim do arquivo for encontrado ou ocorrer algum erro.
Quando o arquivo for aberto para dados binários, fread pode ler qualquer tipo de dados.
fwrite

A função fwrite() funciona como a sua companheira fread(), porém escrevendo no arquivo.
Seu protótipo é:

unsigned fwrite(void *buffer,int numero_de_bytes,int count,FILE *fp);


A função retorna o número de itens escritos. Este valor será igual a count a menos que ocorra
algum erro.
O exemplo abaixo ilustra o uso de fwrite e fread para gravar e posteriormente ler uma variável
float em um arquivo binário.

#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *pf;
float pi = 3.1415;
float pilido;
if((pf = fopen("arquivo.bin", "wb")) == NULL) /* Abre arquivo binário para escrita */
{
printf("Erro na abertura do arquivo");
exit(1);
}
if(fwrite(&pi, sizeof(float), 1,pf) != 1) /* Escreve a variável pi */
printf("Erro na escrita do arquivo");
fclose(pf); /* Fecha o arquivo */
if((pf = fopen("arquivo.bin", "rb")) == NULL) /* Abre o arquivo novamente para leitura */
{
printf("Erro na abertura do arquivo");
exit(1);
}
if(fread(&pilido,sizeof(float),1,pf) != 1) /* Le em pilido o valor da variável armazenada
anteriormente */
printf("Erro na leitura do arquivo");
printf("\nO valor de PI, lido do arquivo e': %f", pilido);
fclose(pf);
return(0);
}

Note-se o uso do operador sizeof, que retorna o tamanho em bytes da variável ou do tipo de
dados.

fseek

Para se fazer procuras e acessos randômicos em arquivos usa-se a função fseek(). Esta move a
posição corrente de leitura ou escrita no arquivo de um valor especificado, a partir de um ponto
especificado. Seu protótipo é:

int fseek (FILE *fp,long numbytes,int origem);

O parâmetro origem determina a partir de onde os numbytes de movimentação serão contados.


Os valores possíveis são definidos por macros em stdio.h e são:

Nome Valor Significado


SEEK_SET 0 Início do arquivo
SEEK_CUR 1 Ponto corrente no arquivo
SEEK_END 2 Fim do arquivo

Tendo-se definido a partir de onde irá se contar, numbytes determina quantos bytes de
deslocamento serão dados na posição atual.

Exemplo:
#include <stdio.h>
#include <stdlib.h>

int main()
{
FILE *fp;
long pos;

pos = 5;
fp = fopen("arquivo.txt","r");
fseek(fp,pos,SEEK_SET);
printf("Posicao :%i Informacao:%c\n",pos,getc(fp));
pos = 3;
fseek(fp,pos,SEEK_CUR);
printf("Posicao :%i Informacao:%c\n",pos,getc(fp));
fclose(fp);
return 0;
}

rewind

A função rewind() de protótipo

void rewind (FILE *fp);

retorna a posição corrente do arquivo para o início.

remove

Protótipo:

int remove (char *nome_do_arquivo);

Apaga um arquivo especificado.

O exercício anterior poderia ser reescrito usando-se, por exemplo, fgets() e fputs(), ou fwrite()
e fread(). A seguir apresentamos uma segunda versão que se usa das funções fgets() e fputs(), e que
acrescenta algumas inovações.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
FILE *p;
char str[30], frase[] = "Este e um arquivo chamado: ", resposta[80];
int i;

/* Le um nome para o arquivo a ser aberto: */


printf("\n\n Entre com um nome para o arquivo:\n");
fgets(str,29,stdin); /* Usa fgets como se fosse gets */
for(i=0; str[i]; i++)
if(str[i]=='\n')
str[i]=0; /* Elimina o \n da string lida */
if (!(p = fopen(str,"w"))) /* Caso ocorra algum erro na abertura do arquivo..*/
{ /* o programa aborta automaticamente */
printf("Erro! Impossivel abrir o arquivo!\n");
exit(1);
}
/* Se nao houve erro, imprime no arquivo, e o fecha ...*/
fputs(frase, p);
fputs(str,p);
fclose(p);

/* abre novamente e le */
p = fopen(str,"r");
fgets(resposta,79,p);
printf("\n\n%s\n", resposta);
fclose(p); /* Fecha o arquivo */
remove(str); /* Apaga o arquivo */
return(0);
}

3.4. Fluxos padrão

Os fluxos padrão em arquivos permitem ao programador ler e escrever em arquivos da maneira


padrão com a qual o já líamos e escrevíamos na tela.

fprintf

A função fprintf() funciona como a função printf(). A diferença é que a saída de fprintf() é um
arquivo e não a tela do computador. Protótipo:

int fprintf (FILE *fp,char *str,...);

Como já poderíamos esperar, a única diferença do protótipo de fprintf() para o de printf() é a


especificação do arquivo destino através do ponteiro de arquivo.

fscanf

A função fscanf() funciona como a função scanf(). A diferença é que fscanf() lê de um arquivo
e não do teclado do computador. Protótipo:

int fscanf (FILE *fp,char *str,...);

Como já poderíamos esperar, a única diferença do protótipo de fscanf() para o de scanf() é a


especificação do arquivo destino através do ponteiro de arquivo.
Talvez a forma mais simples de escrever o programa anterior seja usando fprintf () e fscanf().
Fica assim:

#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *p;
char str[80],c;

/* Le um nome para o arquivo a ser aberto: */


printf("\n\n Entre com um nome para o arquivo:\n");
gets(str);

if (!(p = fopen(str,"w"))) /* Caso ocorra algum erro na abertura do arquivo..*/


{ /* o programa aborta automaticamente */
printf("Erro! Impossivel abrir o arquivo!\n");
exit(1);
}
/* Se nao houve erro, imprime no arquivo, fecha ...*/
fprintf(p,"Este e um arquivo chamado:\n%s\n", str);
fclose(p);

/* abre novamente para a leitura */


p = fopen(str,"r");
while (!feof(p))
{
fscanf(p,"%c",&c);
printf("%c",c);
}
fclose(p);
return(0);
}

4. Compressão de Dados

4.1. Introdução
Ao contrário do que possa parecer, comprimir não é somente reduzir o tamanho de um arquivo:
há várias outras aplicações que utilizam a compressão de dados. Obviamente, a redução do espaço
físico utilizado é um conjunto de aplicações, mas há outro relativo à agilização da trasmissã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. 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 compressã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.
Outra vantagem da compressã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 modems, 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 compressão de dados, neste caso, permite a ampliação de uma rede de
computadores de uma forma alternativa mais barata.

4.2. Tipos de Compressão

Compressão Lógica

A compressã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 compressã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 compressã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.

Compressão Física

A compressão física é aquela realizada sobre dados existentes, a partir dos quais é verificada a
repetição de caracteres para efetivar a redução do número de elementos de dados. Existem dois tipos de
técnicas para sinalizar a ocorrência de caracteres repetidos:
1. um deles indica o caracter (ou conjunto de caracteres) repetido através da substituição por um
caracter especial;
2. outras técnicas indicam a freqüência de repetição de caracteres e representam isto através de
seqüências de bits.
Ambos os tipos de compressão física e suas diversas técnicas serão abordados no decorrer deste
capítulo.

4.3. Compressão Orientada a Caracter

4.3.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 .

a) 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.
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, com apenas um byte até 256 ocorrências de caracter. Caso ocorra mais do que
isso, deve-se utilizar outra representação mais eficiente.

b) codificação run-length estendido

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üencia de codificação:
SO R A 980 SI
onde SO (shift out ) é um caracter especial indicador de início de uma seqüência de caracteres
definida pelo usuário até que SI (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.

c) 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 compressão.
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

4.3.2. 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 5 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.

a) 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 compressã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...
o que demonstra uma redução de 6 bytes no trecho de texto 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á a necessidade de explicitação do caracter
comprimido.
O algoritmo para a técnica apresentada é muito simples:
1. inicializa-se um contador para o cálculo do número de repetições de brancos;
2. lê-se o caracter do arquivo a comprimir;
3. verifica-se se o caracter é um branco;
4. se for, incrementa-se o contador e verifica-se se o número de caracteres ultrapassou a 255
(limite binário); se verdadeiro, colocam-se os caracteres indicadores no arquivo comprimido e
volta-se ao passo 1, caso contrário, volta-se ao passo 2;
5. se não for, verifica-se se o contador é maior que 2, valendo a pena comprimir; se é, colocam-se
os caracteres indicadores no arquivo comprimido, e volta-se ao passo 1, se não é, copiam-se os
caracteres lidos no arquivo comprimido e volta-se ao passo 1

O fluxograma para o algoritmo apresentado é mostrado a seguir:

Figura 10- Algoritmo de Supressão de Caracteres Nulos

b) Mapeamento de Bit
Quando é sabida a existência de múltiplas ocorrências não consecutivas de determinado caracter
no arquivo a comprimir, 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:

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 comprimir e o bit 1 a sua ausência.
O algoritmo para esta técnica necessita do controle do mapa de bits:
1. inicializa-se o mapa de bits colocando todos os bits em zero;
2. inicializa-se o contador;
3. realiza-se a leitura do caracter no arquivo a comprimir;
4. compara-se o caracter lido com o caracter procurado; se forem o mesmo, então vai-se para o
passo 6;
5. troca-se para 1 o bit da posição atual;
6. incrementa-se o contador;
7. verifica-se se o contador chegou a 8; se verdadeiro, então grava-se o mapa de bits e os
caracteres diferentes do comprimido, e volta-se para o passo 1; senão, volta-se ao passo 3.

A seguir é apresentado o fluxograma que descreve o algoritmo:


c) 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.
O algoritmo da técnica comprimento de fileira é simples como os anteriormente analisados.
Consiste no seguinte:
1. inicializa-se um contador de caracteres, destinado ao controle de cada caracter, buscando a
verificação de existência de repetição;
2. inicializa-se um contador de repetições do caracter procurado;
3. faz-se a leitura do caracter no arquivo a comprimir;
4. incrementa-se o contador de caracteres;
5. se o contador de caracteres for igual a 1, o caracter é armazenado e volta-se ao passo 3 para
verificação de repetição;
6. verifica-se se o caracter armazenado é o que procuramos; se for, incrementa-se o contador de
repetições;
7. verifica-se se o contador de repetição é maior ou igual a 4; se for menor, gravam-se os
caracteres lidos no arquivo comprimido e volta-se ao passo 1;
8. realiza-se a gravação dos caracteres indicadores no arquivo comprimido e volta-se ao passo 1.

O fluxograma que permite uma melhor visualização do algoritmo é apresentado a seguir.


d)Codificação Diatômica

Esta técnica de compressão permite a representação de um par de caracteres em apenas um caracter


especial. Normalmente utilizam-se tabelas 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 (ão) 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.
Para a implementação da codificação diatômica segue-se o seguinte algoritmo:
1. lê-se um par de caracteres;
2. verifica-se sua existência na tabela de pares;
3. se exitente, coloca-se o caracter especial correspondente no arquivo comprimido e volta-se ao
passo 1;
4. coloca-se apenas o primeiro caracter no arquivo comprimido;
5. desloca-se o segundo caracter para a primeira posição;
6. lê-se o próximo caracter e volta-se ao passo 2.

O fluxograma correspondente é o seguinte:

e)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 codificação diatômica, 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 compressã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 compressão.
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:
CN
onde C é um caracter escolhido para indicar a compressã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 compressã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.

4.4. Compressã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 compressão portanto, não necessitamos saber qual caracter vai ser comprimido,
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 comprimir, formando tabelas
de probabilidades.
Para sabermos como foi concebido este tipo de compressão, veremos na seção seguinte a Teoria
da Harmonia, a partir da qual teremos uma noção de como se processa a compressão estatística.

4.4.1. Teoria da Harmonia

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 exitente 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:
havendo n símbolos, cada qual com uma probabilidade p . A representação de quantidades em
binário é dada pela base 2 do logaritmo. Foi utilizado um valor n logn por sua proporcionalidade entre
quantidade de informação e tempo. Por outro lado, a fórmula apresentada é semelhante à utilizada para
verificação da energia de um sistema físico, utilizada na Teoria da Entropia.
Na prática, a fórmula anteriormente apresentada permite-nos verificar se é possível a otimização
da quantidade de bits utilizados para representação de determinado conjunto de símbolos. Duas
representações podem ser comparadas para a verificação de qual ocupa menos bits em média. Isso nos
permite concluir que é possível a criação de um método de compactação construído a partir da
probabilidade de ocorrência de símbolos.
De fato, existem técnicas que utilizam a análise da probabilidade para compactação de dados.
Estas são chamadas de estatísticas e serão analisadas nas seções seguintes.

4.4.2. Codificação Huffman

Esta técnica de compressão permite a representação em binário de caracteres a partir de sua


probabilidade de ocorrência. Esta representação é gerada por um sistema de decodificação em árvore
binária, o que impede a ambigüidade na análise do código.
A ambigüidade, neste caso, refere-se a uma decodificação que permite a confusão com outros
caracteres. Por exemplo, determinado caracter C1 tem o código binário 01 e outro caracter C2 tem o
código 0100, isto implica que, ao verificarmos a seqüência binária para C2 poderemos estar
interpretando como C1, ao serem lidos apenas os bits 01. Por isso, a codificação Huffman utiliza o
projeto em árvore binária para projeto dos bits que representam os caracteres, de forma que permitam
uma decodificação única para cada caracter.
A codificação Huffman necessita de que cada caracter tenha um valor de probabilidade de
ocorrência. A partir dos caracteres de menor valor, começa a construção da árvore binária. Por
exemplo, seja a seguinte distribuição:

C1 0,5
C2 0,2
C3 0,2
C4 0,1

para os dois caracteres de menor probabilidade, serão atribuídos os valores 0 para C3 e 1 para
C4. Eles formarão um ramo cuja probabilidade será 0,3, representado graficamente desta forma:

C3 ------- 0 --------
|------ 0,3 ----------
C4 ------- 1 --------

A probabilidade do ramo é a soma das probabilidades das folhas (0,3 = 0,2 + 0,1). Os valores
binários serão membros do código formado. Para a codificação dos próximos caracteres, basta
continuarmos a construção da árvore. O próximo caracter será adicionado à árvore:
C2 ---------------------- ----------- 0 -------
|
C3 ------- 0 -------- ----------------- 0,5 ------
| 1 |
|------ 0,3 -------------
C4 ------- 1 --------

Mais uma vez, o ramo é a soma das probabilidades anteriores (0,5 = 0,2 + 0,3), e a codificação
da divisão recebeu 0 para uma derivação e 1 para outra. O objetivo desta numeração é a construção do
código binário dos caracteres.
Por fim, o último caracter é adicionado à árvore:

C1 -------------------------------------------------------------------- 0 -
|---------------------- 1,0
C2 ---------------------- ----------- 0 ------- |
| 1|
C3 ------- 0 -------- ----------------- 0,5 ------
| 1 |
|------ 0,3 -------------
C4 ------- 1 --------

A probabilidade final da árvore é sempre 1,0, uma vez que necessariamente deve-se atingir
100% das ocorrências de caracteres, permitindo uma codificação total. Uma vez terminada a árvore,
basta a formalização da codificação, que é feita com a leitura dos valores binários, da raiz para as
folhas. Os valores binários lidos serão o código do percurso da raiz até a folha correspondente ao
caracter que se deseja o código. A tabela de códigos fica a seguinte:

C1 0
C2 10
C3 110
C4 111

Desta forma, estão codificados os caracteres através da técnica de Huffman. Observe que o
caracter de maior freqüência possui o menor valor. Isso é exatamente o objetivo da compressão
estatística, uma vez que permite a substituição do caracter de maior ocorrência por apenas um bit.
Assim acontece com os demais caracteres, em ordem crescente do número de bits, conforme a
prioridade.
Cabe salientar, por fim, que a codificação Huffman manteve a propriedade de permitir a
decodificação direta, não permitindo que os bits da codificação de um caracter confundisse com a de
outro.

4.4.3. Codificação Shannon-Fano

A forma desta técnica tem muitas semelhanças com a de Huffman. Necessita-se de uma tabela
com a probabilidade de ocorrência de cada caracter, e de um procedimento para a codificação em
binário. Por outro lado, o procedimento para a codificação, diferentemente de Huffman, baseia-se na
divisão de conjuntos de probabilidades para a obtenção do código binário.
Para a codificação, devemos ter os caracteres com suas probabilidades de ocorrência:
C1 0,1
C2 0,3
C3 0,1
C4 0,5

O passo seguinte é ordenar colocando os caracteres de maior probabilidade no topo da tabela,


até o menor, na base:

C4 0,5
C2 0,3
C1 0,1
C3 0,1

Uma vez feito isso, divide-se a tabela em dois grupos cuja soma de probabilidades seja igual ou
semelhante. No caso da tabela acima, serão obtidos dois grupos, um composto pelo caracter C4 e outro
pelos demais. O primeiro grupo recebe como primeiro valor de código o binário 0 e o segundo recebe
1:

C4 0
C2 1
C1 1
C3 1

Como para o primeiro grupo não há ambigüidade em termos apenas um bit, vamos resolver o
problema do segundo grupo. Para isso, repetimos o procedimento anterior, dividindo em dois
subgrupos de probabilidades equivalentes. O caracter C2 forma o primeiro subgrupo e os demais
formam o segundo. Mais uma vez vamos colocar 0 para distinguir o primeiro e 1 para o segundo:

C4 0 _
C2 1 0
C1 1 1
C3 1 1

Finalmente, para resolução da última duplicidade, repete-se o processo, inserindo o binário 0 na


seqüência do código de C1 e 1 para C3:

C4 0 _ _
C2 1 0 _
C1 1 1 0
C3 1 1 1
4.4.4. Comparação entre Huffman e Shannon-Fano

A geração de código entre as duas técnicas pode ter variações marcantes, sendo algumas vezes
sendo vantagem usar uma ou outra. Para decidirmos qual delas é a melhor para determinado caso, basta
compararmos o tamanho médio obtido por cada código resultante da aplicação das técnicas.
O tamanho médio ocupado por um código gerado pelas técnicas de compressão estatística é
dado por:

onde N é o número de caracteres da tabela de codificação, p é a probabilidade de ocorrência do


caracter i , e t é o número de bits de código ocupado pelo caracter i .
Para exemplificar numa comparação entre as técnicas, seja a tabela:

C3 0,3
C4 0,3
C2 0,3
C1 0,1

Codificando por Huffman, podemos obter os seguintes códigos:

C3 00
C4 01
C2 10
C1 11

O tamanho médio para esta codificação foi:

T = 0,3.2 + 0,3.2 + 0,3.2 + 0,1.2 = 2,0 bits

E codificando por Shannon-Fano, é possível resultar em:

C3 0
C4 10
C2 110
C1 111

E o tamanho obtido foi de:

T = 0,3.1 + 0,3.2 + 0,3.3 + 0,1.3 = 2,1 bits

Os resultados indicam que a codificação Huffman foi melhor executada, ganhando 0,1 bit em
média. Mas isso não acontece por acaso. A codificação Huffman poderia ter sido pior, se não
houvessem sido feitas as combinações iniciais em dois grupos. Por outro lado, a Shannon-Fano poderia
ter sido melhor se fosse escolhido como primeira divisão o intervalo entre C4 e C2.
Para melhor ou para pior, no exemplo acima ambos poderiam ter dado 2,1 ou 2,0. De qualquer
forma, é interessante tentarmos a codificação com um ou outro antes de optarmos por uma delas.
Quando comparamos as duas técnicas, geralmente há uma diferença na codificação que nos permite
aperfeiçoá-la.

4.4.5. Compressão Fixa x Compressão Auto-adaptável

Como vimos, as técnicas de compressão estatística de Huffman e Shannon-Fano permitem


apenas a criação de uma tabela fixa de códigos associados a cada caracter tabelado. Caso as
probabilidades nas quais foram concebidos os códigos da tabela forem distintos para determinado
arquivo a comprimir, este será prejudicado na compressã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 comprimir.
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.
Seja, então, a tabela, contendo o caracter o código e a contagem:

C1 0 0
C2 10 0
C3 110 0
C4 1110 0
C5 1111 0

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 agora será a seguinte:

C3 0 21
C2 10 18
C1 110 10
C5 1110 3
C4 1111 2

como vimos, 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 compressão auto-adaptável ganha-se, obviamente, uma compressão mais eficiente. Por
outro lado, há um problema pela própria variabilidade da tabela: para a descompressão, há a
necessidade de se anexar a tabela ao arquivo comprimido, para que os códigos correspondentes possam
ser devidamente decodificados. Isso pode significar, em alguns casos, perda da compressão
eficientemente obtida.
5. Algoritmos para classificação externa

Da mesma forma que na busca, o que deve ser otimizado para a classificação de arquivos é o
número de acessos. Qualquer método que minimize o número de acessos a disco será bom, pois as
operações físicas de entrada/saida em disco, tomam a maior parte do tempo de qualquer algoritmo. Para
se classificar um tabela na memória existem métodos cujo tempo é proporcional a n (seleção, inserção
(bubble, shell), etc . . .)e outros que são proporcionais a n.log n (base 2) (quick, merge, heap, etc . . .).
Claro que o mesmo vale para os arquivos, pois o algoritmo é o mesmo. Entretanto, no caso de arquivos,
o melhor é otimizar o número de acessos. Para o caso de arquivos alguns fatores devem ser analisados:
a) a quantidade de registros a serem classificados
b) se todos os registros cabem ou não na memória disponível
c) o grau de classificação já existente
d) a complexidade e os requisitos de armazenamento do algoritmo a ser usado
e) se vai haver inserções e remoções (arquivos dinâmicos)

Classificação interna

Se o arquivo cabe todo na memória disponível, o problema está resolvido, pois nada melhor que
ler todo o arquivo, colocá-lo numa tabela na memória (em geral um vetor de structs) classificá-lo por
um bom método de classificação interna (por exemplo quicksort) e gravá-lo novamente classificado.

Classificação externa

O problema é quando não há memória disponível para o arquivo todo. Nesse caso pode ser
usado um algoritmo de classificação externa como é o caso do método de seleção a seguir. Os métodos
de classificação internos nem sempre otimizam a quantidade de acessos a disco, pois não é esse seu
objetivo. Uma outra forma é usar-se um método híbrido interno e externo. Uma forma é mostrada a
frente, deixando na memória apenas a tabela de chaves. Outra forma é usar-se um método de 2 fases
como o mergesort mostrada abaixo:
a) classificação - coloca-se o que couber na memória disponível e classifica-se
b) intercalação - intercala-se a porção já ordenada com outras porções também já ordenadas.

5.1. Método da seleção

Vejamos como fica o método de seleção no arquivo de produtos. O programa abaixo ordena o
arquivo de produtos pelo número de identificação do produto.

// classifica o arquivo de produtos pelo número do produto


#include <stdio.h>
#include <stdlib.h>

struct produto {
int numprod;
char nomprod[20];
int quaprod;
double preprod;
};
main()
{
int i, j, k, jmin;
char nomearq[20];
FILE *fb;
struct produto x, xi, xmin;
// entra com o nome do arquivo a ser classificado
printf("entre o nome do arquivo a classificar:");
scanf("%s", nomearq);
// abre o arquivo binário para leitura e gravação
fb = fopen(nomearq, "r+b");
// posiciona no final do arquivo
fseek(fb, 0, 2);
/**
* ftell – retorna a posição corrente. Quando o arquivo foi aberto em modo binário o valor obtido
* corresponde ao numero de bytes desde o início do arquivo.
* http://www.cplusplus.com/ref/cstdio/ftell.html
*/
// calcula o número de registros do arquivo
k = ftell(fb) / sizeof(struct produto);
// o arquivo tem k registros (0, 1, ... k-1)
// vamos classificá-lo pelo método da seleção, isto é,
// para i = 0,1, ..., k-2 ache o mínimo e troque com i
for (i = 0; i < k-1; i++) {
// le o registro i
fseek(fb, i*sizeof(struct produto), 0);
fread(&x, sizeof(struct produto), 1, fb);
// guarda o registro i e inicia o mínimo com i
xi = x;
xmin = x;
jmin = i;
// procura a partir de i+1
for (j = i+1; j < k; j++) {
// le o registro j
fseek(fb, j*sizeof(struct produto), 0);
fread(&x, sizeof(struct produto), 1, fb);
// compara com o mínimo
if (x.numprod < xmin.numprod) {
// troca o mínimo
xmin = x;
jmin = j;
}
}
// grava xi na posição jmin e xmin na posição i
fseek(fb, jmin*sizeof(struct produto), 0);
fwrite(&xi, sizeof(struct produto), 1, fb);
fseek(fb, i*sizeof(struct produto), 0);
fwrite(&xmin, sizeof(struct produto), 1, fb);
}
fclose(fb);
// mostra no vídeo o arquivo gerado
// abre arquivo binário para leitura
fb = fopen(nomearq, "rb");
while (fread(&x, sizeof(struct produto), 1, fb))
printf("\n %5d - %.20s - %5d - %10.2lf", x.numprod, x.nomprod, x.quaprod, x.preprod);
}
5.2. Ler o arquivo e colocar na memória a tabela de chaves

Uma outra forma de classificar o arquivo, minimizando o número de acessos, é ler todas as
chaves de classificação e colocá-las numa tabela na memória juntamente com o número do registro à
qual pertencem, classificar esta tabela e construir o arquivo ordenado. Esse algoritmo terá exatamente
2.n acessos a disco (n para ler e n para gravar). Claro que só faz sentido se a quantidade de chaves não
for maior que a memória disponível para armazenar a tabela. O programa abaixo faz a classificação
usando o algoritmo descrito. Esse programa é muito mais rápido que o anterior embora não pareça. A
diferença está apenas na quantidade de acessos a disco que é feito por um ou pelo outro.

// classifica o arquivo de produtos pelo número do produto


// le o arquivo todo e guarda numa tabela na memória só o
// número do produto com o registro ao qual ele se refere
// ordena a tabela e pronto

#include <stdio.h>
#include <stdlib.h>
#define num_max_reg 1000
struct produto {
int numprod;
char nomprod[20];
int quaprod;
double preprod;
};

void troca(int *x, int *y) {


int aux = *x;
*x = *y;
*y = aux;
}

main()
{
int i, j, k, jmin;
char nomearq[20], nomearqnovo[20];
FILE *fb, *fnovo;
struct produto x;
int tabprod[num_max_reg], tabreg[num_max_reg];
// entra com o nome do arquivo a ser classificado
printf("entre o nome do arquivo a classificar:");
scanf("%s", nomearq);
// entra com o nome do arquivo a ser gerado
printf("entre o nome do arquivo a criar:");
scanf("%s", nomearqnovo);
// abre o arquivo para leitura
fb = fopen(nomearq, "rb");
// le todo o arquivo, guardando apenas a identificação do produto
// e o número do registro
k = 0;
while (fread(&x, sizeof(struct produto), 1, fb)) {
tabprod[k] = x.numprod;
tabreg[k] = k;
k++;
}
// o arquivo tem k registros (0, 1, ..., k-1)
fclose(fb);
// ordena tabprod permutando também tabreg
for (i = 0; i < k - 1; i++) {
// minimo a partir de i
jmin = i;
for (j = i + 1; j < k; j++)
if (tabprod[j] < tabprod[jmin]) jmin = j;
// troca elemento i com jmin
troca (&tabprod[i], &tabprod[jmin]);
troca (&tabreg[i], &tabreg[jmin]);
}
// basta agora criar o arquivo novo varrendo tabreg
// abre arquivo original e arquivo novo
fb = fopen(nomearq, "rb");
fnovo = fopen(nomearqnovo, "wb");
for (i = 0; i < k; i++){
// le registro tabreg[i] e grava na posição corrente do
// arquivo novo
fseek(fb, tabreg[i]*sizeof(struct produto), 0);
fread(&x, sizeof(struct produto), 1, fb);
fwrite(&x, sizeof(struct produto), 1, fnovo);
}
fclose(fb);
fclose(fnovo);
// mostra no vídeo o arquivo gerado
// abre arquivo binário para leitura
fnovo = fopen(nomearqnovo, "rb");
while (fread(&x, sizeof(struct produto), 1, fnovo))
printf("\n %5d - %.20s - %5d - %10.2lf", x.numprod, x.nomprod, x.quaprod, x.preprod);
}

5.3. Mergesort de arquivos

A solução de deixar uma tabela de chaves na memória para ordenação ainda tem uma limitação.
E quando a memória disponível não é suficiente? Será que há um algoritmo mais eficiente que o
primeiro (método da seleção) para ordenar o arquivo? A tentativa imediata é aplicar os algoritmos de
classificação de tabelas mais eficientes a arquivos. Dentre estes, vejamos uma solução com o
mergesort. Suponha o arquivo de produtos com 8 registros e vamos classificá-lo pelo numprod.
Arquivo original
730
156
420
831
525
85
356
654

Criar novo arquivo (arquivo A) contendo os registros ordenados de 2 em 2, ou seja, fazer o


merge dos registros de 1 em 1.
Arquivo A
156
730
420
831
85
525
356
654

Criar novo arquivo (arquivo B) contendo os registros ordenados de 4 em 4, ou seja, fazer o


merge dos registros de 2 em 2.

Arquivo B
156
420
730
831
85
356
525
654

Criar novo arquivo (arquivo C) contendo os registros ordenados de 8 em 8 (todo o arquivo), ou


seja, fazer o merge dos registros de 4 em 4.

Arquivo C
85
156
356
420
525
654
730
831

Observe que a cada passo fizemos 2.8 acessos ao disco, isto é, leitura de 8 registros e gravação
dos 8 registros. O número máximo de passos é de log 8 (base 2). Assim o total de acessos foi de 2.8.log
8 (base 2) = 2.8.3 = 48. Para um n grande 2.n.log n é bem melhor que n.n. Além disso, o arquivo C
pode ser o A, isto é, vamos permutando os arquivos, pois só precisamos de 2 ao mesmo tempo, além do
original (se é que queremos manter o original).
5.4. Arquivos dinâmicos

Em muitos casos os arquivos tem que ser mantidos ordenados, apesar de operações de inserção
e remoção de registros. São arquivos dinâmicos em geral usados em bancos de dados. Neste caso não é
possível manter-se o arquivo sequencialmente ordenado, pois numa operação de remoção, um espaço é
aberto e numa operação de remoção um espaço é necessário no meio do arquivo.
Uma solução intermediária para arquivos dinâmicos, usando apenas o que aprendemos até agora
seria:
1) Mantem-se 2 arquivos o original ordenado e um novo só com os registros inseridos (supondo
que não são muitos por período).
2) Marcam-se os registros removidos. Isto é, coloca-se um campo a mais em cada registro que
diz se o registro é válido ou não.
3) As operações de consulta verificam primeiro o arquivo original. Se não encontrar o registro
procurado no original, procura no arquivo de novos registros.
4) A cada período (por exemplo a cada 24 horas) faz-se uma consolidação dos 2 arquivos criando
um só arquivo ordenado para o período seguinte. Remove-se também os registros não válidos.

Como no caso da busca em arquivos essa solução não é uma solução definitiva. A estrutura que
permite que um arquivo permaneça ordenado apesar de inserções e remoções de registros, e com a
facilidade de busca da busca binária são as B-árvores.

6. Métodos de Pesquisa

6.1. Pesquisa Sequencial

Consiste no exame de cada registro, a partir do primeiro, até ser localizado aquele que possui,
para a chave de acesso, um valor igual ao argumento de pesquisa, ou então, ser atingido o final do
arquivo, o que significa que o registro procurado não está presente no arquivo.

Passo a Passo :

1 - Posicionar-se no início do arquivo;


2 - Se registro atual = registro desejado então
Sucesso
Terminar
3 - Se registro atual>registro desejado então
Fracasso
Terminar
4- Se registro atual < registro desejado então
avançar um registro;
5- Se não for final de arquivo então
retornar ao passo 2;
6 - Fracasso
7 - Terminar.
6.2. Pesquisa Binária

Na pesquisa binária o primeiro registro a ser consultado é aquele que ocupa a posição média do
arquivo. Se a chave do registro for igual ao argumento de pesquisa, a pesquisa termina com sucesso;
Caso contrário, ocorre uma das seguintes situações :

a. A chave do registro é maior do que o argumento de pesquisa e o processo de busca é repetido para
metade inferior do arquivo;

b. A chave do registro, é menor do que o argumento de pesquisa e o processo de busca é repetido para
metade superior do arquivo;

A busca é encerrada sem sucesso quando a área de pesquisa, que a cada comparação é reduzida a
metade, assumir o comprimento zero.

Passo a Passo:

1- Selecionar todo o conjunto de registros do arquivo;


2- Posicionar-se no meio do conjunto selecionado;
3- Se registro atual=registro desejado então
Sucesso
Terminar
4- Se registro atual<registro desejado então
selecionar metade superior do arquivo e repetir o processo - passo(2)
5- Se registro atual>registro desejado então
selecionar metade inferior do arquivo e repetir o processo -passo(2);
6- Se o conjunto selecionado não possuir elementos então
Fracasso
Terminar
A busca binária faz no máximo 10 comparações para encontrar um registro em um arquivo com
1000 registros. Com a pesquisa sequencial levaria no máximo 1000 comparações para encontrar o
registro procurado, na média levaria 500 comparações. A busca binária é, portanto, mais atrativa que a
busca sequencial, mas existe um preço a ser pago antes de podermos usar a busca binária: ela só
funciona quando a lista de registros está ordenada pela chave que se está usando para realizar a busca.

6.3. Hashing
Hashing é uma técnica que busca realizar as operações de inserção, remoção e busca em tempo
constante. Essa característica é muito importante quando se trabalha com armazenamento secundário
em disco, onde o acesso a um determinado endereço é bastante lento. Algumas aplicações que são
beneficiadas pelo uso de tabelas hash são dicionários e tabelas de palavras reservadas de um
compilador.
Ao invés de organizar a tabela segundo o valor relativo de cada chave em relação às demais, a
tabela hash leva em conta somente o seu valor absoluto, interpretado como um valor numérico.

Imagine que cada elemento em um conjunto possua um número associado a ele e que quaisquer
dois elementos distintos possuam números associados diferentes. Desta forma, poderíamos armazenar
os elementos em um array na posição indicada pelo número associado ao elemento.

Se conseguirmos associar a cada elemento a ser armazenado um número como no exemplo


acima, poderemos realizar as operações de inserção, remoção e busca em tempo constante.
A função que associa a cada elemento de um conjunto U um número que sirva de índice em
uma tabela (array) é chamada função hash.
Uma função hash deve satisfazer as seguintes condições:
1. Ser simples de calcular
2. Assegurar que elementos distintos possuam índices distintos.
3. Gerar uma distribuição equilibrada para os elementos dentro do array.

6.3.1. Aplicabilidade

Em termos práticos, a grande vantagem do hashing está no fato de que, dado um elemento k de
um conjunto P, o valor de hashing h(k) pode ser calculado em tempo constante, fornecendo
imediatamente a classe da partição de P em que o elemento se encontra. Se considerarmos P uma
coleção de elementos a ser pesquisada, é fácil perceber que o processo será muito mais eficiente se a
pesquisa for restrita a uma pequena parte do conjunto P (uma única classe). Suponha o caso de procurar
o nome “Maria” em um conjunto de nomes próprios:

Pesquisa sem Hashing


O hashing pode ser usado como uma técnica de redução do espaço de busca; o processo de pesquisa
será tão mais eficiente quanto menores forem as partições. Se o hashing for perfeito, então o valor de
hashing calculado dará imediatamente a localização do elemento desejado; neste caso, temos um
acesso direto ou randômico como também é chamado, sendo este o tipo de busca mais eficiente de
que dispomos.
6.3.2. Funções Hash

Criar uma função hash satisfazendo as condições acima nem sempre é uma tarefa fácil. Existem
muitos modelos de criação para funções hash. A seguir apresentamos apenas duas formas de criação de
funções hash.
Uma boa função hash é aquela em que toda entrada (slot) do array possui a mesma
probabilidade de ser atingido pela função.

Método da Divisão

O método da divisão consiste no seguinte: suponha que os elementos x que devem ser
armazenados sejam números inteiros, uma função hash que pode ser utilizada para espalhar tais
números em um array (tabela hash) é:

h(x) = x % tablesize
onde tablesize é o tamanho do array.
Principal problema: Elementos distintos podem receber o mesmo índice, por exemplo se
tablesize é 10 e os elementos terminam todos em zero.
Obs: Utilizar tablesize como sendo um número primo minimiza o problema acima e é uma idéia
muito empregada na criação de funções hash.
Para o caso onde o conjunto de elementos é constituído de strings, uma função hash pode ser
obtida somando os valores ascii dos caracteres e procedendo como acima. Um dos problemas é que se
tablesize é muito grande a função não distribui bem os elementos. Por exemplo: suponha tablesize =
10.007 (número primo) e que todas as strings possuam no máximo oito caracteres. Desde que o código
ascii de um caracter é no máximo 127 a função acima assumirá valores entre 0 e 1016, não gerando
uma distribuição equilibrada. Uma dentre as possíveis soluções para esse problema é elevar o valor da
soma dos caracteres ascii ao quadrado e só então dividi-lo pelo tamanho da tabela hash.
A função abaixo descreve a implementação da função hash descrita anteriormente para o caso
de strings.

int Hash(char *a, int stringsize)


{
int hashval, j;
hashval = (int) a[0];
for (j = 1; j < stringsize; j++)
hashval += (int) a[j];
return(hashval % tablesize); /* supondo que tablesize e' global */
}

Método da Dobra

Neste método, as chaves são interpretadas como uma seqüência de dígitos escritos num pedaço
de papel. Ele consiste em "dobrar" esse papel, de maneira que os dígitos se superponham sem levar em
consideração o "vai um" como mostra a figura abaixo.
O processo é repetido até que os dígitos formem um número menor que o tamanho da tabela
hash.
É importante destacar que o método da dobra também pode ser usado com números binários.
Nesse caso, ao invés da soma, deve-se realizar uma operação de "ou exclusivo", pois operações de "e" e
de "ou" tendem a produzir endereços-base concentrados no final ou no início da tabela.

Método da Multiplicação

Neste método, a chave pode ser multiplicada por ela mesma ou por uma constante, e ter o seu
resultado armazenado em uma palavra de memória de s bits. Considere que o número de bits
necessários para endereçar uma chave na tabela hash seja r. Dessa forma, para compor o endereço-base
de uma chave, descartam-se os bits excessivos da extrema direita e da extrema esquerda da palavra. No
exemplo abaixo, separamos os 4 bits centrais da palavra chave, pois a tabela hash tem tamanho 16.
A técnica descrita acima é conhecida como "meio do quadrado".

Método da subtração

Pode ser usado quando as chaves são consecutivas mas não começam em um. Não apresenta
colisões e aplicado em pequenas listas.
Ex.: Nro. de matrícula dos alunos de uma universidade:
20020001
20020002
20020003

H(Nro. Matric)= Nro. Matric - 20020000

Método da Extração de Dígito

Neste método dígitos da chave são extraídos e usados como endereços.


Ex.:
H(Chv) = extrair dígitos 1, 3 e 4 do código do funcionário.
H(547790) = 577
H(856430) = 864
H(523233) = 532

Método quadrático

O valor de hash é obtido através do quadrado de algum dos componentes da função.


Ex.: H(Chv) = Chv2 e extração dos 3 primeiros dígitos.
H(8452) = 84522 = 71436304 = 714

Método Folding

1. Fold Shift
CH = 123456789

123
+ 456
789
____________

1368
Pode-se excluir o primeiro dígito para o hash ficar de acordo com o tamanho desejado.

2. Fold Boundary
CH = 123456789

123 -> inverter -> 321


456 -> inverter -> 654
789 -> inverter -> 987
____________

1962
Pode-se excluir o primeiro dígito para o hash ficar de acordo com o tamanho desejado.

Hashing Universal

Qualquer função hash está sujeita ao problema de criar chaves iguais para elementos distintos.
Dependendo da entrada, algumas funções hash podem espalhar os elementos de forma mais equilibrada
que outras. Desta forma, se pudermos utilizar diferentes funções hash, teremos em média um
espalhamento mais equilibrado.
Uma estratégia que tenta minimizar o problema de colisões é o hashing universal. A idéia é
escolher aleatóriamente (em tempo de execução) uma função hash a partir de um conjunto de funções
cuidadosamente desenhado.
Uma família de funções pode ser gerada da sequinte forma: Suponha tablesize um número
primo. Decomponha x em r+1 bytes (para o caso de strings, decompomos em caracteres ou substrings).
Seja a = < a0, a1, ..., ar > uma sequência de elementos escolhida randomicamente a partir de 0, 1, ...
m -1. Definimos uma família de funções hashing como:

6.3.3. Tratamento de Colisões

Se em um determinado Dado temos a informação ( número de telefone ) , em vez de utilizarmos


todo o número para criar sua chave, o que é muito mais dispendioso computacionalmente utilizamos
apenas os 3 algarismos finais. Com isto a chave será menor, porém, poderão existir números de
telefone que possuam os mesmos 3 algarismos finais. O processo de buscar uma chave que não dê
colisões pode ser impossível como vimos anteriormente. Achar uma função chave que se adapte às
necessidades de armazenamento ocupando o menor espaço possível e sem colisões pode se tornar uma
tarefa memorável.... Na maioria das funções criadas ocorre o que chamamos de colisão, ou seja, mais
de um ou mais dados possuem a mesma chave de pesquisa. Uma das formas de resolver estas colisões é
simplesmente criar uma lista encadeada em cada parte da tabela. Assim todos os elementos que tenham
a mesma chave farão parte de uma lista que parte da posição da chave na tabela hash que pode ser
chamada de área de colisões.
O que deve ser pensado e implementado de forma eficiente é a função de transformação que irá
criar as chaves para os dados. A função deverá transformar para inteiros as partes dos dados que serão
usadas para criar a chave. O ideal é que a função de transformação seja simples de ser implementada e
alterada, e que, para cada chave de entrada, cada uma das chaves resultantes sejam igualmente
prováveis de ocorrerem.

Hashing fechado:

No hashing fechado, o elemento que causou uma colisão é movido para a próxima posição
disponível. Para tal, o vetor deve ser ao menos um pouco maior do que o número esperado de
elementos. Este método é eficiente quando espera-se que os elementos fiquem espalhados pelo vetor,
mas perde sua eficácia se o vetor se tornar muito cheio, ou se os elementos estiverem muito agrupados.
Nestes casos a procura pela próxima posição livre terá um custo muito alto, tornando o próprio método
da tabela hash pior do que outros métodos.

Rehashing:

Uma função de rehash rh recebe o índice de um vetor e produz um outro índice. Se h(chave) já
está ocupado com algum registro com chave diferente, rh é aplicado ao valor de h(chave) para
encontrar o lugar onde o registro pode ser inserido (ou achado). Se a posição rh(h(chave)) estiver
ocupada, calcula-se rh(rh(h(chave))) e assim por diante.

Exemplo:

h(chave) = chave % 1000;


rh(i) = (i+1) % 1000.
typedef struct {
int chave;
TipoReg reg;
} Tabela;

Tabela tab[MAXTAB];

int BuscaInsere (int chave, TipoReg r)


{
int i;
i = h(chave);
while (tab[i].chave != chave && tab[i].chave && CHAVENULA)
i = rh(i);
if (tab[i].chave == CHAVENULA) {
tab[i].chave = chave;
tab[i].reg = r;
}
return i;
}

Observar o loop: pára quando acha a chave ou quando acha espaço vazio (CHAVENULA). Mas pode
ser infinito se:
 a tabela estiver cheia; ou
 a função rh for inadequada.

No primeiro caso, podemos usar um contador para indicar se a tabela está cheia.
Se tívéssemos, por exemplo, as funções de rehash:
rh(i) = (i+2) % 1000;
rh(i) = (i+200) % 1000;
haveriam muitas colisões e o loop poderia ser infinito mesmo havendo ainda espaço na tabela.
A situação ideal é quando rh(rh(i))... cobre o maior número de inteiros entre 0 e MAXTAB-1.
Por exemplo,
rh(i) = (i+c)%MAXTAB.
Neste caso, c e MAXTAB devem ser primos entre si.
Quanto mais eficiente o a função menor é a área de dados necessária, além de ser mais maleavel
para se adaptar a cada novo dado ou necessidade de entrada de dados.

Hashing em separado :

No hashing em separado, as colisões ocorrentes são movidas para uma área em separado,
geralmente no fim do vetor. O vetor deve possuir um espaço para armazenar as colisões, e que não é
acessado pela função hash (nenhum elemento que não colidiu irá para essa área). É um processo rápido,
simples e eficaz, pois, se o elemento procurado não está na posição esperada, ele será procurado na área
de colisões. Tal método, porém, pode se tornar muito caro se o número de colisões for alto, pois os
elementos nessa área deverão ser procurados sequencialmente, além de deixar o vetor em si
relativamente vazio.

6.3.4. Vantagens e Desvantagens

Vantagens

 Simplicidade
 É muito fácil de imaginar um algoritmo para implementar hashing.
 Escalabilidade
 Podemos adequar o tamanho da tabela de hashing ao n esperado em nossa aplicação.
 Eficiência para n grandes
 Para trabalharmos com problemas envolvendo n = 1.000.000 de dados, podemos imaginar uma
tabela de hashing com 2.000 entradas, onde temos uma divisão do espaço de busca da ordem de
n/2.000 de imediato.
 Aplicação imediata a arquivos
 Os métodos de hashing, tanto de endereçamento aberto como fechado, podem ser utilizados
praticamente sem nenhuma alteração em um ambiente de dados persistentes utilizando arquivos em
disco.

Desvantagens

 Dependência da escolha de função de hashing


 Para que o tempo de acesso médio ideal T(n) = c1 . (1/b).n + c2 seja mantido, é necessário que a
função de hashing divida o universo dos dados de entrada em b conjuntos de tamanho
aproximadamente igual.
 Tempo médio de acesso é ótimo somente em uma faixa
 A complexidade linear implica em um crescimento mais rápido em relação a n do que as árvores,
p.ex.
 Existe uma faixa de valores de n, determinada por b, onde o hashing será muito melhor do que uma
árvore.
 Fora dessa faixa é pior.
7. Algoritmos para indexação

Neste capítulo são apresentados algoritmos de indexação, úteis na utilização em sistemas com
grandes quantidades de informações a serem pesquisadas. Para demonstrar sua utilidade os exemplos e
comentários das próximas páginas fazem referência aos maiores consumidores deste tipo de tecnologia,
os chamados Search Engines (máquinas de busca), como o Google, Altavista, etc. Mas essas teorias
podem ser implementadas em sistemas de qualquer porte.

7.1. Índice Invertido


À medida em que os robots4 vão rastreando a web, as páginas encontradas vão sendo
armazenadas nos discos rígidos das Search Engines; as páginas passam por um processo de
compressão, mas todo o texto é armazenado.
Cada página recebe um identificador; a Google, por exemplo, denominou esse identificador
docID (identificador de documento); qualquer referência à página é feita por meio do identificador.
Nesse estágio, a SE já tem um banco de dados contendo páginas da web; entretanto, nesse
formato de armazenamento, torna-se difícil a pesquisa por [palavras-chave], já que, para se analisar o
conteúdo de uma página, é necessário recuperá-la, ler seu conteúdo e armazená-la novamente.
Para facilitar (ou melhor, tornar possível) o trabalho de pesquisa, o próximo passo no trabalho
das SEs é criar um índice, chamado índice invertido (em inglês, inverted index).
A melhor maneira de se entender o processo de indexação é através de um exemplo simples.
Suponhamos que os bots tenham encontrado e armazenado uma página que tenha o seguinte corpo:

Estudo de Ferramentas de Busca

Um tipo de site está se tornando mais e mais popular na internet: as ferramentas de busca.
As ferramentas ou máquinas de busca ajudam usuários a encontrar aquilo que procuram.
Para obter outras informações, acesse www.rankings.com.br

A primeira coisa a fazer é atribuir um identificador para o documento; por exemplo,


docID=E29A.
A seguir, são feitos alguns ajustes no texto.
 Primeiramente, são filtrados os termos muito comuns, como "de", "um", "e", etc, que não são
indexados. Nota: a Google filtra os termos comuns em inglês (que ela chama de "stop words"),
como "of", "to", etc e informa, na página de respostas, que o termo foi filtrado; palavras comuns em
português não são filtradas, mas suporemos no exemplo que elas sejam.
 O ajuste seguinte é nas palavras que sobram: todas as palavras têm seus acentos removidos e a caixa
é convertida para letra minúscula. Além disso, todos os acentos e sinais de pontuação são eliminados
do texto. Isso explica por que pesquisas por [Máquinas de Busca] e [maquinas de busca] geram o
mesmo resultado na Google. Assim, o texto ajustado ficaria:

estudo ferramentas busca

tipo site esta tornando mais mais popular internet ferramentas busca
ferramentas maquinas busca ajudam usuarios encontrar aquilo procuram
para obter outras informacoes acesse www.rankings.com.br

4
Aplicativos que têm a função de recuperar páginas da web, interpretá-las, armazená-las e
extrair os links existentes. Também conhecidos como crawlers, bots ou spiders.
Cada palavra remanescente recebe um identificador, chamado pela Google wordID. No nosso
exemplo, seriam criadas as wordIDs: estudo (wordID=#0001); ferramentas (wordID=#0002); busca
(wordID=#0003); tipo (wordID=#0004); e assim por diante. Observe cada palavra recebe um único
wordID; assim, a segunda ocorrência da palavra ferramentas não causa a criação de uma nova wordID.
O conjunto de palavras compõe o que a Google denominou Lexicon.
O índice invertido é um banco de dados que armazena, para cada palavra (ou seja, cada
wordID), os documentos (docIDs) em que elas ocorreram, bem como detalhes da apresentação da
palavra. A ocorrência de uma wordID em um documento é chamada Hit.
Num exemplo bem simplificado, poderíamos armazenar apenas os Hits (os docIDs em que a
palavra ocorreu) e a posição da palavra no texto. Assim, o início de nosso índice invertido ficaria
assim:

wordID Hits
#0001 (E29A, 1)
#0002 (E29A, 2); (E29A, 12); (E29A, 14)
#0003 (E29A, 3)
#0004 (E29A, 4)
(...) (...)
#0013 (E29A, 18)
(...) (...)

Observe que a a palavra "ferramentas", wordID=#0002, teve três hits registrados; observe
também que a palavra "usuários", apesar de estar na 18a. posição, recebeu wordID="0013", por conta
dos termos que aparecem repetidos.
O processo acima é repetido para todos os documentos armazenados pela Search Engine, e para
todas as palavras (excluídas as stop words) encontradas.
Se no documento seguinte uma palavra nova é encontrada, ela recebe uma wordID, é
adicionada ao Lexicon, e os hits correspondentes passam a ser registrados.
Suponhamos que, num outro documento de docID="390A", a palavra "ferramentas" ocorra na posição
310 do texto. O registro correspondente a essa palavra seria modificado para refletir a ocorrência desse
hit, ficando da seguinte forma:

wordID Hits
(...) (...)
#0002 (E29A, 2); (E29A, 12); (E29A, 14); (390A, 310)
(...) (...)

O índice invertido está completo após o registro da última palavra do último documento.

7.1.1. Utilidades do Índice Invertido


Mesmo nesse modelo bastante simplificado, as utilidades do índice invertido já começam a se
tornar evidentes: para cada palavra pesquisada, é possível saber em quantos documentos ela ocorre (é
dessa forma que a Google informa "Resultados 1 - 10 de aproximadamente 61.000.000 para [brasil]), e
em qual posição do texto elas se encontram; já seria possível fazer uma Search Engine extremamente
rudimentar.
Para o caso em que a pesquisa contém duas ou mais palavras, são necessárias duas ou mais
pesquisas ao índice invertido; os documentos comuns a todas as pesquisas conterão todas as palavras
(mas não necessariamente na ordem pesquisada). Também através de refinamentos das páginas
retornadas para cada palavra, já se poderia implementar alguns operadores booleanos, como "or" e
"not", Para se encontrar as páginas em que as palavras-chave apareçam na ordem em que foram
digitadas, basta fazer uma análise adicional das páginas retornadas para cada palavra, lembrando que
palavras adjacentes têm posições de hits consecutivas.
Entretanto, com alguns aprimoramentos, o índice invertido pode tornar-se muito mais úti.

7.1.2. Aprimorando o índice


Na seção anterior, vimos um modelo básico de índice invertido. Nesse modelo básico, para
cada palavra do Lexicon, armazenamos tão somente os documentos (docIDs) em que ocorrem e as
respectivas posições no texto.
Para aumentar as potencialidades do índice invertido, as Search Engines armazenam muito mais
informações. Por exemplo, pode-se criar um campo para indicar que determinada palavra foi escrita em
negrito ou itálico e, portanto, teria mais destaque do que o restante do texto (o campo armazenaria o
valor "1" para palavras em negrito ou itálico, e "0" para as demais"). No exemplo anterior, a primeira
ocorrência da palavra "ferramentas" está em negrito, e as restantes em texto normal; assim, o registro
dessa palavra (wordID=#0002) ficaria:

wordID Hits
(...) (...)
#0002 (E29A, 2, 1); (E29A, 12, 0); (E29A, 14, 0); (390A, 310, 0)
(...) (...)

De maneira análoga, as Search Engines adicionam outros campos a seus índices, para agregar
informações que considerem relevantes para formação dos rankings; exemplos: ocorrência de palavras
em cabeçalhos HTML (h1, h2, etc); tamanho das fontes; ocorrência de palavras em locais estratégicos
das páginas, como título (texto entre [title] e [/title]) e nos textos-âncoras dos links.
A Google considera esses dois últimos itens (palavras-chave no título e nos textos-âncora) tão
importantes, que criou índices específicos para registrá-los; o docID é a chave primária, para
relacionamento dos índices (ou seja, o docID é o mesmo nos dois índices). No paper original da Google
(The Anatomy of a Large-Scale Hypertextual Web Search Engine), os autores (Larry Page e Sergey
Brin) fazem distinção entre "fancy hits" (que ocorrem apenas no título e nos links) e "plain hits" (que
ocorrem em todo o restante do texto). Esses índices são os pesquisados quando usuários fazem
consultas com os comandos "allinanchor:keyword" e "allintitle:keyword".
Assim, os índices armazenam basicamente dois tipos de dados: as páginas que foram
descobertas (trabalho dos bots) os elementos que as SEs considerem relevantes para formação dos
rankings (ou seja, do algoritmo de ordenamento). Por exemplo, a Google armazena, para cada página, o
valor do respectivo PageRank (na verdade, um outro índice armazena o PageRank, sempre usando o
docID como chave primária).
Após utilizar a técnica de índice invertido para localizar os documentos que contém os termos
pesquisados, o Google utiliza o PageRank para classificar os resultados. Este valor é armazenado no
índice, como citado acima, e seu cálculo será explicado na próxima seção.

7.2. Arquivos de Assinatura

Uma alternativa para a técnica de índices invertidos é a utilização de arquivos de assinatura


(signature files). A principal vantagem dos arquivos de assinatura é que eles não necessitam que um
Lexicon seja mantido em memória durante o processamento das pesquisas. De fato, com esta técnica, o
Lexicon não é mais necessário. Se o vocabulário de palavras criado a partir dos documentos
armazenados for rico, a quantia de espaço necessário para armazenar o Lexicon pode representar uma
fração substancial do espaço ocupado pelos próprios documentos.
Arquivos de assinatura são um método probabilístico para indexar documentos. Cada termo em
um documento é assinalado para uma assinatura randômica, na forma de um vetor de bits. Estes
assinalamentos são feitos usando-se a técnica de hashing.
A assinatura do documento é formada pela assinatura de todos os termos contidos nele. Para
gerar a assinatura de um documento simplesmente realizamos a operação lógica OR na assinatura dos
termos do documento. Por exemplo, consideremos a lista de termos de um texto e suas assinaturas
correspondentes:

Termo Assinatura
cold 1000 0000 0010 0100
days 0010 0100 0000 1000
hot 0000 1010 0000 0000
in 0000 1001 0010 0000
it 0000 1000 1000 0010
like 0100 0010 0000 0001
nine 0010 1000 0000 0100
old 1000 1000 0100 0000
pease 0000 0101 0000 0001
porridge 0100 0100 0010 0000
pot 0000 0010 0110 0000
some 0100 0100 0000 0001
the 1010 1000 0000 0000

Se os documentos são compostos por frases usando estes termos, a assinatura dos documentos
serão:

Documento Texto Assinatura


Pease porridge hot, pease porridge cold, 1100 1111 0010 0101
Pease porridge in the pot, 1110 1111 0110 0001
Nine days old. 1010 1100 0100 1100
Some like it hot, some like it cold, 1100 1110 1010 0111
Some like it in the pot, 1110 1111 1110 0011
Nine days old. 1010 1100 0100 1100

Para verificar se um termo T ocorre em um documento, é preciso verificar se todos os bits que
possuem valor 1 na assinatura do termo estão com o mesmo valor na assinatura do documento. Se não,
T não aparece no documento. Caso contrário T provavelmente ocorre neste documento, porque alguma
combinação das assinaturas de outros termos podem ter mudado o valor da assinatura para 1, portanto,
não pode-se dizer com certeza se T aparece ou não no documento. Por exemplo, se o termo de consulta
T for a palavra it, cuja assinatura é 0000 1000 1000 0010. Analisando-se as assinaturas dos documentos
verificamos que a palavra it pode ocorrer somente nos documentos 4 e 5, cujas assinaturas
respectivamente são: 1100 1110 1010 0111 e 1110 1111 1110 0011 , o que de fato ocorre. Já se
pesquisarmos pela palavra the, cuja assinatura é 1010 1000 0000 0000 podemos identficar que o termo
pode ocorrer nos documentos 2 (1110 1111 0110 0001), 3 (1010 1100 0100 1100), 5(1110 1111 1110
0011) e 6(1010 1100 0100 1100), mas de fato ele ocorre somente no segundo e no quinto. Esta é a
principal desvantagem deste algoritmo, a probabilidade de ocorrerem falsos positivos, mas em
compensação não ocorrem falsos negativos.
Esta técnica pode ser aprimorada para tratar palavras que aparecem com maior frequência em
diferentes buscas, para acelerar as pesquisas. Por exemplo, palavras comuns podem ser representadas
pelo código binário menor, facilitando sua pesquisa.

7.3. Algoritmos de Ranking

7.3.1. Algoritmo PageRank

O Google roda uma combinação única e avançada de sistemas de hardware e software. A


velocidade de retorno que você experimenta pode ser atribuída em parte à eficiência do algoritmo de
pesquisa e em parte aos milhares de PC's conectados formando um super rápido search engine.
O coração do software é o PageRank, um sistema de ranqueamento de páginas da web
desenvolvido pelos fundadores Larry Page e Sergey Brin na Universidade de Standford, em seu paper
The Anatomy of a Large-Scale Hypertextual Web Search Engine.
O PageRank baseia-se na natureza democrática e única da web, utilizando uma vasta estrutura
de links como indicador do volume individual de uma página.
Basicamente, o Google interpreta um link de uma página A para uma página B como um voto,
ou um link que a página recebe; o Google analisa também a página que dá o voto. Votos dados por
páginas que em si são "importantes" têm maior peso e auxiliam a fazer outras páginas "importantes".
Páginas classificadas de "Importantes" e sites de alta qualidade recebem um PageRank maior, que o
Google memoriza toda vez que conduz uma pesquisa. Claro que páginas importantes não têm
significado se elas não responderem a sua pesquisa. Assim, o Google combina o PageRank com um
sistema sofisticado de localização de texto para encontrar as páginas que são importantes e relevantes
para a pesquisa.
O Google vai muito além do número de vezes que uma expressão retorna em uma pesquisa; o
Google examina todos os aspectos do conteúdo da página (e do conteúdo da página a que ela se refere)
para determinar que a expressão é realmente relevante para sua pesquisa.
O algoritmo PageRank é elegantemente simples e calculado como segue:

PR(A) = (1-d) + d (PR(T1)/C(T1) + ... + PR(Tn)/C(Tn))


onde PR(A) é o PageRank da página A
PR(T1) é o PageRank da página T1
C(T1) é o número de links saindo (outgoing links) da página T1 apontando para outras páginas da rede
d é um valor variando de 0 a 1 ( 0 < d < 1) indicando a nota que o Google dá à página de acordo com
sua importância, sendo geralmente usado 0.85
Desta forma, o PageRank de uma página web é calculada como a soma dos PageRanks de todas
as páginas ligadas a ela (seus incoming links), dividido pelo número de links em cada uma destas
páginas (seus outgoing links). Então, existem duas maneiras que podem afetar o valor do PageRank:
 O número de incoming links: quanto mais melhor, pois nenhuma página “apontando” para a página
calculada irá ter um efeito negativo, ou nenhum efeito, no resultado.
 O número de outgoing links na página que aponta para a página calculada; neste caso, quanto
menos melhor. Por exemplo, dadas duas páginas com igual valor de PageRank ligadas a página
calculada, uma com 5 outgoing links e outra com 10, a primeira irá aumentar o valor do PageRank
duas vezes mais que a segunda.
A próxima coisa que pode-se obervar sobre o algoritmo PageRank é que ele não possui relação
com a relevância dos termos pesquisados. Ele simplesmente é uma pequena, e muito importante, parte
dos algoritmos usados pelo Google. Talvez uma boa maneira de olhar para o PageRank é como um
fator de multiplicação, aplicado aos resultados da busca após outras computações terem sido
completadas. O algoritmo Google primeiro calcula a relevância das páginas em seu índice em relação
com os termos da pesquisa, e entáo multiplica esta relevância pelo valor do PageRank para produzir
uma lista final. Quanto maior o PageRank de uma página, melhor será sua posição na lista de
resultados.

7.3.1.1. Deficiências do PageRank

Apesar de ter colocado a Google em vantagem em relação às demais Search Engines, o


algoritmo do PageRank tem algumas deficiências.

Na Seção 6.1 do documento original sobre PageRank, Sergei e Larry escreveram:

"Esses tipos de PageRank personalizados são virtualmente imunes a manipulações movidas por
interesses comerciais. Para uma página conseguir um alto PageRank, ela deve convencer uma página
importante, ou uma porção de páginas sem importância, a linkar para ela. No pior caso, poderemos
ter manipulação na forma de compra de publicidade (links) em sites importantes. Mas isso parece
estar sob controle, já que custa dinheiro..."

Eles estavam errados. Em primeiro lugar, deve-se mencionar que o algoritmo tinha uma
deficiência desde a origem: o PageRank era passado de página a página, independente do conteúdo das
mesmas. Isso significa que um link da homepage da NASA transferia a mesma quantidade de
PageRank, quer o link apontasse para um site sobre astronáutica (tópico correlato ao da página da
NASA), quer o link apontasse para um site sobre filmes dos anos 50 na Chechênia. Assim, uma pessoa
procurando aumentar seu PageRank tinha apenas que conseguir links em outras páginas de alto PR,
sem se importar com o tópico das mesmas.

A título de curiosidade: por volta da mesma época em que Page imaginou o PageRank, outro
pesquisador chamado J. Kleinberg estava desenvolvendo um trabalho chamado Authoritative Sources
in a Hyperlinked Environment (fontes que sejam autoridade em ambientes de hiperlinks), que também
analisava a estrutura de links para atribuir índices de relevância a cada página; a diferença era que, para
calcular o "PageRank" de uma página, o algoritmo de Kleinberg considerava apenas os links contidos
em páginas cujo tópico fosse similar ao da página sob análise.

O problema dessa técnica é que o grafo a ser analisado depende da [palavra-chave], e portanto
deve ser montado em tempo real, para cada pesquisa; o projeto, que recebeu o nome HITS, não foi
adiante porque não havia recursos tecnológicos suficientes para torná-lo comercialmente viável.
Entretanto, alguns anos depois, a patente da HITs foi adquirida pela Teoma.

As pessoas aproveitaram-se da deficiência do PageRank acima mencionada para manipularem


seus rankings; na verdade, pode-se dizer que a Google foi vítima do seu próprio sucesso.

Antes da Google, a compra de espaço publicitário (links de texto ou banners) era comum, mas o
principal objetivo do comprador era conseguir tráfego; ninguém se importava com PageRank.

Após seu explosivo crescimento, todos queriam ter bons rankings na Google. Com o tempo,
ficou evidente que havia grande correlação entre o posicionamento de uma página no ranking e seu
PageRank; mais e mais pessoas aprenderam que, para aumentar seu PageRank, a maneira mais fácil era
obter links em outras páginas de alto PageRank.
Em pouco tempo, PageRank virou uma commodity. Webmasters compravam links interessados
apenas no PageRank, e não na quantidade ou qualidade do tráfego que receberiam. Os links, que
deveriam funcionar como meio de acesso a outras fontes interessantes de informação (esse era o
espírito original do PageRank) passaram a ser objeto de compra e venda.

É bem verdade que muitos (a maioria) dos grandes sites não se envolveu nesse comércio de
PageRank. Os .edus, .govs, as grandes corporações continuaram sua vida normalmente. Entretanto,
alguns grupos de sites rapidamente aderiram ao comércio de PageRank. Por exemplo, alguns grandes
sites de alto PageRank que desde sempre venderam links, agora podiam inserir mais links em mais
páginas, por um preço muito maior; um exemplo desse tipo de site é foxnews.com. Outros sites que se
beneficiaram foram aqueles que, ao longo dos anos, publicaram informações úteis e relevantes,
conquistaram merecidamente vários links, e subitamente viram-se detentores de uma mercadoria
relevante chamada PageRank (vários sites na geocities enquadram-se aqui); muitas pessoas que nunca
pensaram em ter qualquer retorno financeiro de seus sites informativos agora podiam faturar um bom
dinheiro, vendendo PageRank.

Um caso que ficou notório foi o da searchking.com. Esse site atingiu PR7 em sua homepage,
tinha excelentes rankings, e abertamente anunciava a venda de links, como forma de se conseguir
PageRank e melhores rankings. A Google alterou o PR da SearchKing, que viu seu tráfego encolher. A
SearchKing iniciou um processo judicial contra a Google - mas não ganhou.

7.3.2. O algoritmo Hilltop


Uma das deficiências do algoritmo de PageRank é que qualquer link, em qualquer página
contida no índice, aumentava o PageRank (e melhorava o ranking) da página que recebia o link.

Entre vários outros, dois problemas maiores preocupavam a Google: 1) webmasters estavam
comprando links, para aumentar seu PageRank; 2) uma vez tendo construído um site de alto Pagerank,
ficava fácil para os webmasters construírem outros sites e,
de imediato, apontar links de suas próprias páginas e conseguir um bom posicionamento
inicial.
O algoritmo Hilltop atacou esses dois problemas.
O Hilltop foi concebido por Krishna Bharat, quando era pesquisador da Compaq. Leia o paper
original sobre o algoritmo Hilltop, em inglês. Não há informações oficiais de que o Hilltop tenha sido
implementado. Entretanto, os fatos de que Bharat seja hoje pesquisador sênior da Google, e de que os
resultados da Google hoje reflitam muitos dos conceitos expostos no paper, são indícios de que o
Hilltop (ou boa parte dele) foi incorporado ao algoritmo da Google.

O próprio autor descreve a base do Hilltop, na seção 1.2 do paper:


"Nossa técnica é fundamentada nas mesmas suposições dos outros algoritmos baseados em
conectividade, ou seja, que o número e qualidade das fontes que fazem referência a uma página
dão uma boa medida da qualidade da mesma. A diferença chave consiste no fato de que nós
consideramos apenas fontes que sejam "experts" - páginas que tenham sido criadas com o
propósito específico de encaminhar as pessoas aos recursos que procuram. Para responder a uma
pesquisa, nós primeiro computamos uma lista dos maiores experts naquele tópico. A seguir, nós
identificamos links relevantes dentro desse conjunto de experts, e os seguimos para identificar
páginas alvo. Essas páginas alvo são então rankeadas, de acordo com o número e a relevância de
experts não-afiliados que apontam para elas. Dessa forma, o posicionamento de uma página
reflete a opinião coletiva dos melhores experts independentes naquele tópico. Quando um
conjunto de experts não existir, Hilltop não retornará resultados. Assim, Hilltop é focado na
relevância dos resultados, e não na abrangência da pesquisa."

Veja-se, pois, que os links agora não são mais todos iguais.
7.3.3. Análise semântica

Embora as Search Engines indexem cada uma das palavras presentes em um texto, elas não
costumavam fazer análises semânticas, ou seja, não procuravam interpretar o significado do conjunto
de palavras.
Por exemplo, uma página que contivesse unicamente o texto e link "Essa página traz
informações relevantes sobre a compra e venda de imóveis - Clique Aqui" poderia ter uma pontuação
menor do que outra que exibisse o texto "Imóveis apartamentos compra venda barato apartamentos
imóveis casas especialista - Clique Aqui", muito embora esta seja claramente uma tentativa de se
manipular os rankings por meio de repetição de palavras-chave, enquanto aquela pareça ser uma fonte
mais confiável de informações.
A ausência de análise semântica favoreceu também a manipulação de PageRank: a compra e
venda de PR tornou-se difundida porque o PR é (ou era) transmitido independentemente dos tópicos
das páginas que trocavam links; uma análise semântica permitiria, por exemplo, detectar casos em que
uma página sobre educação infantil de PR7 contivesse um link para uma página de cassinos (ou seja,
páginas de contextos semânticos completamente diferentes). Leia mais adiante a seção sobre
Aplicações da técnica.
Para combater esse tipo de distorções, há sinais de que a Google (assim como outras SEs) esteja
implementando técnicas de análise semântica. Há alguns anos, a Google adquiriu a Applied Semantics,
uma empresa focada exatamente na aplicação de estudos semânticos à recuperação de dados na web;
não por coincidência, pouco tempo após a aquisição da Applied Semantics a Google lançou o Adsense,
que se fulcra exatamente na análise do contexto semântico de uma página para escolher um anúncio
que seja relacionado com o tema da página. Outro indício de que a Google tem incorporado análise
semântica ao algo é que Krishna Bharat, hoje pesquisador sênior da Google, principal responsável pela
concepção do algoritmo Hilltop, tem também interesse por esse tópico. Bharat foi co-autor de um
paper, apresentado na 9a. Conferência WWW em 2000, chamado The Term Vector Database: fast
access to indexing terms for Web pages.

7.3.3.1. Term Vector Database

Term Vector Database = Banco de Dados de Vetores de Termos; a palavra "termos" tem o
mesmo sentido que em "termos (palavras, vocábulos) que compõem um texto".
O objetivo é criar um banco de dados que, para cada URI, armazene um vetor de termos.
Simplificadamente, a idéia é reproduzir o trabalho feito há tempos para classificar livros em uma
biblioteca: uma obra sobre "Antecedentes da Lei Áurea" pode ser representada por um conjunto de
palavras como (História, Escravidão, Brasil, Lei Áurea); esse conjunto de palavras é um vetor de
termos simplificado. Esse tipo de classificação pode ser expandido, utilizando-se recursos atuais de
tratamento e recuperação de informações; por exemplo, o vetor pode conter muito mais termos.
Em termos matemáticos, vetor pode ser grosseiramente definido como uma coleção de
elementos semelhantes; vários teoremas tratam de vetores, e podem ser aplicados aos vetores de
termos. Um vetor de termos é, pois, uma coleção de palavras (representadas por números binários).
Para cada URI (ou seja, para cada página), o correspondente vetor de termos é um retrato das palavras
que descrevem a página.
Como construir o vetor de termos de uma dada página? Segundo o trabalho de Bharat, a técnica
adotada foi a seguinte: Primeiramente, constrói-se um léxico, ou seja, um conjunto de todos os termos
que são passíveis de serem reconhecidos e catalogados. No trabalho de Bharat, o léxico consistia das
palavras no "terço intermediário" do índice da Altavista; foram expurgadas as palavras mais utilizadas,
pois palavras que aparecem em muitos vetores não permitem diferenciação entre eles (no caso extremo,
uma palavra que aparecesse em todos os vetores seria inútil para identificá-los), assim como as palavras
menos utilizadas, pois palavras raras não permitem uma boa avaliação de similaridade de páginas, que
era o objetivo do trabalho.
Definido o léxico, cria-se um vetor-mãe, da mesma ordem que o número de termos existentes
no léxico. Cada posição no vetor representa a ocorrência de determinado texto em uma página; o vetor-
mãe, por definição, é [1,1,1,1,1,....,1,1], em que a quantidade de 1s é igual ao número de termos do
léxico.
Torna-se possível, para cada página, construir um vetor de termos específico; o elemento do
vetor será 0 ou 1, conforme o termo existente no léxico ocorra ou não na página; todos os vetores têm o
mesmo comprimento do vetor-mãe. Por exemplo, se o primeiro elemento do vetor-mãe corresponder ao
termo "pesquisa", todas as páginas que contiverem o termo "pesquisa" terão o primeiro elemento do seu
vetor de termos definido como "1".
O trabalho de Bharat foi um pouco mais adiante: para cada termo, não apenas sua ocorrência foi
vetorizada, mas também foi-lhe atribuído um peso. Isso significa que cada elemento do vetor é um par
de números, o primeiro indicando a ocorrência, o segundo o seu peso; Bharat definiu o peso
simplesmente como o número de ocorrências do termo na página, mas admite que definições mais
complexas (e.g., o peso pode ser maior para termos aparecendo em pontos-chave, como título, âncoras,
texto em negrito, etc).
Embora os vetores provavelmente passem por uma análise matemática depurada, já é intuitivo
que a semelhança de vetores indica uma semelhança entre os tópicos de duas páginas. Consideremos
esses três vetores, em um exemplo simplificado:

página A = [1,1,0,0,0,0,1,1,1,0,0,1,0,1]
página B = [1,1,0,1,0,0,1,0,1,0,0,1,0,1]
página C = [0,0,1,0,1,0,0,1,0,0,0,0,1,0]

Fica fácil (para seres humanos) observar que o grau de semelhança semântica entre as páginas
A e B é muito maior do que entre qualquer uma delas e a página C.
Computadores não podem visualizar semelhanças e diferenças, mas podem fazer cálculos com os
vetores; por exemplo, é bem sabido (por matemáticos) que o valor do produto escalar de dois vetores
fornece uma indicação da semelhança entre eles.
Nota: Bharat afirma que é inviável criar vetores de termos em tempo real (provavelmente, o
grande gargalo seria o tempo necessário para baixar a página), daí a necessidade de se ter os vetores
pré-computados armazenados em um banco de dados.

7.3.3.2. Aplicações do Vetor de Termos

A principal utilidade dos vetores de termos é mensurar similaridade entre páginas web;
realizando cálculos com os vetores, os engenheiros da Google conseguem avaliar o grau de semelhança
entre duas páginas.
O trabalho de Bharat menciona duas aplicações do vetor de termos: Topic Distillation e Classificação
de Páginas.
Topic Distillation : Bharat afirma que o trabalho sobre Vetores de Termos foi desenvolvido
com a finalidade se aprimorar o algoritmo de Topic Distillation (algo como seleção de tópicos). Topic
Distillation é uma tentativa de mitigar uma das deficiências do algoritmo de PageRank: em vez de fazer
uma análise genérica de conectividade (como ocorre no cálculo de PR), o algoritmo de Topic
Distillation levaria em conta apenas links existentes em páginas correlatas. O problema era, justamente,
como determinar quais páginas são correlatas; o vetor de termos é uma solução para o problema.

Classificação de Páginas : Como classificar, automaticamente, páginas da web em tópicos?


Uma solução possível é criar um vetor que represente os termos usados com mais freqüência em
páginas do tópico, e a seguir comparar cada página com esse vetor-do-tópico. Como criar um vetor-de-
tópico? Basta rastrear um conjunto de páginas que se saiba, de antemão, tenham foco naquele tópico, e
indexar as palavras encontradas. Por exemplo, Bharat criou doze vetores a partir da indexação das doze
categorias do diretório do Yahoo! Para classificar outras páginas, comparamos o respectivo vetor de
termos a cada um dos doze vetores tópicos; a página é classificada na categoria em que houver maior
semelhança de vetores.

Outras aplicações: O trabalho de Bharat foi apresentado em 2000; desde então, muitas outras
idéias podem ter surgido para aplicação do vetor de páginas, e outras técnicas de análise semântica.
Ademais, é importante observar que os avanços tecnológicos (mais máquinas, mais memória, mais
CPU) podem ter tornado possível a implementação de algoritmos antes impraticáveis (as atualizações
do índice, que eram mensais, tornaram-se praticamente diárias). Uma aplicação evidente é a
transferência seletiva de PageRank: comparam-se os tópicos de duas páginas, e transfere-se tanto mais
PR quanto maior a semelhança de tópicos entre ambas. Outras aplicações são possíveis.
Referências

http://www.ufpa.br/sampaio/curso_de_estdados_2/
http://ead1.eee.ufmg.br/cursos/C/c.html
http://www.ulbra.tche.br/~danielnm/ed/E/polE.html
http://www.lcad.icmc.usp.br/~nonato/ED/Hashing/node33.html
http://www.infotem.hpg.ig.com.br/tem_progr_hash.htm
http://www.markhorrell.com/seo/pagerank.shtml
http://www.globalbrasil.com/teste/google.html#pagerank
http://www.europanet.com.br/euro2003/index.php?cat_id=391
http://www.rankings.com.br/google/
http://www-2.cs.cmu.edu/~guyb/realworld.html

OBS: Apostila Original criada pelo professor Élton Minetto. Editada e Atualizada pelo professor
José Carlos Toniazzo.