domingo, 8 de setembro de 2013

Espalhamento

O objetivo de uma função de espalhamento é distribuir seus elementos na tabela de forma uniforme e de maneira aleatória para evitar aglomeração de valores semelhantes, sem perder a eficiência e rapidez, sempre gerando o mesmo índice na tabela para a mesma chave de entrada. Teremos a função espalhamento denominada hash que irá mapear a chave k para a posição x na tabela, formalmente definido:

hash(k) = x

Quando apresentado a rotina de espalhamento de datas de aniversário na introdução a Tabela de Dispersão, foi apresentado um caso especial conhecido como espalhamento perfeito, no sentido de que duas datas jamais irão gerar o mesmo valor, para criar uma rotina assim, devem-se conhecer todas as entradas possíveis e ser capaz de gerar um valor único para cada uma delas, objetivo alcançado de forma simples no exemplo de aniversários quando se tem a conhecimento da faixa de datas e como obter um valor único da mesma.

Em situações práticas, o algoritmo de espalhamento perfeito é impossível de se obter, a dificuldade se deve a um fator, não é possível prever o conjunto completo de dados que será disperso na tabela, como resultado desenvolve-se soluções de caso geral.

 Qualquer que seja o método para conversão de chaves, lembre-se de que o mais importante é a distribuição uniforme e aleatória de posições na tabela, dois métodos simples podem ser usados:


Divisão: Tendo k como um inteiro, uma das formas mais simples de espalhamento é mapear a posição na tabela é dividir o inteiro pelo tamanho da tabela e utilizar o valor de modulo (resto da divisão):

hash(k) = k mod m

Como exemplo, uma tabela tem m = 1.000 posições, e a chave k = 31.415.926:

31.415.926 mod 1.000 = 926.

Apesar do valor de tabela exemplo ser simples, deve-se evitar utilizar tamanhos de tabelas cujo valor seja potência de 2 que gera repetição de índices em determinadas faixas, note que no exemplo acima o valor de índice termina por ser os ultimo dígitos da chave de entrada, na computação a potência de 2 são os bits de baixa ordem, o que torna irrelevante o processo de obter o modulo da chave, se para isso basta pegar apenas os dígitos finais, como solução, utiliza-se para tamanho da tabela um numero primo, permitindo maior aleatoriedade.

Troca-se m de 1000 por 997, por ser um valor próximo ao da tabela exemplo, a rotina de espalhamento resulta no valor:

31.415.926 mod 997 = 456.


Multiplicação: Como alternativa para a divisão, ao multiplicar o inteiro k por uma constante A na faixa 0 < A < 1; para o exemplo utilizamos a proporção áurea menos 1, com A = 0.618, após essa multiplicação, extrai a parte fracionária fazendo modulo do resultado com 1 e multiplica pelo numero de posições na tabela:

hash(k) = m ( k * A mod 1)

No método da divisão havia a preocupação em ter um tamanho de tabela de valor primo, a vantagem da multiplicação é tornar a escolha do tamanho da tabela independente do calculo, com a tabela podendo assumir um valor de potência de 2, tendo 1000 posições, temos:

1000 * ((31.415.926 * 0,618) mod 1)
1000 * (19.415.042,268 mod 1)
1000 * 0,268
268

Apesar da vantagem na escolha da tabela em relação ao método da divisão, tem como ponto negativo uma quantidade maior de calculo necessário, tornando o método da multiplicação lento quando comparado ao da divisão.

#include <stdio.h>
#include <math.h>

#define TABLE_PRIME_SIZE   997
#define TABLE_SIMPLE_SIZE  1000

typedef unsigned char      BYTE;
typedef unsigned short int WORD;
typedef unsigned long int  DWORD;


WORD HashDiv(DWORD dwKey)
{
       return (WORD) (dwKey % TABLE_PRIME_SIZE);
}

WORD HashMul(DWORD dwKey)
{
       double dFrac;

       dFrac = fmod(dwKey * 0.618, 1);

       return (WORD) (TABLE_SIMPLE_SIZE * dFrac);
}

void main(void)
{
       WORD wIndex;

       wIndex = HashDiv(31415926); printf("indice: %d\n", wIndex);
       wIndex = HashMul(31415926); printf("indice: %d\n", wIndex);
}

Vale observar que apesar do método ser nomeado de multiplicação, pode-se contestar o conceito, afinal multiplicar por um fracionário, equivale a dividir, ambos os métodos acabam utilizando o valor do modulo de diferentes maneiras para obter um valor dentro da faixa de índices da tabela.

Grande parte das rotinas de espalhamento utiliza a entrada k como sendo um inteiro, tornando simples para alterar matematicamente o valor de k dentro da rotina de espalhamento para retornar uma posição na tabela.

Quando a chave k não é inteiro, precisa ser feita a conversão, como converter um conjunto de chaves depende das características da mesma, é importante obter o conhecimento da aplicação em particular para evitar modelos pobres de conversão de palavras em chaves que podem se basear apenas nos caracteres do inicio ou do fim, que acabará por gerar uma quantidade de resultados iguais.  Como alternativa, seleciona-se varias posições, fazendo arranjos e substituições para tornar o elemento ainda mais aleatório e transpor esses caracteres em um numero inteiro.

Um dos criadores da linguagem AWK, Peter J. Weinberger desenvolveu um excelente algoritmo para transformar chaves de variados tipos em inteiros, uma variante de sua versão foi utilizada no UNIX para espalhamento quando se gera o arquivo executável no formato ELF, esse mesmo algoritmo foi publicado no famoso livro do dragão sobre compiladores.

DWORD HashPjw ( const char *strData )
{
       DWORD dwKey = 0, dwTmp;

       while ( *strData )
       {
             dwKey = ( dwKey << 4 ) + *strData++;
             if ( dwTmp = dwKey & 0xF0000000 )
                    dwKey ^= dwTmp >> 24;
             dwKey &= ~dwTmp;
       }

       return dwKey;
}

Como exemplo, observe como a rotina transforma a string “Roberto Bauer” em um valor numérico, em um loop seleciona-se sequencialmente o valor de byte para somar na chave e desloca o resultado em 4 bits para a esquerda.

       while ( *strData )
             dwKey = ( dwKey << 4 ) + *strData++;
. . .

Para evitar o descartar o estouro das operações de soma e deslocamento feitas no calculo da chave no instante em que a mesma esteja preste a romper o limite da Double Word, utiliza-se a condicional abaixo para armazenar o valor dos 4 bits mais significativos da chave até o momento em dwTmp.

. . .
             if ( dwTmp = dwKey & 0xF0000000 )
. . .

Por fim, realizam mais duas operações com os 4 bits, uma operação OU EXCLUSIVO (^) com os bits 4 a 7 e um E LOGICO com COMPLEMENTO A UM (& ~) com os bits de 28 a 31.

. . .
                    dwKey ^= dwTmp >> 24;
             dwKey &= ~dwTmp;
. . .

Segue na tabela a simulação completa, os valores se apresentam em hexadecimal para facilitar a interpretação das operações bit a bit.

Data
K << 4
K + Data
Tmp >> 24
K^=Tmp>>24
 ~Tmp
K &= ~Tmp
R - 52
 00000000  
 00000052    
00000000    
 00000052
 FFFFFFFF
 00000052
o - 6F
 00000520  
 0000058F    
00000000    
 0000058F
 FFFFFFFF
 0000058F
b - 62
 000058F0  
 00005952    
00000000    
 00005952
 FFFFFFFF
 00005952
e - 65
 00059520  
 00059585    
00000000    
 00059585
 FFFFFFFF
 00059585
r - 72
 00595850  
 005958C2    
00000000    
 005958C2
 FFFFFFFF
 005958C2
t - 74
 05958C20  
 05958C94    
00000000    
 05958C94
 FFFFFFFF
 05958C94
o - 6F
 5958C940  
 5958C9AF    
00000050    
 5958C9FF
 AFFFFFFF
 0958C9FF
  - 20
 958C9FF0  
 958CA010    
00000090    
 958CA080
 6FFFFFFF
 058CA080
B - 42
 58CA0800  
 58CA0842    
00000050    
 58CA0812
 AFFFFFFF
 08CA0812
a - 61
 8CA08120  
 8CA08181    
00000080    
 8CA08101
 7FFFFFFF
 0CA08101
u - 75
 CA081010  
 CA081085    
000000C0    
 CA081045
 3FFFFFFF
 0A081045
e - 65
 A0810450  
 A08104B5    
000000A0    
 A0810415
 5FFFFFFF
 00810415
r - 72
 08104150  
 081041C2    
00000000    
 081041C2
 FFFFFFFF
 081041C2

Ao fim da rotina o resultado tem de ser aplicado ao método da divisão ou multiplicação sobre o tamanho da tabela para obter o índice.

. . .
      WORD wIndex;

      wIndex = HashDiv(HashPjw("Roberto Bauer"));
      printf("indice: %d\n", wIndex);
. . .

Como em todas as abordagens, é possível que um algoritmo genérico de espalhamento como o HashPjw não seja tão eficiente como um criado para determinado problema em particular, entretanto percebem-se que aplicações gastam muito pouco tempo processando as rotinas de espalhamento, os pontos necessários para o calculo da chave em índice ser eficientes são:

·         Conter no máximo uma divisão, de preferência a operação de modulo.
·         Gerar uma longa faixa de índices
·         Não usar atributos da informação que tendem a ser comum

A menos que surja alguma necessidade especifica, as rotinas genéricas para espalhamento acima mostradas devem ser suficientes, recomenda-se não perder tempo buscando aperfeiçoar seu desempenho uma vez que menos de 2% de todo o processamento será feito pela função.

Nenhum comentário:

Postar um comentário