Como os valores da tabela de hash são armazenados fisicamente na memória?

7

Questão:

Como os valores da tabela de hash são armazenados na memória, de modo que o espaço se usado com eficiência e os valores não precisem ser realocados com frequência?

Meu entendimento atual (pode estar errado):

Digamos que eu tenho 3 objetos armazenados em uma tabela de hash. Suas funções de hash geram esses valores:

  • 0 0
  • 10
  • 20

Eu presumiria que os ponteiros desses objetos não seriam armazenados nos seguintes endereços de memória, pois haveria grandes lacunas entre eles:

  • startOfHashTable + 0
  • startOfHashTable + 10
  • startOfHashTable + 20

O artigo da Wikipedia sobre tabelas de hash diz que o "índice" é calculado da seguinte forma:

hash = hashfunc(key)
index = hash % array_size 

Então, no meu exemplo, os índices seriam:

  • 0% 3 = 0
  • 10% 3 = 1
  • 20% 3 = 2

Isso elimina as enormes lacunas que mencionei antes. Mesmo com esse esquema de módulo, há problemas quando você adiciona mais objetos à tabela de hash. Se eu adicionar um quarto objeto à tabela de hash, precisaria aplicar% 4 para obter o índice. Isso não invalidaria todos os% 3 que eu fiz no passado? Todos os% 3 anteriores precisariam ser realocados para os% 4 locais?

Pwner
fonte

Respostas:

15

As entradas de uma tabela de hash são armazenadas em uma matriz. No entanto, você entendeu mal a aplicação do operador módulo aos valores de hash. Se a tabela de hash estiver armazenada em uma matriz de tamanho n, então a função hash é calculada no módulo n, independentemente de quantos itens estão armazenados atualmente na tabela. Portanto, no seu exemplo, se você estivesse armazenando os itens em uma matriz de tamanho 6, os três itens com valores de hash 0, 10 e 20 seriam armazenados nos locais 0, 4 e 2, respectivamente. Se você adicionou um quarto elemento com valor de hash, por exemplo, 31, que seria armazenado no local 1, sem precisar mover nenhum dos três primeiros itens. Se sua tabela hash estava ficando cheio e você queria para movê-lo em uma matriz maior, então você precisa recalcular a localização de todos os itens na mesa e movê-los de forma adequada.

David Richerby
fonte
11
Então, você está dizendo que as tabelas de hash são criadas com um tamanho potencial estimado e os itens são realocados apenas quando você precisa aumentar o tamanho ... Portanto, não importa se uma função de hash tem distribuição uniforme. Por exemplo, os valores de hash de 0, 5 e 10 são distribuídos uniformemente, mas quando inseridos em uma tabela de hash de tamanho potencial 5, todos eles colidem no intervalo 0. Seria melhor dizer que hash % table sizedeveria ser distribuído uniformemente, não o hash em si.
Pwner
@Pwner Tudo isso está correto, sim.
David Richerby
11
Como é possível criar uma distribuição uniforme hash % tableSizequando tableSize pode mudar? Os valores de hash de 0, 5, 10 e criar muitas colisões quando o tamanho da tabela é 5, mas não tem colisões quando o tamanho da tabela é 20.
Pwner
11
@Pwner Lembre-se de que as tabelas de hash só esperam operações de tempo constante, se houver. Mas somente se a função hash for (aproximadamente) uniforme.
Raphael
11
@Pwner A distribuição não é literalmente uniforme - mas você gostaria de se aproximar do uniforme.
David Richerby
7

Hash-table geralmente desperdiçam espaço. Muitos algoritmos o fazem, já que as trocas de tempo e espaço são comuns, mas geralmente escondem melhor :) . Como outros algoritmos, as tabelas de hash fazem isso para obter melhor desempenho do tempo.

O primeiro ponto é que você tenta evitar colisões em sua tabela de hash, porque isso mantém o custo do tempo de acesso constante (mas as colisões geralmente são permitidas e podem ser tratadas, permitindo assim que vários itens estejam na mesma entrada, pelo custo do tempo ) O segundo ponto é que você tenta evitar grandes lacunas não utilizadas, porque isso custa memória. O terceiro ponto é que você evita alterar sua função de hash (daí também o tamanho da tabela), pois isso requer a reorganização de toda a tabela, que possui um grande custo de tempo.

Infelizmente, quanto menos lacunas você tiver, maior a probabilidade de uma nova entrada de hash causar uma colisão. Uma boa função de hash, para um determinado conjunto de dados, limitará a probabilidade de colisão, mesmo com o melhor uso do espaço de índice disponível.

Na verdade, você deve considerar que existem dois tipos de tabelas de hash: estáticas e dinâmicas.

Para os estáticos, os dados a serem misturados não são alterados; portanto, você pode tentar encontrar uma função de hash sem colisão para esse conjunto de dados. Isso é chamado de hash perfeito . Mas o melhor é um hash mínimo perfeito , que alcança o resultado sem falhas.

Mas isso não é possível quando os dados a serem misturados mudam dinamicamente, dentro de um grande conjunto de possibilidades. Então você não pode evitar colisões, mas tenta limitá-las tendo lacunas suficientes.

Existem várias técnicas para gerenciar isso de maneira diferente, adaptando o tamanho da tabela ao número de valores que estão sendo divididos em hash, aumentando a tabela quando há muitas colisões ou reduzindo-a quando há lacunas muito grandes. Mas isso deve ser tratado com muito cuidado, usando variações exponenciais da tabela, de modo a limitar o impacto da reorganização da tabela no custo geral do uso da tabela de hash.

Isso pretende ser uma introdução intuitiva. Para obter mais detalhes técnicos e referências, consulte as respostas a esta pergunta: (Quando) é a pesquisa de tabela de hash O (1)? . Hash-tables e hashing são um tópico importante, com muitas variações.

babou
fonte
3

Uma boa maneira de examinar as tabelas de hash é como uma tabela de pesquisa com um intervalo infinito de índices (bem, não muito infinitos, você ainda está limitado pelo limite de valor da chave que está usando).

Digamos que você esteja tentando armazenar alguns valores específicos de sqrt (x) em uma tabela de pesquisa em que X é um número inteiro, seria algo como isto:

[1] = 1
[3] = 1.732
[10000] = 100

Isso resulta em um enraizamento quadrado muito barato, pois, em vez do cálculo expencive, você pode simplesmente buscar o valor da matriz. No entanto, é um uso muito ineficiente da memória porque [2] e [4 - 9999] estão vazios.

Para o resgate, vem a função hash, o objetivo de uma função hash nesse contexto é transformar o índice em algo que realmente se encaixa em uma matriz de tamanho razoável; portanto, por exemplo, isso pode ser feito:

(1) = [5] = 1
(3) = [2] = 1.732
(10000) = [3] = 100

agora todos os três valores se encaixam em uma matriz do tamanho de 6.

Como a função hash consegue isso? A função hash mais básica é (Index% ArraySize), o operador módulo divide o índice que você escolheu pelo tamanho da matriz e fornece o restante, sempre menor do que o tamanho da matriz.

Mas e se vários índices hash para o mesmo resultado? Isso é chamado de colisão de hash e existem diferentes maneiras de lidar com isso. O mais simples deles é armazenar cada valor junto com seu Índice original na matriz, se esse slot da matriz for obtido, avance 1 até que um slot vazio seja encontrado. Ao recuperar o valor, vá para o local indicado pela função hash e faça um loop pelos elementos até encontrar aquele com índice original adequado.

É por isso que uma boa função de hash também é ótima para dispersar os dados, de modo que, se os índices recebidos são seqüenciais ou aleatórios, o resultado do hash deve ser o mais amplamente possível, para manter o custo de acessar dados relativamente constante.

É claro que quanto maior a matriz subjacente, menos colisões você terá, portanto é uma troca entre velocidade e eficiência de tamanho. As tabelas de hash modernas geralmente enchem até ~ 70% e têm menos de 10 colisões por acesso. Juntamente com a função hash, isso significa que cada busca de dados custa aproximadamente 20 ciclos, o que é (para alguns propósitos) um bom compromisso entre velocidade (tabela de pesquisa) e eficiência (lista).

user29075
fonte