- Publicado em
Implementando Tabelas Hash (Hash Tables) com NodeJS e Javascript
- Um Resumo dos Mais Utilizados
- Implementando Arrays com NodeJS e Javascript
- Implementando Listas Ligadas (Linked Lists) com NodeJS e Javascript
- Implementando Pilhas (Stacks) com NodeJS e Javascript
- Implementando Filas (Queues) com NodeJS e Javascript
- Implementando Árvores (Trees) com NodeJS e Javascript
- Implementando Grafos (Graphs) com NodeJS e Javascript
- Implementando Tabelas Hash (Hash Tables) com NodeJS e Javascript
- Implementando Heaps com NodeJS e Javascript
- Implementando Filas de Prioridade (Priority Queues) com NodeJS e Javascript
- Implementando Mapas e Conjuntos (Maps/Sets) com NodeJS e Javascript
- Implementando Árvore de Busca Binária com NodeJS e Javascript
- Implementando Tries (Árvores) com NodeJS e Javascript
- Algoritmos de Grafos com NodeJS e Javascript
- Algoritmos de Ordenação com NodeJS e Javascript
- Implementando Busca Binária em Arrays com NodeJS e Javascript
A Teoria da Coisa
As Hash Tables ou tabelas de hash (é muito ruim ficar traduzindo esse nome vou chamar de Hash Tables blz?!), bom, HashTable é um tipo de estrutura de dados que o foco, ou o objetivo é otimizar a busca, inserção e remoção de elementos, ou seja, deixar tudo isso o mais rápido possível.
Quando usamos Hash Tables isso permite um acesso praticamente instantâneo na informação/dados dos elementos pelas keys
(chaves) únicas... isso te lembra algo? Você acessa o valor de um elemento por uma key/chave, lembra alguma coisa?
Estamos falando de tabelas, mas quando você acessa um objeto JSON, você acessa por uma chave única certo? e acessa direto o valor daquela chave... É uma comparação meio estranha mas é pra te ajudar a entender de forma simples... ai pensa que no JSON você tem uma chave e um valor, e na HashTable você tem uma chave e um valor, a diferença é que na HashTable a gente tem um algoritmo que calcula um valor único para cada chave, então a gente não sabe exatamente o valor dessa chave igual no JSON, mas sabemos que a chave ta lá...e essa chave que leva pra um elemento da nossa tabela.
Mas, relaxa, que você não vai lembrar tudo de cabeça 100% do tempo, por isso, vem aqui pra lembrar sempre que precisar.
Mão na Massa
O JavaScript já tem uma implementação nativa de HashTable
que é o Map, sempre que usamos o Map no NodeJS/Javascript a gente ta usando uma implementação de HashTables.
Mas, pra que eu ia te fazer ler até aqui e falar pra você ir usar o Map
né? Vamo tentar fazer esse trambolho na raça e ver o que sai...
Agora a Prática
Então abre aí o seu VSCode ou sua IDE favorita e vamos lá, crie um arquivo chamado hash-table.js
e cola o código abaixo:
class HashTable {
constructor(size = 100) {
this.table = new Array(size)
this.size = size
}
_hash(key) {
let hash = 0
for (let i = 0; i < key.length; i++) {
hash = (hash + key.charCodeAt(i) * i) % this.size
}
return hash
}
}
Agora vamos explicar um pouco o código acima antes de continuar:
- A gente tem uma classe
HashTable
que recebe osize
como parâmetro (opcional) caso não tenha size a gente coloca como100
o valor padrão - A gente inicia/cria um novo array chamado
table
com o tamanho definido pelosize
e seta a propriedadesize
- A gente tem uma função
_hash
que recebe umakey
como parâmetro e calcula o valor do "hash" baseado nos caracteres da chave e seus valores ASCII- O valor do hash é calculado somando o valor ASCII de cada caractere na chave
- Depois o valor ASCII é multiplicado pela sua própria posição no array da chave
- Depois a gente calcula o valor do
módulo
da soma dessa conta de ascii+posição com o tamanho da tabela para obter o valor final do hash
A função _hash
parece um pouco complexa, e é meio chatinho de entender, mas lembra, o objetivo é ter um valor de hash e que esse valor do hash é a chave que leva pra um elemento da nossa tabela...Lógico que pra fins de teste, você pode ter uma função hash muito mais simples, que retorne um random ou algo do tipo, a ideia é que o valor do hash seja único para cada chave.
Se quiser entender e aprofundar um pouco mais, essa função
_hash
é uma implementação simples da Função de hHash "djb2" que é uma função de hash usada em muitas aplicações.
Agora vamos seguir e implementar mais uns negocinhos, dentro da classe HashTable
logo depois da função _hash
vamos adicionar a função set
, get
e remove
:
set(key, value) {
const index = this._hash(key);
console.log('#set: setting a new value on the hastable', { index, key, value })
if (!this.table[index]) {
this.table[index] = [];
}
this.table[index].push([key, value]);
}
get(key) {
const index = this._hash(key);
console.log('#get: getting the value from the hastable', { index, key })
const items = this.table[index];
if (items) {
for (let item of items) {
if (item[0] === key) {
return item[1];
}
}
}
return undefined;
}
remove(key) {
const index = this._hash(key);
const items = this.table[index];
if (items) {
for (let i = 0; i < items.length; i++) {
if (items[i][0] === key) {
items.splice(i, 1);
return true;
}
}
}
return false;
}
Eu não vou aprofundar muito nas funções acima com um passo a passo, porque a lógica dessas é mais simples, todas elas usam a função _hash
para calcular e descobrir qual é o index
do elemento que vamos colocar, buscar ou remover, e depois usando esse index a gente faz o set
(incluir), get
(busca) e o remove
(deleta) o elemento...O ponto que vale citar aqui é que no set
e no get
colocamos um log pra ver o que ta acontecendo.
Agora vamos adicionar mais umas linhas de código só pra testar a HashTable que a gente fez, no final do arquivo (depois da classe) coloca isso aqui:
const hashTable = new HashTable()
hashTable.set('hello', 'world')
hashTable.set('hash', 'table')
No código aí em cima, a gente tá criando uma nova instância da nossa HashTable
e adicionando dois elementos, um com a chave hello
e valor world
e outro com a chave hash
e valor table
, dai agora vamos colocar uns logs e fazer o get desses elementos, coloca mais esse código e depois vamos executar esse arquivo JS:
console.log('\n#when getting "hello" from table, result should be = "world"')
const resultGetFoo = hashTable.get('hello')
console.log('# result:', {
result: resultGetFoo,
})
console.log('\n#when getting "hash" from table, result should be = "table"')
const resultGetBaz = hashTable.get('hash')
console.log('# result:', {
result: resultGetBaz,
})
Agora, abre o terminal e executa o arquivo hash-table.js
com o NodeJS:
node hash-table.js
Você deve ver algo parecido com isso no seu terminal:
# set: setting a new value on the hastable { index: 85, key: 'hello', value: 'world' }
# set: setting a new value on the hastable { index: 39, key: 'hash', value: 'table' }
# when getting "hello" from table, result should be = "world"
# get: getting the value from the hastable { index: 85, key: 'hello' }
# result: { result: 'world' }
# when getting "hash" from table, result should be = "table"
# get: getting the value from the hastable { index: 39, key: 'hash' }
# result: { result: 'table' }
Explicando:
- A gente adicionou dois elementos na nossa HashTable, um com a chave
hello
e valorworld
e outro com a chavehash
e valortable
- Quando adicionamos, podemos ver nas 2 primeiras linhas o log que mostra o index, a chave e o valor que estamos adicionando, ou seja, quando vamos adicionar um novo item na nossa tabela, o index já é calculado e sabemos onde colocar o elemento na tabela, graças a nossa função
_hash
, esse log é mostrado 2 vezes, uma pra cada elemento que adicionamos ("hello" e "hash") - Depois a gente faz um
get
de cada um desses elementos e vemos que o resultado é o esperado, o valor que a gente adicionou é o valor que a gente recebeu de volta, e podemos ver no log também que não precisamos percorrer toda a tabela pra encontrar "hello", a primeira coisa que oget
faz é calcular o index e ir direto no index que a chave "hello" está, e o mesmo acontece com a chave "hash", nesse momento que vemos o poder das HashTables, imagina se tivesse 1 milhão de itens nessa tabela, se não fosse hash, a gente teria que percorrer sei lá quantos itens pra encontrar o valor, mas com hash, a gente vai direto no index que a chave tá.
Vou parar por aqui e deixar esse código pra você brincar e testar, se quiser, pode adicionar mais elementos, remover, etc... e se tiver alguma dúvida, só falar que a gente tenta se ajudar.
Aonde Você Pode Usar/Ver
Gerenciamento de Cache
Hash Tables são ideais para implementações de cache, porque em vez de realizar uma busca por varios elementos o index é calculado e já sabemos onde encontrar o elemento e se tratando de cache onde precisamos responder o mais rápido possível as Hash Tables caem como uma luva.
Evitar Items Duplicados
Se você tem alguma necessidade de evitar itens duplicados, as Hash Tables conseguem ser muito melhores pra evitar um item duplicado do que outras implementações normais de tabelas, pois se o hash for calculado e já tiver sendo usado você consegue saber que aquele item já existe antes de adicionar um duplicado.
Dicas
Usar funções
_hash
que distribuam as chaves uniformemente pelos índices da tabela para evitar colisões excessivas, quanto mais colisões pior é o desempenhoHash Tables precisam de tratamento de colisões, como encadeamento ou endereçamento, isso depende do caso e implementação
Geralmente temos uma tabela com um tamanho pré-definido e dependendo o que você precisa o ideal é monitorar o quanto a tabela está cheia e redimensionar a tabela ou ter um dimensionamento dinâmico para manter operações eficientes conforme a tabela cresce
Jon, e se eu quiser implementar uma Hash Table com tratamento de colisões, que raio é isso? como eu faço isso?
Vou fazer um post sobre isso em breve, se você quiser, deixa um comentário aqui embaixo pra falarmos sobre isso!
Considerações do Jon
Em geral, estruturas de dados (no caso aqui as HashTables) são estruturas que já tem implementações feitas em muitas linguagens, geralmente tentamos implementar ou entender como elas funcionam pra passar numa prova, pra ter conhecimento e opções pra uma entrevista, curiosidade, ou raramente, porque precisamos de algo especifico que devemos implementar no modo raiz, mas não é algo muito comum e se acontecer, será um desafio que vai fazer você aprender muito.
Mas é importante relembrar e ter na memória que essas estruturas ajudam nós devs a criar aplicações mais eficientes, as vezes você pode resolver um problema de performance só trocando a estrutura que você ta usando, então é sempre bom ter esses conceitos na manga.
E Você?!
Já criou/utilizou algo parecido nos seus projetos ou no seu trampo? Se quiser, comenta aqui, se já fez, experiências, desafios, dicas, etc!!
Vamos explorar esses conceitos juntos, ah e se viu algum bug, erro, melhoria, também comenta aí!