2035001
Algoritmos e
Estruturas de Dados
As tabelas de hash adotam o critério de
converter a chave de pesquisa, por uma relação aritmética, diretamente no índice de um array onde se encontra o valor a ela associado.
A conversão da chave num valor numérico é
feito por uma função de hash.
Tabelas Hash (dispersão)
2 São tabelas (arrays) cujos índices são de alguma
forma relacionadas com os conteúdos das posições respectivas.
O relacionamento é estabelecido por uma função
h:K I, onde:
o domínio K é o espaço de chaves e I é o espaço de índices.
Considerar |K| = n, o número de possíveis chaves. Considerar |I| = M.
Deve-se ter I = {0,1,2,…, M-1} ou I = {1,2,3,…, M}.
Tabelas Hash (dispersão)
3 Exemplo:
K é o conjunto de todas as cadeias alfabéticas I é um inteiro entre 0 e 25
h é o código ASCII do primeiro caractere da cadeia
módulo 26:
ASCII( ‘AMORA’) = 65 e h(65) = 13. ASCII (‘VAZO’) = 86 e h(86) = 8.
Tabelas Hash (dispersão)
4 O valor gerado pela função hash h tem de ser
limitado à dimensão da tabela.
Tabelas Hash (dispersão)
5 Se a chave for numérica, e assumindo que o
array tem 10 posições, podemos conceber a
seguinte função de hash: k mod 10, kK:
Tabelas Hash (dispersão)
6 Chave alfnumérica:
Tabelas Hash (dispersão)
7 Resumindo....
A idéia é obter um método para implementação de
dicionários onde o acesso a uma dada chave pode ser feito em O(1) na média.
No pior caso, entretanto, o acesso pode ter custo
O(n).
Tabelas Hash (dispersão)
8 Idealmente, funções de dispersão deveriam ser
injetivas, isto é, todas as chaves deveriam ser mapeadas por h em um índice distinto.
Isto só é possível se n M
Mesmo assim, pode não ser trivial construir a
função de dispersão
Por exemplo, considere o conjunto S de nomes dos
alunos de um curso. Então
|S| < 100 mas é impossível escrever uma função injetiva para
esse domínio que não leve O(|S|) para ser avaliada
Precisamos considerar como o domínio de h o conjunto de
todas as cadeias alfabéticas (de até 40 caracteres, digamos)
Função Hash (de dispersão)
9 Um caso trivial é h(x) = x. Isto é conhecido como
tabela de endereço direto (Cormen, 2002).
Em geral, n >> M
Quando a função de dispersão não é injetiva,
pode-se ter duas chaves x e y, x ≠ y tais que h(x) = h(y). Se se tentar inserir ambas as chaves na tabela tem-se o que é conhecido como colisão.
Função Hash (de dispersão)
10 Em geral, uma boa função de dispersão deve
reunir as seguintes qualidades
produzir poucas colisões
Depende de se conhecer algo sobre a distribuição das
chaves sendo acessadas
Ex.: se as chaves são códigos alfanuméricos que
começam sempre por ‘A’ ou ‘B’, usar o primeiro caractere das chaves pode levar a muitas colisões
ser fácil de computar
Típicamente, funções contendo poucas operações
aritméticas
ser uniforme (espalhar a chave pelo vetor)
Idealmente, o número máximo de chaves que são
mapeadas num mesmo índice deve ser n/M =
Função Hash (de dispersão)
11 Assumindo K = {0 .. n – 1} e I = {0 .. M – 1}, a função
de dispersão é dada por
h(x) = x mod M
Qual deve ser o valor de M?
não deve ser uma potência de 2
se M = 2k, h(x) = k bits menos significativos de x
não deve ser um número par (impar),
se M é par (impar) então h(x) é par (impar) x é par(impar)
na prática, bons resultados são obtidos com:
M = número primo não próximo a uma potência de 2 (a divisão
depende de todos os bits de x)
M = número sem divisores primos menores que 20
Funções de Dispersão – Método da
Divisão
Chaves sucessivas são mapeadas em índices
sucessivos. Isso pode acarretar problemas.
O método da divisão é simples:
Funções de Dispersão – Método da
Divisão
13
int h(int x){
static int M = 1031; //número primo
return abs(x) % M; }
Para evitar que chaves sucessivas sejam
mapeadas em índices sucessivos, comumente multiplica-se a chave por uma constante k antes de se fazer a divisão (M e k devem ser primos entre si):
h(x) = x*k mod M
Funções de Dispersão – Método da
Divisão
Este método evita usar divisão, já que a divisão é,
em geral, mais lenta que multiplicação inteira.
Toda a aritmética inteira em um computador é
feita módulo W, onde W = 2w e w é o tamanho da
palavra do computador
Suponha que M = 2k para algum k ≥ 1.
Para uma chave inteira x, usa-se a seguinte função
de dispersão:
Funções de Dispersão – Método do
Meio do Quadrado
15
x W
W M x h 2 mod Note que, a razão W/M = 2w-k.
Portanto, para multiplicar (x2 mod W) por M/W
basta movimentar a sequencia de bits de w-k posições a direita.
Sendo assim, está-se retirando k bits do meio do
quadrado da chave.
Código do método (o resultado estará entre 0 e
M-1):
Funções de Dispersão – Método do
Meio do Quadrado
16
int h(int x){
static int k = 10; //M = 1024
static int w = 32; //tamanho da palavra do computador
return x*x >> (w – k); }
Funciona bem quando as chaves são
equiprováveis.
As chaves que tem um grande número de zeros
significativos (não significativos) vão colidir (h(x)=0).
Este método evita (em relação ao método da
divisão):
que chaves consecutivas tenha código hash
consecutivos (espalha bem estas chaves).
que, se M é uma potência de 2, obtenha apenas os
últimas k bits da chave.
Funções de Dispersão – Método do
Meio do Quadrado
Variação do método do meio do quadrado que
abranda algumas de suas deficiências.
Neste método, multiplica-se a chave x por uma
constante a cuidadosamente escolhida.
Depois extrai-se os k bits centrais do resultado.
A função de dispersão é:
Qual deve ser o valor de a?
Funções de Dispersão – Método da
Multiplicação
18
a x W
W M x h * mod a não deve ter grande número de zeros
significativos
a não deve ter grande número de zeros não
significativos
Deve-se escolher a e W primos entre si.
Há muitas escolhas para a. Uma delas, para uma
aritmética de 32 bits (W = 232), é
a = 265.435.769.
Representação binária de a:
00001111 11010010 00111010 01111001
Funções de Dispersão – Método da
Multiplicação
Este valor de a não possui muitos zeros
significativos e não significativos.
a e W são primos entre si. O trecho de código a
seguir ilustra o método da multiplicação:
Funções de Dispersão – Método da
Multiplicação
20
int h(int x){
static int k = 10; //M = 1024
static int w = 32; //tamanho da palavra do computador
static unsigned a = 265435769; //ou deve ser long
return x*a >> (w – k); }
No método de Fibonacci, usa-se o número
chamado razão áurea (ou dourada) :
Existe uma relação entre e a sequencia de
Fibonacci Fn:
Funções de Dispersão – Método de
Fibonacci
212045868343
8749894848
1,61803398
2
5
1
n n
n F ˆ 5 1 Faz-se, então
A tabela apresenta alguns valores apropriados
para a para diversos w (tamanho da palavra):
Esta função espalha bem as chaves consecutivas.
Funções de Dispersão – Método de
Fibonacci
22 W a W a W/ 216 40 503 232 2 654 435 769 264 11 400 714 819 323 198 485Funções de Dispersão – Método da
Dobra
Suponha que a chave seja dada por uma
seqüência de dígitos escritos numa folha de papel.
O método consiste em dobrar sucessivamente a
folha de papel após o j-ésimo dígito somando os dígitos que se superpoem (sem fazer o “vai um”)
5 8 7 3 2 1 3 8 + = 8 5 3 8 3 8 0 4 + = 0 4 23
Funções de Dispersão – Método da
Dobra
Uma outra variação consiste em fazer a dobra de
k em k bits, ou seja, considerando os “dígitos” 0 e
1 da representação binária do número. O resultado é um índice entre 0 e 2 k – 1
Nesse caso, ao invés de somar os bits, utiliza-se
uma operação de ou-exclusivo entre os bits
Não se usa “e” (“ou”) pois estes produzem
resultados menores (maiores) que os operandos Exemplo: Suponha k = 5
71 = 00010001112
h(71) = 000102 xor 001112 = 001012 = 5
Funções de Dispersão –
Implementação
Todos os métodos vistos dispersão chaves de
valores inteiros.
Dependendo da aplicação, as chaves podem ser
letras, cadeias de caracteres, números reais, …
Em geral, dados K e a constante positiva M, uma
função de dispersão tem a forma:
25
0
,
1
,
2
,
,
1
:
K
M
Funções de Dispersão –
Implementação
Na prática, é conveniente interpretar h como
uma composição de funções f e g, onde:
26
K
f :
0
,
1
,
2
,
,
1
:
M
g
Assim:
f
x
g
f
g
h
:
1a parte: deve-se encontrar um mapeamento
adequado de conjunto de chaves K para Z.
2a parte: deve-se encontrar um mapeamento
adequado de inteiros não negativos para [0, M-1]
Funções de Dispersão –
Implementação
Chaves inteiras (short, int, long, char…)
27
x
x
f
Chaves em ponto flutuante (float, double, …)
Todo número real x 0 pode ser escrito na forma
(padrão IEEE 754 para float de 32 bits):
s em
x
1
2
onde:
0
,
1
x
0
,
5
m
1
1
.
023
e
1
.
024
Funções de Dispersão –
Implementação
28
Podemos ter a seguinte definição para f:
0 2 1 2 0 0 x W m x x fonde W = 2w tal que w é o tamanho da palavra.
Mostra-se que x e y (duas chaves distintas)
colidem se suas mantissas diferem de
W
2 1
x e -x colidem, pois o sinal não é considerado.
O expoente também não é considerado. Se x e y
Funções de Dispersão –
Implementação
Chaves de cadeias de caracteres.
Uma cadeia de caracteres, s, de tamanho n é uma
sequencia de caracteres:
29
s s sn
s 0, 1,,
Uma maneira simples para f é:
1 0 n i i s s f Cadeias formadas com os mesmos caracteres
Funções de Dispersão –
Implementação
Alternativamente, podemos construir f da
seguinte forma (no segundo caso limita-se a faixa de f(s)): 30
s B s f
s B s W f n i i i n n i i i n mod ou 1 0 1 1 0 1
Funções de Dispersão –
Implementação
31
int f(char *s, int n){ int result = 0;
for(int i=0; i<n; ++i)
result = result*B + s[i]; return result;}
Código para f usando a regra de Horn:
Onde B pode ser um número primo. Java usa
B=31.
Pode-se também fazer B = 2b (usa-se shift).
Valores particularmente bons para B: 33, 37, 39
e 41. Estudos experimentais. (GOODRICH, M. T.; et al. Bookman. 2002. 696 p.)
Funções de Dispersão –
Implementação
Outra forma é construir f usando números
aleatórios: 32
1 0 n i i i p s s f Neste caso, pi é um inteiro de um conjunto de
pesos gerados aleatoriamene para 0 i n-1.
Pode-se gerar um peso para cada um dos 256
caracteres ASCII em cada posição i=0..n-1. Assim, p[n, 256].
Tratamento de Colisões –
Encadeamento Exterior
Mesmo com boas funções de dispersão, à
medida que o fator de carga = n/M (número de chaves armazenadas / número de índices) aumenta, a probabilidade de haver colisões aumenta.
Tratamento de Colisões –
Encadeamento Exterior
De maneira geral qualquer tabela de
espalhamento precisa prever algum esquema para tratamento de colisões.
Tratamento de Colisões –
Encadeamento Exterior
Uma das maneiras mais empregadas para
lidar com colisões é permitir que cada posição da tabela seja ocupada por mais de uma chave
Em vez de guardar uma chave, guarda-se uma
lista de chaves
Na verdade, pode-se usar qualquer estrutura –
uma árvore, por exemplo – mas como a ocorrência de colisões deve ser relativamente rara, uma lista ordenada ou não costuma ser suficiente
Tratamento de Colisões –
Encadeamento Exterior
36
Exemplo de tabelas de dispersão usando
Tratamento de Colisões –
Encadeamento Exterior
Quantas comparações podemos esperar em
média para um acesso a chaves não presentes (buscas sem sucesso)?
Supomos que h é uma função uniforme, que o
fator de carga da tabela é e que as listas são não ordenadas
Tratamento de Colisões –
Encadeamento Exterior
Então a probabilidade de h computar cada índice
i é uniforme e igual a 1/M
O número de comparações feitas ao se acessar a
entrada i da tabela é o comprimento da lista Li
Então, Carga de Fator 1 Médio Custo 1 0
M n L M M i i 38Tratamento de Colisões –
Encadeamento Exterior
Quantas comparações podemos esperar em
média para um acesso a chaves presentes (buscas bem-sucedidas)?
Mostra-se que este custo é
39 M 2 1 2 1 CM
Tratamento de Colisões –
Encadeamento Exterior
Portanto, se mantemos o fator de carga baixo
(menor que uma constante ), temos que a complexidade média da busca é O(1)
A única desvantagem do encadeamento exterior
é que ele requer o uso de estruturas externas e com isso o uso de alocação dinâmica de memória e o “overhead” correspondente
Para contornar isso pode-se usar encadeamento
interior ou endereçamento externo
Tratamento de Colisões –
Encadeamento Interior
A idéia é usar como nós das listas as próprias
entradas da tabela
Há duas variantes
41
Na primeira, a tabela de M entradas é dividida
em duas porções:
A função de dispersão h retorna apenas índices
na primeira porção – de 0 a p–1. Assim, por exeplo: h(x) = x*k mod p.
A segunda porção – índices de p a M-1 é usada
Tratamento de Colisões –
Encadeamento Interior
Na primeira…
Pode acontecer que a área de overflow seja toda
tomada sem que todas as entradas da tabela tenham sido usadas
Pode-se aumentar a área de overflow
diminuindo-se p, mas isso também é ineficiente. No limite, p = 1 e a tabela resume-se a uma lista encadeada
Tratamento de Colisões –
Encadeamento Interior
Na primeira…
Pode acontecer que a área de overflow seja toda
tomada sem que todas as entradas da tabela tenham sido usadas
Tratamento de Colisões –
Encadeamento Interior
Na segunda variante, todo o espaço de
endereçamento é usado
O maior problema dessa abordagem é que
pode haver colisões secundárias, isto é colisões entre chaves não sinônimas (h(x)
h(y))
44
Função h: h(x) = x mod 7
Ordem de inclusão das chaves: 28, 35, 14, 70 e 19
Tratamento de Colisões –
Encadeamento Interior
Quando ocorre uma colisão, a chave é
armazenada na primeira posição livre após
h(x), a posição d, digamos
Se agora incluirmos y≠x tal que h(y)=d,
teremos a fusão das listas correspondentes a
h(x) e h(y), diminuindo a eficiência do
esquema
Tratamento de Colisões –
Encadeamento Interior
Um outro problema refere-se às dificuldades
introduzidas no processo de exclusão:
Não se pode simplesmente retirar o elemento da
sequência de valores na tabela
Além do valor de chave especial que indica
“posição vazia”, é preciso criar um valor de chave especial que indica “elemento removido”
Uma inserção posterior pode reaproveitar
posições marcadas com “elemento removido”. É possível após verificar se o elemento a ser inserido não se encontra na tabela.
Tratamento de Colisões –
Encadeamento Interior
Na verdade, encadeamento interior com
espaço de endereçamento único não é uma boa idéia, já que os problemas são os mesmos encontrados no tratamento de colisões por endereçamento aberto, sendo que nesse último temos a vantagem de não precisar de ponteiros.
No encadeamento interior aproveita-se
melhor o espaço reservado para a tabela (vetor).
Tratamento de Colisões –
Endereçamento Aberto
Ao invés de usar ponteiros, utiliza-se uma outra
função de dispersão que indica o próximo índice a ser tentado. Em geral temos a função de dispersão h (x, i) onde
x é a chave
i = 0, 1, 2, …, M-1 é o número da tentativa
h(x, i) tem que ser desenhada de tal forma a
visitar todos os M endereços em M tentativas
No pior caso, M tentativas são feitas
Tratamento de Colisões –
Endereçamento Aberto
Tentativa linear:
h (x, i) = (h’ (x) + i) mod M
Tem a desvantagem de agrupar tentativas
consecutivas 49 Após inclusão das chaves: 26, 72 e 27 Função h: h(x) = x mod 23
Tratamento de Colisões –
Endereçamento Aberto
Tentativa quadrática
h (x, i) = (h’ (x) + c1i + c2i2 + c0) mod M
Em geral utiliza-se apenas i2 para o termo
quadrático.
Resolve o problema do agrupamento primário
chaves x e y tais que h’(x) = h’(y) geram a mesma
seqüência de tentativas.
Teorema (Preiss, 2001): Quando M é um número
primo, as primeiras M/2 tentativas são distintas. Provar (pg 203).
Pelo teorema, se ≥ 0.5 então …
Tratamento de Colisões –
Endereçamento Aberto
Comparação entre tentativas linear/quadrática
Tratamento de Colisões –
Endereçamento Aberto
Dispersão dupla
h (x, i) = (h’ (x) + i h’’ (x)) mod M
A distribuição das chaves é mais aleatório que os
anteriores.
As sequencias de tentativas são identicas
somente se h’(x) = h’(y) e h” (x) = h”(y).
Para que os endereços-base obtidos
correspondam a varredura de toda a tabela,
h”(x) e M devem ser primos entre si.
Ou, fazer M primo.
Tratamento de Colisões –
Endereçamento Aberto
Exemplo: h(x, i) = h’(x) + ih”(x) mod M Onde: M = 7, h’(x) = x mod 7 h”(x) = 1 + x mod 5 Calcular as posições de cada
chave na figura ao lado.
Limitações
A tabela de dispersão é uma estrutura de dados do
tipo dicionário, que não permite armazenar elementos repetidos, recuperar elementos sequencialmente (ordenação), nem recuperar o elemento antecessor e sucessor.
Para otimizar a função de dispersão é necessário
conhecer a natureza da chave a ser utilizada.
No pior caso, a ordem das operações pode ser O(n),
caso em que todos os elementos inseridos colidirem.
Função Hash (de dispersão)
54 Limitações
As tabelas de dispersão com endereçamento aberto
podem necessitar de redimensionamento. Suas aplicações incluem:
Banco de dados;
implementações das tabelas de símbolos dos
compiladores;
na programação de jogos para acessar rapidamente
a posição para qual o personagem irá se mover e na implementação de um dicionário.
Ver algoritmos em (SZWARCFITER, 3ª Ed, 2010)