Escolar Documentos
Profissional Documentos
Cultura Documentos
CHAPECÓ
ACEA
CURSO DE CIÊNCIA DA COMPUTAÇÃO
Estrutura de Arquivos
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:
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.
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.
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.
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.
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.
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.
Cálculos de espaço
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
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 é
O tempo total para leitura de um arquivo de registro NL, se partirmos do princípio de que não há
rebobinamento, é dado por
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.
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.
O disco fixo da Figura 3 consiste em seis pratos com material ferromagnético em sua superfície
proporcionando o meio de armazenamento.
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.
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.
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:
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
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
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.
À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.
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
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:
Estes dois valores podem ser aproximados para uma função linear de forma:
F(n)= m x n + s
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:
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).
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.
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.
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
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.
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.
- Á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:
10 175 BILL -
20 275 NARA -
30 -
40 -
50 -
|----------------ÁREA DE EXTENSÃO----------------|
2.5. Introdução aos Arquivos Indexados
- Í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
- 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.
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.
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.
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
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
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
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():
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.
A função putc é a primeira função de escrita de arquivo que veremos. Seu protótipo é:
int putc (int ch,FILE *fp);
#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
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 é:
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);
Arquivos pré-definidos
putc(ch, stdout);
fgets
Para se ler uma string num arquivo podemos usar fgets() cujo protótipo é:
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:
ferror e perror
Protótipo de ferror:
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 é:
#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 é:
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
remove
Protótipo:
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;
/* 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);
}
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:
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:
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE *p;
char str[80],c;
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.
Compressão Lógica
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.
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.
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.
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
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
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 ...
onde Mb é o mapa de bits correspondente à compressão do caracter a . Este mapa é composto, para o
exemplo, dos bits:
e)Substituição de Padrões
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.
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
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
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:
C3 0,3
C4 0,3
C2 0,3
C1 0,1
C3 00
C4 01
C2 10
C1 11
C3 0
C4 10
C2 110
C1 111
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.
C1 0 0
C2 10 0
C3 110 0
C4 1110 0
C5 1111 0
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.
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.
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.
#include <stdio.h>
#include <stdlib.h>
#define num_max_reg 1000
struct produto {
int numprod;
char nomprod[20];
int quaprod;
double preprod;
};
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);
}
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
Arquivo B
156
420
730
831
85
356
525
654
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
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 :
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:
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.
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:
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.
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
Método quadrático
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
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:
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:
Tabela tab[MAXTAB];
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.
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
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.
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
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.
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.
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:
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.
"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.
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.
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.
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.
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.
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.
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.