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?
hash % table size
deveria ser distribuído uniformemente, não o hash em si.hash % tableSize
quando 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.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.
fonte
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:
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:
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).
fonte